Volatile的学习
首先先介绍三个性质
可见性
可见性代表主内存中变量更新,线程中可以及时获得最新的值。
下面例子证明了线程中可见性的问题
由于发现多次执行都要到主内存中取变量,所以会将变量缓存到线程的工作内存,这样当其他线程更新该变量的时候,该线程无法得知,导致该线程会无限的运行下去。
public class test1 {
private static int flag = 1;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (flag == 1){
}
},"t1");
t1.start();
Thread.sleep(1000);
flag = 2;
}
}
疑问
当我们在这个死循环中加入一个synchronized关键字的话就会将更新
猜测:synchronized会使更新当前线程的工作内存
public class test1 { private static int flag = 1; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ while (flag == 1){ synchronized ("1"){ } } },"t1"); t1.start(); Thread.sleep(1000); flag = 2; } }
原子性
即多线程中指令执行会出现交错,导致数据读取错误。
比如i++的操作就可以在字节码的层面可以被看成以下操作
9 getstatic #9 <com/zhf/test3/test2.i : I> 获得i
12 iconst_1 将1压入操作数栈
13 isub 将两数相减
14 putstatic #9 <com/zhf/test3/test2.i : I> 将i变量存储
然后在多线程的情况下,会出现以下程序出现非0的结果。
public class test2 {
private static int i;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int j = 0; j < 400; j++) {
i++;
}
});
Thread t2 = new Thread(()->{
for (int j = 0; j < 400; j++) {
i--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
设计模式—两阶段停止使用volatile
@Slf4j
public class test3 {
private Thread monitor;
private volatile boolean flag = false;
public static void main(String[] args) {
test3 test3 = new test3();
test3.monitor = new Thread(()->{
while (true){
Thread thread = Thread.currentThread();
if (test3.flag){
log.debug("正在执行后续");
break;
}
try {
log.debug("线程正在执行");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
// 当进程在睡眠过程中被Interrupte()打断此时isInterrupted()为false
// 从而当异常被抓住后会继续执行
// 所以要调用下面方法继续将isInterrupted()置为true
// thread.interrupt();
}
}
});
test3.start();
try {
Thread.sleep(5500);
} catch (InterruptedException e) {
e.printStackTrace();
}
test3.stop();
}
public void stop(){
flag = true;
monitor.interrupt();
}
public void start(){
monitor.start();
}
}
设计模式—犹豫模式
具体实现,最常见的就是单例模式。
首先是饿汉模式,这里的多线程安全是由JVM保证的,对象是在类加载的加载阶段创建的。
class SingtonHungry{
private static Object object = new Object();
// 饿汉模式
public synchronized Object getObject() {
return object;
}
}
其次就是饿汉模式,最常见的不过就是下面的进行多线程安全的方案。文章后面会对其进行优化。
class SingtonLazy{
private Object object;
// 懒汉模式
// 由于这样的话不管有没有创建出对象都要加锁然后才能取对象,性能太差
public synchronized Object getObject() {
if (object == null){
object = new Object();
return object;
}
return object;
}
}
有序性
JVM会对指令进行重排序,其和CPU的流水线操作类似,当需要流水线操作的时候,需要进行优化的时候,就会对CPU指令进行重排序优化。
当操作的顺序变了之后,就会出现问题。可能会导致条件的提前触发等等。
Volatile使用
使用域: Volatile只能在类的静态成员变量或者成员变量上。
volatile标识符能够让线程强制去读主存的该变量的值,保证了线程变量的可见性。
volatile标识符能够让线程去顺序执行该变量的操作,保证了执行变量的语句的有序性
-
在读取该变量时,会为其添加读屏障。在该读屏障之后的代码不会放在读屏障之前执行。
-
在写该变量时,会为其添加写屏障。在该写屏障之前的代码不会在屏障之后执行。
所以在volatile的修饰下,能够保证变量的可见性和有序性,但并不能保证其的原子性。
class SingtonLazy{
// 加上volatile的主要目的就是防止在synchronized内的代码指令重排,正常是先构造好对象然后赋对象地址
// 导致object会被首先赋予了地址,导致其不为null,然而构造方法还没有开始构造
// 被其他的线程拿走会出现使用出错。
private static volatile Object object;
// 懒汉模式
public static Object getObject() {
if (object != null){
return object;
}else{
// 这里可能出现这里的线程还没有为其进行声明对象,但已经由线程进入了等待锁
// 所以需要在这里来一个为空判断。
synchronized (SingtonLazy.class){
// 这里可能会出现指令重排,所以要加上volatile
if(object == null){
object = new Object();
}
return object;
}
}
}
}
实现单例的另外一个方式
public class Singleton {
// 当使用到ObjectHolder才会进行到这个静态内部类的加载,同时才会创建该类
// 也是属于懒汉式
private static class ObjectHolder{
static final Singleton singleton = new Singleton();
}
}
synchronized补充
首先在synchronized代码块中,它会保证代码块中的可见性,原子性和有序性。
有序性仅仅是表现在synchronized的执行后最后的结果都是一样的,并不会阻止JVM在其内部进行代码的重排序。就比如上个例子来说,在synchronized代码块中最后代码的执行结果都是一样的,但可能由于其优化,导致其他线程出错。