Volatile
主要有两个作用:
1、线程可见性:用volatile
修饰的变量,可以在不同线程之间互相看到,如果一旦发生修改,其他线程就会刷新当前的变量,保证变量的数据一致性
2、阻止指令重排序:cpu执行指令是乱序的,如果加了volatile
,可以防止cpu对指令重排序
这个是网上看到的,记录一下
下面这个代码,无论加不加volatile,都会执行到end,暂时没搞懂
package thread;
import java.util.concurrent.TimeUnit;
public class TestVolatile {
private static boolean isRunning = true;
void m() {
System.out.println("start");
while (isRunning) {
System.out.println("is Running");
}
System.out.println("end");
}
public static void main(String[] args) {
TestVolatile testVolatile = new TestVolatile();
new Thread(testVolatile::m, "test").start();
try {
// 系统睡眠1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
TestVolatile.isRunning = false;
}
}
补充:上面的代码找到为什么没有一直执行的原因了,是因为我在while循环内加了一个输出语句,具体的原因可以看下这篇博客:https://blog.csdn.net/C_AJing/article/details/103307797
cpu架构
ALU:cpu里面的计算单元
超线程:一个ALU对应多个PC
上下文切换:cpu从一个线程切换到另一个线程执行,简称 上下文切换
一般来说,1个cpu核心对应2个线程,两个线程之间互相切换,可以实现超线程
cache line 缓存行:数据存入到缓存里面,是按行存储的,而cpu读取数据的时候,是按每个缓存行去读取的,一个缓存行大小是8字节,cpu会把相邻的变量尽量的存入一个缓存行里面
下面的程序,会对x,y添加了volatile关键字,保证数组中第一个元素和第二个元素的线程可见性,如果x和y没有被存入同一个缓存行内,就会导致cpu读取很慢
package thread;
public class CachePadding {
private static class T {
public volatile long x = 0L;
}
public static T[] array = new T[2];
static {
array[0] = new T();
array[1] = new T();
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (long i = 0; i < 1000_0000L; i++) {
array[0].x = i;
}
});
Thread t2 = new Thread(() -> {
for (long i = 0; i < 1000_0000L; i++) {
array[1].x = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime() - start) / 100_0000);
}
}
运行结果:
下面是一个新的程序,可以对比一下上面的代码,多了一个Padding类,用于补齐缓存行,这样的话,就不会把数组的第一个元素和第二个元素存入同一个缓存行内,这样就不会反复的读取和修改
package thread;
public class CachePaddingMore {
private static class Padding {
// 每个long类型的变量占用8个字节,下面的p1~p7一共占用56个字节
public volatile long p1 = 0L, p2 = 0L, p3 = 0L, p4 = 0L, p5 = 0L, p6 = 0L, p7 = 0L;
}
private static class T extends Padding{
// 这个地方占用8个字节
public volatile long x = 0L;
}
public static T[] array = new T[2];
// 这个时候,array[0]和array[1]必然不可能在同一个cache line内
static {
array[0] = new T();
array[1] = new T();
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (long i = 0; i < 1000_0000L; i++) {
array[0].x = i;
}
});
Thread t2 = new Thread(() -> {
for (long i = 0; i < 1000_0000L; i++) {
array[1].x = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime() - start) / 100_0000);
}
}
运行结果:
消耗时间比上一个少了接近一半,这就是修改了缓存行的存储方式的结果
cpu的执行方式是乱序的,从下面的代码可以看出来:
package thread;
public class T04_Disorder {
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;
Thread one = new Thread(() -> {
//由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
shortWait(100000);
a = 1;
x = b;
});
Thread other = new Thread(() -> {
b = 1;
y = a;
});
one.start();
other.start();
one.join();
other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >= end);
}
}
如果说,cpu没有乱序执行的话,最终的结果应该是(0,1),但是我们运行了10万次以后发现,各种情况都会出现:
这就说明,cpu执行代码的顺序,一定不是按照我们定义的顺序去执行的,而是哪一个线程抢占到资源以后,哪一个线程就会执行
指令乱序执行会出什么问题?
首先我们看个例子,单例模式的实现我们应该都知道,下面这种是实现的代码:
package thread;
/**
* 单例模式
* 缺点:还未使用就会完成实例化
*/
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {
}
public static Mgr01 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr01 mgr01 = Mgr01.getInstance();
Mgr01 mgr02 = Mgr01.getInstance();
System.out.println(mgr01.hashCode());
System.out.println(mgr02.hashCode());
}
}
输出如下:
我们会发现,两个对象的hashcode是一致的,那么就表明,这两个对象是一样的
但是这种实现方式有个缺点,就是如果还没调用getInstance
的方法,对象就已经完成实例化了,那么有没有改进的方式?下面的这种就可以避免
package thread;
public class Mgr02 {
private static Mgr02 INSTANCE;
private Mgr02() {
}
public static Mgr02 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr02();
}
return INSTANCE;
}
public static void m() {
Mgr02 mgr02 = getInstance();
System.out.println(mgr02.hashCode());
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(Mgr02::m).start();
}
}
}
这种的话,会去判断当前的INSTANCE
对象是否已经初始化过了,如果初始化过了,就会返回初始化以后的对象,否则就会初始化一遍以后返回
但是这种写法在多线程的访问下,会是安全的吗?能不能保证只创建了一个对象?我们看下运行结果:
每个对象的hashcode都是不一样的,或许你又会问,怎么证明呢?那么我们试试单线程调用,就可以看出结果了,首先代码做一点小小的修改
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
Mgr02.m();
}
}
然后运行结果如下:
我们会发现,对象的hashcode都是一样的,所以多线程下的单例模式,还是有问题的,不只会创建一个对象,而是会创建多个对象,那么怎么接近呢?很简单,直接加一个synchronized就行了
public static synchronized Mgr02 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr02();
}
return INSTANCE;
}
然后我们会发现,synchronized给整个方法上锁了,范围太大,那么能不能缩小一点?可以啊,我们可以这么改一下
package thread;
public class Mgr02 {
private static volatile Mgr02 INSTANCE;
private Mgr02() {
}
public static Mgr02 getInstance() {
if (INSTANCE == null) {
synchronized (Mgr02.class) {
// 双重检查
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr02();
}
}
}
return INSTANCE;
}
public static void m() {
Mgr02 mgr02 = getInstance();
System.out.println(mgr02.hashCode());
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(Mgr02::m).start();
}
}
}
这么写的好处有什么呢?首先,我们在初始化之前,加了一把锁,如果其他线程来的时候,判断一下当前对象是否为空,如果为空,那么就会给这个对象加一把锁,然后再去判断一次,这个对象是否为空,那么,为什么要判断两次呢?因为如果第一个线程,进入方法以后,判断当前对象为空,然后加锁,初始化对象,然后解锁返回的时候,如果同时再来一个线程,进入方法以后,判断当前对象,因为还没有返回初始化完的对象,这个时候对象还是为空,就会有可能又重新的初始化一遍,就会有问题。所以要加一个二次检查,用来防止多重初始化。
这种单例模式也叫 DCL单例(double check lock),双重加锁单例
那么,我们的INSTANCE
要不要加volatile
关键字?答案是肯定要加
我们先看个东西:
package thread;
public class NewObject {
public static void main(String[] args) {
Object object = new Object();
}
}
上面这个图是这段代码的字节码,我们可以看到,首先NEW是创建一个对象,INVOKESPECIAL
是调用构造方法,ASTORE
分配存储空间,
当我们创建一个对象的时候,他有一个中间态,属于一个半初始化状态,如果在创建对象的时候,INVOKESPECIAL
和ASTORE
两个指令发生了指令重排序,那么就会出现问题,所以我们要加volatile
关键字来防止指令重排序
那么,volatile
是怎么防止指令重排序的?因为他给指令加了一个内存屏障。那么,什么是内存屏障?内存屏障两边的指令不可以重排,可以保证有序。
如上图所示,指令1和指令2之间,是不允许重排序的
而在java虚拟机里面的实现,其实就是使用了汇编语言里面的锁总线的方法,来防止指令重排序的