上一章我们说了多线程编程所带来的好处( java并发编程 ||Thread生命周期详解),但是既然有那么多好处,肯定也会带来一些问题,这一章我们就来看看它带来的问题以及解决的办法。
多线程所带来的问题?
线程不安全
1.首先我们举一个例子来证明线程的不安全
我们对一个数自增1000次,并且用多线程来实现。
- /**
- * @Author Dark traveler
- * @Note 我心净处,何处不是西天。
- * @Descrption
- * @E-Mail : 1029149772@qq.com
- * @Date : Created in 9:02 2020-3-18
- */
- public class NoSafeThread {
- private static int sum = 0;
-
- public static void incr(){
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- sum++;
- }
-
- public static void main(String[] args) {
- for(int i=0,len=1000;i<len;i++){
- new Thread(()->{
- incr();
- }).start();
- }
- //确保所有程序都运行完成了
- TimeUnit.SECONDS.sleep(2);
- System.out.println(sum);
- }
- }
我们会发现,我们一直得不到我们想要的答案1000,始终是一个小于1000的随机值,但是从逻辑上来说,分明应该是1000才对啊?
这是为什么呢,对,这就代表了线程是不安全的,我们现在new了1000个线程来调用这个方法,很多线程可能会同时进入这个方法,也就意味着可能会有两个线程同时拿到sum = 0的初始值,然后同时进入方法incr(),并且线程1自增以后把sum赋值为1,但是线程2此时并没有拿到这个1,因为它们两个都进入了这个incr()方法,所以线程2又把0自增成为1,并且把sum再次赋值为1,这样就成为了1次重复操作,当很多次这种情况出现的时候,就出现了上面那种情况。
2.怎么解决线程不安全
既然出现了线程不安全,会让n个线程同时进入一个方法,那我们只要想办法同时只有一个线程进入这个方法,那就没问题了吧。所以java就引入了锁的概念,我们来看看怎么用锁把上面那个答案变回1000。
- /**
- * @Author Dark traveler
- * @Note 我心净处,何处不是西天。
- * @Descrption
- * @E-Mail : 1029149772@qq.com
- * @Date : Created in 9:02 2020-3-18
- */
- public class NoSafeThread {
- private static int sum = 0;
-
- private static Object lock = new Object();
-
- public synchronized static void incr(){
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- //锁定代码块
- /* synchronized (lock){
- sum++;
- }*/
- sum++;
- }
-
- public static void main(String[] args) throws InterruptedException {
- for(int i=0,len=1000;i<len;i++){
- new Thread(()->{
- incr();
- }).start();
- }
- //确保上面线程都运行完毕了
- TimeUnit.SECONDS.sleep(2);
- System.out.println(sum);
- }
- }
我们使用了关键字synchronized来完成对方法的上锁,使得每一时刻只有一个线程进入这个方法,这样就能确保线程安全了,那我们来看看synchronized关键字的用法。
3.synchronized关键字(重量级锁)
synchronized的三种使用方法,代码如下
*修饰实例方法:demo()
*修饰静态方法:demo3()
*修饰代码块:demo2()
- /**
- * @Author Dark traveler
- * @Note 我心净处,何处不是西天。
- * @Descrption
- * @E-Mail : 1029149772@qq.com
- * @Date : Created in 11:03 2020-3-18
- */
- public class LockThread {
- //2种表现形式,放在方法层面和代码块层面来控制
- //2种作用范围,对象锁还是类锁 区别:是否跨对象跨线程被保护
-
- //修饰方法层面,对象锁
- public synchronized void demo(){
-
- }
- //也是对象锁,比较灵活
- public void demo2(){
- //todo
- synchronized (this){
- //保护存在线程安全的变量
- }
- }
-
- //加了static,全局锁
- public synchronized static void demo3(){
-
- }
- //也是全局锁
- public void demo4(){
- synchronized (LockThread.class){
-
- }
- }
-
- public static void main(String[] args) {
- LockThread lockThread1 = new LockThread();
- LockThread lockThread2 = new LockThread();
-
- //不是同一个对象,不存在互斥特性,不排队
- new Thread(()->lockThread1.demo()).start();
- new Thread(()->lockThread2.demo()).start();
-
- //存在互斥性,要排队
- new Thread(()->lockThread1.demo3()).start();
- new Thread(()->lockThread2.demo3()).start();
- }
- }
总结:
1.锁的共享性和互斥性,只有共享项相同的锁才会有互斥作用。
2.锁的两种表现形式,锁整个方法和锁相应的代码块。
3.锁的两种作用范围,锁单个对象实例和锁整个类。
synchronized为什么能起到锁的作用?锁在内存中是怎么存储的。
我们启用synchronized关键字的时候,会去取得这个锁的对象,我们先来看看一个对象在内存中的内存布局。
查看hotspot源码,我们可以看到对象头中具体放了哪些东西。
所以我们发现,锁其实都是放到对象头中的,如果我们使用 synchronized关键字的时候,它会去取得这个对象的头中的锁信息,通过这个锁中的信息来判断是否加锁。
4. 1.6jdk以后锁的升级
虽然使用synchronized关键字可以保证线程的安全,但是降低了效率,那既想保证线程安全,又想保证性能怎么办呢?所以jdk1.6以后,想到了一种锁升级的情况,只有到重量级锁的时候才阻塞,之前的锁都不阻塞。
无锁 -》 偏向锁 -》轻量级锁 -》重量级锁
首先来说下锁的升级流程,假设有两个线程ThreadA/ThreadB访问同步代码块
1.只有ThreadA去访问(大部分情况属于这种)-》引入偏向锁标记(ThreadA的ThreadId,偏向锁的标志)
2.ThreadA和ThreadB交替访问-》轻量级锁(自旋锁)
3.多个线程同时访问-》申请重量级锁,阻塞
无锁-》偏向锁:
当线程A去访问同步代码块的时候,先通过CAS来比较,实现原子性,检查对象头中是否存储了线程1,如果没有存储就通过cas来替换,把线程A的id加入锁对象头中,并且加上偏向锁的标志,获得偏向锁布局就变成下图。
注:
CAS 乐观锁,compare and swap(value,expect,update),就相当于判断下当前的值是不是最新的。我们通过下面的图来简单说明下cas的作用原理,假设我们当前内存中有个值为 int i =0,当下面的CAS开始时,先读取当前的值E = 0,然后计算这个值(比如计算过程为++),那么计算值 V = 1,接下来并不是直接赋值,而是再比较一次当前值E和现在内存中这个 i的值,现在叫N,是不是相等,如果是相等,就把计算值更新为V,CAS结束。
当然上面是最完美的状态,在这个比较当前值E和内存中的最新值N的时候,也会出现不相等的状态,那么当前CAS就会失败,不会去更新新值。
除了失败以外,还有一种可能,就是有其它线程对当前值进行了操作,比如另一个线程把这个值从0改成1,又从1改成了0,其它线程是操作过这个值的,但是在CAS中的比较却是能成立的,这就是CAS中的ABA问题,那么这种问题怎么解决呢,或者说怎么感知到呢,那就是加一个标志,可以是版本号,也可以是一个布尔值,当读取这个内存中的i的时候,把它的版本号一起取过来,只有有线程对它进行过操作,就升级一下版本号,这样就能解决ABA问题了。
偏向锁-》轻量级锁(自旋锁):
此时当线程B来访问同步代码块,它也会通过CAS来比较对象头中的锁,线程A和B的id当然不相同,所以这个比较一定会失败,然后它就会去把线程A暂停,并且把它的偏向锁给撤销,把这个锁对象的锁标志和线程id给情况,这里又会分为两种情况,如果线程A已经做完了同步代码块中的指令,A就直接把锁对象释放,由线程B来把线程id放进锁对象,访问同步代码块;如果线程A并没有做完同步代码块中的指令,那么就会升级成轻量级锁,然后两者以轻量级锁的方式来竞争这个锁对象。
轻量级锁-》重量级锁
当升级成轻量级锁以后,线程B会启动自旋,就是在一个循环中反复通过cas来获取锁。(因为绝大部分线程在获得锁以后会在非常短的时间内释放锁,所以这种情况阻塞损耗性能不值得)因为自旋也会占用cpu资源,所以在自旋到指定次数以后,还没有获得轻量级锁,锁就会膨胀成重量级锁,然后直接阻塞。
所以自旋就有两种形式:
1.设置自旋次数 preBliockSpin,jvm设置
2.自适应自旋,推荐,根据线程获得锁的自旋次数来调整,虚拟机知道你上一次自旋锁是成功的,那么它会觉得你下一次也很可能成功,所以它会自动调整自旋锁的次数。
注:
自旋锁,一个循环
for(;;){
if(cas){
return ;//获得锁成功
}
}
重量级锁
升级到重量级锁以后,没有获取到锁的线程会被阻塞,blocked状态,我们来看看升级到重量级锁做了什么。
使用重量级锁后,我们发现对象里面有一个配对监视器,3和5,表示拿到锁和释放锁,11是异常也释放锁,这是一个对象监视器,ObjectMonitor。
看hotspot,我们会发现每个对象都存在一个ObjectMonitor,这也是重量级锁的核心。
monitor -》MutexLock,把用户态改变成内核态,基于操作系统底层来实现互斥,所以这也是比较耗费性能的原因。
也就是说重量级锁后,一个线程拿到对象监视器,成功后调用指令monitorenter,然后操作同步代码块,其它线程就被放到一个同步队列中,直到完成后调用指令monitorexit来唤醒同步队列中的一个。
总结:
偏向锁-》轻量级锁:一个是通过cas,原子替换;一个是自旋来不断尝试,不会阻塞线程,所以性能高。
重量级锁:通过阻塞来加锁,性能较低。
synchroized关键字把这些都封装到了jvm中,所以越简单的使用,底层的实现其实越复杂。
5、wait、notify、notifyall
线程的通信机制
我们用个代码演示一下:
- /**
- * @Author Dark traveler
- * @Note 我心净处,何处不是西天。
- * @Descrption
- * @E-Mail : 1029149772@qq.com
- * @Date : Created in 15:22 2020-3-18
- */
- public class ThreadA extends Thread{
-
- private Object lock;
-
- public ThreadA(Object lock){
- this.lock = lock;
- }
-
- @Override
- public void run() {
- synchronized (lock){
- System.out.println("start ThreadA");
- try {
- lock.wait();//实现线程的阻塞
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("end ThreadA");
- }
- }
- }
-
- /**
- * @Author Dark traveler
- * @Note 我心净处,何处不是西天。
- * @Descrption
- * @E-Mail : 1029149772@qq.com
- * @Date : Created in 15:22 2020-3-18
- */
- public class ThreadB extends Thread{
-
- private Object lock;
-
- public ThreadB(Object lock){
- this.lock = lock;
- }
-
- @Override
- public void run() {
- synchronized (lock){
- System.out.println("start ThreadB");
- lock.notify();//实现线程的唤醒
- System.out.println("end ThreadB");
- }
- }
- }
-
- /**
- * @Author Dark traveler
- * @Note 我心净处,何处不是西天。
- * @Descrption
- * @E-Mail : 1029149772@qq.com
- * @Date : Created in 15:26 2020-3-18
- */
- public class waitNotifyDemo {
- public static void main(String[] args) {
- Object lock = new Object();
- ThreadA threadA = new ThreadA(lock);
- threadA.start();
- ThreadB threadB = new ThreadB(lock);
- threadB.start();
- }
- }
wait:实现线程的阻塞,并且释放当前的同步锁
motify/notifyall:唤醒被阻塞的单个线程(全部线程)
用图来表示一下刚刚的流程