1.1. 并发编程

在并发编程的操作中,程序运行过程中,会将需要运算修改的数据从主内存复制一份到线程中,在线程计算结束会将此数据重新刷入主内存,在并发中,就有可能造成一些问题,比如:

i=i+1;

线程执行时,会从主内存中读取i的值,然后复制一份到高速缓存中,然后CPU指令进行+1操作,然后将数据写入到高速缓存,再将高速缓存中i最新的值刷到主内存。 单线程不会有任何问题,但是多线程场景下就会有问题了。如:

  • (缓存一致性问题)假设 初始值为0,那么在两个线程执行完成之后我们期望的结果应该是2,但是,当多线程并发时,线程A和b拿到初始值都为0,A进行+1操作,写回高速缓存,写回主内存;此时线程B中初始值依然为0,也进行+1操作,写回高速缓存,写回主内存,这是得到的最终结果是1,而不是2

1.2. 并发编程的三个概念

在并发编程中,通常会遇到三个问题:原子性、可见性、有序性

1.2.1. 原子性

一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么都不执行

1.2.2. 可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值 例子:

//线程1执行的代码
int i=0;
        i=10;

//线程2执行的代码
        j=i;

假设有A.B两个线程并发操作,A操作执行到i=10这句话时,会将初始值i=0加载到线程A的高速缓存中去做修改赋值10,那么线程A的高速缓存中的值变为了10,但是却没有立即写回到主内存。这时候线程B执行j=i操作,它会先去主内存读取值加载到B的高速缓存中,这时主内存中i的值还是0,那么就会造成j的值为0,而不是10

1.2.3. 有序性

即程序执行的顺序按照代码的先后顺序执行。 JMM优化会在编译后的代码进行指令重排序,指令重排序的意义:使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率。在多线程操作下,指令重排序可能导致程序不能保证有序性。

  • 什么叫指令重排?处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。处理器在进行重排序时是会考虑指令之间的数据依赖性
  • 多线程执行指令重排导致异常的例子如下:
//线程1:
context=loadContext();   //语句1
        inited=true;             //语句2

//线程2:
        while(!inited){
        sleep()
        }
        doSomethingwithconfig(context);

如上流程,有线程1,2并发执行,在编译后可能会对代码进行指令重排,因为线程1内的两行代码并没有数据依赖性,所以可能先执行语句2,这时候线程2在判断时,跳过循环,直接执行doSomethingwithconfig(context) 方法,然而,此时线程1还未初始化context,所以导致程序出错

  • 指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性

1.3. Java内存模型

Java内存模型规定所有的变量都是存在主存当中,每个线程都有自己的工作内存。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。 如图,如果要修改主内存的10这个值,那么T1,T2线程都会先从主内存中将10这个值拷贝回自己的工作内存中去,修改完之后再刷新回主内存

1.3.1. valatile

了解了java的内存模型,那么可以正式来看一下volatile是怎么解决这些问题的吧

volatile关键字的两层语义

一旦一个共享变量(类的成员变量、静态成员变量)被volatile修饰之后,那就具备了两层语义:

  1. 保证了不同线程对这个变量进行操作时的可见性
  2. 禁止进行指令重排序

volatile不能保证原子性

  • 原子性可以使用synchronized来保证,但是会造成执行效率慢的问题
  • 可以使用atomic原子类保证,atomic原子类原理是使用CAS自旋锁(compare and swap),比较并交换
Copyright & copy lviter@163.com            updated 2024-02-06 09:54:55

results matching ""

    No results matching ""