【JUC 四】单例模式 CAS Unsafe ABA 原子引用 公平锁 非公平锁 可重入锁 (递归锁) 自旋锁 死锁...

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:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作! 如果不是就一直循环

缺点:

  1. 循环会耗时
  2. 一次性只能保证一个共享变量的原子性
  3. 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、堆栈信息

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

土味儿~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值