【Java并发编程学习 3】理解Java内存模型,多线程之间通信,重排序

1 线程之间如何通信及线程之间如何同步

在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线程之间以如何来交换信息。一般线程之间的通信机制有两种:共享内存和消息传递。

Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

2 Java内存模型

共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

在这里插入图片描述
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

下面通过示意图来说明线程之间的通信
在这里插入图片描述

总结:什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。

3 Volatile

3.1 什么是Volatile

可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。

在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存。

Volatile 保证了线程间共享变量的及时可见性,但不能保证原子性

3.2 代码示例

测试删除 volatile 关键字试试看**

package com.lijie;

class ThreadVolatileDemo extends Thread {
	//可以尝试注释volatile关键字试试看,看看不加此关键字是否多线程使用的是否是一个变量
	public volatile boolean standard = true;

	public void run() {
		System.out.println("开始执行子线程....");
		while (standard) {
		}
		//设置flag 为 false才会执行到线程停止
		System.out.println("线程停止");
	}
	public void setStandard(boolean standard) {
		this.standard = standard;
	}
}

public class ThreadVolatile {
	public static void main(String[] args) throws InterruptedException {
		ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
		threadVolatileDemo.start();
		Thread.sleep(3000);
		threadVolatileDemo.setStandard(false);
		System.out.println("设置成false");
		Thread.sleep(1000);
		System.out.println(threadVolatileDemo.standard);
	}
}

3.3 Volatile特性

1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。

3.5 volatile 性能

volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

4 重排序

4.1 什么是重排序

程序执行的顺序按照代码的先后顺序执行。
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

int a = 5;    //语句1
int r = 3;    //语句2
a = a + 2;    //语句3
r = a*a;      //语句4

则因为重排序,他还可能执行顺序为(这里标注的是语句的执行顺序) 2-1-3-4,1-3-2-4
但绝不可能 2-1-4-3,因为这打破了依赖关系。
显然重排序对单线程运行是不会有任何问题,但是多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

4.2 重排序实际执行的指令步骤

在这里插入图片描述

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

这些重排序对于单线程没问题,但是多线程都可能会导致多线程程序出现内存可见性问题。

4.3 重排序遵守的规则

as-if-serial:

  1. 不管怎么排序,结果不能改变
  2. 不存在数据依赖的可以被编译器和处理器重排序
  3. 一个操作依赖两个操作,这两个操作如果不存在依赖可以重排序
  4. 单线程根据此规则不会有问题,但是重排序后多线程会有问题

4.4 模拟重排序问题代码示例

下面代码正确的情况下是输出正常的,先执行完了初始化方法,在执行实际的方法

package com.lijie;

public class Test {
    int a = 0;

    //初始化方法 1
    public void writer() {
        a = 1;
    }

    //实际执行方法,需要调用完初始化方法才可使用 2
    public void reader() {
        if (a == 1) {
            System.out.println("执行成功方法" + a);
        } else {
            System.out.println("初始化失败");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        //创建多线程创建开始
        Thread thread = new Thread(new Runnable() {
            public void run() {
                //调用初始化方法
                test.writer();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                //调用实际化方法
                test.reader();
            }
        });
        //正常情况就是执行完初始化方法在执行实际的执行方法
        thread.start();
        thread2.start();
    }
}

如果:初始化还没完的时 开始执行实际方法该怎么办,例如:
初始化未完成的情况下就执行了实际方法,这就出现了重排序

package com.lijie;

public class Test {
    int a = 0;

    //初始化方法 1
    public void writer() {
        try {
            Thread t = Thread.currentThread();
            t.sleep(500);
        } catch (Exception e) {

        }
        a = 1;
    }

    //实际执行方法,需要调用完初始化方法才可使用 2
    public void reader() {
        if (a == 1) {
            System.out.println("执行成功方法" + a);
        } else {
            System.out.println("初始化失败");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        //创建多线程创建开始
        Thread thread = new Thread(new Runnable() {
            public void run() {
                //调用初始化方法
                test.writer();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                //调用实际化方法
                test.reader();
            }
        });
        //正常情况就是执行完初始化方法在执行实际的执行方法
        thread.start();
        thread2.start();
    }
}

4.5 怎么解决多线程重排序的问题

Java中可通过volatile在一定程序上保证顺序性,另外还可以通过synchronized和lock锁和保证原子性一样的来保证顺序性。 还可以利用ThreadLocal,让访问某个线程拥有自己局部变量巧妙的来保证。

还有除了从应用层面保证目标代码段执行的顺序性外,JVM还通过被称为happens-before原则隐式地保证顺序性。

4.6 什么是happens-before原则

简单来说就是:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量A赋值为1,那后面一个操作肯定能知道A已经变成了1。

发布了68 篇原创文章 · 获赞 56 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_43122090/article/details/104954483