赞
踩
Java 内存模型(Java Memory Model 简称JMM)是一种抽象的概念,并不真实存在,指一组规则或规范,通过这组规范定义了程序中各个变量的访问方式。java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
JMM规定:
工作内存和主内存的关系图:
看代码,定义了一个成员变量 falg,一个子线程负责修改flag的值,另外一个子线程根据flag的值判断是否跳出空循环,实际执行结果为下图,可见,线程0对flag的修改并没有影响到线程1;这就是多线程下共享变量的修改会存在不可见性。
原因就是Thread-1一直访问的都是自己本地内存中的flag,而没有从主内存中去更新flag,所以没办法跳出循环。
- public class TestVolatile {
- // 定义一个成员变量
- static boolean flag = true;
-
- public static void main(String[] args) {
- new Thread(() -> {
- System.out.println(Thread.currentThread().getName() + "start");
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- flag = false;
- System.out.println(Thread.currentThread().getName() + "end");
- }).start();
-
- new Thread(() -> {
- System.out.println(Thread.currentThread().getName() + "start");
- while (flag) {
- // 空转
- }
- System.out.println(Thread.currentThread().getName() + "end");
- }).start();
- }
- }
3.1、volatile关键字处理,还是上述代码,只要在flag属性前加一个volatile关键字,就可以了。
3.2、 加锁处理,看代码
线程1赋值修改flag,线程2不断读取flag,可以看到flag被线程1修改后,线程2是可以读取到变化之后的结果的。
- public class TestVolatile {
- // 定义一个成员变量
- static boolean flag = true;
-
- static final Object lock = new Object();
-
- public static void main(String[] args) {
- test02(lock);
- }
-
- private static void test02(Object lock) {
- new Thread(() -> {
- System.out.println(Thread.currentThread().getName() + "start");
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- flag = false;
- System.out.println(Thread.currentThread().getName() + "end");
- }).start();
-
- new Thread(() -> {
- System.out.println(Thread.currentThread().getName() + "start");
-
- while (true) {
- synchronized (lock) {
- if (flag) {
- System.out.println("flag = " + flag);
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- } else {
- System.out.println("flag = " + flag);
- break;
- }
- }
- }
-
- System.out.println(Thread.currentThread().getName() + "end");
- }).start();
- }
- }
执行结果
volatile是不保证原子性操作的。
- static volatile int count = 0;
-
- public static void main(String[] args) throws InterruptedException {
- for (int i = 0; i < 10; i++) {
- new Thread(() -> {
- for (int j = 0; j < 10000; j++) {
- count++;
- }
- System.out.println(Thread.currentThread().getName() + "==========> count=" + count);
- }).start();
- }
- }
如上述代码,定义了一个volatile修饰的int类型变量,启动10个线程去执行++操作,每个线程修改10000次,按理说修改后的值应该为100000,但是每次执行的结果都没有到100000
发生上述问题的原因在于,count++这个操作不是原子性的,他包含三个步骤:
假设某一时间,两个线程都执行到了步骤1,读取到的count值是100 ,然后线程1的CPU时间片到了,停止执行,此时2线程继续执行23步骤,将主内存的值修改为101,这个时候线程1继续执行,但是因为1已经执行了,没有重新去主内存中取值,因此执行23操作后,新值为101,然后往主内存修改的值也是101。
解决原子性办法,
1、加锁
- static final Object lock = new Object();
-
- static volatile int count = 0;
-
- public static void main(String[] args) throws InterruptedException {
- for (int i = 0; i < 10; i++) {
- new Thread(() -> {
- for (int j = 0; j < 10000; j++) {
- synchronized (lock){
- count++;
- }
- }
- System.out.println(Thread.currentThread().getName() + "==========> count=" + count);
- }).start();
- }
- }
2、使用atomic包 // 底层CAS,不多介绍了。
- static AtomicInteger a = new AtomicInteger();
-
- public static void main(String[] args) throws InterruptedException {
- for (int i = 0; i < 10; i++) {
- new Thread(() -> {
- for (int j = 0; j < 10000; j++) {
- a.getAndIncrement();
- }
- System.out.println(Thread.currentThread().getName() + "==========> count=" + a);
- }).start();
- }
- }
什么是重排序?为了提高性能,编译器和处理器常常会对指令进行重新排序。一般重排序分为以下三种:编译器优化重排序,指令级并行重排序,内存系统重排序。
重排序就是为了提高处理初度,如下图。
经典案例,以下代码执行完毕,i 和 j 的值有可能是多少?经过测试,i=0,j=0的情况也会出现!这就是指令重排序导致的问题,
-
- import java.util.concurrent.CountDownLatch;
-
- public class T01_Disorder {
- private static int x = 0, y = 0;
- private static int a = 0, b = 0;
-
- public static void main(String[] args) throws InterruptedException {
-
- for (long i = 0; i < Long.MAX_VALUE; i++) {
- x = 0;
- y = 0;
- a = 0;
- b = 0;
- CountDownLatch latch = new CountDownLatch(2);
-
- Thread one = new Thread(new Runnable() {
- public void run() {
- a = 1;
- x = b;
-
- latch.countDown();
- }
-
- });
-
- Thread other = new Thread(new Runnable() {
- public void run() {
- b = 1;
- y = a;
-
- latch.countDown();
- }
- });
- one.start();
- other.start();
- latch.await();
- String result = "第" + i + "次 (" + x + "," + y + ")";
- if (x == 0 && y == 0) {
- System.err.println(result);
- break;
- }
- }
- }
-
- }
jvm级别,识别到volatile关键词,会执行jvm内存屏障,包括 loadload 屏障、storestore 屏障、loadstore屏障、storeload 屏障(其中load是读,store是写);
a) 会在写之前加 storestore,写之后加storeload,保证在自己写之前完成其他的写,在自己写完之后才能继续其他的读
b) 会在读之后加上loadload 和 loadstore ,保证在自己读完之后其他的才能读,自己读完之后,其他的才能写
简单的说,如果 A Happens Before B ,那么,A的操作对B,都是可见的。
Happens Before模型是由8条具体规则组成的:
总的来说,volatile的本质是告诉JVM,变量在工作内存(寄存器)中的值是不确定的,需要从主存中去取,synchronized则是直接锁住当前变量,只有当前线程可以访问,其他线程阻塞。
synchronized关键字可以保证原子性和可见性,但是没有办法保证顺序性(即可以被指令重排序),new 一个对象可以分为以下三个步骤:
假设new 对象的过程发生了指令重排序,步骤2和3互换。虽然创建对象加锁了,但是加锁线程1执行完1和3之后失去了CPU时间片,此时初始化对象还没有完成,这个时候线程2执行,在外层判断的时候INSATNCE!=null, 直接就获取对象执行操作了,但是因为该对象实际还没有初始化完成呢,因此线程2返回的就是一个空对象。
而volatile关键字禁止指令重排序,就避免了上述问题。
- public static T01 getInstance(){
- private static volatile T01 INSTANCE;
- private T01 (){
- //私有构造器,外部不能new
- }
-
- if(INSTANCE==null){
- synchronized (T01.class){
- if(INSTANCE==null){
- INSTANCE=new T01();
- }
- }
- }
- return INSTANCE;
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。