0%

java多线程编程基础(一)

什么是线程

在讨论什么是线程前有必要先说下什么是进程,因为线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动, 是系统进行资源分配和调度的基本单位, 线程则是进程的一个执行路径, 一个进程中至少有一个线程,进程中的多个线程共享进程的资源。操作系统在分配资源时是把资源分配给进程的, 但是CPU 资源比较特殊, 它是被分配到线程的, 因为真正要占用C PU 运行的是线程, 所以也说线程是CPU 分配的基本单位。

线程的创建与启动

Java 中有三种线程创建方式,分别为:

Runnable

实现Runnable 接口的run 方法。使用lambda简化代码。

1
2
Runnable task = () -> { System.out.println("Task #1 is running"); };
new Thread(task).start();

Thread

继承Thread 类并重写run 的方法

1
2
3
4
5
6
7
8
9
10
public class ThreadTest extends Thread {
@Override
public void run() {
System.out.println("child thread run");
}

public static void main(String[] args) {
new ThreadTest().start();
}
}

FutureTask

使用FutureTask 方式。FutureTask实现了Callable接口,能够获取异步任务的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
//创建异步任务
FutureTask<Integer> futureTask=new FutureTask<>(()->{
System.out.println("task#2: cal 1+2");
return 1+2;
});
//启动任务
new Thread(futureTask).start();

//等待任务执行完毕,并返回结果
int result=futureTask.get();

System.out.println("result is "+result);

线程通知与等待

wait()和notify()

当一个线程调用一个共享变量的wait()方法时, 该调用线程会被阻塞挂起, 直到发生下面几件事情之一才返回:

  • 其他线程调用了该共享对象的notify()或者notifyAll()方法;
  • 其他线程调用了该线程的interrupt()方法, 该线程抛出InterruptedException 异常返回。

一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。不同于notify()函数只会唤醒一个被阻塞到该共享变量上的线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用wait 系列方法而被挂起的线程。

notifywait系列方法,都需要当前线程获取到了共享变量的监视器锁后,才可以调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Thread thread1=new Thread(()->{
synchronized (lock){
try {

while (!prepare){
System.out.println(Thread.currentThread()+"阻塞挂起中...");
lock.wait();
}
System.out.println(Thread.currentThread()+"被唤醒");
} catch (InterruptedException e) {
System.out.println("线程被中断");
e.printStackTrace();
}
}
});
thread1.start();

new Thread(()->{
synchronized (lock){
try {
Thread.sleep(1000);
prepare=true;
System.out.println(Thread.currentThread()+"唤醒其他阻塞挂起的线程");
lock.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();

image-20211008205319745

另外需要注意的是,当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的

虚假唤醒

另外需要注意的是,一个线程可以从挂起状态变为可以运行状态( 也就是被唤醒)即使该线程没有被其他线程调用notify()notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。
虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。退出循环的条件是满足了唤醒该线程的条件。

1
2
3
4
5
6
synchronized (obj){
while (!prepare){
//阻塞挂起
obj.wait();
}
}

wait超时

  • wait(long timeout):该方法相比wait() 方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后, 没有在指定的timeout ms 时间内被唤醒,那么该函数还是会因为超时而返回。

线程的其他方法

join

在项目实践中经常会遇到一个场景,就是是需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源, 需要等待多个线程全部加载完毕再汇总处理。Thread 类中有一个简单的join 方法就可以做这个事情。(事实上不太使用该方法,AQS中的CountdownLatch更为强大和常用)

sleep

Thread 类中有一个静态的sleep方法,当一个执行中的线程调用了Threadsleep 方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU 的调度,获取到CPU 资源后就可以继续运行了。不同于wait方法,线程在调用sleep方法后,该线程所拥有的监视器资源,比如锁还是持有不让出的。

yield

Thread 类中有一个静态的yield 方法,当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。当一个线程调用yield 方法时, 当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU 的那个线程来获取CPU 执行权,这也是yieldsleep不同的地方。

线程中断

Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行, 而是被中断的线程根据中断状态自行处理。

  • void interrup() 方法: 中断线程, 例如,当线程A 运行时,线程B 可以调用钱程A的interrupt()方法来设置线程A 的中断标志为true 并立即返回。设置标志仅仅是设置标志, 线程A 实际并没有被中断, 它会继续往下执行。如果线程A 因为调用了wait 系列函数、join 方法或者sleep 方法而被阻塞挂起,这时候若线程B 调用线程A 的interrupt() 方法,线程A 会在调用这些方法的地方抛出InterruptedException 异常而返回。
  • boolean isinterrupte() 方法: 检测当前线程是否被中断,如果是返回true , 否则返回false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Thread thread=new Thread(()->{
try {
float i=0;
while (i<1000) {
i++;
}
System.out.println(i);
}
catch (Exception e){
e.printStackTrace();
}
finally {
// clean up if required
}

});


thread.start();

thread.interrupt();
System.out.println("中断线程"+thread);

image-20211008212955074

可以看到,程序没有抛出异常,而是正常执行。

线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

死锁的产生必须具备以下四个条件:

  • 互斥条件: 指线程对己经获取到的资源进行排它性使用, 即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  • 请求并持有条件: 指一个线程己经持有了至少一个资源, 但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源。
  • 不可剥夺条件: 指线程获取到的资源在自己使用完之前不能被其他线程抢占, 只有在自己使用完毕后才由自己释放该资源。
  • 环路等待条件: 指在发生死锁时, 必然存在一个线程→资源的环形链, 即线程集合{TO , TL T2 ,…, Tn }中的TO 正在等待一个Tl 占用的资源, Tl 正在等待T2 占用的资源,……Tn 正在等待己被TO 占用的资源。

一个线程死锁的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class DeadLock {

final static Object resource1=new Object();
final static Object resource2=new Object();

public static void main(String[] args) {
Thread threadA=new Thread(()->{
System.out.println("threadA 尝试获取 resource1中...");
synchronized (resource1){
System.out.println("threadA 获取 resource1成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadA 尝试获取 resource2中...");

synchronized (resource2){
System.out.println("threadA 获取 resource2成功");

}
}
});


Thread threadB=new Thread(()->{
System.out.println("threadB 尝试获取 resource2中...");
synchronized (resource2){
System.out.println("threadB 获取 resource2成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("threadB尝试获取 resource1中...");

synchronized (resource1){
System.out.println("threadB 获取 resource1成功");

}
}
});
threadA.start();
threadB.start();
}
}

image-20211008214107190

如图所示,threadAthreadB形成了线程死锁。

image-20211008213421323

关于如何避免线程死锁,操作系统中有详细介绍,这里不赘述。