什么是线程
在讨论什么是线程前有必要先说下什么是进程,因为线程是进程中的一个实体,线程本身是不会独立存在的。进程是代码在数据集合上的一次运行活动, 是系统进行资源分配和调度的基本单位, 线程则是进程的一个执行路径, 一个进程中至少有一个线程,进程中的多个线程共享进程的资源。操作系统在分配资源时是把资源分配给进程的, 但是CPU 资源比较特殊, 它是被分配到线程的, 因为真正要占用C PU 运行的是线程, 所以也说线程是CPU 分配的基本单位。
线程的创建与启动
Java 中有三种线程创建方式,分别为:
Runnable
实现Runnable
接口的run
方法。使用lambda
简化代码。
1 | Runnable task = () -> { System.out.println("Task #1 is running"); }; |
Thread
继承Thread
类并重写run
的方法
1 | public class ThreadTest extends Thread { |
FutureTask
使用FutureTask
方式。FutureTask
实现了Callable
接口,能够获取异步任务的返回值。
1 | //创建异步任务 |
线程通知与等待
wait()和notify()
当一个线程调用一个共享变量的wait()
方法时, 该调用线程会被阻塞挂起, 直到发生下面几件事情之一才返回:
- 其他线程调用了该共享对象的
notify()
或者notifyAll()
方法; - 其他线程调用了该线程的
interrupt()
方法, 该线程抛出InterruptedException
异常返回。
一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait
系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。不同于notify()
函数只会唤醒一个被阻塞到该共享变量上的线程,notifyAll()
方法则会唤醒所有在该共享变量上由于调用wait
系列方法而被挂起的线程。
notify
和wait
系列方法,都需要当前线程获取到了共享变量的监视器锁后,才可以调用。
1 | Thread thread1=new Thread(()->{ |
另外需要注意的是,当前线程调用共享变量的
wait()
方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的
虚假唤醒
另外需要注意的是,一个线程可以从挂起状态变为可以运行状态( 也就是被唤醒)即使该线程没有被其他线程调用notify()
、notifyAll()
方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒。
虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()
方法进行防范。退出循环的条件是满足了唤醒该线程的条件。
1 | synchronized (obj){ |
wait超时
wait(long timeout)
:该方法相比wait()
方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后, 没有在指定的timeout ms 时间内被唤醒,那么该函数还是会因为超时而返回。
线程的其他方法
join
在项目实践中经常会遇到一个场景,就是是需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源, 需要等待多个线程全部加载完毕再汇总处理。Thread 类中有一个简单的join
方法就可以做这个事情。(事实上不太使用该方法,AQS
中的CountdownLatch
更为强大和常用)
sleep
Thread 类中有一个静态的sleep
方法,当一个执行中的线程调用了Thread
的sleep
方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU 的调度,获取到CPU 资源后就可以继续运行了。不同于wait
方法,线程在调用sleep
方法后,该线程所拥有的监视器资源,比如锁还是持有不让出的。
yield
Thread 类中有一个静态的yield 方法,当一个线程调用yield
方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。当一个线程调用yield
方法时, 当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU 的那个线程来获取CPU 执行权,这也是yield
和sleep
不同的地方。
线程中断
Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行, 而是被中断的线程根据中断状态自行处理。
void interrup()
方法: 中断线程, 例如,当线程A 运行时,线程B 可以调用钱程A的interrupt()
方法来设置线程A 的中断标志为true 并立即返回。设置标志仅仅是设置标志, 线程A 实际并没有被中断, 它会继续往下执行。如果线程A 因为调用了wait
系列函数、join
方法或者sleep
方法而被阻塞挂起,这时候若线程B 调用线程A 的interrupt()
方法,线程A 会在调用这些方法的地方抛出InterruptedException
异常而返回。boolean isinterrupte()
方法: 检测当前线程是否被中断,如果是返回true
, 否则返回false
。
1 | Thread thread=new Thread(()->{ |
可以看到,程序没有抛出异常,而是正常执行。
线程死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
死锁的产生必须具备以下四个条件:
- 互斥条件: 指线程对己经获取到的资源进行排它性使用, 即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
- 请求并持有条件: 指一个线程己经持有了至少一个资源, 但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源。
- 不可剥夺条件: 指线程获取到的资源在自己使用完之前不能被其他线程抢占, 只有在自己使用完毕后才由自己释放该资源。
- 环路等待条件: 指在发生死锁时, 必然存在一个线程→资源的环形链, 即线程集合{TO , TL T2 ,…, Tn }中的TO 正在等待一个Tl 占用的资源, Tl 正在等待T2 占用的资源,……Tn 正在等待己被TO 占用的资源。
一个线程死锁的例子:
1 | public class DeadLock { |
如图所示,threadA
和threadB
形成了线程死锁。
关于如何避免线程死锁,操作系统中有详细介绍,这里不赘述。