Volatile和CAS
volatile
首先,它是一个关键字,可以是一个变量在多线程的情况下保持可见性,也可以保证有序性,禁止指令重新排序
可见性:
A,B两个线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道,使用volatile关键字,会让所有线程都读到变量的修改值,因为volatile将会强制所以线程都会去堆内存中读取变量的值,底层用了缓存一致性协议,比如MESI;但是volatile并不能保证多个线程共同修改变量时所带来的不一致问题,也就是不能保证原子性
有序性:
指令重新排序问题:cpu的速度是内存的100倍,原来执行一条指令的时候它是一步一步的顺序执行,但是现在cpu为了提高效率,它会把指令并发的来执行,第一个指令执行到一半的时候第二个指令可能开始执行了,这就叫流水线式的执行,在这种新的架构的设计基础之上呢想充分的利用这一点,那么就要求你的编译器把你的源码编译完的指令之后可能进行一个指令的重新排序
比如DCL单例,写法如下:
单例:单例的意思就是保证在jvm的内存里永远只有某一类的一个实例,比如在我们的项目中没必要new很多对象,比如sdk对象,权限管理者
饿汉式单例
第一种
/**
* @author 地表最强
* @date 2022/9/2 22:58
* @params
*
* 懒汉式单例:
* 类加载到内存后,被实例化一个单例,JVM保证线程安全
* 简单实用,推荐使用
* 唯一缺点,不管用没有用到,类加载时就完成实例化
*/
public class Test1 {
private static final Test1 INSTANCE = new Test1();
public static Test1 getInstance(){
return INSTANCE;
}
//私有化构造方法
private Test1(){};
public static void main(String[] args) {
Test1 t1 = Test1.getInstance();
Test1 t2 = Test1.getInstance();
System.out.println(t1==t2);
}
}
第二种
使用静态代码块
public class Test2 {
private static final Test2 INSTANCE ;
static {
INSTANCE = new Test2();
}
private Test2(){};
public static Test2 getInstance(){
return INSTANCE;
}
public static void main(String[] args) {
Test2 t1 = Test2.getInstance();
Test2 t2 = Test2.getInstance();
System.out.println(t1==t2);
}
}
如果要求在用的时候才进行初始化,还要线程安全的,只能通过如下懒汉式单例来保证了
懒汉式单例
第一种
方法上加synchronized关键字,保证方法里从头到尾就只有一个线程在执行,每次拿到的是第一个抢到锁的线程创建的对象
虽然达到了所需初始化的目的,但却到来了效率下降,每次调用方法,都需要去获取锁
public class Test3 {
private static Test3 INSTANCE;
private Test3(){};
public static synchronized Test3 getInstance(){
if (INSTANCE == null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Test3();
}
return INSTANCE;
};
public static void main(String[] args) {
Test3 t1 = Test3.getInstance();
Test3 t2 = Test3.getInstance();
System.out.println(t1==t2);
}
}
第二种
双检锁:当第一个线程来了判断ok,确定值为空,然后进行下面的初始化过程,假设第一个线程把这个INSTANCE已经初始化了,第二个线程,第一个线程检查等于空的时候第二个线程也等于空,所以第二个线程在if(INSTANCE==null)这句话的时候停住了,暂停之后第一个线程已经初始化完成了释放锁,第二个线程拿到锁,注意,拿到这把锁的线程还会进行一次检查,由于第一个线程已经初始化完成了,第二个线程就不会去new了,直接返回初始化完成的对象,因此,双检锁是能保证线程安全的,也是更细粒度的一个锁,这叫锁细化,也是锁优化的一步
public class Test4 {
private static volatile Test4 INSTANCE;
private Test4(){};
public static Test4 getInstance(){
if (INSTANCE == null){
synchronized (Test4.class){
if (INSTANCE == null){
INSTANCE = new Test4();
}
}
}
return INSTANCE;
};
public static void main(String[] args) {
Test4 t1 = Test4.getInstance();
Test4 t2 = Test4.getInstance();
System.out.println(t1==t2);
}
}
问题:双检锁单例,需要加volatile关键字吗?
答案:肯定要加,不加volatile会出先指令重排问题,比如第一个线程new 对象经过我们的编译器编译之后分成了三步;1.给对象申请内存空间,赋默认值 2.给成员变量赋初始值 3.把对象指针指向内存空间,当第一个线程new 对象,因为指令重排序,导致先执行了1.3步,这时第二个线程来了,进行判断,对象已经存在了,就把这个半初始化的对象直接返回去用了,所以说synchronized不能保证代码的有序性,需要加volatile关键字来禁止指令重排
那么volatile是怎么保证有序性的呢?
它是通过底层的内存屏障来保证的,如loadfence原语指令(读屏障),storefence原语指令(写屏障)
CAS
cas号称无锁优化,自旋锁
我们通过Atomic类(原子的)来了解它,由于某一些特别常见的操作,老是来回加锁,加锁的情况特别多,所以干脆java就提供了这些常见的操作这么一个类,这些类的内部就是自动带了锁,当然这些锁的实现不是synchronized重量级锁,而是CAS的操作来实现的
举几个简单的例子,凡是以Atomic开头的都是用CAS这只操作来保证线程安全的这么一些个类,比如AtomicInteger的意思就是里面包含了一个int类型,这个int类型的自增count++是线程安全的,还有拿值等等线程安全的,由于我们在工作开发中经常性的有那种需求,一个值所有的线程共同访问它往上递增,所以jdk专门提供了这样的一些类,
AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.incrementAndGet();
System.out.println(atomicInteger);
分析AtomicInteger的incrementAndGet()方法内部的实现原理:
在底层调用了unsafe类的getAndAddInt方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
在unsafe中的getAndAddInt方法里面可以看到调用了compareAndSwapInt方法也就是cas
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
cas方法有三个参数: cas(v,Expected,NewValue)
v第一个参数是要改的那个值;Expected是期望当前这个值会是几;NewValue要设定的新值;
比如:原来的值是3,我这个线程想改这个值的时候一定期望现在的值是3,是3才改,不是的话,说明已经有另外的线程修改了这个值,那我当前线程在cas重新试一下,把期望值改成4,如果没有其他线程修改这个值,那好,就把值改成5
Expected如果对的上期望值,NewValue才会去对其修改,进行新的值设定的时候,这个过程之中来了一个线程把你的值改了怎么办,我就可以在试一遍,或者失败,这个就是cas操作
问题1:
当你判断的时候,发现是我期望的值,还没有进行新增设定的时候值发生了改变怎么办,cas是cpu原语支持,也就是说cas操作是cpu指令级别上的支持,中间不能被打断的
通过:compxhg指令,在操作系统多核的情况下,指令会加lock compxhg
问题2:ABA问题
**描述:**假如说你有一个值,我拿到这个值是1,我想把它改成2,我拿到1用cas操作,期望值是1,准备变成2,在这个过程中,没有其他的线程改过我肯定是可以更改的,但是如果有一个线程先把这个1变成2后来又变回1,中间值更改过,他不会影响我这个cas下面操作,这就是ABA问题.
**解决:**如果是int类型的,最终值是你期望的,也没有关系,这种没有关系可以不去管这个问题,如果你确实想管这个问题可以加版本号,做任何一个值的修改,修改完之后加一,后面检查的时候连带版本号一起检查,还有AtomicStampedReference也可以解决,它内部加了一个时间戳
如果是基本类型:无所谓,不影响结果值
如果是引用类型:就像是你的女朋友和你分手之后又复合,中间经历了别的男人,那她可能不是和你刚在一起的那个样了
扩展知识:Unsafe类
这个类简单了解一下就行,这个类里面的方法非常非常的多,而且这个类除了用反射使用之外,其他不能直接使用,不能直接使用的原因和ClassLoader有关,上面可以看到Atomic类内部下面都是CompareAndSet这样的操作,那个CompareAndSet就是在Unsafe这个类里面完成的.