[笔记]Java多线程基础——内存

2017-09-10  本文已影响86人  蓝灰_q

因为处理器主频在硬件发展上的瓶颈,摩尔定律基本失效,现在真正起作用的是并行处理的Amdahl定律,毕竟,现在计算机的瓶颈在于存储和通信,而不是运算本身,并行运算可以更充分地发挥运算的能力,也是提升计算机性能。

一致性的问题

并行运算通过让多个处理器并行来提升性能,但是多处理器并行就会有内存上的一致性问题。

  1. 硬件上的缓存一致性
    运算不是处理器自己的事情,处理器必须与内存交互(读参数,写结果),但是处理器和内存在速度上相差几个数量级,硬件上的一个解决办法是在处理器和内存之间插一组高速缓存(Cache),每个处理器都有一块儿Cache,Cache的速度相对更接近运算器,这能提高运算速度,但是,多个处理器的Cache同时与内存交互时,就有可能发生冲突,这就是缓存一致性的问题。
  2. 指令上的乱序执行和指令重排序
    为了充分利用处理器的运算能力,一段代码中的各个语句可能被分配到不同的处理器共同处理,最后再把处理结果重组,这样处理速度更快,但是各语句执行的先后顺序就不一定是代码的先后顺序了,这就是乱序执行。
    JVM在编译时,也会对生成的指令做类似的操作,叫做指令重排序,指令重排序能通过多处理器并行来提升性能,但是语句的执行顺序会被重排,这可能会破坏代码中的逻辑顺序。
  3. JVM的主内存和工作内存
    Java不像C/C++,不会让程序直接操作硬件和OS上的内存,而是提供了JVM的虚拟机内存。JVM内存分为主内存和线程上的工作内存。
    主内存
    对应物理内存,存放对象、静态对象、常量等。(可以理解为堆,但其实不是一个维度)
    工作内存
    对应Cache甚至处理器的寄存器,每个线程都有自己的工作内存,存放线程私有的方法参数和局部变量。(可以理解为虚拟机栈的栈帧中的局部变量表,但其实不是一个维度)
    线程不能直接操作主内存的数据,只能在工作内存中操作目标对象的拷贝副本(一般只拷贝对象的reference和线程要使用的字段),最后再把拷贝副本写回主线程;线程之间也不能直接传递对象,要通过主内存中转。

JVM的主内存和工作内存需要频繁交互,为了确保数据一致性,定义了8种操作(并没有开放给用户),确保以主内存为准:

  1. lock,一个线程独占,其他线程不可用
    同一个线程可以lock多次,但是就需要unlock同样次
    会清空本地内存数据,重新从主内存load或assign(确保lock后的数据与主内存一致)
  2. unlock,线程不再占用,其他线程可以使用
    必须是lock过的对象,而且是本线程lock过的对象
    前面必须先write+store(unlock前必须写回主内存)
  3. read,从主内存读出
    后面必须load
  4. load,复制到工作内存
    必须先read
  5. use,在工作内存中使用该对象
    必须是load或assign过的(工作内存不能增加对象,对象必须来自主内存)
  6. assign,在工作内存中为该对象赋值
    后面必须做write(修改过的对象必须写回主内存)
  7. write,从工作内存读出
    前面必须assign过(没修改过的对象不需要写回主内存)
    后面必须store
  8. store,写到主内存
    前面必须先write

原子性、可见性、有序性

实现并发操作的一致性,核心就是原子性、可见性和有序性。

volatile、synchronized和ReentrantLock同步机制

前面提到,JVM中有volatile和synchronized两个同步机制,其中volatile是轻量级的,synchronized是重量级的,JVM还提供了ReentrantLock重入锁。

volatile boolean flag=false;
public void shutdown(){
      flag=true;
}
public void dowork(){
      while(!flag){...} //仅利用volatile的可见性
}

先行发生

在Java中解决有序性的问题,并不是全都需要同步器,有一些是天然就能确保先后关系的规律:(先行发生仅指操作上的先后顺序,不是时间上的先后顺序)
1.程序次序,同一线程内,是按代码逻辑顺序。
2.管程锁定,同一个锁对象,时间上一定是先unlock,然后才能lock
3.volatile,一个volatile变量,时间上一定是先写,然后才能读
4.线程启动,Thread一定是先start
5.线程终止,Thread一定是最后执行终止(Thread.join())
6.线程中断,一定是先执行了Thread.interrupt(),然后线程的代码才能检测到中断事件
7.对象终结,对象的初始化一定在finalize()之前
8.传递性,操作的先后顺序是可传递的,A在B前,B在C前,则A一定在C前。
先行发生与时间先后没有关系,因为线程在时间上先发生的操作,与并发执行时的顺序并没有太大关系,操作上的先发生不代表时间上的先发生,时间上的先发生也不代表操作上是先发生的。

对双锁检测单例模式的解读

我们看一个双锁检索的DCL单例

public class Singleton{
    private volatile static Singleton instance; //避免指令重排序
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance == null){ //volatile确保可见性,且读操作性能较高
             //用synchronized确保原子性、可见性和有序性
             synchronized(Singleton.class){ //这是一个静态类方法,所以要用class类做全局同步锁
                 if(instance == null){ //volatile在使用时刷新,再检查一次
                     instance=new Singleton();
                 }
             }
        }
        return instance;
    }
}

引用

《深入理解Java虚拟机》
synchronized(this)与synchronized(class)
Java集合及concurrent并发包总结(转)
深入理解java虚拟机 精华总结(面试)

上一篇 下一篇

猜你喜欢

热点阅读