You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
android-notes/blogs/Java/并发/Java 并发编程线程基础.md

10 KiB

Java 并发编程线程基础

目录

  1. 线程的创建与运行
  2. 线程等待通知机制
  3. 等待线程执行完成的 join 方法
  4. 让线程睡眠的 sleep 方法
  5. 让出 CPU 执行权的 yield 方法
  6. 线程中断
  7. 线程死锁
  8. 守护线程与用户线程

线程的创建与运行

创建线程有两种方式,一种是继承 Thread 重写它的 run 方法,一种是实现 Runnable 接口作为参数传给 Thread。两种方式的优劣其实就是单继承多实现的优劣。

Thread 只能执行 Runnable,也就是不带执行结果的,如果需要执行结果就需要使用 Callable,可以用 FutureTask 包装 Callable 传给 Thread 执行,因为 FutureTask 是实现了 Runnable 接口的,通过它的 get 方法就可以拿到执行结果,同时也可以拿到它的运行状态。没有参数的 get 方法在任务未执行完成时会一直阻塞的,也可以传一个超时时间,在超时时间内未完成就会抛 TimeoutException。

线程的等待通知机制

等待通知机制涉及到的两个函数 wait 和 notify/notifyAll 都是 Object 的方法。

wait() 函数

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

  1. 其他线程调用了该共享变量的 notify/notifyAll 方法
  2. 其他线程调用了该线程的 interrupt 方法,该线程抛出 InterruptedException 异常返回

另外需要注意的是,如果调用 wait 方法的线程没有事先获取该对象的监视器锁,则调用 wait 方法时调用线程会抛出 IllegalMonitorStateException 异常。

那么一个线程如何才能获取一个共享变量的监视器锁呢?

  1. 执行 synchronized 同步代码块时,使用该共享变量作为参数

    synchronized (共享变量) {
    
    }
    
  2. 调用该共享变量的方法,并且该方法使用了 synchronized 修饰

    synchronized void add () {
        // ...
    }
    

另外需要注意的是,一个线程可以从挂起状态变为可运行状态,也就是被唤醒,即使该线程没有被其他线程调用 notify/notifyAll 方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。

虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停的去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用 wait 方法进行防范。退出循环的条件是满足了唤醒该线程的条件。

synchronized (obj) {
	while (条件不满足) {
		obj.wait()
	}
}

如上代码是经典的调用共享变量 wait 方法的实例,首先通过同步块获取 obj 上面的监视器锁,然后在 while 循环内调用 obj 的 wait 方法。

另外需要注意的是,当前线程调用共享变量的 wait 方法后会只释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。为什么要释放锁呢?这是必然的,不然就会引起死锁,可以以生产者消费者的阻塞队列为例说明。

wait(long timeout) 函数

该方法相比 wait 方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享变量的该方法挂起后,没有在指定的 timeout ms 时间内被其他线程调用该共享变量的 notify/notifyAll 方法唤醒,那么该函数还是会因为超时而返回。如果将 timeout 设置为 0 则和 wait 方法效果一样,因为在 wait 方法内部就是调用了 wait(0)。需要注意的是,如果传了一个负数则会抛出 IllegalArgumentException 异常。

notify() 函数

一个线程调用共享对象的 notify 方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待线程是随机的。

此外,被唤醒的线程不能马上从 wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。

类似 wait 系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的 notify 方法,否则会抛出 IllegalMonitorStateException 异常。

notifyAll() 函数

不同于在共享变量上调用 notify 函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll 方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。

等待线程执行完成的 join 方法

join 是 Thread 里面一个无参且返回值为 void 的方法,当线程 A 调用线程 B 的 join 方法后会被阻塞,等待线程 B 执行完成,如果此时有其他线程调用了线程 A 的 interrupt 方法中断了线程 A 时,线程 A 会抛出 InterruptedException 异常而返回。

让线程睡眠的 sleep 方法

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

和 wait 一样,如果传入一个负数也会抛出 IllegalArgumentException 异常。

让出 CPU 执行权的 yield 方法

Thread 类中有一个静态的 yield 方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的 CPU 使用,但是线程调度器可以无条件忽略这个暗示。我们知道操作系统是为每个线程分配一个时间片来占有 CPU 的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了 Thread 类的静态方法 yield 时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。

当一个线程调用 yield 方法时,当前线程会让出 CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 CPU 的那个线程来获取 CPU 执行权。

yield 方法一般用在自旋中。

总结:sleep 与 yield 方法的区别在于,当线程调用 sleep 方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用 yield 方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

线程中断

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

void interrupt()

中断线程。例如,当线程 A 运行时,线程 B 可以调用线程 A 的 interrupt() 方法设置线程 A 的中断标志为 true 并立即返回。设置标志仅仅是设置标志,线程 A 实际并没有被中断,它会继续往下执行。如果线程 A 因为调用了 wait 系列函数、join 方法或者 sleep 方法而被阻塞挂起,这时候若线程 B 调用线程 A 的 interrupt() 方法,线程 A 会在调用这些方法的地方抛出 InterruptedException 异常而返回。

boolean isInterrupted()

检测当前线程是否被中断,如果是返回 true,否则返回 false。

public boolean isInterrupted() {
    // 传递 false,不清楚标志位
	return isInterrupted(false);
}
Thread.interrupted()

检测当前线程是否中断,如果是返回 true,否则返回 false。与 isInterrupted 方法不同的是,该方法如果发现当前线程被中断,则会清楚标志位,并且该方法是 static 方法,Thread 类可以直接调用。

public static boolean interrupted() {
    // 传递 true,清除标志位
	return currentThread().isInterrupted(true);
}

线程死锁

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

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

  • 互斥条件

    指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。

  • 请求并持有条件

    指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已经被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不会释放自己已经获得的资源。

  • 不可剥夺条件

    指线程获取的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完后主动释放该资源。

  • 环路等待条件

    指在发生死锁时,必然存在一个线程-资源的环形链。

那么如何避免死锁呢?

要想避免死锁,只需要破坏掉至少一个构成死锁的条件即可,但是目前只有请求并持有和环路等待条件是可以被破坏的。

造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁。

守护线程与用户线程

Java 中的线程分为两类,分别是 daemon 线程(守护线程)和 user 线程(用户线程)。JVM 在运行用户线程的同时,还运行着很多守护线程,比如有垃圾回收线程。那么守护线程和用户线程有什么区别呢?区别之一是当最后一个用户线程结束时,JVM 正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响 JVM 的退出。