线程(三)
一线程状态线程的状态是一个枚举类型 Thread.State1.1 NEWnew安排了工作还未开始行动即Thread对象创建了还没startpublic 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 TERMINATEDterminated工作完成了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状态才可以调用start1.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上加sleep500来确保当前的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这个代码对应三个指令1load把内存中的数据加载到cpu寄存器里 2 add把cpu寄存器中的值1 3save把寄存器的值写回内存线程的调度是随机的一个线程在cpu上执行呢随时可能被从cpu上调度走所以当cpu执行这三步的时候有可能执行到一半被调走但是调度的时候至少保证当前的“指令”执行完不会一个指令执行到一半被调度走所以执行顺序有无数种保证一个程序线程安全就是在每一种情况下都没有bug其中几种情况如下以上结果是正常的以上结果是不正常的t1和t2是两个线程是两套上下文两套寄存器彼此之间不会修改对方的寄存器线程安全产生的原因1【根本】线程调度是随机的一个线程执行到任何一个指令都可能被从cpu上调度走2多个线程同时修改同一个变量1不是同时一个线程先操作一个线程后操作------没问题2多个线程同时读取这个变量--------没问题联想String是不可变对象的意义有1方便放到常量池中缓存 2方便被计算hash值 3线程安全所以有的编程语言解决线程安全 并发编程问题就采用了不可变方案3多个线程同时修改不同变量------没问题3【直接原因】针对变量的修改操作不是原子的4内存可见性引起的线程安全问题5指令重排序引起的线程安全问题解决方法通过锁让一个变量只能被一个线程修改这个过程中其他线程只能等注意锁不是让线程停止调度加锁的过程中线程仍然是可以被调度出cpu的而是只要你持有锁别人就不能使用在Java中使用锁大部分的语言中对于锁的操作都是分为两个方法来进行的egLocker locker;locker.lock();count;locker.unlock();但是Java是通过synchronized关键字搭配代码块的方式实现的egsynchronized锁对象{count}在Java中任何一个对象都可以作为锁对象哪怕是一个StringHashMapThread........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 locksynchronized锁对象{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;i50000;i){ counter.add(); } }); Thread t2 new Thread(() - { for(int i 0;i50000;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会判定是否是同一个线程如果是则无事发生没有真加锁最外层才可以真正解锁Qjvm怎么知道在哪一层真正解锁Ajvm可以给锁维护一个计数器每次进入{ 计数器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观察可得状态为blocked3M个线程N把锁哲学家就餐问题日常开发的过程中涉及多个线程多个锁的时候就很容易产生死锁死锁是很严重的问题1一旦出现死锁程序就会有严重bug2死锁问题是典型的“概率性问题”重要死锁的四个必要条件1锁是“互斥”的synchronized是互斥的不代表所有的锁都是互斥的2锁不可被抢占线程1获取锁A线程2也获取锁B此时线程2阻塞等待一直等到线程1释放锁线程2不能强行把锁抢夺过来对于synchronized来说也具备这个条件小tips互斥和不可抢占的区分为互斥是线程1拿到锁线程2也尝试获取锁就会堵塞而不可抢占是线程1拿到锁线程2拿不到锁不一定堵塞可以堵塞也可以放弃总之不可以把锁抢占3保持再请求请求和保持一个线程获取到锁A在持有锁A的情况下再去获取锁B由此可得如果通篇只有一把锁是在可重入的情况下就不会死锁即使有多把锁如果不去嵌套使用也不会死锁4循环等待/环路等待即使你有多个锁请求保持的方式进行使用了得是多个线程的等待锁的顺序出现循环了才会产生死锁如果不是循环等待就不会死锁由此可得只要约定好多个线程按照固定的顺序加锁就可以避免循环等待比如对锁编号统一按照从小到大/从大到小此时就可以避免循环等待了