java基础二:多线程编程

写在开始

这是java基础系列的第二篇,关于java多线程。java语言完美的支持了多线程编程,多线程、并发也是java在web开发和移动端开发中经常使用的技术,可以说不了解java多线程与没系统学习过java编程没有区别。本文也是管中窥豹,试图介绍java多线程编程技术,难免有未尽周到之处,望不吝评论指正。

线程与进程

进程是一个程序开启运行的过程,在一个进程中它具有自己独立的内存空间和数据集合,开启一个进程系统需要为其分配内存、映射硬件接口等。进程的操作包括:创建进程、撤销进程、切换进程等,进行这其中任何一个操作都要完成一系列硬件和软件操作,因此对进程的操作是十分费时的,通过进程实现计算机的并行编程并不现实。

在进程之上可以开启多个线程,它们就像一个线程中的多个子任务,例如开启视频播放软件就是开启一个进程,在视频播放的同时可以调节音量、亮度字幕等,这些操作都是通过多线程实现的,实际上线程的“并行”并不是严格意义上的同步,而是在极短的时间间隔内依次做了多件事,形成了同步的假象。要实现真正意义的并行仍要开启多进程,例如你既想看视频又想听音乐只能开启一个视频播放软件和音乐播放软件分别播放视频和音乐。因此多线程的同步效果称为并发,多进程的同步效果称为并行

java多线程

使用Thread类开启多线程

继承Thread类,重写run()方法

1
2
3
4
5
6
7
import java.lang.Thread;
class myThread extends Thread{
@Override //重写run方法
public void run(){
... //线程代码

}

线程开启使用start()方法

1
2
myThread mt=new myThread();
mt.start();

缺点:单继承

继承Runnable接口

通过继承Thread实现多线程的方法虽然简单但是有很大的缺陷,继承类只能是单继承,因此为了实现多继承的功能,可以使用第二种多线程编程方法,继承Runnable方法实现多线程,由于Runnable是一个接口因此可以实现多继承

1
2
3
4
5
6
7
8
import java.lang.Runnable;
class myThread implements Runnable{
@Override
public void run()
{
... //线程代码
}
}

开启线程

1
2
Thread T1=new Thread(new myThread());
T1.start();

Thread其中的一种构造方法是以Runnable为输入参数的,因此可以将run方法写在Runnable的子类中以构造函数的方式传入到Thread中再以Thread.start()开启线程。

注意:所有的线程开启都是以Thread.start()开启的。

特点:解决多继承的问题、进程函数无返回值

使用Callable开启带有返回值的线程

1
2
3
4
5
6
7
8
9
10
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class myThread implements Callable<type>{ //此处为泛型<>中要填写返回值类型
@Override
public type call()
{
... //线程代码
return type var; //返回相应的类
}
}

开启线程

1
2
3
4
FutureTask<type> tsk=new FutureTask<>(new myThread());
Thread t1=new Thread(tsk);
t1.start(); //开启线程
tsk.get(); //获取线程返回值,此处注意抛出异常

FutureTask接受Callable参数的构造函数,FutureTask又是Runnable的子类,因此他们可以关联

线程同步

当多线程同时访问同一共享变量时就会出现变量的同步问题,又称作线程安全。与线程安全相关的几个java操作特性:

  • 原子性 该操作是不可分割的一旦开始就必须完成,不能被打断。一般类型的大多数操作是不满足原子性的,JDK atomic包中提供了各种满足原子性的数据类型。
  • 可见性 保证变量对所有的线程都是可见的,即某一线程对变量做出改变,其余线程可以得知。使用java提供的volatile关键字修饰对象。
  • 不变性 对象不可改变一定是线程安全的,使用java中的final关键字修饰对象或引用,注意引用不可变,但是引用的内容是可以该变的,因此即使是final关键字修饰,仍要家锁操作。

    锁机制

    在多线程编程中使用的最多的线程同步方法还是使用线程锁。

使用synchronized关键字修饰方法

1
2
3
4
public  synchronized String call()
{
... //线程代码
}

被synchronized修饰的方法不能被其他线程打断(也不一定),其内部的多线程共享变量是线程安全的,但是也造成了性能的下降。

使用Lock子类为代码块加锁:
Lock是java中的一个接口,实现这个接口的子类为ReentrantLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public  String call()
{
Lock lock=new ReentrantLock();
lock.lock(); //加锁
try{
System.out.println(this.title+":"+i); //上锁代码块
}
catch(Exception e){

}
finally{
lock.unlock(); //解锁
}
}

注意上锁的代码块必须在try{}catch{}中运行,运行过程中无论抛出任何异常都要先解锁,因此解锁操作要放在finally{}模块中。

Lock是比较老的上锁机制,其使用方法比较灵活,加锁的位置不同对性能的影响也不同,虽然synchronized效率较低,但是使用方法简单,而且是主推的方法,并且通过不断更新其性能也在不断提升,是推荐的加锁方法。