赞
踩
回顾
Ⅰ.可重入特性:同一个线程针对同一个对象,多次加锁(嵌套枷锁),解决可重入问题:
Ⅱ.死锁:由于不正确的加锁,导致程序中的某些线程被卡死了(严重bug)
Ⅲ.产生死锁的必要条件(缺一不可)
避免锁嵌套使用,如果确实需要嵌套使用锁,一定要约定好加锁的顺序
package thread; import java.util.Scanner; public class Demo23 { private static int count = 0; public static void main(String[] args) { Thread t1 = new Thread(()->{ while (count==0){ } System.out.println("t1执行结束"); }); Thread t2 =new Thread(()->{ Scanner scanner = new Scanner(System.in); System.out.println("请输入一个整数:"); count = scanner.nextInt(); }); t1.start(); t2.start(); } }
预期效果:t1首先会进入循环,用户输入非0整数,就会使t1线程退出循环,结束线程
但是t1实际上并没有真正出现退出的情况,上述问题产生的原因,就是内存可见性,上述代码,是一个线程写,一个线程读,下面通过站在指令的角度来理解:
while (count==0){
}
上述while循环涉及如下指令:
于是产生了内存可见性问题:
上述过程,确实是多线程产生的问题,但是另一方面,也是编译器优化/VM优化产生的问题,正常来说,优化操作,需要保证逻辑是等价的。但是很遗憾,编译器/JVM 在单线程代码中,优化是比较靠谱的,一旦程序引入多线程了编译器/JVM 判断也就不那么准确了
但是,如果上述代码中,循环体内存在IO操作或者阻塞操作(sleep),这就会使循环的执行速度大幅度降低,由于IO操作不能被优化掉(IO操作反复执行的结果是不相同的),而且IO操作的速度慢于load,于是此时就没有优化load的必要的
引入volatile关键字,给变量修饰上这个关键字之后,此时编译器就知道这个变量是“反复无常的”,不饿能按照上述策略进行优化了
package thread; import java.util.Scanner; public class Demo23 { private volatile static int count = 0; public static void main(String[] args) { Thread t1 = new Thread(() -> { while (count == 0) { } System.out.println("t1 执行结束"); }); Thread t2 = new Thread(() -> { Scanner scanner = new Scanner(System.in); System.out.println("请输入一个整数: "); count = scanner.nextInt(); }); t1.start(); t2.start(); } }
private volatile static int count = 0;
:告诉编译器,不要触发上述优化,具体在Java中是让javac生成字节码的时候产生了“内存屏障”(javac生成的特定的字节码)相关的指令,但是这个操作和之前synchronized保证的原子性没有任何关系
volatile是专门针对内存可见性的场景来解决问题的
类似的,上述内存可见性也可以使用synchronized在一定程度上解决,引入synchronized是因为加锁操作本身太重量了,相对于load来说,开销更大,编译器自然就不会对load优化了(类似前面讲到的加sleep/IO操作)
优化是javac和java配合完成的工作,统称为编译器优化
在Java中,编译器有且仅有javac,java是jvm(运行环境)
IDEA是IDE,是集成开发环境,涵盖代码编译器、依赖管理器、编译器、调试器、工程管理工具等
编译器什么时候优化,什么时候不优化,也是一个“玄学”问题
JMM:Java内存模型
JMM对上述问题的表述:当t1执行的时候,要从工作内存(CPU寄存器+缓存)中读取count的值,而不是从主内存中,后续t2修改count,也是会先修改工作内存,同步拷贝到主内存,但是由于t1没有重新读取主内存,最终导致t1没有感知到t2的修改
之前提到的join是等待线程结束,此处提到的等待通知,等待代码中会给我们进行显式的通知(不一定要结束),可以更加精细的控制线程之间的执行顺序了
系统内部,线程是抢占式执行,随即调度的,程序员也是有手段干预的,通过“等待”的方式,能够让线程一定程度的按照预期的顺序来执行,虽然无法主动让某个线程被调度,但是可以主动让某个线程等待(就给别的线程机会了)
如果某个线程频繁获取释放锁,由于获取的太快,以至于其他线程得不到CPU资源,这种问题称为线程饿死
系统中的线程调度是无序的,上述情况很可能出现,虽然不会像死锁那样卡死,但是可能会卡住一下,对于程序的效率是有影响的
例子
等待通知机制就能够解决上述问题
通过条件,判定当前逻辑是否能够执行,如果不能执行,就主动wait(主动进行阻塞),这样就把执行的机会让给别的线程了,避免该线程进行一些无意义的重试,等到后续条件时机成熟了(需要其他线程进行通知),再让阻塞的线程被唤醒
package thread;
public class Demo24 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("等待之前");
synchronized (object){
object.wait();
}
System.out.println("等待之后");
}
}
object.wait();
是Object类提供的方法,任何一个对象都有这个方法
此处wait也是会被Interrupt打断,wait和sleep一样,能够自动清空标志位
wait内部做的事情不仅仅是阻塞等待,还要解锁,准且来说,wait解锁的同时进行等待,相比之下sleep是阻塞等待但是和锁无关,所以线程得先加上锁,那么,wait必须放到synchronized内部使用
通过另一个线程,调用notify来唤醒阻塞的线程
package thread; import java.util.Scanner; public class Demo25 { public static void main(String[] args) { Object locker = new Object(); Thread t1 = new Thread(() ->{ synchronized (locker){ System.out.println("t1等待之前"); try { locker.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("t1等待之后"); } }); Thread t2 = new Thread(() ->{ Scanner scanner = new Scanner(System.in); synchronized (locker){ System.out.println("t2通知之前"); // 借助 scanner 控制阻塞. 用户输入之前, 都是阻塞状态. scanner.next(); locker.notify(); System.out.println("t2通知之后"); } }); t1.start(); t2.start(); } }
用户输入内容之后,此时就会使next接触阻塞,进一步的执行到notify,notify就会唤醒上述wait操作,从而使t1能够回到RUNNABLE状态,并且参与调度
倘若:t2先执行,有可能t2先执行了notify,此时t1还没有wait,那么locker上没有现成的wait,直接notify不会有任何效果(也不会抛出异常),但是后续t1进入wait之后,没有别的线程将其唤醒了
package thread; public class Demo26 { public static void main(String[] args) throws InterruptedException { Object locker = new Object(); Thread t1 = new Thread(() -> { synchronized (locker) { System.out.println("t1 等待之前"); try { locker.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("t1 等待之后"); } }); Thread t2 = new Thread(() -> { synchronized (locker) { System.out.println("t2 等待之前"); try { locker.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("t2 等待之后"); } }); Thread t3 = new Thread(() -> { synchronized (locker) { System.out.println("t3 通知之前"); locker.notify(); System.out.println("t3 通知之后"); } }); t1.start(); t2.start(); Thread.sleep(100); t3.start(); } }
notify只能唤醒多个等待线程中的一个,notify唤醒的线程是随机的
locker.wait(1000);
:超过1000ms还没有被notify,就自动唤醒
locker.notifyAll();
:唤醒所有的等待线程
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。