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缺点:
- 循环会耗时
- 一次性只能保证一个共享变量的原子性
ABA问题:狸猫换太子
主内存中A=1,线程B都取到之后,B是CAS(1,3),CAS(3,1),两种操作之后a还是=1,A线程取到值后是CAS(1,2),只要B操作够快,a还是1,线程A就不知道中间经历了什么

如果解决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
}
}
死锁
死锁测试,怎么排除死锁
解决问题
- 使用
jps -l定位进程号 - 使用
stack 进程号找到死锁

浙公网安备 33010602011771号