多线程(3)
我们已经知道了如何创建一个线程启动一个线程中断一个线程接下来就谈谈如何等待一个线程1 等待一个线程join()多个线程之间并发执行随机调度。在程序员的角度不太喜欢随机的东西。join就能够要求多个线程之间结束的先后顺序。比如在主线程调用t.join()就是让主线程等待“t”结束。虽然可以通过sleep的休眠时间来控制线程结束的先后顺序但是这样的设定不太科学有的时候希望t先结束然后main紧跟着t就结束通过sleep就不太靠谱。代码示例运行结果这个运行结果是不一定的有时候可能main先结束有时候可能t先结束在相同的时间内基于上述问题我们使用join来决定哪个线程先结束1无参版本代码示例运行结果因为t.join()是在main线程中调用的所以main线程会等待t线程直到结束当执行到t.join()此时main线程就会“阻塞等待”一直等到t线程执行完毕,join才会继续执行只要t线程不结束join就会一直等待下去但是这样不好。举个生活中的例子你和你的好朋友约好上午9点一起出去逛街但是你的好朋友一直没有出现你就在哪里苦苦的等待浪费时间~~正确的做法应该是等到一定的时间你就不应该继续等了~所以Java提供了带参数的版本指定“超时时间”等待的最长时间2带参数的版本代码示例运行结果注意如果3000之内t刚过1000就结束了此时join就会立刻执行不会等满3000如果超过3000,t还没结束此时join也不会等了继续往下走纳秒这里就不考虑了因为计算机除了实时操作系统中很难进行ns级别的精确的时间测量而且线程本身调度开销往往就会达到ms级别2 休眠当前线程休眠当前线程sleep是我们比较熟悉的方法但有一点需要注意因为线程的调度是不可控的所以这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。原因代码调用了sleep相当于让当前线程让出CPU的资源后续时间到了的时候需要操作系统内核把这个线程重新调到CPU上才能继续执行时间到是允许被调度了而不是立刻执行sleeo(0)使用了sleep的特殊写法意味着让当前线程立即让出CPU资源等待重新被调度。如果发现一个模块CPU占用率比较高影响到了其他模块的正常执行就可以使用sleep操作3 线程状态我们知道进程状态主要有两个就绪和阻塞这是从操作系统的角度看待的。线程状态1New安排了工作还未开始行动new了Thread对象还未开始start代码示例运行结果2TERMINATED工作完成了内核中的线程结束了但是thread对象还在代码示例运行结果3RUNNABLE可工作的又可以分成在工作中和即将开始工作就绪1线程正在CPU上执行 2线程随时可以去CPU上执行代码示例运行结果本身是一段CPU指令循环执行也是需要在CPU上运行4TIMED_WAITING表示排队等待着其他事情线程处在RUNNABLE过程中因为一些原因比如sleep变为TIMED_WAITING又因为一些原因sleep时间到了变回RUNNABLE指定时间的阻塞线程阻塞不参与CPU调度不继续执行了阻塞的时间是有上限的代码示例运行结果另外join(时间)也能进入到TIMED_WAITING状态代码示例运行结果在jconsoler中就可以看到main线程的状态为TIMED_WAITING5WAITING表示排队等待其他事情是没有超时时间的阻塞等待比如没有参数的join和TIMED_WAITING相对代码示例运行结果在jconsoler中就可以看到main线程的状态为WAITING总结图6BLOCKED稍后介绍表示排队等待其他事情也是一种阻塞比较特殊由于锁导致的阻塞4 线程安全多线程带来的风险——线程安全重点如果不理解线程安全问题是很难写出正确的多线程代码的举个例子运行结果为什么呢原因线程是并发执行的调度是随机的我们这里面一共有3个线程看到0是因为main先执行打印了。要想得到结果我们可以使用join代码示例这里值得注意的是到底谁先join谁后join无所谓分两种情况1t1先结束t2后结束main先在t1.join阻塞等待t1结束main在t2.join阻塞等待t2结束main继续执行后续打印最终结果打印的是就是t1和t2都执行完的值2t2先结束t1后结束main先在t1.join阻塞等待t2结束t1继续阻塞t1结束t1.join继续执行main执行到t2.join因为t2已经结束了此处的t2.join是不会阻塞的所以main继续执行后续打印。以上两种情况总的阻塞时间是一样的区别在于是分两个jon各自阻塞一会还是在一个join阻塞完。运行结果:……你会发现每次运行结果都不一样且和预期结果不一样。很显然出现了bug多线程并发执行引起的如果把两个线程变为串行执行一个线程执行完之后再执行下一个。代码示例运行结果线程安全问题线程不安全由于多线程的并发执行的代码引起的bug线程安全一个代码在多线程的并发执行的环境下不会出现类似于上述的bug为什么呢站在CPU的角度上述代码的关键操作就是count这个操作看起来是一行代码其实对应到3个CPU指令1.load把内存中的值count变量读取到CPU的寄存器辅助存储中2.add把指定寄存器的值进行1的操作结果还是在这个寄存器中3.save把寄存器中的值写回到内存中操作系统对于线程的调度是随机的执行123这三条指令的时候很可能执行其中一部分该线程就被调走了可能123 可能1 调度走 调回来23 ……随机调度抢占式执行是线程安全问题的罪魁祸首调度顺序示例调度顺序不止下面这些还有很多很多存在无数种可能性针对以上四种情况我对第一张图片和最后一张图片进行解释其他情况原理都类似首先我们知道两个线程在CPU上执行的时候可能是并发执行在一个CPU上执行两个线程有不同的上下文也可能是并行执行在不同的CPU执行既然有两个线程我就画两个CPU第一张注意看时间轴谁先执行谁后执行初始t1线程load就把内存中的数据count0加载到CPU寄存器中t1线程add把指定寄存器中的值进行1操作t1线程的save把寄存器中的值返回给内存t2线程的load add save 操作和t1操作逻辑一样这里就不一一展示了结果我们发现按照这个次序调度执行结果是正确的我们再来看最后一张图片初始这个次序调度和第一张不一样所以我一步一步的解释一下注意看时间轴谁先执行谁后执行t1线程的loadt2线程的loadt2线程的addt1线程的addt1线程的savet2线程的save我们可以看到明明是两次的却只加了一次通过以上讨论可以感受到如果两个线程load到的数据都是0意味着一定会少加一次如果一个线程load到的数据是0一个load到的数据是1那么最后的结果才是正确的一个线程的load得在另一个线程的save之后上述代码的运行结果一定是100000那么有没有可能50000呢???答案是可能的但概率低举两个例子这里我就不在解释逻辑了直接在图片上显示1三次自增但结果是12四次自增但结果是1如果自增的次数少比如50次出现问题的概率就会变得很低了因为有可能在执行t2.start()之前t1就执行完了。原因线程安全问题产生的原因1根本操作系统对于线程的调度是随机的抢占式执行2多个线程同时修改同一个变量count没问题1一个线程修改一个变量2多个线程不是同时修改同一个变量3多个线程修改不同变量4多个线程读取同一个变量修改操作—写取值操作—读33修改操作不是原子的原子如果修改操作只是对应到一个CPU指令就可以认为是原子的因为CPU不会出现“一条指令执行一半”的情况不是原子的、--、、-……是原子的4内存可见性问题引起的线程不安全5指令重排序引起的线程不安全4和5后续再讨论解决知道原因了我们就可以解决问题了1对于操作系统的底层设定我们左右不了2对于2的原因我们可以调整代码结构规避一些线程不安全的代码的但是不通用3对于3的原因是Java中解决线程安全问题最主要的方案加锁通过加锁操作把不是原子的操作打包成原子的操作计算机中的锁和生活中的锁是同样的概念互斥/排他一旦把锁加上了其他人要想加锁就得阻塞等待计算机中不允许暴力拆锁注意加锁操作不是把线程锁死到CPU上禁止这个线程被调走而是禁止其他线程重新加这个锁避免其他线程的操作在当前的线程执行过程中插队加锁的语法在Java中使用synchronized这样的关键字搭配代码块来实现类似的效果synchronized(){ //进入代码块就相当于加锁//执行一些要保护的逻辑} //出了代码块就相当于解锁 这个里面填写的是用来加锁的对象要加锁、解锁前提是得有一个“锁” 在Java中任何一个对象都可以用作“锁”注意这个对象的类型是啥不重要重要的是是否有多个线程尝试针对同一个对象加锁代码示例两个线程针对同一个对象locker加锁才会产生互斥效果一个线程加锁另一个线程就得阻塞等待等到第一个线程解锁后才有机会注意如果针对的是不同的锁对象就不会产生互斥效果线程安全问题不会得到任何改变举个生活中的例子女生锁男生线程如果两个男生同时追同一个女生如果一个男生追上了加锁另一个男生就得阻塞等待等到他们俩分手才有机会如果是追不同的女生不同的锁对象就互不影响上面的加锁的代码只是针对count这个操作相当于只有count这个操作涉及到互斥count串行执行如果想使用currentThread必须确保两个线程针对的是同一个对象进行加锁因为两个线程分别使用currentThread获取的是不同的对象就不会产生互斥效果代码示例两个cur分别是不同的对象一个是t1另一个是t2不会产生互斥效果正确的代码但是这样看起来太别扭了不推荐这样的写法synchronized的变种写法可以使用synchronized修饰方法代码示例正常情况下是这样的进一步变形为再进一步变形为这样就相当于给this加锁值得注意的是还有一个特殊情况static修饰的方法不存在this此时synchronized修饰static方法相当于针对类对象进行加锁比如就相当于OK啦到此结束