目录
一、单例模式
什么是单例模式:一个类只有一个实例
单例模式的几种实现方式:
- 饿汉式
- 一般懒汉式,线程不安全
- 加锁懒汉式,线程安全
- DCL双重校验锁
- 静态内部类
- 枚举
1.饿汉式
特点
- 构造方法私有化
- 定义成员变量new一个实例作为初始值(饿)
- 提供获取实例的静态方法
- 优点:没有加锁,执行效率会提高
- 缺点:类加载时就初始化,浪费内存资源
举例:
/**
* 饿汉式
*/
public class HungryMan {
// 因为是getInstance是静态方法,因此程序开始,HungryMan对象就被加载进内存,因此内存中就存在了下面data1-data4的数据,导致浪费内存空间
private byte[] data1 = new byte[1024 * 1024];
private byte[] data2 = new byte[1024 * 1024];
private byte[] data3 = new byte[1024 * 1024];
private byte[] data4 = new byte[1024 * 1024];
// 私有构造方法
private HungryMan(){
}
private static final HungryMan HUNGRY_MAN = new HungryMan();
public static HungryMan getInstance(){
return HUNGRY_MAN;
}
}
2.一般懒汉式
特点
- 需要使用单例时,单例才初始化占用内存
- 单线程下没问题,但是多线程下是不安全的,会出现创建多个不同实例
- 这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程,因为没有加锁 synchronized,所以严格意义上它并不算单例模式
举例:
/**
* 一般懒汉式
* 单线程下没问题,但是多线程有可能出现问题
*/
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan(){
System.out.println(Thread.currentThread().getName() + " 线程拿到了实例!");
/*输出 发现多线程下懒汉式 不能保证一个单例
1 线程拿到了实例!
2 线程拿到了实例!
0 线程拿到了实例!
*/
}
public static LazyMan getInstance(){
// 需要使用单例的情况下 且 实例还没有被创建 才创建单例,
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
// 模拟是个线程获取实例
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
},String.valueOf(i)).start();
}
}
}
3.加锁懒汉式
特点
- 相比一般的懒汉式,在获取实例的静态方法前加synchronized关键字,可以保证多线程之间的同步问题,保证了单例模式
- 优点:第一次调用才初始化,避免内存浪费
- 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率
举例:
/**
* 加synchronized懒汉式
*/
public class LazyManSynchronizedMethod {
private static LazyManSynchronizedMethod lazyMan;
private LazyManSynchronizedMethod(){
System.out.println(Thread.currentThread().getName() + " 线程拿到了实例!");
/*输出 发现多线程下懒汉式 不能保证一个单例
0 线程拿到了实例!
*/
}
// 获取单例的方法加上synchronized锁,保证了单例的线程安全
public static synchronized LazyManSynchronizedMethod getInstance(){
// 需要使用单例的情况下 且 实例还没有被创建 才创建单例,
if(lazyMan == null){
lazyMan = new LazyManSynchronizedMethod();
}
return lazyMan;
}
public static void main(String[] args) {
// 模拟是个线程获取实例
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyManSynchronizedMethod.getInstance();
},String.valueOf(i)).start();
}
}
}
4.DCL双重校验锁
特点
- DCL即 double-checked locking
- 相比一般懒汉式,通过if双重判断 + synchronized 同步代码块 来解决保证线程之间的同步问题
- 相比加锁的懒汉式,不是在方法前面加synchronized从而影响效率,这种方法效率更高
- 这种方式采用双锁机制,安全且在多线程情况下能保持高性能
/**
* DCL双重锁 懒汉式
*/
public class LazyManDCL {
private static LazyManDCL lazyMan;
private LazyManDCL(){
System.out.println(Thread.currentThread().getName() + " 线程拿到了实例!");
/*输出 : 0 线程拿到了实例!
发现多线程下DCL懒汉式 能保证一个单例
*/
}
public static LazyManDCL getInstance(){
// DCL 双重校验判断
// 第一次判断,没有实例的时候给类加锁
if(lazyMan == null){
synchronized (LazyManDCL.class){
// 第二次判断,没有实例的时候,创建实例
if(lazyMan == null){
lazyMan = new LazyManDCL();
}
}
}
return lazyMan;
}
public static void main(String[] args) {
// 模拟10个线程获取实例
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyManDCL.getInstance();
},String.valueOf(i)).start();
}
}
}
问题:
- 这样做还是不够的,因为 lazyMan = new LazyManDCL(); 在JMM模型中并不是原子性操作,创建实例大致分为了三步:
- 堆中分配内存空间
- 执行构造方法,初始化实例
- 将实例变量指向内存空间
- 其中最后 两步 的顺序是可以变得,也就是可以按照 1 2 3 或者 1 3 2 的顺序执行,但是在多个线程共同执行时,有可能就会出现问题:
- 第一个线程1执行了 1 3 剩下 2 未执行,因为3 执行了地址已经存在 if(实例 == null )就不再成立,但是此时实例还没构造完成
- 另一个线程2执行到 if(实例 == null) 发现不成立,就会认为对象已经创建成功,直接返回实例,但是得到的实例因为还没有初始化,得到的就是一个null
解决方案:
- 使用volatile关键字禁止指令重排 解决 创建实例非原子性 的问题
- 这才是一个完整的懒汉式单例模式保证线程安全的方案
举例:
/**
* DCL双重锁 懒汉式
*/
public class LazyManDCL {
// 加上 volatile 关键字 解决创建对象指令重排(非原子性)问题
private static volatile LazyManDCL lazyMan;
private LazyManDCL(){
System.out.println(Thread.currentThread().getName() + " 线程拿到了实例!");
/*输出 : 0 线程拿到了实例!
发现多线程下DCL懒汉式 能保证一个单例
*/
}
public static LazyManDCL getInstance(){
// DCL 双重校验判断
// 第一次判断,没有实例的时候给类加锁
if(lazyMan == null){
synchronized (LazyManDCL.class){
// 第二次判断,没有实例的时候 获得单例
if(lazyMan == null){
lazyMan = new LazyManDCL();
}
}
}
return lazyMan;
}
public static void main(String[] args) {
// 模拟10个线程获取实例
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyManDCL.getInstance();
},String.valueOf(i)).start();
}
}
}
5.反射机制破解单例模式(枚举除外)
概念
- 单例模式要求构造方法私有化,达到只有一个实例的目的,但是通过反射可以忽略构造方法的私有化,得到实例,破坏单例模式
- 基本步骤
- 反射获得无参构造器对象
- 无参构造器对象设置忽略构造方法私有化
- 无参构造对象.newInstance()得到实例
举例:
package cn.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class LazyManDCLReflection {
// 加上 volatile 关键字 解决创建对象指令重排(非原子性)问题
private static volatile LazyManDCLReflection lazyMan;
private LazyManDCLReflection(){
System.out.println(Thread.currentThread().getName() + " 线程拿到了实例!");
/*输出 : 0 线程拿到了实例!
发现多线程下DCL懒汉式 能保证一个单例
*/
}
// 获取单例
public static LazyManDCLReflection getInstance(){
// DCL 双重校验判断
// 第一次判断,没有实例的时候给类加锁
if(lazyMan == null){
synchronized (LazyManDCLReflection.class){
// 第二次判断,没有实例的时候 获得单例
if(lazyMan == null){
lazyMan = new LazyManDCLReflection();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 1.单例模式获取
LazyManDCLReflection instance = LazyManDCLReflection.getInstance();
System.out.println(instance);
// 2.反射获取
// 获取LazyManDCLReflection类的无参构造器
Constructor<LazyManDCLReflection> declaredConstructor = LazyManDCLReflection.class.getDeclaredConstructor(null);
// 忽略构造器私有设置
declaredConstructor.setAccessible(true);
// 创建实例
LazyManDCLReflection instance2 = declaredConstructor.newInstance();
System.out.println(instance2);
}
}
输出:
main 线程拿到了实例!
cn.test.LazyManDCLReflection@1540e19d
main 线程拿到了实例!
cn.test.LazyManDCLReflection@677327b6
5.1 解决反射绕过单例模式问题 -- 红绿灯标志
思路:
- 既然反射是通过调用构造器去创建实例的,那么就在私有构造方法中增加一个判断:
- 先定义一个成员变量作为标志:private static Boolean flag = false
- 创建单例调用构造方法时,在构造方法中就将flag = true
- 当第二次通过反射去调用构造器创建对象的时候,因为flag 值改变,就抛出异常处理
- 但是这样仍然存在问题,即虽然 标志位flag 是私有的,还是可以通过反射获取到flag,将标志位flag重新修改为true,又可以重新破坏单例模式
举例:
package cn.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class LazyManDCLReflection {
// 加上 volatile 关键字 解决创建对象指令重排(非原子性)问题
private static volatile LazyManDCLReflection lazyMan;
// 增加一个标志位
private static boolean flag = true;
private LazyManDCLReflection(){
if (flag == true) {
flag = false;
}else {// 进入此代码块,说明有人在使用反射破环单例模式,那么抛出异常
throw new RuntimeException("请勿使用反射破环单例模式!");
}
}
// 获取单例
public static LazyManDCLReflection getInstance(){
// DCL 双重校验判断
// 第一次判断,没有实例的时候给类加锁
if(lazyMan == null){
synchronized (LazyManDCLReflection.class){
// 第二次判断,没有实例的时候 获得单例
if(lazyMan == null){
lazyMan = new LazyManDCLReflection();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 1.单例模式获取
LazyManDCLReflection instance = LazyManDCLReflection.getInstance();
System.out.println(instance);
// 2.反射获取
// 获取LazyManDCLReflection类的无参构造器
Constructor<LazyManDCLReflection> declaredConstructor = LazyManDCLReflection.class.getDeclaredConstructor(null);
// 忽略构造器私有设置
declaredConstructor.setAccessible(true);
// 创建实例
LazyManDCLReflection instance2 = declaredConstructor.newInstance();
System.out.println(instance2);
}
}
输出:
cn.test.LazyManDCLReflection@1540e19d // 第一次正常通过getInstance获取单例
Exception in thread "main" java.lang.reflect.InvocationTargetException // 第二次使用反射获取
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at cn.test.LazyManDCLReflection.main(LazyManDCLReflection.java:55)
Caused by: java.lang.RuntimeException: 请勿使用反射破环单例模式!
at cn.test.LazyManDCLReflection.<init>(LazyManDCLReflection.java:19)
... 5 more
5.2 使用枚举防止反射破环单例模式
关于枚举:
- 使用枚举测试反射破坏单例模式时候,发现报的错不是 Cannot reflectively create enum objects,而是找不到指定构造方法
- 枚举的构造参数虽然在IDE集成环境看到的枚举类的构造器是无参构造,而且通过进入字节码文件的当前目录,cmd进入控制台,输入命令:javap -p Xxx.class 反编译字节码文件查看,枚举类也是只有一个无参构造,但其实是枚举的构造方法是 有两个 参数的构造方法
- enum也是一个特殊的类,可以通过反编译字节码查看 javap -p Xxx.class
D:\software\IDEA\idea_workspace\test001\out\production\test001\cn\test>javap -p EnumSingle
警告: 二进制文件EnumSingle包含cn.test.EnumSingle
Compiled from "EnumSingle.java"
public final class cn.test.EnumSingle extends java.lang.Enum<cn.test.EnumSingle> {// 枚举类也是一个继承Enum的类========================
public static final cn.test.EnumSingle INSTANCE;
private static final cn.test.EnumSingle[] $VALUES;
public static cn.test.EnumSingle[] values();
public static cn.test.EnumSingle valueOf(java.lang.String);
private cn.test.EnumSingle(); // 无参构造=====================================
private static cn.test.EnumSingle getInstance();
static {};
}
- 打开jad生成的反编译文件,才发现枚举类并不是空参构造 ,而是
private EnumSingle(String s, int i) {super(s, i);}
测试反射破坏枚举单例模式:
package cn.test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum EnumSingle {
// 定义一个实例
INSTANCE;
// 私有化构造器
private EnumSingle() {}
// 获取实例
private static EnumSingle getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 测试反射破坏枚举单例模式
// 反射获取到构造器
Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
// 设置忽略构造器私有化
constructor.setAccessible(true);
// 获取实例
EnumSingle enumSingle = constructor.newInstance();
System.out.println(enumSingle);
}
}
输出:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at cn.test.EnumSingle.main(EnumSingle.java:25)
结果分析:
- 显然 反射不能破坏 枚举的单例模式 抛了异常:java.lang.IllegalArgumentException: Cannot reflectively create enum objects
二、理解CAS
概念:
- CAS,compare and swap的缩写,中文翻译成比较并交换。它是乐观锁的一种体现,CAS 操作包含三个操作数 —— 内存值(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
举例:
import java.util.concurrent.atomic.AtomicInteger;
/*
简单cas举栗
*/
public class Demo1 {
public static void main(String[] args) {
AtomicInteger num = new AtomicInteger(100);
// 参数为 (期望值 ,更新后的值),当100与AtomicInteger的默认值相同时,就会将100更新为200
num.compareAndSet(100,200);
System.out.println(num.compareAndSet(200, 300)); // true 更新成功
System.out.println(num.compareAndSet(200, 300)); // false 更新失败
System.out.println(num.get());
}
}
关于Unsafe类
- java不能直接操作地址,需要调用native方法即调用c++操作内存,Unsafe类里面都是native方法
- AtomicsInteger的getAndIncrement()方法底层就是调用的Unsafe类的getAndAddInt()方法
源码:
======= AtomicsInteger 的 cas 方法 =======
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
============== Unsafe 源码 ==========
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;
}
源码理解:
-
以上源码的:
-
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); -
var1:当前对象 var2:当前对象的内存值 var5:从当前对象取出来的内存值
-
this.compareAndSwapInt(var1, var2, var5, var5 + var4 的意思是:取出当前对象var1中的内存值var2与var5作比较,如果相同,就将var5 = var5+var4(这就是一个新值),最后将var5返回
-
本质是自旋锁 -- 判断内存位置的值是否是当前期望的值,是就更新,不是就一直自旋等待
CAS缺点:
- 自旋锁循环会耗时
- 一次性只能保证一个共享变量的原子性
- 会存在ABA问题
三、原子引用解决ABA问题
1.举栗理解ABA问题
- 正常线程1、2之间穿插了一个捣乱线程,但是捣乱线程的操作了数据,又将数据数据恢复到1线程数据修改完成后的状态,2线程正常执行任务,它根本不知道捣乱线程的有修改过数据
代码举例:
package cn.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class LazyManDCLReflection {
private static AtomicInteger num = new AtomicInteger(1000);
public static void main(String[] args) {
new Thread(()->{
num.compareAndSet(1000,2000);
// 获取AtomicInteger的值2000
System.out.println(num.get());
},"正常线程1").start();
// 休眠2s
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 捣乱线程虽然不影响最终的结果,但是正常流程之间加了一个其他操作
new Thread(()->{
num.compareAndSet(2000,1000);
System.out.println(num.get());
num.compareAndSet(1000,2000);
System.out.println(num.get());
},"捣乱线程").start();
// 休眠2s
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
num.compareAndSet(2000,3000);
System.out.println(num.get());
},"正常线程2").start();
}
}
2.ABA的危害
- 一般场景下ABA并不会出现什么问题,但是当涉及到中间过程的时候就会出问题
- 场景:有一天,老铁到ATM机去取款,使用ATM查询之后,老铁发现它银行卡的余额还有
200
,于是老铁想去100
块给女朋友买小礼物,但是老铁取款时,在点击取款按钮后机器卡了一下,滑稽老铁下意识又点了一下,假设这两部取款操作执行图如下:
- 如果没有出现意外,即使按下两次取款按钮也是正常的,但是在这两次CAS操作之间,如图老铁的朋友给它转账了100块,导致第一次CAS扣款100后的余额从100变回到了200,这时第二次CAS操作也会执行成功,导致又被扣款100块,最终余额是100块,这种情况是不合理的,合理的情况应该是第二次CAS仍然失败,最终余额为200元。
3.原子引用解决ABA问题
- 简单理解就是带版本号的原子操作,为了避免ABA出现,因此在每一次数据被修改以后,都会更新一个版本号或者时间戳
- AtomicReference<V> 类可以提供原子引用操作,时间戳可以作为版本号标记
- 另外需要注意AtomicReference<Integer> 因为涉及Integer 127到-128的整数常量池,可能导致CAS操作失败
package cn.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class LazyManDCLReflection {
// AtomicStampedReference第一个参数是内存值,第二个是版本号
private static AtomicStampedReference<Integer> num = new AtomicStampedReference(1, 1);
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
// 打印版本号
System.out.println("A线程版本号:"+ num.getStamp());
// 将num的值改为2,compareAndSet方法:第一个参数是预期值,第二个参数是要修改为的值,死三个参数是版本号,第四个参数是更新版本号
// 操作成功的条件:
// 1. 核对版本号是否与上一次修改后的版本号一致
// 2. 1与num的内存值相等
// 3. 将1改为2
System.out.println(num.compareAndSet(1, 2, num.getStamp(), num.getStamp() + 1));
}, "A").start();
// 线程睡眠2S, 保证A线程执行完成
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
// 打印版本号
System.out.println("捣乱线程版本号1:"+ num.getStamp());
// 将num的值改为3
System.out.println(num.compareAndSet(2, 3, num.getStamp(), num.getStamp() + 1));
// 打印版本号
System.out.println("捣乱线程版本号2:"+ num.getStamp());
// 将num的值改回来
System.out.println(num.compareAndSet(3, 2, num.getStamp(), num.getStamp() + 1));
}, "捣乱线程").start();
// 线程睡眠2S, 保证捣乱线程执行完成
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
// 打印版本号
System.out.println("B线程版本号:"+ num.getStamp());
// 将num的值改为66
System.out.println(num.compareAndSet(2, 66, 1, num.getStamp() + 1));
}, "B").start();
}
}
输出:
A线程版本号:1
true
捣乱线程版本号1:2
true
捣乱线程版本号2:3
true
B线程版本号:4
false