JUC并发编程07:单例模式、CAS算法和原子引用
单例模式
单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例。
单例模式一般分为两种:饿汉式和懒汉式。
饿汉式,代码如下:
package com.wunian.juc.single;
/**
* 单例模式:饿汉式
* 单例思想:构造器私有
*/
public class Hungry {
//浪费空间 不是我们需要的
private byte[] data=new byte[10*1024*1024];
private Hungry(){}
private final static Hungry HUNGRY=new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
懒汉式,基础版,代码如下:
package com.wunian.juc.single;
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName() + " start");
}
private static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null){
lazy = new Lazy();
}
return lazyMan;
}
public static void main(String[] args) {
// 多线程下单例失效
for (int i = 0; i < 10; i++) {
new Thread(()->{
Lazy.getInstance();
}).start();
}
}
}
DCL
(双重校验锁)懒汉式,代码如下:
package com.wunian.juc.single;
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName() + " start");
}
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null){
synchronized (Lazy.class){
if (lazy == null){
lazy = new Lazy(); // 请你谈谈这个操作!它不是原子性的
// java创建一个对象
// 1、分配内存空间
// 2、执行构造方法,创建对象
// 3、将对象指向空间
// A 先执行13,这个时候对象还没有完成初始化!
// B 发现对象为空,B线程拿到的对象就不是完成的
}
}
}
return lazy;
}
public static void main(String[] args) {
// 多线程下单例失效
for (int i = 0; i < 10; i++) {
new Thread(()->{
Lazy.getInstance();
}).start();
}
}
}
单例之所以安全,是因为构造器私有的。但是构造器私有也不安全,使用反射就可以绕过构造器直接创建对象,代码如下:
package com.wunian.juc.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
/**
* 单例模式:DCL(Double Check Lock 双重校验锁)懒汉式
*/
public class Lazy {
private static boolean protectedCode=false;//标记参数,防止被反编译破坏
private Lazy(){
synchronized (Lazy.class){
if(protectedCode==false){
protectedCode=true;
}else{
//病毒代码、文件无限扩容
throw new RuntimeException("不要试图破坏我的单例模式");
}
}
}
private volatile static Lazy lazy;
public static Lazy getInstance(){
//双重校验锁
if(lazy==null){
synchronized (Lazy.class){
if(lazy==null) {
lazy=new Lazy();//创建对象不是原子性的,还是存在不安全
//java创建一个对象
//1.分配内存空间
//2.执行构造方法,创建对象
//3.将对象指向空间
//如果A先执行1 3,这个时候对象还没完成初始化!B发现对象为空,B线程拿到的对象就不是完成的
}
}
}
return lazy;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
//反射安全吗?,官方推荐我们单例真的是DCL懒汉式吗?
//Lazy lazy1=Lazy.getInstance();
//得到无参构造器
// Constructor<Lazy> declaredConstructors = Lazy.class.getDeclaredConstructor(null);
// Lazy lazy2=declaredConstructors.newInstance();//创建对象
// Lazy lazy3=declaredConstructors.newInstance();//创建对象
// //hashcode不一样,所以还是不安全 反射根本不需要通过构造器
// //System.out.println(lazy1.hashCode());
// System.out.println(lazy2.hashCode());
// System.out.println(lazy3.hashCode());
//如何破坏在反编译过程中的保护参数
Constructor<Lazy> declaredConstructors = Lazy.class.getDeclaredConstructor(null);
declaredConstructors.setAccessible(true);
Lazy lazy4=declaredConstructors.newInstance();//创建对象
//获取参数对象,必须是在知道参数名称的情况下
Field protectedCode=Lazy.class.getDeclaredField("protectedCode");
//重新将参数值设置为false
protectedCode.setAccessible(true);
protectedCode.set(lazy4,false);
Lazy lazy5=declaredConstructors.newInstance();//创建对象
System.out.println(lazy4.hashCode());
System.out.println(lazy5.hashCode());
}
}
这个时候就要使用枚举类
了。枚举是一个类,实现了枚举的接口,反射无法破坏枚举。代码如下:
package com.wunian.juc.single;
import com.sun.org.apache.bcel.internal.generic.INSTANCEOF;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 单例模式:使用枚举类
* 枚举是一个类,实现了枚举的接口
* 反射 无法破坏枚举
*/
public enum SingleEnum {
INSTANCE;
public SingleEnum getInstance(){
return INSTANCE;
}
}
//至少在做一个普通的jvm的时候,jdk源码没有被修改的时候,枚举就是安全的
//可以通过修改 jdk/jre/lib/rt.jar中java.lang.reflect.Constructor.class来破坏枚举(见Constructor类)
class Demo{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//通过jad.exe反编译可知,SingleEnum类只有一个有参构造器
//Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(null);
Constructor<SingleEnum> declaredConstructor=SingleEnum.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
// throw new IllegalArgumentException("Cannot reflectively create enum objects");
SingleEnum singleEnum1=declaredConstructor.newInstance();
SingleEnum singleEnum2=declaredConstructor.newInstance();
System.out.println(singleEnum1.hashCode());
System.out.println(singleEnum2.hashCode());
//这里面没有无参构造!JVM才是王道
//java.lang.NoSuchMethodException: com.wunian.juc.single.SingleEnum.<init>()
}
}
运行代码会发现,报了一个异常:Cannot reflectively create enum objects
,因此无法通过反射破坏枚举。但是如果修改jdk源码,枚举也可能变得不安全,但至少一般情况下枚举还是安全的。
CAS算法
CAS,CompareAndSwap
,比较并交换。CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
示例代码如下:
package com.wunian.juc.jmm;
import java.util.concurrent.atomic.AtomicInteger;
/**
* CAS:比较并交换
*/
public class CompareAndSwapDemo {
public static void main(String[] args) {
//AtomicInteger默认为0
AtomicInteger atomicInteger=new AtomicInteger(5);
//compareAndSwap CAS 比较并交换
//如果这个值是期望的值,则更新为指定的值,交换成功返回true,否则返回false
System.out.println(atomicInteger.compareAndSet(5,20));
System.out.println(atomicInteger.get());//输出当前的值
System.out.println(atomicInteger.compareAndSet(20, 5));
}
}
分析AtomicInteger类的getAndIncrement方法
getAndIncrement
方法实现了int++的原子性操作,它底层是如何实现的呢?来看看它的源码:
// unsafe可以直接操作内存
public final int getAndIncrement() {
// this 调用的对象
// valueOffset 当前这个对象的值的内存地址偏移值
// 1
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5; // ?
do { // 自旋锁(就是一直判断!)
// var5 = 获得当前对象的内存地址中的值!
var5 = this.getIntVolatile(this, valueOffset); // 1000万
// compareAndSwapInt 比较并交换
// 比较当前的值 var1 对象的var2地址中的值是不是 var5,如果是则更新为 var5 + 1
// 如果是期望的值,就交换,否则就不交换!
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
可以看出,getAndIncrement的底层是通过自旋锁
和CAS算法
来实现的。
CAS的缺点
循环开销很大。
内存操作,每次只能保证一个共享变量的原子性。
可能出现ABA问题。
原子引用
什么是ABA问题?
简单来说就是狸猫换太子,例如有两个线程T1和T2,T1线程希望通过CAS算法将一个变量的值由100更新为1,结果在更新过程中睡眠了3秒,在这三秒中T2线程也通过CAS算法先将该变量值更新由100更新为1,然后又将该变量值由1再次更新为100,整个过程看起来似乎该变量的值没有改变,但是对于T1线程来说,数据已经改动了。
如何解决ABA问题?
可以使用原子类,通过增加一个版本号来解决,原理和乐观锁一样。例如,小明和小花同时更新一个数据,小明先睡了三秒,结果数据先被小花更新了,这时版本号发生变化,小明再去更新就会失败,代码如下:
package com.wunian.juc.jmm;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* ABA问题 1-100-1
* 通过version字段加1来实现数据的原子性
*/
public class ABADemo {
//version =1
static AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
//其他人员 小花 需要每次执行完毕+1
new Thread(()->{
int stamp=atomicStampedReference.getStamp();//获得版本号
System.out.println("T1 stamp01=>"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp()+1);
System.out.println("T1 stamp02=>"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp()+1);
System.out.println("T1 stamp03=>"+atomicStampedReference.getStamp());
},"T1").start();
//乐观的小明,sleep过程中数据被小花改过,版本号发生变化,无法完成更新
new Thread(()->{
int stamp=atomicStampedReference.getStamp();//获得版本号
System.out.println("T2 stamp01=>"+stamp);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result=atomicStampedReference.compareAndSet(100,1,stamp,stamp+1);
System.out.println("T2是否修改成功:"+result);
System.out.println("T2 stamp02=>"+atomicStampedReference.getStamp());
System.out.println("T2 当前获取得最新的值=>"+atomicStampedReference.getReference());
},"T2").start();
}
}
推荐阅读
扫码关注“人玉林风”,获取更多精彩内容。