线程(四)

线程(四)
一Java标准库中的线程安全类Java标准库很多都是线程不安全的这些类可能会涉及到多线程修改共享数据又没有任何加锁措施ArrayListLinkedListHashMapTreeMapHashSetTreeSetStringBuilder但是还是有一些线程安全的使用了一些锁机制来控制vector(不推荐-----------------------核心方法上直接加了synchronized但是不是加了 synchronized就线程安全也不是不加synchronizedHashTable(不推荐-----------------就一定线程不安全ConcurrentHashMapStringBuffer二内存可见性引起的线程安全问题public class Demo15 { private static boolean flag true; public static void main(String[] args) { Object locker new Object(); Thread t1 new Thread(() - { while (flag) { } System.out.println(t1 结束); }); Thread t2 new Thread(() - { // 让用户进行输入, 根据用户输入, 修改 flag System.out.println(请输入任意内容, 终止线程 t1); Scanner scanner new Scanner(System.in); scanner.next(); // 通过修改 flag 让 t1 线程结束. flag false; System.out.println(flag flag); }); t1.start(); t2.start(); } }运行这个代码后可以发现虽然进行了用户输入flag也变成了true但是线程1依旧没有结束这个代码也是有bug的有线程安全问题的t1线程z在读取t2线程在修改原因涉及编译器优化即编译器会自动对代码进行调整通过这样的调整就可以使得代码的执行效率变高Java不变但是编译生成的字节码和指令做出调整其中这个优化的大前提是确保调整前后的逻辑和调整之前是完全等价的大部分情况下编译器优化后的逻辑都和原始逻辑等价但是如果代码中包含多线程就不一定了可能会存在误判所以在以上的代码中的问题就是编译器优化在多线程的环境下产生“误判”导致优化后的逻辑不一样了问题产生的具体流程在这个语句中短时间内循环了很多次load和cmp1load是读取内存开销远远高于cmp操作1循环多次之后发现每次load到的结果都是一样的3编译器在当前线程中没有发现修改操作修改操作是用户输入对于计算机来说时间很久此时编译器优化就会把load操作优化成读取cpu寄存器/读取cpu的缓存不去真正读内存数据了而修改操作在另一个线程此时编译器都不知道这个修改到底能不能真的执行到-------t2线程把内存修改了但是t1线程感知不到这个内存flag的变化对t1线程“不可见”只要让while循环中load时间占比变低此时就不容易触发优化了------加sleeppublic class Demo15 { private static boolean flag true; public static void main(String[] args) { Object locker new Object(); Thread t1 new Thread(() - { while (flag) { try { Thread.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(t1 结束); }); Thread t2 new Thread(() - { // 让用户进行输入, 根据用户输入, 修改 flag System.out.println(请输入任意内容, 终止线程 t1); Scanner scanner new Scanner(System.in); scanner.next(); // 通过修改 flag 让 t1 线程结束. flag false; System.out.println(flag flag); }); t1.start(); t2.start(); } }运行这个代码之后t1正常结束了这说明当load时间占比变低了此时编译器就不会想着优化flag的读内存操作了核心矛盾在sleep而不是load了三volatile关键字volatile修饰的变量可以保证“内存可见性”给flag变量加上volatile修饰此时编译器就会对volatile的操作避免进行优化了程序员显示的告诉编译器这个地方不能优化小tips内存可见性问题和编译器优化直接相关由于编译器优化规则十分复杂某个操作啥时候优化不好判定稳妥起见应该在有风险的地方都加volatile比如发现某个变量在线程1中读取线程2中修改就把volatile加上虽然不知道是不是会优化但是加上万无一失常见的一些问题1JVM全称为Java Memory Model ------Java内存模型一个线程在工作的时候就会把需要访问的变量从“主内存”内存拷贝到“工作内存”cpu寄存器/缓存基于“工作内存”进行一些逻辑判定上述这样的场景相当于t1线程把数据从主内存拷贝到工作内存之后一直在判定工作内存t2线程修改的是t2的工作内存进一步的拷贝到主内存中导致t1访问的是自己的工作内存没法感知到主内存的变化四wait/notify等待/通知某个线程t1某个逻辑希望后执行另一个线程t2先执行就可以让t1 wait等待等到t2执行完一定逻辑之后notify通知t1让t1继续执行区别于joinjoin是等待t2执行结束而wait/notify不要求线程结束这两个方法属于Object类的方法所以任何一个对象都有wait和notify一般使用Objct就可以了public class Demo16 { public static void main(String[] args) throws InterruptedException { Object object new Object(); System.out.println(wait 之前); object.wait(); System.out.println(wait 之后); } }throws InterruptedException是因为wait会阻塞等待随时可能会被其他线程通过interrupt唤醒报错了报错信息中的monitor监视器对应synchronized监视器锁因为wait会释放锁而释放锁的前提是加锁但是在这个代码中并没有加锁所以一定是先拿到锁再谈释放public class Demo16 { public static void main(String[] args) throws InterruptedException { Object object new Object(); synchronized (object) { System.out.println(wait 之前); object.wait(); System.out.println(wait 之后); } } }加锁后执行正常了注意加锁的锁对象要和调用wait的对象是同一个在Java中notify也要搭配锁使用public class Demo17 { public static void main(String[] args) { Object locker new Object(); Thread t1 new Thread(() - { synchronized (locker) { System.out.println(t1 wait 之前); try { locker.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(t1 wait 之后); } }); Thread t2 new Thread(() - { // 让用户输入任意内容, 通知 t1 Scanner scanner new Scanner(System.in); System.out.println(请输入任意内容唤醒 t1); scanner.next(); synchronized (locker) { locker.notify(); } }); t1.start(); t2.start(); } }使用场景1可以用来控制线程内部逻辑的先后顺序2解决“线程饿死”问题当只释放锁不阻塞时可能会导致后面的线程一直迟迟的捞不着锁synchronized (locker) { System.out.println(t1 wait 之前); 执行wait会做三件事 try { 1释放锁 locker.wait(); 2阻塞等待 } catch (InterruptedException e) { 3当notify到达之后解除阻塞重新获取锁 throw new RuntimeException(e); 继续执行 } System.out.println(t1 wait 之后); }其实执行wait是两件事因为1和2是原子操作不可以拆分如果拆分了可能会产生bugnotify操作是没有副作用的即使没有线程wait也可以调用notify只不过没有任何作用也不会报错抛异常也可以有多个线程wait通过notify唤醒public class Demo18 { public static void main(String[] args) { Object locker new Object(); Thread t1 new Thread(() - { synchronized (locker) { System.out.println(t1 wait 之前); try { locker.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(t1 wait 之后); } }); Thread t2 new Thread(() - { synchronized (locker) { System.out.println(t2 wait 之前); try { locker.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(t2 wait 之后); } }); Thread t3 new Thread(() - { synchronized (locker) { System.out.println(t3 wait 之前); try { locker.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(t3 wait 之后); } }); Thread t4 new Thread(() - { Scanner scanner new Scanner(System.in); System.out.println(请输入任意内容, 唤醒线程); scanner.next(); synchronized (locker) { // 当有多个线程等待时, notify 是唤醒随机的一个. // 此处的随机不是数学上的 概率均等, 而是调度器有自己的规则, 我们感知不到, 干预不了~~ locker.notify(); } }); t1.start(); t2.start(); t3.start(); t4.start(); } }notifyAll唤醒所有线程带有超时时间的wait如果为locker.wait10000约定了等待的上限为10000mswait的死等模式----------线程状态WAITINGwait带有超时时间的模式-----------线程状态TIMED_WAITINGwait和sleep的区别1wait提供两个版本一个死等一个带有超时时间sleep只有带时间版本2wait可以通过notify唤醒也可以通过interrupt提前终止sleep只能通过interrupt提前终止

最新新闻

日新闻

周新闻

月新闻