JMM(java内存模型)
-
JMM屏蔽了底层不同计算机的区别,描述了Java程序中线程共享变量的访问规则,以及在jvm中将变量存储到内存和从内存中读取变量这样的底层细节。
-
JMM有以下规定:
-
所有的共享变量都存储与主内存中,这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
-
每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
-
线程对变量的所有操作(读和写)都必须在工作内存中完成,而不能直接读写主内存中的变量。
-
不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值传递需要通过主内存中转来完成。
-
多线程下变量的不可见性:
public class test7 {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
while (true) {
if (t.isFlag()) {
System.out.println("停不下来了"); // 不会执行到这里
}
}
}
}
class MyThread extends Thread {
private boolean flag = false;
// private volatile boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag被修改了");
}
public boolean isFlag() {
return flag;
}
}
原因:
- 子线程t从主内存读取到数据放入其对应的工作内存
- 将flag的值更改为true,但flag的值还没有写回主内存
- 此时main方法读取到了flag的值为false
- 当子线程t将flag的值写回主内存后,主线程没有再去读取主内存中的值,所以while(true)读取到的值一直是false。
volatile 的特性
-
volite 可以实现并发下共享变量的可见性;
-
volite 不保证原子性;
-
volite 可以防止指令重排序的操作。
使用原子类来保证原子性:
public AtomicInteger(): 初始化一个默认值为0的原子型Integer public AtomicInteger(int initialValue): 初始化一个指定值的原子型 Integer int get(): 获取值 int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前的值。 int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。 int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的 value)相加,并返回结果。 int getAndSet(int value): 以原子方式设置为newValue的值,并返回旧值
private static AtomicInteger atomicInteger = new AtomicInteger(); Runnable r = () -> { for (int i = 0; i < 100; i++) { atomicInteger.incrementAndGet(); } };
有时为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。重排序可以提高处理的速度。
volatile写读建立的happens-before关系
happens-before :前一个操作的结果可以被后续的操作获取。
happens-before规则:
-
程序顺序规则(单线程规则)
同一个线程中前面的所有写操作对后面的操作可见
-
锁规则(Synchronized,Lock等)
如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程
1和线程2可以是同一个线程)
-
volatile变量规则:
如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都
对线程2可见(线程1和线程2可以是同一个线程)
-
传递性:
A h-b B , B h-b C 那么可以得到 A h-b C
-
join()规则:
线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive() 成功返回后,都对t2可见。
-
start()规则:
假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来
线程B开始执行前对线程B可见。注意:线程B启动之后,线程A在对变量修改线程B未必可见。
public class VisibilityHP {
int a = 1;
int b = 2;
private void write() {
a = 3;
b = a;
}
private void read() {
System.out.println("b=" + b + ";a=" + a);
}
public static void main(String[] args) {
while (true) {
VisibilityHP test = new VisibilityHP();
new Thread(new Runnable() {
@Override
public void run() {
test.write();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
test.read();
}
}).start();
}
}
}
没给b加volatile,那么有可能出现a=1 , b = 3 。因为a虽然被修改了,但是其他线程不可见,而b恰好其他线程可见,造成了b=3 , a=1。
如果使用volatile修饰long和double,那么其读写都是原子操作
volatile在双重检查加锁的单例中的应用
饿汉式(静态常量)
public class Singleton01 {
private static final Singleton01 Intance = new Singleton01();
private Singleton01() {}
public static Singleton01 getIntance() {
return Intance;
}
}
饿汉式(静态代码块)
public class Singleton02 {
private final static Singleton02 Intance;
static {
Intance = new Singleton02();
}
private Singleton02() {}
public static Singleton02 getInstance() {
return Intance;
}
}
懒汉式(线程安全,性能差)
public class Singleton03 {
private static Singleton03 Instance;
private Singleton03() {}
public static synchronized Singleton03 getInstance() {
if (Instance == null) {
Instance = new Singleton03();
}
return Instance;
}
}
懒汉式(volatile双重检查模式,推荐)
public class Singleton04 {
private static volatile Singleton04 Instance = null;
private Singleton04() {}
public static Singleton04 getInstance() {
if (Instance == null) {
synchronized (Singleton04.class) {
if (Instance == null) {
//创建对象的过程是非原子操作
Instance = new Singleton04();
}
}
}
return Instance;
}
}
此处加上volatile 的作用:
① 禁止指令重排序。
创建对象的过程要经过以下几个步骤s:
a. 分配内存空间
b. 调用构造器,初始化实例
c. 返回地址给引用
原因:由于创建对象是一个非原子操作,编译器可能会重排序,即只是在内存中开辟一片存储空间后直接返回内存的引用。而下一个线程在判断 instance 时就不为null 了,但此时该线程只是拿到了没有初始化完成的对象,该线程可能会继续拿着这个没有初始化的对象继续进行操作,容易触发“NPE 异常”。
② 保证可见性
静态内部类单例方式
public class Singleton05 {
private Singleton05() {}
private static class SingletonInstance {
private static final Singleton05 INSTANCE = new Singleton05();
}
public static Singleton05 getInstance() {
return SingletonInstance.INSTANCE;
}
}
- 静态内部类只有在调用时才会被加载,jvm在底层会保证只有一个线程去初始化实例,下一个线程获取实例时就直接返回。
- 相比于双重检查,静态内部类的代码更简洁。但基于volatile的双重检查有一个额外的优势:除了可以对静态字段实现延迟加载初始化外,还可以对实例字段实现延迟初始化。
volatile使用场景
-
volatile适合做多线程中的纯赋值操作:如果一个共享变量自始至终只被各个线程赋值,而没有其他操作,那么可以用volatile来代替synchronized,因为赋值操作本身是原子性的,而volatile又保证了可见性,所以足以保证线程安全。
-
volatile可以作为刷新之前变量的触发器,可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的值后,触发获取到该变量之前的操作都将是最新可见的。
public class test8 { int a = 1; int b = 2; int c = 3; volatile boolean flag = false; public void write() { a = 100; b = 200; c = 300; flag = true; } public void read() { while (flag) { System.out.println("a=" + a + " " + "b=" + b + " " + "c=" + c); break; } } public static void main(String[] args) { test8 test8 = new test8(); new Thread(() -> { test8.write(); }).start(); new Thread(() -> { test8.read(); }).start(); } }
volatile 和synchronized的区别
- volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
- volatile保证数据的可见性,但是不保证原子性,不保证线程安全。
- volatile可以禁止指令重排序,解决单例双重检查对象初始化代码执行乱序问题。
- volatile可以看做轻量版synchronized,volatile不保证原子性,但是如果对一个共享变量只进行纯赋值操作,而没有其他操作,那么可以使用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就保证了线程安全。