并发一 java内存模型和线程安全

事先声明 看zejian博客:并发专题 受益良多
https://blog.csdn.net/javazejian/article/category/6940462

1.线程不安全实例

涉及到JVM的运行时内存区域,这里不做讨论

通过一个代码引发的问题去展开探讨(不要纠结业务逻辑是否可优化)

public class MyThread extends Thread {

    private boolean isRestFlag = false;//休息的指令
    private boolean isWorkFlag = true;//干活的指令

    public MyThread(String name) {
        super(name);
    }

    public MyThread(String name, boolean isRestFlag) {
        super(name);
        this.isRestFlag = isRestFlag;
    }

    private static int anInt = 0;//干的事情大家都知道 多线程共享
    private static List<Integer> anIntList = new ArrayList<>();

    @Override
    public void run() {
        //老公就是一直干活
        while (isWorkFlag) {
            // anInt++ 分解成本来的两个动作;
            int temp = anInt;
            anIntList.add(anInt);
            anInt = temp + 1;
//            System.out.println(anInt++);
            if (isRestFlag) {
                //老婆下令结束 老公就可以休息了
                isWorkFlag = false;
                System.out.println(Thread.currentThread().getName() + "老公休息吧");
            }
        }

        System.out.println(Thread.currentThread().getName() + "繁忙的一天结束了");
    }


    public static void main(String[] args) throws InterruptedException {
        new MyThread("老公线程~~").start();//老公在干活
        TimeUnit.SECONDS.sleep(1);
        new MyThread("老婆线程~~", true).start();//老婆说休息

        TimeUnit.SECONDS.sleep(3);
        System.out.println("主线程结束 anInt:" + anInt);

        System.out.println("anInt++ 中多线程情况下是否会取到重复的值" + anIntList.stream().collect(Collectors.groupingBy(Function
                .identity(), Collectors.counting())).entrySet().stream().filter(x -> x.getValue() > 1).findFirst());
    }
}
/* 打印信息  (备注 老公线程一直没有结束)
老婆线程~~老公休息吧
老婆线程~~繁忙的一天结束了
主线程结束 anInt:9346170
Exception in thread "main" java.lang.NullPointerException: element cannot be mapped to a null key
*/
  • 线程间数据共享 场景1

这段程序很简单,就是两个线程 都操作两个变量 然后发现老公线程一直没有结束,空指针异常也是因为老公线程一直在处理list,而造成list foreach过程中抛异常, 为什么,老公线程一直没有结束–>isWorkFlag是实例变量,存储在栈区,属于线程独有的,无法共享,这个问题是入门级问题,不懂的可以看下java运行时内存
所以对应的放入栈区,简单的就是static修饰成类变量

    private static boolean isWorkFlag = true;//干活的指令

    /* 打印信息  (备注老公线程一直没有结束,但anInt++多线程下少被重复取到了)
     老婆线程~~老公休息吧
    老婆线程~~繁忙的一天结束了
    老公线程~~繁忙的一天结束了
    主线程结束 anInt:5880983
    anInt++ 中多线程情况下是否会取到重复的值Optional[5880982=2]
    */
  • 线程安全问题 场景2

线程间数据的传递解决了,来了个新问题,就是多线程下anInt++被重复取到,换个说法就是虽然线程间数据共享,but执行两次+1操作得到还是1,这可以理解为线程不安全,是由于anInt++两步动作不是原子操作引起的

  • 又一种线程安全问题 场景3

我们再调整一下代码,去掉anIntList相关的 在效果方面system.out和list.add一样

//            anIntList.add(anInt);

/* 老公线程又没有结束... 也就是static修饰的isWorkFlag 没有共享!
老婆线程~~老公休息吧
老婆线程~~繁忙的一天结束了
主线程结束 anInt:127657172
anInt++ 中多线程情况下是否会取到重复的值Optional.empty
*/

现在是isWorkFlag没有被看见,这也是线程安全问题,是由于数据可见性引起的

要解决线程安全问题得先了解java内存模型

2.java内存模型

JMM:java memory model

这里写图片描述

JMM是一种抽象的规定,并没有具体实现,规定了工作内存与主内存之间的交互方式,在多个线程同时操作主内存的情况下,如下图,在A线程修改数据为2的情况下,线程B是读到的是1呢还是2呢,这是不确定的,而这种不确定性 就是线程不安全的根因(线程操作主内存数据时存在不确定性就是线程不安全),而JMM的规定可以解决这个问题,同时JMM也为jvm与硬件内存的跨平台提供的解决方式
这里写图片描述

JMM规定了是围绕原子性,可见性,有序性三个特性展开的
原子性:和数据库原子性类似
可见性:读取时屏蔽工作内存的值,直接读取主内存的值,而写时立即会写主内存,同时通过这个内存屏障禁止指令重排序引起的多线程可见性问题(指令重排序知道cpu有这个优化手段就行)
有序性:使用内存屏障确保不会出现指令重排序,保证程序的有序性

扫描二维码关注公众号,回复: 1163305 查看本文章

对应的解决方案:

原子性:除了JVM自身提供的对基本数据类型读写操作的原子性外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性
可见性和有序性:volatile关键字

场景3之所以static变量仍未能读取到主内存最新数据,是因为一直运行anInt++,根本不需要去主内存读取数据,所以isWorkFlag一直未刷新,而日常开发中较少碰到多线程情况下如此简单的业务场景,而略复杂的业务场景都需要读主内存数据,比如system.out/list.add(动态拓展时native方法)
所以上面代码的解决方案就是保证可见性或加synchronized也行

private volatile static boolean isWorkFlag = true;

JMM与jvm运行期内存区域的关系

没有关系,这不是一个层次的区分,java内存模型是一种抽象,jvm运行期内存区域是具体的数据存放划分,两者之间没有直接关系,仅有一些相似之处就是jvm栈可以比拟工作内存,堆可以比拟主内存

猜你喜欢

转载自blog.csdn.net/Wen_ching_zhou/article/details/80497093