多线程——3线程安全问题及分析

多线程安全问题&线程通信

一、内容安排

  1. 实现两个线程累加一个变量到10_000
  2. 累加超过10_000的原因分析
  3. synchronized解决安全问题
  4. synchronized可以加在那些地方
  5. synchronized面试考点
  6. 后续更新

二、文章内容

1. 实现两个线程累加一个变量到10_000

  • 实现步骤

    1. 定义静态变量NUM等于0, MAX等于10_000
    2. 定义任务Runable,任务中实现只要NUM<MAX就不断累加NUM
    3. 开启两个线程同时执行任务
    4. 观察最终结果
  • 具体代码

    package com.huangguoyu.article3;
    
    public class ThreadSecurity {
        //静态变量
        static int NUM = 0;
        static int MAX = 10_000;
        public static void main(String[] args) {
            //定义任务
            Runnable task = () -> {
                while (NUM < MAX) {
                    NUM++;
                }
            };
            //线程1
            Thread t1 = new Thread(task);
            //线程2
            Thread t2 = new Thread(task);
            t1.start();
            t2.start();
            //让主线程等待两个线程执行完成打印最后的结果
            try {
                t1.join();
                t2.join();
                System.out.println("最终的NUM等于" + NUM);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
  • 最终效果

    注意: 大家机器上可能跑出来的结果是正确的, 可以多跑几次或则多开几个线程; 确实不行就看下面的分析通过idea的多线程调试实现

    9993
    9994
    9995
    9996
    9997
    9998
    9999
    10000
    10001
    最终的NUM等于10001
    Disconnected from the target VM, address: '127.0.0.1:51903', transport: 'socket'
    
    Process finished with exit code 0
    

2.累加超过10_000的原因分析

  • 首先通过上面的实验我们可以分析发现最终的结果并不是我你们想要的, 那这个产生的原因是什么呢;

  • 出错根本原因在于CPU对多线程执行之间的切换
    在这里插入图片描述

  • 接下来我们通过idea的调试来看看

    1. 首先在while条件判断哪一行打断点并且右键断点处设置当NUM==9999时拦截断点, 操作如下
      在这里插入图片描述

    2. 用debug模式启动程序,断点拦截下来之后先让线程1进入循环
      在这里插入图片描述

    3. 切换线程到线程二,让线程2也进入循环
      在这里插入图片描述

    4. 最后放开两个线程的断点可以看到结果就是,当NUM=9999时,两个线程同时进入了while中这样NUM就会被++两次,也就导致NUM不是我们预期的结果

3.synchronized解决安全问题

  • synchronized: 此关键字能够使得方法或则代码块中的代码在执行时只能有一个线程进入, 这样咱们就可以在while最外层添加一个synchronized代码块保证只能有一个线程进入,这也就使得同时只能有一个线程执行。

  • 具体代码

    • 此处代码中可以发现synchronized需要指定一个对象作为锁,只有持有当前锁的线程才能够进入;注意锁对象必须唯一,所以此处选择了ThreadSecurity字节码对象作为锁
            //定义任务
            Runnable task = () -> {
                synchronized (ThreadSecurity.class) {
                    while (NUM < MAX) {
                        NUM++;
                        System.out.println(Thread.currentThread().getName() + "==>" + NUM);
                    }
                }
            };
    
  • 效果:虽然此处问题是解决了,大家同样可以使用多线程的调试方式去查看能否让线程二也进入,你会发现线程二处于阻塞状态;并且也可以从打印中看的出来所有的累加都是一个线程做了,那也就是说加了线程相当于把我们的任务串行化了,那我们多线程的意义在哪里呢;咱们这里只要有一个线程抢到锁了就会把while执行完所以另外一个线程根本没办法参与累加工作, 其实这里咱们锁的范围太大了我们把整个while都锁住了,我们完全可以吧代码修改一下,把锁的粒度(范围)降低这样两个线程就都有可能抢到锁了,代码修改如下

  • 修改后

            //定义任务
            Runnable task = () -> {
                while (true) {
                    synchronized (ThreadSecurity.class) {
                        if (NUM < MAX) {
                            NUM++;
                            System.out.println(Thread.currentThread().getName() + "==>" + NUM);
                        } else {
                            break;
                        }
                    }
    
                }
            };
    

4.synchronized可以加在那些地方

  • 成员方法上,可以修改任务代码查看执行效果

        //成员方法
        public synchronized void add() {
            NUM++;
        }
    
  • 静态方法上

        //静态方法
        public synchronized static void add() {
            NUM++;
        }
    
  • 代码块,如案列中的写法

5.synchronized面试考点

  • synchronized加在不同的地方他们的锁对象分别是什么

    • 成员方法上锁对象为:this
    • 静态方法上锁对象为:当前类的字节码对象
    • 代码块上:指定的对象
  • synchronized和显示锁性能谁更好

    • 实际上jdk每个版本都在对该关键字进行优化,目前来说次关键字性能不比显示锁差(后续我们聊了显示锁后一起做测试)

6.后续更新

  • 下次内容
    • 线程之间如何通信
    • 实现简单的生产者消费者模型
    • 自定义显示锁实现同步操作
发布了11 篇原创文章 · 获赞 11 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/sui_feng_piao_guo/article/details/102906090