一,线程状态
线程的状态是一个枚举类型 Thread.State
1.1 NEW
new:安排了工作,还未开始行动
即Thread对象创建了,还没start
public class Demo11 { public static void main(String[] args) { Thread t = new Thread(() -> { while(true){ System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); System.out.println(t.getState()); t.start(); } }运行结果为:
1.2 TERMINATED
terminated:工作完成了
Thread对象还在,但是系统中的线程已经销毁(入口方法已经执行完了)
public class Demo11 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 0; i <3; i++) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); System.out.println(t.getState()); t.start(); t.join(); System.out.println(t.getState()); } }执行结果为:
小tips:两次调用start()的时候,在第二次调用start的时候会抛出异常
即Thread对象得是NEW状态才可以调用start
1.3 RUNNABLE:就绪状态
runnable:可工作的,又可以分成正在工作中和即将开始工作
就绪状态:1)正在CPU上执行
2)随时可以调度到cpu上执行(当前没在cpu上)
public class Demo11 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 0; i <3; i++) { System.out.println("hello thread"); // try { // Thread.sleep(1000); // } catch (InterruptedException e) { // throw new RuntimeException(e); // } } }); System.out.println(t.getState()); t.start(); System.out.println(t.getState()); t.join(); System.out.println(t.getState()); } }1.4 阻塞状态
1.4.1 BLOCKED:排队等着其他事情
由于锁竞争引起的堵塞
1.4.2 TIMED_WAITING:排队等着其他事情
由于sleep引起的堵塞或者join(超时时间)引起的堵塞
即有固定期限的堵塞
public class Demo11 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 0; i <3; i++) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); System.out.println(t.getState()); t.start(); System.out.println(t.getState()); t.join(); System.out.println(t.getState()); } }运行结果显示为runnable而不是timed_waited是因为t.getState()在sleep休眠之前调用了,所有为了确保得到一个timed_waited的状态,可以在getState上加sleep(500)来确保当前的getState是在t线程sleep过程中调用的
public class Demo11 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { for (int i = 0; i <3; i++) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); System.out.println(t.getState()); t.start(); Thread.sleep(500); System.out.println(t.getState()); t.join(); System.out.println(t.getState()); } }1.4.3 WAITING:排队等着其他事情
join()死等引起的堵塞
即没有固定期限的堵塞
public class Demo15 { public static void main(String[] args) throws InterruptedException { Thread mainThread = Thread.currentThread(); Thread t = new Thread(() -> { try { mainThread.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } }); // 在 start 之前调用的 System.out.println(t.getState()); t.start(); // 此处加上 sleep(500) 更好的确保当前的 getState 是在 t 线程 sleep 过程中调用的 Thread.sleep(500); System.out.println(t.getState()); t.join(); System.out.println(t.getState()); } }在这个代码中main主线程和t线程相互等待对方,所以就卡死了
改为
mainThread.join(1000);
1.5 通过第三方工具查看线程的状态
jconsole
二,线程安全
1)某个代码,在单线程环境下执行,没有任何问题,但是如果在多线程环境下执行,就有bug
此时,这个代码就叫“线程不安全”或者叫做存在”线程安全问题“
2)如果这个代码,单线程下和多线程下,都没有任何问题,此时,就叫做”线程安全“
public class Demo12 { private static int count = 0; public static void main(String[] args) throws InterruptedException { // 创建两个线程, 针对同一个变量进行循环自增 Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } }); t1.start(); t2.start(); t1.join(); t2.join(); // 此处预期的结果是 10w System.out.println("count = " + count); } }执行这个代码可以发现,执行结果次次不一样而且和预期的结果相差较大
出现bug的原因如下:
从计算机如何工作的角度,即cpu执行指令的角度来看待问题
count++这个代码对应三个指令(1)load:把内存中的数据,加载到cpu寄存器里 (2) add:把cpu寄存器中的值+1 (3)save:把寄存器的值写回内存
线程的调度是随机的,一个线程在cpu上执行呢,随时可能被从cpu上调度走,所以当cpu执行这三步的时候,有可能执行到一半被调走(但是调度的时候,至少保证当前的“指令”执行完,不会一个指令执行到一半被调度走)
所以执行顺序有无数种,保证一个程序,线程安全,就是在每一种情况下都没有bug
其中几种情况如下:
以上结果是正常的
以上结果是不正常的
t1和t2是两个线程,是两套上下文(两套寄存器)彼此之间不会修改对方的寄存器
线程安全产生的原因:
1)【根本】线程调度是随机的,一个线程执行到任何一个指令都可能被从cpu上调度走
2)多个线程同时修改同一个变量
1)不是同时,一个线程先操作,一个线程后操作------没问题
2)多个线程同时读取这个变量--------没问题
联想String是不可变对象的意义有:1,方便放到常量池中缓存 2,方便被计算hash值 3,线程安全
所以有的编程语言,解决线程安全 并发编程问题就采用了不可变方案
3)多个线程同时修改不同变量------没问题
3)【直接原因】针对变量的修改操作不是原子的
4)内存可见性引起的线程安全问题
5)指令重排序引起的线程安全问题
解决方法:
- 通过锁,让一个变量只能被一个线程修改,这个过程中,其他线程只能等
注意:锁不是让线程停止调度,加锁的过程中,线程仍然是可以被调度出cpu的,而是只要你持有锁,别人就不能使用
在Java中使用锁:大部分的语言中,对于锁的操作,都是分为两个方法来进行的
eg:Locker locker;
locker.lock();
count++;
locker.unlock();
但是Java是通过synchronized关键字,搭配代码块的方式,实现的
eg:synchronized(锁对象){
count++;
}
- 在Java中,任何一个对象都可以作为锁对象,哪怕是一个String,HashMap,Thread........Object
- 锁对象的用途,只有一个,用来判定多个线程之间,是否存在“锁竞争”/“锁冲突”,和在{}里操作哪个对象的属性没有关系
- 锁竞争:两个线程针对同一个对象加锁,此时,一个线程能够先拿到锁,另一个线程就只能堵塞等待,等待锁被释放
- 如果两个线程,针对不同的对象加锁,就没有锁竞争,也就没有阻塞等待
在 synchronized(锁对象){
count++;
}
中,{ 相当于加锁,} 相当于解锁,即进入代码块===> 加锁,出了代码块===>解锁
public class Demo12 { private static int count = 0; public static void main(String[] args) throws InterruptedException { // 创建两个线程, 针对同一个变量进行循环自增 //定义一个锁对象 //可以是任意的对象 Object locker = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker){ count++; } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker){ count++; } } }); t1.start(); t2.start(); t1.join(); t2.join(); // 此处预期的结果是 10w System.out.println("count = " + count); } }此时运行结果正确
流程过程图如下:
但是不是写了synchronized就一定线程安全,得看是否正确使用
线程安全的核心是通过锁竞争,把count++的并发 => 串行了 == 相当于把count++作为原子操作(其他线程不能插队)
1)错误使用1:使用不同的锁对象导致没有锁竞争
Object locker = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker){ count++; } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker2){ count++; } } });2)错误使用2:一方加锁,另一方不加锁
Object locker = new Object(); //Object locker2 = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker){ count++; } } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } });三,synchronized关键字 - 监视器锁monitor lock
synchronized(锁对象){
count++;
}
设计成代码块的方式,有效防止unlock忘记调用的问题
在synchronized中只要脱离了代码块,锁就能被释放,无论是return还是throw异常
3.1 synchronized使用方法的变种
1,最常规的用法
synchronized(锁对象){
count++;
}
2,修饰方法
class Counter{ private int count = 0; private Object locker = new Object(); public void add(){ synchronized (locker){ count++; } } public int getCount(){ return count; } } public class Demo13 { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(() -> { for(int i = 0;i<50000;i++){ counter.add(); } }); Thread t2 = new Thread(() -> { for(int i = 0;i<50000;i++){ counter.add(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.getCount()); } }上述代码运行结果是正确的,但是如果将locker从成员变量改成局部变量则不行
如:
两个线程对象针对同一个对象加锁才能够产生阻塞
但是当locker变成局部变量时,每调用一次方法就会生成一个新的对象
也可以使用this即使用counter来作为锁对象,谁调用的add方法,谁就作为锁对象
public void add(){ //Object locker = new Object(); synchronized (this){ count++; } }还有一个等价的写法为:
synchronized public void add(){ count++; }synchronized修饰一个方法,相当于进入方法就针对this加锁,出了方法就解锁
但是这个对于修饰静态方法来说,没有this,所以是针对类对象加锁
synchronized public static void doWork() { synchronized (Counter.class) { } }3.2 synchronized的特性
synchronized用的锁是存在Java对象头里的
对象头是即图中的红色部分
Object obj = new Object();
1)可重入
synchronized是一个可重入锁
public class Demo14 { public static void main(String[] args) { Object locker = new Object(); Thread t1 = new Thread(() -> { synchronized (locker) { synchronized (locker) { System.out.println("t1"); } } }); t1.start(); } }在这个代码中,locker对象初始情况下是空闲状态,就可以加锁成功
再次尝试针对locker加锁,此时locker是锁定状态,此时加锁操作,就要阻塞等待,阻塞到第一次加锁释放为止
但是要想释放第一个锁就要走到右括号,但是要走到右括号就要经过第二个锁的逻辑,但是第二个锁要想接触阻塞,还得等走到右括号========>死锁
但是程序执行结果是正常的,这是因为synchronized是可重入锁,意味着一个线程,针对一把锁,加锁两次或者多次,不会产生死锁
可重入锁的判定逻辑:
记录哪个线程持有了这把锁,当收到加锁请求的时候判定加锁的线程是不是已经持有锁的线程了,如果是,无事发生(不会阻塞),如果不是,产生锁竞争,产生阻塞了
public void func1() { synchronized (this) { synchronized (this) { synchronized (this) { synchronized (this) { // .... } } } } }- 对于上述这个代码,只有第一个synchronized是真正加锁,其他的synchronized会判定是否是同一个线程,如果是则无事发生(没有真加锁)
- 最外层才可以真正解锁
- Q:jvm怎么知道在哪一层真正解锁?
- A:jvm可以给锁维护一个计数器,每次进入{ 计数器+1,每次到达 } 计数器-1,计数器减到0的时候,就可以真正解锁了
可重入锁,不可以解决所有的死锁情况
死锁的场景:
1)一个线程,一把锁,对锁加锁两次 => 可重入锁可以解决
2)两个线程,两把锁,线程1对锁A 加锁,同时线程2对锁B加锁,线程1再对B加锁,线程2再对A加锁 -------可重入锁不可以解决
public class Demo14 { public static void main(String[] args) { Object lockerA = new Object(); // 1 Object lockerB = new Object(); // 2 Thread t1 = new Thread(() -> { synchronized (lockerA) { try { // 加 sleep 是为了确保对方线程已经把锁拿到了. Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (lockerB) { System.out.println("t1"); } } }); Thread t2 = new Thread(() -> { synchronized (lockerB) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (lockerA) { System.out.println("t2"); } } }); t1.start(); t2.start(); }通过jconsole观察可得:
状态为blocked
3)M个线程,N把锁
(哲学家就餐问题)
日常开发的过程中,涉及多个线程,多个锁的时候,就很容易产生死锁
死锁是很严重的问题:
1,一旦出现死锁,程序就会有严重bug
2,死锁问题,是典型的“概率性问题”
(重要)死锁的四个必要条件:
1)锁是“互斥”的
synchronized是互斥的,不代表所有的锁都是互斥的
2)锁不可被抢占
线程1获取锁A,线程2也获取锁B,此时线程2阻塞等待,一直等到线程1释放锁,线程2不能强行把锁抢夺过来
对于synchronized来说也具备这个条件
小tips:互斥和不可抢占的区分为:互斥是线程1拿到锁,线程2也尝试获取锁,就会堵塞,而不可抢占是线程1拿到锁,线程2拿不到锁,不一定堵塞,可以堵塞也可以放弃,总之不可以把锁抢占
3)保持再请求(请求和保持)
一个线程,获取到锁A,在持有锁A的情况下,再去获取锁B
由此可得,如果通篇只有一把锁,是在可重入的情况下,就不会死锁,即使有多把锁,如果不去嵌套使用,也不会死锁
4)循环等待/环路等待
即使你有多个锁,请求保持的方式进行使用了,得是多个线程的等待锁的顺序出现循环了,才会产生死锁
如果不是循环等待,就不会死锁
由此可得:只要约定好,多个线程按照固定的顺序加锁,就可以避免循环等待,比如,对锁编号,统一按照从小到大/从大到小,此时,就可以避免循环等待了