并发:两个或多个事件在用一个时间段内发生
并行:两个或多个事件在同一时刻发生(同时发生)
在我们的计算机中,如果你的CPU是多核的,这个核的数量就是处理任务的线程的数量,比如你是双核的,那你的计算机便能并行处理两个任务。如果你只有单核,但又有多个任务或者双核有两个以上的任务,这时候怎么办呢?这时候就是并发处理任务了。
并发处理时,我们的CPU会在多个线程间反复横跳,一下子执行A线程,一下子执行B线程。由于CPU操作的时间是毫秒甚至是纳秒级别的,所以对我们来说可以忽略不记,可以理解把他们当作是同时发生的,但对于某一个时间点,都只有一个线程在工作。
大家有没有想过,当一个线程没抢到锁的时候在干嘛呢?调用了wait()
后在干嘛呢?答案就是:它们都存在某个地方里,静静等待着,上面两种情况分别对应两个地方,竞争锁失败后就进入了锁池;调用wait()
后就进入了等待池。那什么是锁池和等待池呢?
锁其实就是一个对象的对象头里维护了一个monitor对象,而其实对象还维护了两个set:EntrySet 和 WaitSet,但他们也被叫做锁池和等待池。
锁池
当一个线程竞争锁失败后,就会在锁池里挂起,当锁被释放时,锁池里的线程会再重新来争夺这把锁。
注意一点:这里指竞争锁指的是使用synchronized关键字,使用lock并不会进入锁池。
等待池
当一个线程调用wait()
方法后就会进入等待池,而进入了等待池要么是超时等待然后苏醒,要么就要用notify
()方法去唤醒等待池里的线程。
同时也要注意,等待池并非是指线程处于等待状态就会进入等待池,线程只有使用wait()
方法才能进入等待池,sleep()
、join()
等方法虽然也会进入等待状态,但不会进入等待池。
且一个线程从等待池醒来,不会直接回到运行状态,而是会进入锁池队列去竞争锁,只有竞争到锁才会进入运行状态。
看图应该就很好理解了,阻塞状态的线程就是位于锁池里的线程,等待状态的线程却不一定是等待池里的线程。
且还有一个点需要注意:如同锁是一个对象的所特有的,不同对象的锁不是同一个,这里不同对象的锁池和等待池也是不相同的,wait()
方法不是线程的,而是Object类里的方法,这里也可以看出来这一点。
且对于锁池和等待池,它们名字都有set,但事实上也可以看成队列,虽然不一定满足FIFO(先进先出):
notify()
方法时,就会唤醒等待池队列里的第一个线程,这里就满足FIFO了。网上有一些说法是说唤醒是“随机的”,但这里的随机指的是JDK版本,在JDK1.8是唤醒队列头的线程,其他版本可能会有不同的操作,所以说是随机的。对于线程的状态,网上众说纷纭,有的说五种,有的说七种,但实际上应该是六种,这一点可参考并发编程神书《Java并发编程的艺术》,也可以从源码中得出,线程的状态在Java中是有规定的,由一个枚举类规定:
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
*
* - {@link Object#wait() Object.wait} with no timeout
* - {@link #join() Thread.join} with no timeout
* - {@link LockSupport#park() LockSupport.park}
*
*
* A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called {@code Object.wait()}
* on an object is waiting for another thread to call
* {@code Object.notify()} or {@code Object.notifyAll()} on
* that object. A thread that has called {@code Thread.join()}
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
*
* - {@link #sleep Thread.sleep}
* - {@link Object#wait(long) Object.wait} with timeout
* - {@link #join(long) Thread.join} with timeout
* - {@link LockSupport#parkNanos LockSupport.parkNanos}
* - {@link LockSupport#parkUntil LockSupport.parkUntil}
*
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
初始状态,new一个实例出来,线程就进入了初始状态。
运行状态,这是一个复合状态,里面包括了就绪状态(READY)和运行中状态(RUNNING)。
就绪(READY):就绪状态只是说你资格运行,但还必须和其他线程竞争CPU的调度,才能真正执行。
start()
、当前线程sleep()
方法结束、调用当前线程的yield()
方法、其他线程join()
结束、等待用户输入完毕、锁池里的线程获得锁等都可以进入就绪状态。运行中(RUNNING):即被调度程序选中的READY状态的线程,会自动调用run()
方法。
阻塞状态,线程被锁阻塞。
等待状态,处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
网上对线程的状态分类有许多说法,其中一种说法就是将BLOCKED和WAITING总结到一起,都称为BLOCKED,不过一种是同步阻塞,一种是等待阻塞,这种说法其实也可以接受,能理解即可。
有许多种方法可以进入WAITING状态,比如wait()、sleep()、join()
等,这些方法还有很多值得细说的地方,有很多细节,下面会细讲,所以个人觉得WAITING也是最复杂的一个状态,很多方法都有相关。
这里还要注意一个点,上面讲BLOCKED状态的时候,讲到一点就是使用LOCK锁不会使线程进入BLOCKED状态,而是会进入WAITING状态,因为LOCK锁底层使用的使用的是一个LockSupport.park()
方法,这个方法是进入WAITING状态。
超时等待状态,处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
这个状态其实和WAITING状态差不多,只不过是多了一点,能设置线程等待的时间,超时会自动苏醒而已。
终止状态,当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生,在一个终止的线程上调用start()方法,会抛出异常。
这里有一个小问题?我们该如何关闭一个线程呢?或许你会去找线程的API,你会看到有一个方法stop()
,但很遗憾它是过期的,jdk不推荐我们使用它,具体原因下面讲解方法的时候会解释。
这里用一个例子来理解下以上这几个状态:
public class Test implements Runnable{
private Object o = new Object();
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o){
System.out.println("synchronized:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
Thread thread1 = new Thread(test,"A");
Thread thread2 = new Thread(test,"B");
thread1.start();
thread2.start();
}
}
看以上两个线程,首先线程被new出来处于NEW状态,然后调用start()
方法变成READY状态,然后线程1被调度变成RUNNING,线程2是READY状态。这里没有演示出两个线程互相争夺CPU调度,因为线程1是先start的,所以是线程1先获得调度,如果有同时多个线程start的话就会出现争抢调度的情况。
然后线程1执行run()
方法,遇到sleep()
变成WAITING,然后线程2变成RUNNING,执行run()
方法,遇到sleep()
变成WAITING。然后线程1醒来变成RUNNING,遇到同步代码块变成BLOCKED,然后线程2也醒来变成RUNNING,遇到同步代码块变成BLOCKED。
它们一起争抢锁,最终获得锁的线程变成READY,再变成RUNNING,最后是TERMINATED,而未获得锁的线程就会一直处于BLOCKED状态。
我们都知道,线程中有许多方法,比如sleep()
、join()
、yield()
等,这些方法其实本质上就是切换线程的状态而已。
suspend()
、resume()
、stop()
对于这三个方法,大家可能会比较陌生,因为很少用,也因为它们是过期的。那它们有什么作用呢?通俗一点来说,它们分别对应我们播放视频中对应的暂停、恢复、停止的操作。
它们的功能看起来还行呀,挺人性化的,为什么会过期呢?主要原因是因为它们在切换线程状态的时候不能正常的释放资源,可能会引发一系列问题:
suspend()
、resume()
调用suspend()
方法后,线程不会释放已经占有的资源(比如锁),而是会占有资源进入别的线程状态,这样可能会导致死锁问题。
那这样的话,这两个方法已经过期了,假如我们要使线程暂停恢复的话要怎么做呢?答案就是使用等待唤醒机制来代替这两个方法实现这个功能。
stop()
调用stop)
方法后,会终结一个线程但不会保证线程资源的正常释放,通常是没有给予线程释放资源的工作机会,就把它给杀死了,这样会导致程序出现一些意想不到的错误。
同理,这个方法不能使用,那我们该如何正确的关闭线程呢?答案就是使用标志位来给线程一个标志,标志线程是否应该继续执行,具体做法有两种:一种是使用线程提供给我们的interrupt()
,这个下面会细说;另一种就是我们自己手动写一个修改标志位的关闭方法,这里讲一下后面那种,看代码:
public class Test{
public static void main(String[] args) throws InterruptedException {
Shutdown shutdown = new Shutdown();
new Thread(shutdown).start();
Thread.sleep(100);
shutdown.shutdown();
}
private static class Shutdown implements Runnable{
private long i;
private volatile boolean stop = false;
@Override
public void run() {
while (!stop){
i++;
}
System.out.println(i);
}
public void shutdown(){
stop = true;
}
}
}
这里设置了一个状态标志位stop,当stop是false的时候,线程就会一直运行while循环,我们也设置了一个shutdown()
方法,作用是把stop改为true,这样便可使线程退出while循环,从而关闭掉线程,这里要注意:线程不是马上就被关闭的,它还能执行while循环后面的一部分东西,所以假如我们要释放资源或者一些必须的操作就可以放在while循环下面,且状态标记量要用volatile修饰!!!
interrupt()
、interrupted()
、isinterrupted()
承接上文,我们说我们要中断线程会设置一个状态标记量,线程其实就已经帮我们维护好了一个标记状态量了,我们可以使用这三个方法来调整和判断它的标记状态量,从而来中断线程,但这三个方法其实也挺复杂的,有很多细节需要我们注意。
先来讲下这三个方法分别有什么作用:
interrupt()
:其作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。interrupted()
:注意这是Thread类的静态方法,用法和sleep()
相同,作用是测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false,除非第一次调用这个方法后,在第二次调用之前又再次中断了线程一次,这样就会第二次调用就会返回true。isinterrupted()
:作用是只测试此线程是否被中断 ,不清除中断状态。或许你会很奇怪,用标志量来中断线程,为什么还要有我们手动设置一个或者使用 interrupt()
两种方式?只用其中一种不香吗?答案只有一个,它们其实是有所不同的。
来假设一种场景,假如一个线程在运行,然后运行一半他去睡觉了(sleep)或者去等待队列了(wait),这时要中断他,就会有两种处理结果:要么睡完再断,要么马上断别睡了。这里两种处理结果就分别对应我们的两种中断方式,很明显手动设置状态量是前者,interrupt()
是后者。
是的,当一个线程处于等待状态(这里是指wait、sleep、join方法,其他方法或许也可以,但没试过不敢妄下结论)时,是能被interrupt()
打断的。下面看下细节:
sleep()
线程A正在使用sleep()暂停着,此时外面调用(比如主方法)threadA.interrupt()
打断了线程A,线程A就会从等待状态中退出,然后直接抛出异常,且此时状态标记量依旧是false,即使它调用了interrupt()
方法。
wait()
线程A调用了wait()进入了等待状态,也可以用interrupt()取消。不过这时候要注意锁定的问题。当对等待中的线程调用interrupt()时,会先重新获取锁后,再抛出异常。在获取锁定之前,是无法抛出异常的,标记量也依旧是false。
join()
当线程以join()等待其他线程结束时,当它被调用interrupt(),它与sleep()时一样,会马上跳到catch块里.。标记量也依旧是false。
还有一个小细节:中断一个“已终止的线程”不会产生任何操作。结合以上特点,因为线程在等待状态被打断会抛出异常,所以中断线程的方法就和我们手动设置标记量有一丢丢不同,看一下代码:
public class Test{
public static void main(String[] args) throws InterruptedException {
RunTest rt = new RunTest();
Thread t = new Thread(rt);
t.start();
TimeUnit.SECONDS.sleep(2);
t.interrupt();
}
}
//不同的run方法设计
class RunTest implements Runnable{
//适用于线程没有进入等待状态的情况
public void run1(){
//接收到中断信号时,由于while循环判断不成立退出
while(!Thread.interrupted()){
...
}
System.out.println("正常退出");
}
//错误方法
public void run2(){
double d = 1;
//接收到中断信号时,不会中断正在运行的操作,只有当操作完成后,检查中断状态时会退出
while(!Thread.interrupted()){
while(d<3000){
d = d + (Math.PI+Math.E)/d;
}
}
System.out.println("Exit "+d);
}
//最优写法,即使线程会进入等待状态也可以解决
public void run3(){
try{
while(!Thread.interrupted()){
Thread.sleep(100);
//接收到中断信号时,由于while循环判断不成立退出,不抛出异常
}
System.out.println("正常退出");
}catch(Exception e){
System.out.println("被打断");
}
}
//此种设计不好,try-catch要在while外
public void run4(){
while(!Thread.interrupted()){
try{
TimeUnit.SECONDS.sleep(1);
//接收到中断信号,捕获异常并清除中断状态,所以不退出,所以这种不是良好的设计方式,如果想要退出,需要在catch语句中
}catch(Exception e){
System.out.println("被打断");
}
}
System.out.println("正常退出");
}
}
yield()
礼让方法,这个方法比较简单,就是将一个正处于运行中的线程给切换成就绪状态,让出当前CPU执行权。
但要注意一点:就是不一定礼让成功,也就是说不一定线程调用yield()
就一定能成功让出CPU执行权,有可能在再次的竞争CPU执行权的时候这个线程又再次获胜。yield()
方法只是给了其他线程一个竞争的机会。
sleep()
大家都很熟悉的一个方法了,作用大家应该都很清楚,但有一个细节需要注意:这是一个静态方法,作用是让当前线程沉睡,注意是当前线!!!
接下来就是这个方法的核心:使线程进入超时等待状态,sleep方法只会释放CPU使用权,不会释放同步锁,也不会进入等待队列,醒来的时候进入就绪状态。
wait()
、notify()
这两个方法也是老朋友了,承接上文我们说到的suspend()
、resume()
,我们说他们已经过期,但是可以用等待唤醒机制来代替它们,这里的等待唤醒机制就是由wait()
、notify()
两个方法组成的,下面先讲下这两个方法具体作用。
wait()
、wait(long mills)
等待方法,将线程从运行状态转为等待状态或者超时等待状态,将线程置于等待队列中。且线程将释放同步锁,线程只有被通知唤醒或则被中断才能醒来。即释放CPU执行权,也释放同步锁。
notify()
、notifyAll()
唤醒方法,网上很多说法是说随机唤醒,前面也已经讲过了,这里的“随机”是指根据不同的JDK版本可能会有不同的唤醒顺序,在JDK1.8,唤醒方法就是唤醒等待队列头的线程。notifyAll()
就不必说了,唤醒所有线程。
这里还要注意被notify()
或者notifyAll()
唤醒的线程,都会从wait()
方法后开始执行,而不是重新执行,且执行的前提是要获得锁,被唤醒只是代表从等待队列进入了锁池队列。
接下来讲一下这两个方法共有的两个特点:
//必须要这样写
Object o = new Object();
synchronized (o){
o.wait();
}
//这样会报异常
Object o = new Object();
o.wait();
o1.wait()
,是无法用o2.notify()
来唤醒的。最后来讲一下,这两个方法的实际运用:等待唤醒机制,也叫消费者生产者模式。
等待唤醒机制(生产者消费者模式)
先说下运用场景:有两个线程,其中线程1在工作前需要一些准备工作,只有准备工作做好才允许它运行,而线程2的工作就是来完成线程1的准备工作,于是就形成了以下这种机制:
有两个角色生产者、消费者分别对应线程2、线程1,消费者消费之前要等生产者生成东西,这两个角色要分别满足以下原则:
wait()
,被通知后依然要检查条件(while循环)synchronized(对象){
while(条件不满足){
对象.wait();
}
doSomething(); //执行消费者逻辑
}
synchronized(对象){
doSomething(); //改变条件
对象.notify();
}
join()
、join(long mills)
个人认为,这个是最难理解的方法了。它的作用就是插队!!!首先我们先来看下它的运用场景:两个线程AB,都调用start()
方法了,这一点很重要,线程要想使用join()
就必须已经启动了。然后现在是线程A在执行,然后线程B想插队的话,只需要在线程A中使用线程B调用join()
就可以完成插队。
看下代码,来理解下:
public static void main(String[] args) throws Exception {
System.out.println("start");
Thread t = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
t.join();
System.out.println("end");
}
结果输出:
start
0
1
2
3
4
end
线程t开始后,接着加入t.join()方法,t线程里面程序在主线程end输出之前全部执行完了,说明t.join()阻塞了主线程直到t线程执行完毕。如果没有t.join(),end可能会在0~5之间输出。
OK,那join()
方法的原理以及调用join()
方法后线程的状态又发生了什么样的变化呢?先看下join()
方法源码:
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();//获取当前时间
long now = 0;
if (millis < 0) {
//判断不说了
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
//这个分支是无限期等待直到b线程结束
while (isAlive()) {
wait(0);
}
} else {
//这个分支是等待固定时间,如果b没结束,那么就不等待了。。。
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
首先我们要注意到join()
方法是用synchronized修饰的一个同步方法,且我们看它的源码,除去一些判断,join()
方法其实就是等待唤醒机制。我们其实可以把代码看成以下这样,比较容易理解:
public class Test{
public static void main(String[] args) throws Exception {
System.out.println("start");
Thread t = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
synchronized (t){
while (t.isAlive()){
t.wait();
}
}
System.out.println("end");
}
}
使用上面那个例子来讲解一下,首先在主方法中开启了t线程,然后调用了t.join()
,我们由源码已经知道join()
方法是一个同步方法,那同步方法是用的哪个锁?this对象咯,那这里t.join()
使用的this对象是谁?没错就是线程t这个对象,也就是说主线程获得了t线程这个对象的锁。
while (isAlive()) {
wait(0);
}
再看源码,join()
方法里那个循环,只要t线程活着,持有t线程对象锁的线程就要去等待,那这里说的持有t线程对象锁的线程是谁呢?没错正是main线程,所以main线程进入了等待状态。
然后由于主线程进入了等待状态,于是它让出了CPU执行权,由于前面t线程已经开启了,所以这个时候就会执行t线程,这也是为什么线程的start()
要写在join()
前面的原因。
这个时候你可能会问了,咦那又是怎么唤醒主线程的呢?t线程里面也没写唤醒主线程呀,怎么t线程执行完了主线程就醒了呢?这就是一个藏得很深的点了,这个是JVM底层的实现,一个线程结束的时候,就会自动调用一下notifyAll()
的方法,所以这样t线程执行完,main线程也就醒了过来了。
讲到这里应该就很清楚了吧,总结一下:假如有线程为A, join线程为B。 A不会释放已经持有的对象obj1、obj2…的对象锁。在同步代码块发生join的时候线程A请求获得线程B的对象锁,获得后再通过wait方法释放A持有的B锁,同时令A进入B的等待队列。最后线程B执行完毕,自动调用唤醒方法唤醒线程A。
其实就相当于线程A获得了obj1的锁,然后进入同步代码块,在里面执行了B.join()
,然后又获得了一个锁,这个锁是B,然后调用了B.wait()
进入了B的等待队列里,等待被唤醒。
还有一个小细节就是,线程A里面调用了线程B后,它是进入了等待状态,但是线程B执行完毕以后,它一般都会直接回到就绪状态,而不是阻塞状态,因为线程A所存在的等待队列的锁是线程B的,一般这个锁只给了线程A,也就是说一般只有线程A会获得线程B对象锁,所以它无需竞争直接进入就绪状态。