17、单例模式
构造器私有
不允许外部类通过构造器创建对象 (反射机制可以破坏)
1)饿汉式
直接创建类对象。不管用不用
会浪费内存
多线程是安全的(非反射破坏)
package singleMode;
/**
* @author ajun
* Date 2021/7/8
* @version 1.0
* 单例模式:饿汉式
*/
public class HungryDemo {
//直接创建类对象。不管用不用。
private static HungryDemo HUNGRY_DEMO = new HungryDemo();
//私有构造器
private HungryDemo(){
}
//获取实例的对外接口
public static HungryDemo getInstance(){
return HUNGRY_DEMO;
}
}
hungry
英 [ˈhʌŋɡri] 美 [ˈhʌŋɡri]
adj.
感到饿的;饥饿的;挨饿的
2)懒汉式
开始时只定义类对象变量,赋值null。使用的时候再用new创建
节约内存
多线程下是不安全的。因为new操作不是原子性的
package singleMode;
/**
* @author ajun
* Date 2021/7/8
* @version 1.0
* 单例模式:懒汉式
*/
public class LazyDemo {
//先定义类变量,不创建
private static LazyDemo lazyDemo = null;
//私有构造器
private LazyDemo(){
}
//获取类对象的对外接口
public static LazyDemo getInstance(){
// 使用的时候,再用new创建
if(lazyDemo == null){
lazyDemo = new LazyDemo();
}
return lazyDemo;
}
}
3)DCL 懒汉式 (推荐)
DCL:双重检测锁
多线程下不加volatile,new时,虽然有双重检测,但仍有可能会发生指令重排,是不安全的。所以要加上 volatile
package singleMode;
/**
* @author ajun
* Date 2021/7/8
* @version 1.0
* 单例模式:懒汉式(双重检测锁)
*/
public class DCLLazyDemo {
//先定义类变量,不创建
//加上volatile,禁止指令重排
private volatile static DCLLazyDemo dclLazyDemo = null;
//私有构造器
private DCLLazyDemo(){
}
//获取类对象的对外接口
public static DCLLazyDemo getInstance(){
// 使用的时候,再用new创建
// 双重检测锁 DCL
if(dclLazyDemo == null){
synchronized (DCLLazyDemo.class){
if(dclLazyDemo == null){
dclLazyDemo = new DCLLazyDemo();
/**
* 由于对象创建不是原子性操作
* 1. 分配内存空间
* 2. 使用构造器创建对象
* 3. 将对象指向内存空间
*/
/**
* 可能会发生指令重排
* 123
*
* 132
*
* 这是就需使用volatile 关键字来防止指令重排
*/
}
}
}
return dclLazyDemo;
}
//多线程下不加volatile,new时,虽然有双重检测,但仍有可能会发生指令重排,是不安全的。所以要加上 volatile
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
System.out.println(DCLLazyDemo.getInstance());
}).start();
}
}
}
4)静态内部类 (推荐)
package singleMode;
/**
* @author ajun
* Date 2021/7/8
* @version 1.0
* 单例模式:内部静态类
*/
public class InnerDemo {
//私有构造器
private InnerDemo(){
}
//静态内部类
private static class InnerClass{
private static final InnerDemo INNER_DEMO = new InnerDemo();
}
//获取类对象的对外接口
public static InnerDemo getInstance(){
return InnerClass.INNER_DEMO;
}
}
- 采用了类装载的机制来保证初始化实例时只有一个线程
- 静态内部类方式在 外部类 被装载时并 不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载内部类,从而通过内部类完成外部类的实例化
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的
- 优点:线程安全,利用静态内部类特点实现延迟加载,效率高,代码结构清晰
- 结论:推荐使用
5)单例不安全 (反射)
1、一个原生,一个反射生成
package singleMode;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* @author ajun
* Date 2021/7/8
* @version 1.0
* 反射破坏单例模式
*/
public class ReflectionDemo {
//先定义类变量,不创建
//加上volatile,禁止指令重排
private volatile static ReflectionDemo reflectionDemo = null;
//私有构造器
private ReflectionDemo(){
}
//获取类对象的对外接口
public static ReflectionDemo getInstance(){
// 使用的时候,再用new创建
// 双重检测锁 DCL
if(reflectionDemo == null){
synchronized (ReflectionDemo.class){
if(reflectionDemo == null){
reflectionDemo = new ReflectionDemo();
}
}
}
return reflectionDemo;
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
ReflectionDemo reflectionDemo1 = ReflectionDemo.getInstance();
//思路:用反射机制,调用私有构造器创建,绕过 getInstance 方法
Constructor<ReflectionDemo> constructor = ReflectionDemo.class.getDeclaredConstructor();
constructor.setAccessible(true);
ReflectionDemo reflectionDemo2 = constructor.newInstance();
System.out.println(reflectionDemo1);
System.out.println(reflectionDemo2);
}
}
对策
在构造器中判断对象是否存在
2、两个都反射生成
升级对策
增加信号符,做进一步判断
进一步破坏:
用反射破坏私有信号符变量
3、防止反射破坏单例模式终极方案:枚举
6)枚举最终反编译代码
1、枚举类
package singleMode;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* @author ajun
* Date 2021/7/8
* @version 1.0
* 枚举单例
*/
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class test{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
EnumSingle instance1 = EnumSingle.INSTANCE;
//EnumSingle instance2 = EnumSingle.INSTANCE;
Constructor<EnumSingle> enumSingleConstructor = EnumSingle.class.getDeclaredConstructor();//访问无参构造器
enumSingleConstructor.setAccessible(true);//爆破
EnumSingle instance2 = enumSingleConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
2、反编译查看源码
- IDEA
- javap
- jad.exe
3、最终源代码
package singleMode;
public final class EnumSingle extends Enum
{
public static EnumSingle[] values()
{
return (EnumSingle[])$VALUES.clone();
}
public static EnumSingle valueOf(String name)
{
return (EnumSingle)Enum.valueOf(singleMode/EnumSingle, name);
}
//有参构造器:两个参数
private EnumSingle(String s, int i)
{
super(s, i);
}
public EnumSingle getInstance()
{
return INSTANCE;
}
public static final EnumSingle INSTANCE;
private static final EnumSingle $VALUES[];
static
{
INSTANCE = new EnumSingle("INSTANCE", 0);
$VALUES = (new EnumSingle[] {
INSTANCE
});
}
}
4、调用有参构造器破解枚举类
EnumSingle instance1 = EnumSingle.INSTANCE;
//EnumSingle instance2 = EnumSingle.INSTANCE;
Constructor<EnumSingle> enumSingleConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);//获取有参构造器
enumSingleConstructor.setAccessible(true);
EnumSingle instance2 = enumSingleConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
5、总结
枚举类是安全的单例模式,无法通过反射去破坏
18、深入理解CAS
1)什么是CAS ?
compareAndSet:比较并交换
package cas;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author ajun
* Date 2021/7/8
* @version 1.0
* CAS:compareAndSet 比较并更新
*/
public class CASDemo {
public static void main(String[] args) {
//创建原子型Integer类
AtomicInteger atomicInteger = new AtomicInteger(2021);
//期望 并 更新
//public final boolean compareAndSet(int expect, int update)
//如果期望值 expect 达到时,就更新为 update;更新成功返回 true,否则返回 false
//CAS 是 cpu 的并发原语
System.out.println(atomicInteger.compareAndSet(2021, 666));//true
System.out.println(atomicInteger.get());//666
atomicInteger.incrementAndGet();//加1操作;666 + 1 = 667
System.out.println(atomicInteger.get());//667
//期望值1000,实际值667,所以不成功,返回 false
System.out.println(atomicInteger.compareAndSet(1000, 888));//false
System.out.println(atomicInteger.get());//667
}
}
2)Unsafe类
unsafe:不安全的
Unsafe 类可以通过 native 调用 C++ ,从而操作内存
CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作! 如果不是就一直循环
缺点:
- 循环会耗时
- 一次性只能保证一个共享变量的原子性
- ABA问题
3)ABA 问题
ABA问题(狸猫换太子)
package cas;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author ajun
* Date 2021/7/8
* @version 1.0
* CAS:compareAndSet 比较更新
*/
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2021);
//期望 并 更新
//public final boolean compareAndSet(int expect, int update)
//如果期望值 expect 达到时,就更新为 update;更新成功返回 true,否则返回 false
//CAS 是 cpu 的并发原语
//线程A
//把初始值2021改为666,然后再还原为2021
System.out.println(atomicInteger.compareAndSet(2021, 666));//true
System.out.println(atomicInteger.get());//666
System.out.println(atomicInteger.compareAndSet(666, 2021));//true
System.out.println(atomicInteger.get());//2021
//线程B
//调用初始值2021,改为888;这时初始值2021已被线程A修改过,但线程B是不知情的。这就是ABA问题
System.out.println(atomicInteger.compareAndSet(2021, 888));//true
System.out.println(atomicInteger.get());//888
}
}
19、原子引用
解决ABA问题。引入原子引用! 对应的思想: 乐观锁
atomic:原子的
automatic:自动化
- 带版本号的原子操作 !
package aba;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author ajun
* Date 2021/7/9
* @version 1.0
* 解决ABA问题:AtomicStampedReference
* 带版本号的原子引用
*/
public class AtomicDemo {
public static void main(String[] args) {
// 定义原子引用类
// 参数一:初始引用值
// 参数二:版本号
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(10,1);
// 线程A
new Thread(() -> {
// 获得第一次版本号
int stamp1 = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "线程 第1次版本号:" + stamp1);
// 休眠2秒
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改初始值
System.out.println(atomicStampedReference.compareAndSet(
10, // 期望值
66, // 修改后的值
atomicStampedReference.getStamp(), // 当前版本号
atomicStampedReference.getStamp() + 1 // 修改后的版本号
));
// 获得第二次版本号
int stamp2 = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "线程 第2次版本号:" + stamp2);
// 还原初始值
System.out.println(atomicStampedReference.compareAndSet(
66,
10,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1
));
// 获得第三次版本号
int stamp3 = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "线程 第3次版本号:" + stamp3);
},"A").start();
// 线程B
new Thread(() -> {
// 获得第一次版本号
int s1 = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "线程 第1次版本号:" + s1);
// 休眠3秒
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改初始值
System.out.println(atomicStampedReference.compareAndSet(
10, // 期望值
88, // 修改后的值
atomicStampedReference.getStamp(), // 当前版本号
atomicStampedReference.getStamp() + 1 // 修改后的版本号
));
// 获得第二次版本号
int s2 = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "线程 第2次版本号:" + s2);
},"B").start();
}
}
//输出结果
A线程 第1次版本号:1
B线程 第1次版本号:1
true
A线程 第2次版本号:2
true
A线程 第3次版本号:3
true
B线程 第2次版本号:4
测试数值应在
-128 ~ 127
之间,否则测试不通过;
20、各种锁的理解
1)公平锁 非公平锁
-
公平锁:非常公平,先来后到,不允许插队
-
非公平锁:非常不公平,允许插队(默认)
public ReentrantLock() {
sync = new NonfairSync(); //无参默认非公平锁
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();//传参为true为公平锁
}
2)可重入锁 (递归锁)
可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁
- synchronized版本的可重入锁
public class TestLock {
public static void main(String[] args) {
TestPhone phone = new TestPhone();
new Thread(()->{
//在调用sendMessage的方法时已经为phone加上了一把锁
//而call方法由为其加上了一把锁
phone.sendMessage();
},"A").start();
new Thread(()->{
phone.sendMessage();
},"B").start();
}
}
class TestPhone {
public synchronized void sendMessage() {
System.out.println(Thread.currentThread().getName() + "sendMessage");
call();
}
public synchronized void call() {
System.out.println(Thread.currentThread().getName() + "call");
}
}
- Lock版本的可重入锁
如果方法内调用了其它方法,两个方法都有锁,一开始执行外部方法时,就可以拿到相关的所有锁,包括内部方法的锁;不是执行到内部方法语句时才拿到
public class Lock2 {
public static void main(String[] args) {
TestPhone phone = new TestPhone();
new Thread(()->{
//在调用sendMessage的方法时已经为phone加上了一把锁
//而call方法由为其加上了一把锁
phone.sendMessage();
},"A").start();
new Thread(()->{
phone.sendMessage();
},"B").start();
}
}
class TestPhone {
private Lock lock = new ReentrantLock();
public void sendMessage() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " :sendMessage");
call();
} finally {
lock.unlock();
}
}
public void call() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " : call");
} finally {
lock.unlock();
}
}
}
3)自旋锁 spinLock
//自定义自旋锁
public class SpinLockDemo {
//定义原子引用
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "=======>Lock");
//自旋锁
//由两个线程操作
//第一个直接获取成功不需要自旋
//第二个由于thread不为null所以会自旋
while(!atomicReference.compareAndSet(null, thread)){
//如果当前是 null,就改为 thread,相当于加锁
//如果当前不是 null,就循环等待
}
}
//解锁
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "=====> unLock");
//如果当前是 thread ,就改为 null,相当于解锁
atomicReference.compareAndSet(thread, null);
}
public static void main(String[] args) throws InterruptedException {
SpinLockDemo lock = new SpinLockDemo();
new Thread(()->{
lock.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
lock.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
}).start();
}
}
4)死锁
1、介绍
互斥
占有等待
循环等待
不可抢占
2、测试代码
package lock;
import java.util.concurrent.TimeUnit;
/**
* @author ajun
* Date 2021/7/9
* @version 1.0
* 测试死锁
*/
public class KillLockTest {
public static void main(String[] args) {
String a = "a";
String b = "b";
new Thread(new KillLock(a,b)).start();
new Thread(new KillLock(b,a)).start();
}
}
class KillLock implements Runnable{
private String lockA;
private String lockB;
public KillLock(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName() + " 得到了锁:" + lockA + ",正试图获得锁:" + lockB);
// 休眠2秒
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName() + " 得到了锁:" + lockB + ",正试图获得锁:" + lockA);
}
}
}
}
3、排查
用 java 命令查看堆栈信息
1)使用jps -l
定位进程号
2)使用 jstack
查看进程信息,找到死锁问题
jstack 进程号
排查问题的常见方式?
1、日志
2、堆栈信息