【架构师面试-JUC并发编程-2】-JMM并发编程原理

1:为什么需要JMM

JMM是java memory model(Java内存模型)

如果不存在内存模型的概念,运行的结果依赖于处理器,不同的处理器结果不同,无法保证并发安全。所以需要一个标准,让多程序运行的结果可预期。

JMM是一种规范,需要各个JVM的实现来遵守JMM规范,以便开发者可以利用这些规范,更方便地开发多线程程序。(JVM有多种实现,有oracle,有openjdk的)

重点的3点:重排序、原子性、可见性

2:重排序

1:什么是重排序

第一种情况

jmm/OutOfOrderExecution.java

package jmm;
/**
 * 描述:演示重排序的现象 “直到达到某个条件才停止”,去测试小概率事件
 */
public class OutOfOrderExecution {
    //这义4个变量
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
 
    public static void main(String[] args) throws InterruptedException {
        //线程one
        Thread one = new Thread(new Runnable() {
            @Override
            public void run() {
                a = 1;
                x = b;
            }
        });
        //线程two
        Thread two = new Thread(new Runnable() {
            @Override
            public void run() {
                b = 1;
                y = a;
            }
        });
        one.start();
        two.start();
        //让主线程等待
        one.join();
        two.join();
 
        //打印结果
        System.out.println("x=" + x + ",  y=" + y);
    }
}

分析

开始x =0,后来x=b,开始y=0,后来y=a

如果线程1先运行且运行完,a=1,x=b,而线程2还没有运行,赋值b=1还没有去做,b还为0,将b给x,此时x=0

当第2个线程运行,修改y=a,而a已经被第1个线程修改为1,所以y=1

这只是其中一种情况。

这4行代码的顺序会决定最终x和y的结果,一共有3种情况:

1)线程1先运行

a=1;x=b(0);b=1;y=a(1),最终结果是x=0,y=1

2)线程2先运行

b=1;y=a(0);a=1;x=b(1),最终结果是x=1,y=0

3)线程1第一行去赋值b,线程2第一行去赋值a,然后再分别执行两个线程的第2行代码,去赋值x和y

b=1;a=1;x=b(1);y=a(1),最终结果是x=1,y=1

第二种情况

将两个线程换下顺序执行

jmm/OutOfOrderExecution.java

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

第三种情况

要求两个线程同时开始,使用一个工具类CountDownLatch(起到闸门的作用)

jmm/OutOfOrderExecution2.java

package jmm;
 
import java.util.concurrent.CountDownLatch;
 
/**
 * 描述:     演示重排序的现象 “直到达到某个条件才停止”,去测试小概率事件
 */
public class OutOfOrderExecution {
 
    //这义4个变量
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
 
    public static void main(String[] args) throws InterruptedException {
 
        //初始值表示需要几次倒计时
        CountDownLatch latch = new CountDownLatch(1);
 
        //线程one
        Thread one = new Thread(new Runnable() {
            @Override
            public void run() {
          //在线程需要等待的地方加上栅栏,当收到发射信号时线程1和线程2同时执行
                try {
                    latch.await();
                    a = 1;
                    x = b;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //线程two
        Thread two = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
          //在线程需要等待的地方加上栅栏,当收到发射信号时线程1和线程2同时执行
                    latch.await();
                    b = 1;
                    y = a;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
            }
        });
        two.start();
        one.start();
        //放开闸门,让线程1和线程2同时运行
        latch.countDown();
        //让主线程等待
        one.join();
        two.join();
 
        //打印结果
        System.out.println("x=" + x + ",  y=" + y);
    }
}

如果第3种情况不好测试,可以试试改成下面的代码,来测试小概率事件。

jmm/OutOfOrderExecution2.java

package jmm;
 
import java.util.concurrent.CountDownLatch;
 
/**
 * 描述:     演示重排序的现象 “直到达到某个条件才停止”,去测试小概率事件
 */
public class OutOfOrderExecution2 {
 
    //这义4个变量
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
 
    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (; ; ) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;
 
            CountDownLatch latch = new CountDownLatch(3);
 
            //线程one
            Thread one = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.countDown();
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a = 1;
                    x = b;
                }
            });
            Thread two = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.countDown();
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    b = 1;
                    y = a;
                }
            });
            two.start();
            one.start();
            latch.countDown();
            one.join();
            two.join();
 
            String result = "第" + i + "次(" + x + "," + y + ")";
            if (x == 1 && y == 1) {
                System.out.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
 
}

第四种情况

X=0,y=0

jmm/OutOfOrderExecution2.java

if (x == 0 && y == 0) {
    System.out.println(result);
    break;
} else {
    System.out.println(result);
}

 这是因为发生重排序了,4行代码的执行顺序的其中一种可能

y=a; (a开始为0,所以y=0)

a=1;

x=b;

b=1;

在线程1内部的两行代码的实际执行顺序和代码在java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序。这里被颠倒的是y=a和b=1这两行语句。

2:解决重排序

jmm/OutOfOrderExecution.java

//这义4个变量
private volatile static int x = 0, y = 0;
private volatile static int a = 0, b = 0;

3:可见性

1:可见性带来的问题

jmm/FieldVisibility.java

package jmm;
 
/**
 * 描述:     演示可见性带来的问题
 */
public class FieldVisibility {
   //类的两个成员变量,现在用两个线程对变量进行操作。
     int a = 1;
     int b = 2;
 
    private void change() {
        a = 3;
        b = a;
    }
 
    private void print() {
        System.out.println("b=" + b+";a=" + a);
    }
 
    public static void main(String[] args) {
        while (true) {
            FieldVisibility test = new FieldVisibility();
 
            //线程1调用change()
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();
 
            //线程2调用println()
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }
 
    }
 
}

分析:

情况1:线程1先启动运行,执行change(),b=3;a=3

情况2:线程2先执行,先执行print(),b=2;a=1

情况3:b=2;a=3是两个线程交替执行,线程1将a=3,然后第2个线程就进行打印

情况4(体现可见性): b=3; a=1,线程1先运行,将a=3,b=3,但是线程1修改后的a=3,b=3并不一定会传达给线程2,线程2可能只看到对b的修改,但没有看到对a的修改,所以b=3,但是线程2没有看到线程1对a的修改,线程2可以看到原始的a=1

由于结要情况4的结果不好找,我们安装一个Grep Console插件

改进

jmm/FieldVisibility.java

volatile int a = 1;
volatile int b = 2;

2:为什么会有可见性问题

这是因为CPU有多级缓存,导致读的数据过期

如果所有核心都只用一个缓存,就没有内存可见性问题。

每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。

3:JMM主内存和本地内存

Java作为高级语言,屏蔽了底层的实现细节。用JMM定义了一套读写内存数据的规范,使我们不再需要关心一级缓存、二级缓存、三级缓存这些问题。但是JMM抽象出了主内存和本地内存的概念。

这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM的一个抽象,是对寄存器、一级缓存、二级缓存等的抽象。

 

 

如图:一个线程(就是一个核心)和自己的工作内存沟通,不同的线程工作内存是不互通的。线程通过buffer和主内存沟通。线程间的交互也只能通过主内存进行。

总结:

1)所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存中的拷贝。

2)线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中。

3)主内存是多个线程共享的,但线程间不共享工作内存。如果线程间需要通信,必须借助主内存中转来完成。

所有的共享变量存在于主内存中,每个线程都有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致可见性问题。

4:Java内存模型中的原子性、有序性、可见性是什么

并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

1:原子性

一个操作或多个操作要么全部执行完且执行过程不被中断,要么就不执行。

2:可见性

当多个线程同时访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

3:有序性

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

对于单线程,在执行代码时jvm会进行指令重排序,处理器为了提高效率,可以对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证保存最终执行结果和代码顺序执行的结果是一致的。

5:Volatile和synchronized的关系

volatile可以看成是轻量版的synchronized

如果一个共享变量自始至终只被各个线程赋值,而没有其它操作,则可以用volatile代替synchronized,因为赋值自身是有原子性的,而volatile又保证了可见性,所以可以保证线程安全。

Volatile属性的读写操作是无锁的,但它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以它低成本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不要迷恋发哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值