Java高并发系列(二)——JMM模型与并发三大问题

一、线程通信机制

  1. 线程通信两种方式
    在命令式编程中,线程之间的通信机制通常有两种:共享内存和消息传递

  2. 共享内存并发模型
    通信上:在线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。同步上:同步需显式进行的,开发者必须显示指定某个方法或者某段代码需要在线程之前互斥执行。

  3. 消息传递模型
    通信上:线程之间没有公共状态,必须通过明确的发送消息来显式进行通信。同步上:由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

  4. Java内存模型
    Java并发使用的是共享内存模型,线程之间的通信是隐式进行的,整个通信过程对Java使用者是透明的,因此在进行多线程之前有必要了解这种工作机制,否则可能会遇到多线程导致的难以理解的内存异常问题。

二、Java内存模型(Java Memory Model, JMM)

  1. JSR-133下载

JSR-133: Java Memory Model and ThreadSpecification
提取码:c39j

  1. JMM模型作用

JVM为了屏蔽各个硬件平台和操作系统对内存访问机制的差异化,提出了JMM的概念。
3. ##### JMM模型简图
在这里插入图片描述
本图来自《深入理解Java内存模型》,可以看到线程拥有私有的本地内存空间,而线程之间的共享变量(实例字段、静态域、数组元素)存储在主存中。线程读写变量的操作都是对本地内存进行的,需要某个变量时先从主内存中取出并拷贝一份副本,修改时先修改本地副本再通过JMM写入主存,可见线程之间的数据交互必须以主存为媒介。

  1. happens-before

    定义:如果一个操作happens-before另一个操作,那么第一个操作的执行结果对第二个操作可见,且第一个操作的执行应在第二个操作之前。两个操作之间如果存在happens-before关系,并不意味着必须严格按照happens-before规定的顺序来执行,只要重排序之后的结果与不排序一致,则重排序之后也是符合规范的。

    规则:

序号规则描述
1程序顺序规则一个线程中的操作happens-before此线程该操作的任意后续操作
(程序按代码顺序执行)
2监视器规则对一个monitor的lock操作应该happens-before这个monitor的unlock操作
3volatile变量规则对一个volatile变量的写操作happens-before任意后续对这个变量的读操作
4传递性A happens-before B,B happens-before C,则A happens-brfore C

三、并发面临的三大问题

  1. 可见性(Visibility)

定义:如果线程A中的一个操作对线程B是可见的,那么这个操作的执行成功后结果将立刻被线程B发现。

//可见性测试代码
public class Visibility {

    static boolean continueFlag = true;
    public static void main(String[] args) throws InterruptedException {

        new Thread(()->{
            int a = 0;
            while (continueFlag){

            }
        }).start();

        Thread.sleep(1000);

        continueFlag = false;

        System.out.println(continueFlag);

    }

}

结果:
在这里插入图片描述
可以看到本来continueFlag标志变为了false就应该结束了,但是左边的红色方块并没有变灰,代表仍然没有结束程序。

解决方法:问题是线程一直工作没有去主存取最新的数据造成的。因此我们可以用volatile修饰continueFlag参数,那么当continueFlag被修改时,就会通知其他线程,线程也就能获取到最新值了,也就是说volatiole保证了可见性。

如果在循环体中加入System.out.println(“XXX”)就能结束了,原因??
目前还未能搞明白,等搞清楚了再来补上。

  1. 有序性(Ordering)

定义:程序执行的顺序按照代码的先后顺序执行。

//有序性测试代码
public class Ordering {
    private static int x,y,a,b;
    private static CountDownLatch countDownLatch;
    public static void main(String[] args) throws InterruptedException {

        while (true){
            x = y = 0;
            a = b = 0;
            countDownLatch = new CountDownLatch(2);
            Thread one = new Thread(()->{
                countDownLatch.countDown();

                a = 1;
                x = b;
            });

            Thread two = new Thread(()->{
                countDownLatch.countDown();

                b = 1;
                y = a;
            });

            one.start();
            two.start();

            countDownLatch.await();

            System.out.println("result:" + x + "," + y);
            if (x == 0 && y == 0){
                break;
            }
        }

        System.out.println("hhh没想到吧,我出来了");

    }

结果:程序产生了x == 0 && y == 0的结果使得程序结束。
在这里插入图片描述

原预期结果:

  • 如果线程one先执行,那么x,y应该为0,1(x=b为初始定义值)
  • 如果线程two先执行,那么x,y应该为1,0(y=a为初始定义值)

那x,y为0,0的情况怎么来的呢?
x,y同时为0的情况只有x=b在a=1之前执行,y=a在b=1之前执行才能发生。发生这种情况的原因是编译器或虚拟机对指令进行了重排序,导致了x=b排到了a=1之行而y=a排到了b=1之前。

解决方法:使用volatile关键字,volatiole关键字可以利用内存屏障禁止重排序,可以避免有序性问题。

  1. 原子性(Atomicity)

定义:一个或多个操作一次性全部执行要么都不执行,且执行过程中不会被其他操作打断。

//测试代码
public class Atomicity {

    static volatile int a = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread one = new Thread(()->{
           for (int i = 0 ; i < 10000; i++){
               a++;
           }
        });

        Thread two = new Thread(()->{
            for (int i = 0 ; i < 10000; i++){
                a++;
            }
        });

        one.start();
        two.start();
        //开启线程one和two后sleep等待1s,然后再输出a以保证a是两个线程最终修改结果
        Thread.sleep(1000);
        System.out.println(a);
    }
}

结果:原本正确的预期最终a的值应该为20000,但是结果只有1.5w+,我们不是用volatile保证可见性了么,a的修改应该是立刻通知其他线程的不是么?
在这里插入图片描述

分析:我们来看看i++具体操作,第一步读a——>第二步i+1——>第三步结果赋值给i,这三步中可见性只能保证了第一步读a是正确的,但是不保证赋值时正确。比如刚开始为0,两个线程都读到了a为0,线程one将a修改为1时,线程two比线程one晚一步赋值,这时会产生a被两次赋值为1了,这就就是问题产生的原因。

解决方法:使用AtomicXXX类或者synchronized关键字来保证原子性。

//1.AtomicInteger方式代码
public class Atomicity {

    static AtomicInteger a = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {

        Thread one = new Thread(()->{
           for (int i = 0 ; i < 10000; i++){
               a.getAndAdd(1);
           }
        });

        Thread two = new Thread(()->{
            for (int i = 0 ; i < 10000; i++){
                a.getAndAdd(1);
            }
        });

        one.start();
        two.start();
        //开启线程one和two后sleep等待1s,然后再输出a以保证a是两个线程最终修改结果
        Thread.sleep(1000);
        System.out.println(a);
    }
}

//2.synchronized方式代码
public class AtomicitySynchronized {
    static volatile int a = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread one = new Thread(()->{
            for (int i = 0 ; i < 10000; i++){
                synchronized (AtomicitySynchronized.class){
                    a++;
                }
            }
        });

        Thread two = new Thread(()->{
            for (int i = 0 ; i < 10000; i++){
                synchronized (AtomicitySynchronized.class){
                    a++;
                }
            }
        });

        one.start();
        two.start();
        //开启线程one和two后sleep等待1s,然后再输出a以保证a是两个线程最终修改结果
        Thread.sleep(1000);
        System.out.println(a);
    }
}

四、下篇

Java高并发系列(三)——重量级锁synchronized

参考书目:

  • 《深入理解Java内存模型》
  • 《Java并发编程的艺术》
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值