JUC最终章

单例模式

在运行期间,保证某个类只创建一个实例,保证一个类仅有一个实例,并提供一个访问它的全局访问点。

volatile在单例模式使用频率最高

饿汉式单例:一上来就把对象加载了

public class Hungry{
  //可能会浪费空间
  private byte[] data1 = new byte[1024*1024];
  private byte[] data2 = new byte[1024*1024];
  private byte[] data3 = new byte[1024*1024];
  
  private Hungry(){
    
  }
  private final static Hungry HUNGRY = new Hungry();
  
  public static Hungry getInstance(){
    return HUNGRY;
  }
} 
//饿汉式的缺点就是,可能在还不需要此实例的时候就已经把实例创建出来了,没起到lazy loading的效果。优点就是实现简单,而且安全可靠。

懒汉式单例:用的时候再加载对象

public class LazyMan{
  private LazyMan(){ //只要是单例模式,就构造器私有
    System.out.println(Thread.currentThread().getName()+"ok");
  }
  //private static LazyMan lazyMan;
  private volatile static LazyMan lazyMan;
  //双重检测锁模式 懒汉式单例 DCL懒汉式
  public static LazyMan getInstance(){
    if(lazyMan == null){
      synchronized(lazyMan.class){
        if(lazyMan == null){
          lazyMan = new LazyMan();//不是一个原子性操作
          /*
          1.分配内存空间 2.执行构造方法(LazyMan()),初始化对象 3.把这个对象指向这个空间
          期望:123
          指令重排可能是:132  如果有多个线程,第二个线程的时候,由于先执行3指向空间了,判断不等于null,就return了,但是此时lazyMan还没有完成构造
          所以需要避免指令重排
          */
        }
      }
    }
    return lazyMan;
  }
}
//这里采用了双重校验的方式,对懒汉式单例模式做了线程安全处理。通过加锁,可以保证同时只有一个线程走到第二个判空代码中去,这样保证了只创建 一个实例。这里还用到了volatile关键字来修饰singleton,其最关键的作用是防止指令重排。

静态内部类

public class Holder{
  private Holder(){
    
  }
  
  public static Holder getInstance(){
    return InnerClass.HOLDER;
  }
  public static class InnerClass{
    private static final Holder HOLDER = new Holder();
  }
}
//通过静态内部类的方式实现单例模式是线程安全的,同时静态内部类不会在Singleton类加载时就加载,而是在调用getInstance()方法时才进行加载,达到了懒加载的效果。

//似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。

单例不安全,因为有反射。

所以选择👇

枚举自带单例模式

最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。

//enum本身也是一个class类
public enum EnumSingle{
  INSTANCE;
  
  public EnumSingle getInstance(){
    return INSTANCE;
  }
}
//直接通过Singleton.INSTANCE.getInstance()的方式调用即可。

深入理解CAS

Compare and Swap 比较和替换

CAS有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。

public class CASDemo{
  public static void main(String[] args){
    //atomicXXX 原子类型
    AtomicInteger atomicInteger = new AtomicInteger(2020);//初始值
    //如果我的期望达到了(2020),那么就更新(2021),否则不更新
    atomicInteger.compareAndSet(2020,2021);
  }
}

Unsafe类

AtomicXXX中大量的用到了unsafe,用的是unsafe的CAS操作。

Unsafe类提供了硬件级别的原子操作

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操作。

自旋锁

CAS 要解决的问题就是保证原子操作,也就是说操作一旦开始,就不能被打断,直到操作完成。

在多线程环境下,原子操作是保证线程安全的重要手段。

自旋,自己旋转,翻译成人话就是循环,一般是用一个无限循环实现。

CAS问题

比较当前工作内存中的值和主内存的值,如果这个值是期望的,那么执行操作。如果不是就一直循环。

CAS缺点:

  1. 循环会耗时
  2. 一次性只能保证一个共享变量的原子性

ABA问题:狸猫换太子

主内存中A=1,线程B都取到之后,B是CAS(1,3),CAS(3,1),两种操作之后a还是=1,A线程取到值后是CAS(1,2),只要B操作够快,a还是1,线程A就不知道中间经历了什么

![image-20210203094330494](/Users/wangyiran/Library/Application Support/typora-user-images/image-20210203094330494.png)

如果解决ABA问题?

原子引用👇

原子引用

带版本号的原子操作,对应的思想:乐观锁

⚠️Integer使用了对象缓存机制,默认范围是-128~127,在这个范围内之间的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,可以直接使用==进行判断,这个区间之外的所有数据,都会在堆上产生,不会复用已有对象,要是用equals方法进行判断。

public class CASDemo{
  //如果AtomicStampedReference泛型是一个包装类,要注意对象的引用问题
  static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);//初始值,初始时间戳
  public static void main(String[] main){
    
    new Thread(()->{
      int stamp = atomicStampedReference.getStamp();//获得版本号
      System.out.println("a1=>"+stamp);
      TimeUnit.SECONDS.sleep(1);
      System.out.println(atomicStampedReference.compareAndSet(1,2,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1));//期望值,修改值,期望时间戳,修改时间戳,print出来是true/false
      System.out.println("a2=>"+atomicStampedReference.getStamp());
      System.out.println(atomicStampedReference.compareAndSet(2,1,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1));
      System.out.println("a2=>"+atomicStampedReference.getStamp());
      
    },"a".start());
    
    new Thread(()->{
      int stamp = atomicStampedReference.getStamp();//获得版本号
      System.out.println("b1=>"+stamp);
      TimeUnit.SECONDS.sleep(2);
      System.out.println(atomicStampedReference.compareAndSet(1,6,stamp,stamp+1));
      System.out.println("b2=>"+atomicStampedReference.getStamp());

    },"b".start());
  }
}

公平锁&非公平锁

公平锁:不能插队,必须先来后到

非公平锁:可以插队,默认是非公平锁

Lock lock = new ReentrantLock(); 非公平锁

Lock lock = new ReentrantLock(true); 公平锁

可重入锁

递归锁

拿到了外面的锁就可以拿到里面的锁,自动获得

//两个方法都执行完才会释放锁,正常是一个方法执行完就释放锁
public class Demo1{
  public static void main(String[] args){
    Phone phone = new Phone();

    new Thread(()->{
      phone.sms();
    },"A".start());
    new Thread(()->{
      phone.sms();
    },"B".start());
  }
}
class Phone{
  public synchronized void sms(){
    System.out.println(Thread.currentThread().getName()+"sms");
    call();//这里也有锁
  }
  public synchronized void call(){
    System.out.println(Thread.currentThread().getName()+"call");
  }
}
/*
执行结果:
Asms
Acall
Bsms
Bcall
*/

自旋锁

spinlock

不断的自循环,直到结果为true

public class SpinlockDemo{
  AtomicReference<Thread> atomicReference = new AtomicReference<>();//不赋值就是null
  //加锁
  public void myLock(){
    Thread thread = Thread.currentThread();
    System.out.println(Thread.currentThread.getName());
    //自旋锁CAS
    while(!atomicReference.compareAndSet(null, thread)){
      //如果现在是null,就把thread放进去
    }
  }
  //解锁
  public void myUnlock(){
    Thread thread = Thread.currentThread();
    System.out.println(Thread.currentThread.getName());
    atomicReference.compareAndSet(thread, null);//如果现在是thread,就改为null
  }
}

死锁

死锁测试,怎么排除死锁

解决问题

  1. 使用jps -l 定位进程号
  2. 使用stack 进程号 找到死锁
posted @ 2021-02-23 10:51  GladysChloe  阅读(45)  评论(0)    收藏  举报