多线程2(Thread)
线程的状态
1.NEW:线程创建出来了,但是还是没有被start。
创建好Thread后,先使用getState方法查看状态,再start,看到的就是NEW。
2.TERMINATED :线程执行结束,入口方法执行结束,但是Thread对象还在。
线程结束后调用getstate,观察到的线程状态就是terminated。
3.RUNNABLE :线程执行过程中一直是此状态。
在线程start和join之间创建就可以看到RUNNABLE状态。
4.WAITING,TAME_WAITING:这两个状态都是阻塞状态,WAITING是无时间限制的阻塞状态;
TAME_WAITING是有时间限制的阻塞状态。
我们可以通过jconsole观察。
5.BLOCKED : 通过“加锁”产生的阻塞。
线程安全问题(重点)
通俗的讲就是,一段代码在单线程情况下运行没事,但是在多线程下就会产生BUG。
出现线程安全的原因
1.(根本原因)是操作系统随机调度的问题(抢占式执行),
2.多个线程在修改同一个变量时,就会产生线程安全问题(多个线程读同一个变量没事),例如:
产生这样的原因就涉及到“原子性问题”,像count++这样的操作,站在CPU的角度上来看,就是三条指令,当单线程执行时,CUP就会将这三条指令按顺序一条一条执行,但是当多线程执行时,由于操作系统调度的随机性,可能在这一条线程执行到一半时,另一条线程将加工到一半的变量拿走,这样就会导致结果错误。
3.修改操作,不是原子的:
像事务中的原子性就是将多条sql语句打包成一个“原子”,在线程安全问题中,像count++这样的操作,就涉及到多条CPU指令,本质上CPU指令层面不是原子的,是会拆分成多个指令的。
但在Java中对内置类型和引用类型赋值(=)就是原子的。
4.内存可见性
解决线程安全问题(synchronized)
解决线程安全的方法是通过加锁实现,将要执行的代码打包成一个整体,从而达到原子性这样的效果。
加锁是通过synchronized这个关键字。
进入synchronized中就是加锁,离开synchronized(){}就是解锁,synchronized进行加锁需要一个锁对象,这个对象只能是引用类型,不能是内置类型。
synchronized关键字执行原理
synchronized并不是将count++中的三个指令都打包成一个指令,也不是在CPU上一口气全部执行完。
而是加锁的这个线程会影响到别的线程,而且是同一个线程。
例如:当一个线程在执行任务时,当另一个线程想要执行同一个任务时,此时就会触发阻塞,等到第一个线程执行完了再由第二个线程执行。
一个线程加锁而另一个线程不加锁就不会触发阻塞;两个线程同时加同一把锁才会产生锁竞争,有阻塞的效果。
synchronized常见用法
1.将一段代码块进行加锁
2.修饰一个普通方法,就可以省略锁对象。
这个加锁相当于对this加锁,和单独使用一个锁对象进行加锁没有区别。
3.修饰一个静态方法,这里是针对类对象进行加锁。
synchronized的可重入
如果针对同一把锁连续加两次。
上述代码当第一个synchronized执行后,开始执行第二个synchronized,而第二的所需的锁对象和第一个一样,而第一个synchronized还没执行完,此时就会在第二个synchronized处产生阻塞,而产生阻塞就会让第一个synchronized无法继续执行,这样就产生了死锁(deadlock)。
但是上述的代码在Java中不会触发死锁(并不是Java中没有死锁,死锁有很多种形式),是因为Java中有“可重入”这样的性质。
可重入锁:一个线程,针对同一把锁,连续加锁多次,不会触发死锁,这样的锁就叫可重入锁。
可重入锁只能解决死锁中的其中一种情况:一个线程两把锁。
死锁的几种情况
1.一个线程两把锁:在Java中引入了可重入这样的性质,所以这种情况在Java中不会触发死锁。
2.两个线程两把锁:
上述代码就产生了死锁,这是因为第一个线程和第二个线程都拿了各自的锁,当一方想要调用另一方的锁时,但双方的锁对象还没有释放,此时就会产生阻塞,因而产生死锁。
3.N个线程M个锁
如何避免死锁
构成死锁的四个必要条件:
1.锁是互斥的
2.锁不可被抢占:线程1拿到这个锁,线程2也要拿这把锁,此时线程2就会触发等待,而不是直接抢占。
3.请求和保持:拿到一把锁的情况下,不释放,再去请求第二把锁。
4.循环等待:等待关系,与其他线程等待关系构成循环。
解决方法:
1.打破请求和保持,将第一把锁释放,再去申请第二把锁。
2.通过规定使用锁的顺序,避免等待关系构成循环。
内存可见性引发的线程安全问题(volatile)
上述代码当线程2修改了flag的值后,线程1并没有检测到线程2的修改,这就是内存可见性引发的问题。
内存可见性问题其实是由编译器自身的优化产生,就以上面的例子:
编译器会从内存中读到寄存器中,再从寄存器中读取,但是编译器多次读到的数据都是一样的,而这样的开销比较大,所以编译器就不会再去内存中读取,而是在寄存器中读,这样我们修改的数据就无法被编译器读取到,自然产生问题。
大多数情况下,编译器优化可以做到“逻辑不变”,但是在特殊情况下会出现误判,导致逻辑发生变化,特别是“多线程代码”,这是因为线程调用是随机的。
此时就要使用Java中的一个关键字:volatile
当加上volatile之后,我们修改的值就会被编译器识别到。
当然如果要执行的任务比较大,编译器就不会进行例如上面的优化。
但是volatile只在内存可见性问题中起作用,在其他线程安全中不涉及,volatile和synchornized是两个不一样的维度。