为什么需要JMM(Java Memory Model)?
- C语言不存在内存模型的概念,很多行为依赖于处理器,不同处理器的操作会导致运行结果不一样,所以不能保证并发安全
- JMM是一组规范,也是工具类和关键字的原理
- JMM最重要的三点内容:重排序、可见性、原子性
一、重排序
1、重排序例子
-
下面是一个演示重排序的例子,一个线程执行
a = 1; x = b
,一个线程执行b = 1; x = b
public class OutOfOrderExecution { 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); 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 + ")"; //四种情况:0,1/1,0/1,1/0,0 if (x == 0 && y == 0){ System.out.println(result); break; } else { System.out.println(result); } } } }
-
分析会出现以下三种情况:
- a = 1; x = b(0); b = 1; y = a(1)
- b = 1; y = a(0); a = 1; x = b(1)
- b = 1; a = 1; x = b(1); y = a(1)
-
但是还发生了一种情况,
x = 0; y = 0
,原因是发生了一种可能的重排序,可以使用volatile解决,即保证可见性
2、重排序的好处和三种情况
- 重排序的好处就是能对指令进行优化,提高处理速度
- 重排序有三种情况:
- 编译器优化
- CPU指令重排
- 由于内存的多级缓存,线程之间出现可见性问题,导致“重排序”这一现象出现
二、可见性
1、可见性演示例子
-
以下例子演示的是可见性,即一个线程无法看见其他线程的全部操作,以下代码中,一个线程执行change方法,另一个线程执行print方法
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(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.change(); } }).start(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } test.print(); } }).start(); } } }
-
运行过程中会出现三种常见情况:
- a = 3; b = 3
- a = 1; b = 2
- a = 3; b = 2
- a = 1; b = 3 这是一种不常见的情况,出现的原因就是因为一个线程执行了
a = 3; b = a;
,而另一个线程无法看见该线程的全部操作,忽略了a = 3
\
2、为什么会有可见性问题?
- CPU有多级缓存,导致读的数据过期
- 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在主内存和CPU之间多了cache层
- 线程间的对于共享变量的可见性问题不是由多核引起的,而是由多级缓存引起的
- 如果所有核心都只用一个缓存,那么不会存在内存可见性问题
- 每个核心都会将自己需要的数据读到独占内存中,数据修改后也是写入到缓存中,然后等待刷入主存中。所以会导致有些核心读取的值是一个过期的值
3、JMM的抽象:主存和本地内存
-
什么是主内存和本地内存?
本地内存并不是真正为每个线程分配一块内存,而是对于寄存器、一级缓存、二级缓存等的抽象
-
主内存和本地内存的关系
- 所有变量都存储在主内存中,同时每个线程也有独立的工作内存,工作内存中的变量内容是主内存中的拷贝
- 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中
- 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,要借助主内存中转来完成
4、Happens-Before原则
- 第一种解释:happens-before规则是用来解决可见性问题的,在时间上,动作A发生在动作B之前,B保证能看见A
- 第二种解释:如果第一个操作happens-before于另一个操作,那么第一个操作对于第二个操作是可见的
5、volatile关键字
-
volatile
是一种同步机制,比synchronized
或者Lock
相关类更轻量,因为使用volatile不会发生上下文切换等开销很大的行为;但volatile做不到synchronized那样的原子保护,仅能在有限的场景下才能发挥作用 -
volatile适用场合一:纯赋值操作
如果一个共享变量自始至终只被各个线程赋值,而没有其他操作,那么可以用volatile来代替synchronized或者代替原子变量,因为赋值自身具有原子性,而volatile又保证了可见性,所以足以保证线程安全
下面例子中,在 setDone() 方法中自始至终都是赋值操作
done = true;
,所以无论运行多少次,结果都是truepublic class UseVolatile implements Runnable { volatile boolean done = false; static AtomicInteger realA = new AtomicInteger(); public static void main(String[] args) throws InterruptedException { Runnable r = new UseVolatile(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(((UseVolatile) r).done); System.out.println(realA.get()); } @Override public void run() { for (int i = 0; i < 10000; i++) { setDone(); realA.incrementAndGet(); } } private void setDone() { done = true; } }
但是下面这个例子就不一样了,flipDone() 方法中,
done = !done
,这个不是赋值操作,即使加了volatile关键字也无法保证线程安全public class NoVolatile2 implements Runnable { volatile boolean done = false; static AtomicInteger realA = new AtomicInteger(); public static void main(String[] args) throws InterruptedException { Runnable r = new NoVolatile2(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(((NoVolatile2) r).done); System.out.println(realA.get()); } @Override public void run() { for (int i = 0; i < 10000; i++) { flepDone(); realA.incrementAndGet(); } } private void flepDone() { done = !done; } }
-
适用场合二:触发器
在可见性的演示例子中,b 就是起到了触发器的作用,它保证了
b = 0;
之前的操作对其它线程可见volatile int b = 2; ... private void change() { ... ... a = 3; //写入volatile变量 b = 0; } private void print() { //读出 if (b == 0) { System.out.println("b=" + b + ";a=" + a); } } ...
-
volatile的两点作用
- 可见性:读到一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存中读取最新值,写一个volatile属性会立即刷入到主内存
- 禁止指令重排序优化:解决单例双重锁乱序问题
三、原子性
1、什么是原子性?
- 一系列的操作,要么全部执行成功,要么全部不执行,操作是不可分割的
- 原子操作有哪些?
- 除了long和double之外的基本类型的赋值操作
- 所有引用reference的赋值操作
java.concurrent.Atomic.*
包中所有类的原子操作
- 简单地把原子操作组合在一起,不能保证整体依然具有原子性
2、单例模式
-
饿汉式(静态常量):可以保证线程安全
public class Singleton1 { private final static Singleton1 INSTANCE = new Singleton1(); private Singleton1() { } public static Singleton1 getInstance() { return INSTANCE; } }
-
饿汉式(静态代码块):线程安全
public class Singleton2 { private final static Singleton2 INSTANCE; static { INSTANCE = new Singleton2(); } private Singleton2() { } public static Singleton2 getInstance() { return INSTANCE; } }
-
懒汉式:线程不安全,会出现多个线程竞争
public class Singleton3 { private static Singleton3 instance; private Singleton3() { } public static Singleton3 getInstance() { //如果多个线程同时竞争,这时instance为空,会创建多个实例,不是单例 if (instance == null) { instance = new Singleton3(); } return instance; } }
-
使用同步方法的懒汉式:线程安全,但是有性能问题
public class Singleton4 { private static Singleton4 instance; private Singleton4() { } public synchronized static Singleton4 getInstance() { if (instance == null) { instance = new Singleton4(); } return instance; } }
-
使用synchronized代码块的懒汉式:线程安全,但会多次创建实例,不是单例的效果
多个线程进入了if判断条件里,即运行到了“**”的位置,在同步代码块里一个线程执行完了又到另一个线程执行,会创建多个实例
public class Singleton5 { private static Singleton5 instance; private Singleton5() { } public static Singleton5 getInstance() { if (instance == null) { //** synchronized (Singleton5.class) { instance = new Singleton5(); } } return instance; } }
-
双重检查:在同步代码块中再检查一次,并且要使用volatile关键字来防止新建对象时发生重排序,推荐使用
public class Singleton6 { private volatile static Singleton6 instance; private Singleton6() { } public static Singleton6 getInstance() { if (instance == null) { synchronized (Singleton6.class) { if (instance == null) { instance = new Singleton6(); } } } return instance; } }
新建对象时有三个步骤,如果不用volatile来修饰创建的对象,编译器有可能发生指令重排序,构造方法还没执行,对象已经具有了内存地址,值不是null
正常顺序 重排序 1.分配对象的内存空间 1.分配对象的内存空间 2.初始化对象 2.设置instance指向刚才刚分配的内存地址 3.设置instance指向刚才刚分配的内存地址 3.初始化对象 -
静态内部类:懒汉行为,在加载外部类时并不会立刻实例化内部类;并且内部类只实例化一次,保证了单例效果;推荐使用
public class Singleton7 { private Singleton7() { } private static class SingletonInstance { private static final Singleton7 INSTANCE = new Singleton7(); } public static Singleton7 getInstance() { return SingletonInstance.INSTANCE; } }
-
枚举单例,推荐使用,最佳
public enum Singleton8 { INSTANCE; public void whatever() { } }