java 中鼎鼎有名的 AQS
维护 private volatile int state
状态实现了用户态的锁。你若是不了解 volatile
,你看 AQS
的源码应该很难理解为何Lock
能保证线程安全。java
volatile
绝对是你打通 java 的任督二脉的首要条件。votaile
的特性很简单,可见性
和 禁止指令重拍
,若是你本身写代码验证过这两个特色,接下来的内容应该对你帮助不大。面试
单例模式
中 懒汉式
的写法(DCL)是能够检验你对 volatile
的了解,这也是面试中被问频率较高的问题。小程序
本文将会介绍以下内容:缓存
volatile
的可见性是什么,有什么用volatile
禁止指令重排是个什么东东计算机 CPU
与 主存
交互的逻辑大体如图,CPU
的运算速度是 主存
的 100 倍左右,为了不 CPU
被主存拖慢速度。当CPU
须要一个数据的时候,会先从 L1
找,找到直接使用;L1
中未找到,会去 L2
中,L2
中找不到会去 L3
,L3 找不到再去主存加载到 L3
,再从 L3
加载到 L2
,再从 L2
加载到 L1
;安全
这样提升的计算速度,同时也面临数据不一致问题。微信
主存中如今有一个变量 a=1,CPU1 a+1 以后,将结果 a=2 放入到 L1
去,可是后续代码计算还会用到 a,这时 CPU1 不会将 a=2 同步到主存中去。以后 CPU2
也从主存中取出变量 a(a=1),CPU2 将 a+2 的结果放入到 L1 中。这样就形成了数据不一致问题。缓存一致性协议就是为了解决这个问题的。性能
以上是计算机底层的实现原理,JAVA 在本身的虚拟机中执行,也有本身的内存模型,但无论怎么样,底层仍是依靠的 CPU
指令集达到缓存一致性。JAVA 的内存模型屏蔽了不一样平台缓存一致性协议的不一样实现细节,定义了一套本身的内存模型。spa
java 虚拟机中的变量所有储存在主存中,每一个线程都有本身的工做内存,工做内存中的变量是主存变量的副本拷贝(使用那些变量,拷贝那些),每一个线程只会操做工做内存的变量,当须要保存数据一致性的时候,线程会将工做内存中的变量同步到主存中去。volatile
就是让线程改变了 a 以后,回写到主存中,已达到缓存一致。线程
接下来代码体会一下,带不带 volatile 的区别。code
public class VolatileDemo {
private static int a = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (a == 0) {
}
}, "线程 1").start();
System.out.println("修改 a=1 以前");
Thread.sleep(3000);
a = 1;
System.out.println("修改 a=1 以后");
}
}复制代码
运行这个程序,代码会一直运行,不会中止。这是由于 线程1
的工做内存 a 为 0,而主线程尽管修改了 a,但不会达到线程1从新加载主存中的变量 a。
public class VolatileDemo {
// 代码的区别只是加了 volatile
private static volatile int a = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (a == 0) {
}
}, "线程 1").start();
System.out.println("修改 a=1 以前");
Thread.sleep(3000);
a = 1;
System.out.println("修改 a=1 以后");
}
}复制代码
打印 修改 a=1 以后
程序中止。这是由于 volatile
标记的变量 a,主线程修改以后,并同步回主存,当其余的线程再使用变量 a 的时候,java 内存模型会让线程从主存加载变量 a。这就是 volatile
的 可见性
特色。
java 中的字节码最终都会编译成机器码(CPU 指令)执行,CPU 在保证单线程中执行结果不变的状况下,能够对指令进行指令重排已达到提升执行效率。
public class VolatileOrdering2 {
static int b = 1;
public static void main(String[] args) throws InterruptedException {
int a = 0;
b = 2;
a += 1;
System.out.println(a);
}
}复制代码
上述代码指令重排执行顺序的可能:
int a=0;
a+=1;
System.out.println(a);
int b = 2;复制代码
网上也有人写的 demo 验证可能会发生指令重排的小程序
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(new Runnable() {
public void run() {
//因为线程one先启动,下面这句话让它等一等线程two. 读着可根据本身电脑的实际性能适当调整等待时间.
//shortWait(100000);
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
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);
}
}复制代码
假设指令重排不会发生,那么 result
将不会打印,实际循环 n 次以后会打印 result
。
volatile
能够禁止指令重排。
大体简单理解,加了内存屏障以后,代码分红 1,2,3部分。1 部分代码你怎么指令重排我无论,可是 1 部分代码执行完了以后,必须执行 2 部分代码,再执行 3 部分代码。
public class SingletonDemo {
private static final SingletonDemo INSTANCE = new SingletonDemo();
private SingletonDemo() {
}
public static SingletonDemo getInstance() {
return SingletonDemo.INSTANCE;
}
}复制代码
通常项目中咱们用这种用法便可,简单方便,也没谁闲着无聊利用别的手段给你打破单例。
饿汉式无论你用不用这个单例,只要类加载,单例就给你初始化好了。有的人就想让其懒加载,节约那可怜的内存,用的时候单例再实例化。
public class SingletonDemo1 {
private SingletonDemo1() {
}
public static SingletonDemo1 getInstance() {
System.out.println("SingletonDemo1Holder 类加载");
return SingletonDemo1Holder.getInstance();
}
private static class SingletonDemo1Holder {
private static final SingletonDemo1 INSTANCE = new SingletonDemo1();
public static SingletonDemo1 getInstance() {
return SingletonDemo1Holder.INSTANCE;
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println(SingletonDemo1.getInstance());
System.out.println(SingletonDemo1.getInstance());
}
}复制代码
运行的时候加上这个 -XX:+TraceClassLoading
会打印加载的类。
从图中咱们能够看到调用 SingletonDemo1.getInstance()
的时候,才加载的 SingletonDemo1Holder
类,再实例化单例,达到懒加载的要求。
以上单例的实现看着没啥技术含量,下面介绍一下 DCL
(Double-checked locking),双重检查锁的实现,这也是面试会问到的点。
public class SingletonDemo2 {
// 考点在这里,要不要加 volitale
private volatile static SingletonDemo2 INSTANCE;
private SingletonDemo2() {
}
public static SingletonDemo2 getInstance() {
if (INSTANCE == null) {
synchronized (SingletonDemo2.class) {
if (INSTANCE == null) {
// 对象实例化
INSTANCE = new SingletonDemo2();
}
}
}
return INSTANCE;
}
}复制代码
对象实例化实际能够分为几个步骤:
一、分配对象空间
二、初始化对象
三、将对象指向分配的内存空间
当指令重排的时候,2 和 3 会进行重排序,致使有的线程可能拿到未初始化的对象调用,存在风险问题。
volatile
给咱们带来了变量 可见性
的功能,可是当使用不当,会掉入另外一个 伪共享
的坑。先看 demo.
public class VolatileDemo3 {
private static volatile Demo[] demos = new Demo[2];
// @sun.misc.Contended
private static final class Demo {
private volatile long x = 0L;
}
static {
demos[0] = new Demo();
demos[1] = new Demo();
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (long i = 0; i < 10000_0000L; i++) {
demos[0].x = i;
}
});
Thread thread = new Thread(() -> {
for (long i = 0; i < 10000_0000L; i++) {
demos[1].x = i;
}
});
long start = System.nanoTime();
thread.start();
thread1.start();
thread.join();
thread1.join();
long end = System.nanoTime();
long runSecond = (end - start) / 100_0000;
System.out.println("运行毫秒:" + runSecond);
}
}复制代码
上述代码,存在伪共享的状况,我电脑运行 运行毫秒:2764
// 运行的时候,须要加上参数 -XX:-RestrictContended
public class VolatileDemo3 {
private static volatile Demo[] demos = new Demo[2];
@sun.misc.Contended
private static final class Demo {
private volatile long x = 0L;
}
static {
demos[0] = new Demo();
demos[1] = new Demo();
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (long i = 0; i < 10000_0000L; i++) {
demos[0].x = i;
}
});
Thread thread = new Thread(() -> {
for (long i = 0; i < 10000_0000L; i++) {
demos[1].x = i;
}
});
long start = System.nanoTime();
thread.start();
thread1.start();
thread.join();
thread1.join();
long end = System.nanoTime();
long runSecond = (end - start) / 100_0000;
System.out.println("运行毫秒:" + runSecond);
}
}复制代码
上述代码,使用 @sun.misc.Contended
避免伪共享,我电脑运行 运行毫秒:813
类似的用法在 ConcurrentHashMap
能够看到,
@sun.misc.Contended
static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}复制代码
上述代码展现了伪共享会下降代码的运行速度。什么是伪共享呢。
还记得 Cpu 中的 L1
L2
L3
吗,主存中的数据加载到 Cpu 的高速缓存的最小单位就是 缓存行
(64 bit)。Cpu 的缓存失效,也是以缓存行为单位失效。
当 Cpu
从内存加载数据的时候,它会把可能会用到的数据和目标数据一块儿加载到 L1/L2/L3
中。上述代码的变量 private static volatile Demo[] demos = new Demo[2];
这两个变量被一块儿加载到同一个缓存行中去了,一个线程修改了其中的 demos[0].x
致使缓存行失效,另外一个线程修改 demos[1].x = i;
的时候发现缓存行失效,会去主存从新加载新的数据,两个线程相互影响致使不停从内存加载,运行速度天然下降了。
@sun.misc.Contended
做用就是让其单独在一个缓存行中去。
咱们也能够经过对齐填充,而避免伪共享。
缓存行
一般都是 64 bit。而 long 为 8 个 bit,咱们本身补充 7 个没有用 long 变量就可让 x 和 7个没用的变量单独一个缓存行
public class VolatileDemo3 {
private static volatile Demo[] demos = new Demo[2];
private static final class Demo {
private volatile long x = 0L;
// 缓存行对齐填充的无用数据
private volatile long pading1, pading2, pading3, pading4, pading5, pading6, pading7;
}
static {
demos[0] = new Demo();
demos[1] = new Demo();
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (long i = 0; i < 10000_0000L; i++) {
demos[0].x = i;
}
});
Thread thread = new Thread(() -> {
for (long i = 0; i < 10000_0000L; i++) {
demos[1].x = i;
}
});
long start = System.nanoTime();
thread.start();
thread1.start();
thread.join();
thread1.join();
long end = System.nanoTime();
long runSecond = (end - start) / 100_0000;
System.out.println("运行毫秒:" + runSecond);
}
}复制代码
本文由 张攀钦的博客 www.mflyyou.cn/ 创做。 可自由转载、引用,但需署名做者且注明文章出处。
如转载至微信公众号,请在文末添加做者公众号二维码。微信公众号名称:Mflyyou