工作中JUC中并发&锁的常用知识

回顾

进程/线程

  • 进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
  • 线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程可以利用进程所有拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
  • 总结:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

并发/并行

  • 并发:指两个或多个事件在同一个时间段内发生,并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能
  • 并行:指两个或多个事件在同一时刻发生(同时发生)
    在这里插入图片描述

线程的创建方式(三种)

1.继承Thread类
2.通过实现Runnable接口
没有返回值,效率相比较于Callable较低。
3.使用Callable和Future创建线程

线程的状态

线程一共有6种状态,相互之间可以互相转换。
在这里插入图片描述

wait / sleep 的区别

  • 来自不同的类
    这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类
  • 有没有释放锁(释放资源)
    最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。sleep是线程被调用时,占着cpu去睡觉,其他线程不能占用cpu,os认为该线程正在工作,不会让出系统资源,wait是进入等待池等待,让出系统资源,其他线程可以占用cpu
  • 使用范围不同
    wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(Sleep只是放大问题的发生性)
synchronized(x){
    
    
	//或者wait()
	x.notify()
}
  • 是否需要捕获异常
    sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。

synchronized(底层:队列实现) 和 lock

  • 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • synchronized会自动释放锁(a 线程执行完同步代码会释放锁;b 线程执行过程中发生异常会释放锁),Lock需在finally中必须手工释放锁(unlock()方法释放锁),否则容易造成线程死锁,但是更安全;
  • 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可FairSync,NonFairSync)
  • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

代码对比

// 资源类 OOP
class Ticket {
    
    
    // 属性、方法
    private int number = 30;

    // 卖票的方式
    // synchronized 本质: 队列,锁
    public synchronized void sale(){
    
    
        if (number>0){
    
    
            System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
        }
    }

}
// Lock三部曲
// 1、 new ReentrantLock();
// 2、 lock.lock(); // 加锁
// 3、 finally=>  lock.unlock(); // 解锁
class Ticket2 {
    
    
    // 属性、方法
    private int number = 30;
    Lock lock = new ReentrantLock(); // 默认是非公平锁实现
    // 公平锁FairSync:十分公平,先来先到 缺点:3h->3s
    // 非公平锁NonfairSync: 可以不按照先来后到的顺序
    public void sale(){
    
    
        lock.lock(); // 加锁
        try {
    
    
           // 业务代码
            if (number>0){
    
    
                System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock(); // 解锁
        }
    }

}

单例模式

单例模式,顾名思义就是只有一个实例,并且自己负责创建自己的对象,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
核心代码:构造方法私有化,private
1.饿汉式
饿汉式是最简单的单例模式的写法,保证了线程的安全,在很长的时间里,我都是饿汉模式来完成单例的,因为够简单,后来才知道饿汉式会有一点小问题,看下面的代码:

// 饿汉式单例
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 byte[] data4 = new byte[1024*1024];

    private Hungry(){
    
    

    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
    
    
        return HUNGRY;
    }
}

在Hungry类中,我定义了四个byte数组,当代码一运行,这四个数组就被初始化,并且放入内存了,如果长时间没有用到getInstance方法,不需要Hungry类的对象,这不是一种浪费吗?我希望的是 只有用到了 getInstance方法,才会去初始化单例类,才会加载单例类中的数据。所以就有了 第二种单例模式:懒汉式。

2.懒汉式

正常的 懒汉式单例:

// 懒汉式单例
public class LazyMan1 {
    
    

    private LazyMan1(){
    
    
        System.out.println(Thread.currentThread().getName()+"Start");
    }

    private static LazyMan1 lazyMan;

    public static LazyMan1 getInstance() {
    
    
        if (lazyMan == null) {
    
    
            lazyMan = new LazyMan1();
        }
        return lazyMan;
    }

    // 测试并发环境,发现单例失效
    public static void main(String[] args) {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(() -> {
    
    
                lazyMan.getInstance();
            }).start();
        }
    }
}

多加一层检测可以避免问题,也就是DCL懒汉式!

// 懒汉式单例
public class LazyMan1 {
    
    

    private LazyMan1(){
    
    
        System.out.println(Thread.currentThread().getName()+"Start");
    }

    private static LazyMan1 lazyMan;

    public static LazyMan1 getInstance() {
    
    
        synchronized (LazyMan.class) {
    
    
            if (lazyMan == null) {
    
    
                lazyMan = new LazyMan1();// 不是一个原子性操作
            }
        }
        return lazyMan;
    }

    // 测试并发环境,发现单例失效
    public static void main(String[] args) {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(() -> {
    
    
                lazyMan.getInstance();
            }).start();
        }
    }
}

DCL懒汉式的单例,保证了线程的安全性,又符合了懒加载,只有在用到的时候,才会去初始化,调用效率也比较高,但是这种写法在极端情况还是可能会有一定的问题。因为lazyMan = new LazyMan();不是原子性操作,至少会经过三个步骤:

  1. 分配对象内存空间
  2. 执行构造方法初始化对象
  3. 设置instance指向刚分配的内存地址,此时instance !=null;

由于指令重排,导致A线程执行 lazyMan = new LazyMan();的时候,可能先执行了第三步(还没执行第二步),此时线程B又进来了,发现lazyMan已经不为空了,直接返回了lazyMan,并且后面使用了返回的lazyMan,由于线程A还没有执行第二步,导致此时lazyMan还不完整,可能会有一些意想不到的错误,所以就有了下面一种单例模式。

这种单例模式只是在上面DCL单例模式增加一个volatile关键字来避免指令重排:

private volatile  static LazyMan1 lazyMan;

    public static LazyMan1 getInstance() {
    
    
        synchronized (LazyMan.class) {
    
    
            if (lazyMan == null) {
    
    
                lazyMan = new LazyMan1();// 不是一个原子性操作
            }
        }
        return lazyMan;
    }

3.静态内部类
还有这种方式是第一种饿汉式的改进版本,同样也是在类中定义static变量的对象,并且直接初始化,不过是移到了静态内部类中,十分巧妙。既保证了线程的安全性,同时又满足了懒加载。

// 静态内部类
public class Holder {
    
    
    private Holder(){
    
    

    }
    public static Holder getInstace(){
    
    
        return InnerClass.HOLDER;
    }
    public static class InnerClass{
    
    
        private static final Holder HOLDER = new Holder();
    }
}

4.反射
反射是一个比较霸道的东西,无视private修饰的构造方法,可以直接在外面newInstance,破坏单例模式。

public static void main(String[] args) {
    
        
	try {
    
            
		LazyMan lazyMan1 = LazyMan.getInstance();
		// 使用反射创建lazyMan2        
		Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
		declaredConstructor.setAccessible(true);        
		LazyMan lazyMan2 = declaredConstructor.newInstance();
		System.out.println(lazyMan1.hashCode());
		System.out.println(lazyMan2.hashCode());        
		System.out.println(lazyMan1 == lazyMan2);   
	} catch (Exception e) {
    
            
		e.printStackTrace();   
	}
}

我们分别打印出lazyMan1,lazyMan2的hashcode,lazyMan1是否相等lazyMan2,结果显而易见,不相等
那么,怎么解决这种问题呢?

private LazyMan(){
    
    
        synchronized (LazyMan.class) {
    
    
            if(lazyMan != null) {
    
    
                throw new RuntimeException("不要试图用反射破坏单例模式");
            }
        }
    }

在私有的构造函数中做一个判断,如果lazyMan不为空,说明lazyMan已经被创建过了,如果正常调用getInstance方法,是不会出现这种事情的,所以直接抛出异常!
但是这种写法还是有问题:
上面我们是先正常的调用了getInstance方法,创建了LazyMan对象,所以第二次用反射创建对象,私有构造函数里面的判断起作用了,反射破坏单例模式失败。但是如果破坏者干脆不先调用getInstance方法,一上来就直接用反射创建对象,我们的判断就不生效了:

public static void main(String[] args) {
    
        
        try {
    
            
            Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);       
             declaredConstructor.setAccessible(true);        
            LazyMan lazyMan1 = declaredConstructor.newInstance();        
            LazyMan lazyMan2 = declaredConstructor.newInstance();        
            System.out.println(lazyMan1.hashCode());        
            System.out.println(lazyMan2.hashCode());   
        } catch (Exception e) {
    
            
            e.printStackTrace();   
        }
    }

那么如何防止这种反射破坏呢?

 private LazyMan(){
    
    
        synchronized (LazyMan.class) {
    
    
            if (flag == false) {
    
    
                flag = true;
            } else {
    
    
                throw new RuntimeException("不要试图用反射破坏单例模式");
            }
        }
    }

在这里,我定义了一个boolean变量flag,初始值是false,私有构造函数里面做了一个判断,如果flag=false,就把flag改为true,但是如果flag等于true,就说明有问题了,因为正常的调用是不会第二次跑到私有构造方法的,所以抛出异常。

看起来很美好,但是还是不能完全防止反射破坏单例模式,因为可以利用反射修改flag的值。

public static void main(String[] args) {
    
    
        try {
    
    
            // 通过反射创建对象            
            Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
            Field field = LazyMan.class.getDeclaredField("flag");
            field.setAccessible(true);

            // 通过反射实例化对象            
            declaredConstructor.setAccessible(true);
            LazyMan lazyMan1 = declaredConstructor.newInstance();
            System.out.println(field.get(lazyMan1));
            System.out.println(lazyMan1.hashCode());

            //通过反射,修改字段的值!            
            field.set(lazyMan1,false);
            LazyMan lazyMan2 = declaredConstructor.newInstance();
            System.out.println(field.get(lazyMan2));
            System.out.println(lazyMan2.hashCode());
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

并没有一个很好的方案去避免反射破坏单例模式,
5.枚举
枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

// enum 是一个什么-> 本身也是一个Class类
public enum EnumSingle {
    
    

    INSTANCE;

    public EnumSingle getInstance(){
    
    
        return INSTANCE;
    }

}

class Test{
    
    

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    
    
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }

}

在这里插入图片描述
点击进去declaredConstructor.newInstance();的底层实现:

@CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
    
    
        if (!override) {
    
    
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
    
    
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
    
    
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

线程计数常用辅助类

1.CountDownLatch (计数器减法)

import java.util.concurrent.CountDownLatch;

// 计数器(减法)
public class CountDownLatchDemo {
    
    
    // 必须要执行任务的时候,才使用,原理:
    //CountDownLatch 主要有两个方法,当一个或多个线程(调用 await 方法时,这些线程会阻塞,因为计数器不为0)
    // 其他线程调用CountDown方法会将计数器减1(调用CountDown方法的线程不会阻塞)
    // 当计数器变为0时,await 方法阻塞的线程会被唤醒,继续执行
    public static void main(String[] args) throws InterruptedException {
    
    
        // 案例:教室中的6个人都走了,在执行关门的操作,每走一个学生数量减一
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <=6 ; i++) {
    
    
            new Thread(()->{
    
    
                System.out.println(Thread.currentThread().getName()+" Go out");
                countDownLatch.countDown(); // 数量-1
            },String.valueOf(i)).start();
        }
        countDownLatch.await(); // 等待计数器归零,然后再向下执行(当计数器变成0,await就会被唤醒,执行下面的操作,否则下面的方法不执行)
        System.out.println("Close Door"); //
    }
}

2.CyclicBarrier (计数器加法)

public class CyclicBarrierDemo {
    
    
    public static void main(String[] args) {
    
    
        /**
         * 集齐7颗龙珠召唤神龙
         */
        // 召唤龙珠的线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
    
    
            System.out.println("召唤神龙成功!");
        });

        for (int i = 1; i <=7 ; i++) {
    
    
            final int temp = i;
            // lambda能操作到 i 吗
            new Thread(()->{
    
    
                System.out.println(Thread.currentThread().getName()+"收集"+temp+"个龙珠");
                try {
    
    
                    cyclicBarrier.await(); // 等待(等待这7个线程执行完了,计算器变成7,才会执行上面的线程方法)
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
    
    
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

3.Semaphore(计数信号量)

// 限流
public class SemaphoreDemo {
    
    
    public static void main(String[] args) {
    
    
        // 案例:6个车3个停车位
        // 线程数量:停车位! 限流 -> 一次只能执行三个线程,这个三个线程释放之后,剩下的线程在进来执行
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <=6 ; i++) {
    
    
            new Thread(()->{
    
    
                // acquire() 得到
                try {
    
    
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"离开车位");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } finally {
    
    
                    semaphore.release(); // release() 释放
                }

            },String.valueOf(i)).start();
        }

    }
}

原理:(信号量主要用于两个目的:一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。)
在信号量上我们定义两种操作:
(1)acquire(获取)当一个线程调用 acquire 操作时,他要么通过成功获取信号量(信号量-1)要么一直等下去,直到有线程释放信号量,或超时
(2)release (释放)实际上会将信号量的值 + 1,然后唤醒等待的线程。

读写锁 (ReadWriteLock)

ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
public interface ReadWriteLock {
Lock readLock();// 返回共享锁(读锁)
Lock writeLock(); //返回独占锁(写锁)
}
所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock适用于读多写少的并发情况。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 独占锁(写锁) 一次只能被一个线程占有
 * 共享锁(读锁) 多个线程可以同时占有
 * ReadWriteLock
 * 读-读  可以共存!
 * 读-写  不能共存!
 * 写-写  不能共存!
 */
public class ReadWriteLockDemo {
    
    
    public static void main(String[] args) {
    
    
        MyCache myCache = new MyCache();

        // 写入
        for (int i = 1; i <= 5 ; i++) {
    
    
            final int temp = i;
            new Thread(()->{
    
    
                myCache.put(temp+"",temp+"");
            },String.valueOf(i)).start();
        }

        // 读取
        for (int i = 1; i <= 5 ; i++) {
    
    
            final int temp = i;
            new Thread(()->{
    
    
                myCache.get(temp+"");
            },String.valueOf(i)).start();
        }

    }
}

// 加锁的
class MyCacheLock{
    
    

    private volatile Map<String,Object> map = new HashMap<>();
    // 读写锁: 更加细粒度的控制 (对于写入,一般的锁非公平锁也可以解决)
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    // 存,写入的时候,只希望同时只有一个线程写
    public void put(String key,Object value){
    
    
        readWriteLock.writeLock().lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName()+"写入"+key);
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+"写入OK");
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            readWriteLock.writeLock().unlock();
        }
    }
    // 取,读,所有人都可以读!
    public void get(String key){
    
    
        readWriteLock.readLock().lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName()+"读取"+key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName()+"读取OK");
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            readWriteLock.readLock().unlock();
        }
    }

}

/**
 * 自定义缓存
 */
class MyCache{
    
    

    private volatile Map<String,Object> map = new HashMap<>();

    // 存,写
    public void put(String key,Object value){
    
    
        System.out.println(Thread.currentThread().getName()+"写入"+key);
        map.put(key,value);
        System.out.println(Thread.currentThread().getName()+"写入OK");
    }

    // 取,读
    public void get(String key){
    
    
        System.out.println(Thread.currentThread().getName()+"读取"+key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName()+"读取OK");
    }

}

阻塞队列

在这里插入图片描述
在这里插入图片描述

阻塞队列的用处:

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起。

为什么需要 BlockingQueue?

好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue 都给你一手包办了。在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

队列的四组API?

客户混合使用,根据公司的业务来操作。
在这里插入图片描述

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;

public class Test {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        test4();
    }
    /**
     * 抛出异常
     */
    public static void test1(){
    
    
        // 队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));
        // IllegalStateException: Queue full 抛出异常!
        // System.out.println(blockingQueue.add("d"));

        System.out.println("=-===========");

        System.out.println(blockingQueue.element()); // 查看队首元素是谁
        System.out.println(blockingQueue.remove());


        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());

        // java.util.NoSuchElementException 抛出异常!
        // System.out.println(blockingQueue.remove());
    }

    /**
     * 有返回值,没有异常
     */
    public static void test2(){
    
    
        // 队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));

        System.out.println(blockingQueue.peek());
        // System.out.println(blockingQueue.offer("d")); // false 不抛出异常!
        System.out.println("============================");
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll()); // null  不抛出异常!
    }

    /**
     * 等待,阻塞(一直阻塞)
     */
    public static void test3() throws InterruptedException {
    
    
        // 队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        // 一直阻塞
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
        // blockingQueue.put("d"); // 队列没有位置了,一直阻塞
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take()); // 没有这个元素,一直阻塞

    }


    /**
     * 等待,阻塞(等待超时)
     */
    public static void test4() throws InterruptedException {
    
    
        // 队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        blockingQueue.offer("a");
        blockingQueue.offer("b");
        blockingQueue.offer("c");
        // blockingQueue.offer("d",2,TimeUnit.SECONDS); // 等待超过2秒就退出
        System.out.println("===============");
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        blockingQueue.poll(2,TimeUnit.SECONDS); // 等待超过2秒就退出
    }
}

同步队列

synchronousQueue 没有容量。与其他的 BlockingQueue 不同,SynchronousQueue是一个不存储元素的 BlockingQueue 。每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

import java.sql.Time;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

/**
 * 同步队列 (最多只能存储一个元素)
 * 和其他的BlockingQueue 不一样, SynchronousQueue 不存储元素
 * put了一个元素,必须从里面先take取出来,否则不能在put进去值!
 */
public class SynchronousQueueDemo {
    
    
    public static void main(String[] args) {
    
    
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>(); // 同步队列

        new Thread(()->{
    
    
            try {
    
    
                System.out.println(Thread.currentThread().getName()+" put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName()+" put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName()+" put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        },"T1").start();


        new Thread(()->{
    
    
            try {
    
    
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+"=>"+blockingQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+"=>"+blockingQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+"=>"+blockingQueue.take());
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        },"T2").start();
    }
}

在这里插入图片描述

线程池
必会: 三大方法+七大参数+四种拒绝策略

什么是池化技术?

程序的运行,其本质上,是对系统资源(CPU、内存、磁盘、网络等等)的使用。如何高效的使用这些资源是我们编程优化演进的一个方向。今天说的线程池就是一种对CPU利用的优化手段。
池化技术简单点来说,就是提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用率,提升性能等。在编程领域,比较典型的池化技术有:线程池、连接池、内存池、对象池等。

线程池的好处?

线程池做的工作主要是:控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用,控制最大并发数,管理线程
(1).降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2).提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
(3).提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。

线程池的三大方法?
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo02 {
    
    
    // Executors 看做是一个工具类
    public static void main(String[] args) {
    
    

        // 创建线程池的三大方法
        ExecutorService threadPool1 = Executors.newSingleThreadExecutor();// 单个线程
        ExecutorService threadPool2 = Executors.newFixedThreadPool(5);// 创建固定的线程池的大小
        ExecutorService threadPool3 = Executors.newCachedThreadPool();// 可伸缩的,遇强则强,遇弱则弱

        try {
    
    
            // 只有一个线程执行
            // 最多5个线程执行
            // 不确定的
            for (int i = 0; i < 10; i++) {
    
    
                threadPool1.execute(() -> {
    
    
                    System.out.println(Thread.currentThread().getName() + "  ok");
                });
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            threadPool1.shutdown(); // 用完记得关闭
        }
    }
}

查看三大方法的底层源码,发现本质都是调用了 new ThreadPoolExecutor ( 7 大参数 )

ThreadPoolExecutor 的七大参数?

在这里插入图片描述
上面的各个参数,解释如下:

  1. corePoolSize:在线程池中始终维护的线程个数。
  2. maxPoolSize:在corePooSize已满、队列也满的情况下,扩充线程至此值。
  3. keepAliveTime/TimeUnit:maxPoolSize 中的空闲线程,销毁所需要的时间,总线程数收缩回corePoolSize。
  4. blockingQueue:线程池所用的队列类型。
  5. threadFactory:线程创建工厂,可以自定义,有默认值Executors.defaultThreadFactory()。
  6. RejectedExecutionHandler:corePoolSize已满,队列已满,maxPoolSize 已满,最后的拒绝策略。

正因为会导致这种OOM,所以阿里巴巴Java开发手册要求创建线程的时候不要使用Executors ,使用new ThreadPoolExecutor(七大参数)进行创建:
在这里插入图片描述
(1).corePollSize:核心线程数。在创建了线程池后,线程中没有任何线程,等到有任务到来时才创建线程去执行任务。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
(2).maximumPoolSize:最大线程数。表明线程中最多能够创建的线程数量,此值必须大于等于1。
最大线程到底该如何定义(用处:调优)?
1、CPU 密集型,几核,就是几,可以保持CPu的效率最高!

// 获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());
2、IO  密集型   > 判断你程序中十分耗IO的线程,
    // 程序   15个大型任务 io十分占用资源!

(3).keepAliveTime:空闲的线程保留的时间。
(4).TimeUnit:空闲线程的保留时间单位:

  • TimeUnit.DAYS; //天
  • TimeUnit.HOURS; //小时
  • TimeUnit.MINUTES; //分钟
  • TimeUnit.SECONDS; //秒
  • TimeUnit.MILLISECONDS; //毫秒
  • TimeUnit.MICROSECONDS; //微妙
  • TimeUnit.NANOSECONDS; //纳秒

(5).BlockingQueue< Runnable>:阻塞队列,存储等待执行的任务。参数有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue可选。
(6).ThreadFactory:线程工厂,用来创建线程,一般默认即可
(7).RejectedExecutionHandler:队列已满,而且任务量大于最大线程的异常处理策略。有以下取值 :

  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)T
  • hreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
ThreadPoolExecutor 场景案例:

//举例:8个人进银行办理业务
1、1~2人被受理(核心大小core =2,最大=5)
2、3~5人进入队列(Queue=3)
3、6~8人到最大线程池(扩容大小max)
4、再有人进来就要被拒绝策略接受了
在这里插入图片描述

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

/**
 * new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
 * new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!->main线程执行输出
 * new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
 * new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常!
 */
public class Demo01 {
    
    
    public static void main(String[] args) {
    
    
        List  list = new ArrayList();
        // 自定义线程池!工作 ThreadPoolExecutor
        ExecutorService threadPool = new ThreadPoolExecutor(
                2, //平时就开两个窗口
                5,// 最多就是5个窗口
                3,
                TimeUnit.SECONDS,// 超过3秒窗口没有办理,就关闭3个窗口
                new LinkedBlockingDeque<>(3),// 银行业务候客区,最多3个人,超过3个,出发开启额外的三个窗口
                Executors.defaultThreadFactory(),// 线程工厂
                new ThreadPoolExecutor.DiscardOldestPolicy() //拒绝策略
        );
        try {
    
    
            // 超过 RejectedExecutionException
            for (int i = 1; i <= 9; i++) {
    
    
                // 触发了最大线程数执行,线程池中的5个线程都会开启,因为候客区满了,一共8个人2+3=5<8
                // 最大承载:Deque + max  超出触发拒绝策略(4种)
                threadPool.execute(()->{
    
    
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            // 线程池用完,程序结束,关闭线程池
            threadPool.shutdown();
        }
    }
}

线程是否越多越好?

一个计算为主的程序(专业一点称为CPU密集型程序)。多线程跑的时候,可以充分利用起所有的cpu核心,比如说4个核心的cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。但是如果线程远远超出cpu核心数量 反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。
因此对于cpu密集型的任务来说,线程数等于cpu数是最好的了。
如果是一个磁盘或网络为主的程序(IO密集型)。一个线程处在IO等待的时候,另一个线程还可以在CPU里面跑,有时候CPU闲着没事干,所有的线程都在等着IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道IO的速度比起CPU来是慢到令人发指的。所以开多线程,比方说多线程网络传输,多线程往不同的目录写文件,等等。此时 线程数等于IO任务数是最佳的。

JMM

什么是JMM(Java 内存模型)?
JMM 本身是一种抽象的概念,并不真实存在,它描述的是一组规则或者规范~

JMM 关于同步的规定:
1、线程解锁前,必须把共享变量的值刷新回主内存
2、线程加锁前,必须读取主内存的最新值到自己的工作内存
3、加锁解锁是同一把锁

JMM内存模型:

JMM规定了内存主要划分为主内存和工作内存两种。
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的
在这里插入图片描述

  • lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
  • unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
  • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
  • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
  • store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
  • write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

JMM对这八种指令的使用,制定了如下规则:

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量对一个变量进行unlock操作之前,必须把此变量同步回主内存

线程A感知不到线程B操作了值的变化!如何能够保证线程间可以同步感知这个问题呢?
只需要使用Volatile关键字即可!volatile 保证线程间变量的可见性

volatile

请你谈谈你对volatile的理解?
1、保证可见性
2、不保证原子性
3、禁止指令重排

代码验证可见性

import java.util.concurrent.TimeUnit;

public class JMMDemo {
    
    
    // 不加 volatile,控制台输出1之后,程序程序就会死循环,不会停止!
    // 加 volatile 可以保证可见性
    private volatile static int num = 0;
    public static void main(String[] args) {
    
     // main

        new Thread(()->{
    
     // 线程 1 对主内存的变化不知道的
            while (num==0){
    
    

            }
        }).start();

        try {
    
    
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        num = 1;
        System.out.println(num);
    }
}

不保证原子性
原子性理解:不可分割,完整性,也就是某个线程正在做某个具体的业务的时候,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败。

// volatile 不保证原子性
public class VDemo03 {
    
    

    // 原子类的 Integer
    private volatile static int num = 0;

    // 加上synchronized,输出结果是2万,不加的话输出结果是不是2万
    // 变量num 使用volatile定义输出结果也不是2万
    public static void add(){
    
    
        // num++; // 不是一个原子性操作
    }
    public static void main(String[] args) {
    
    

        //理论上num结果应该为 2 万
        for (int i = 1; i <= 20; i++) {
    
    
            new Thread(()->{
    
    
                for (int j = 0; j < 1000 ; j++) {
    
    
                    add();
                }
            }).start();
        }
        // 存活的线程数量大于2(Java默认的两个线程),表示上面的线程没有执行完
        while (Thread.activeCount()>2){
    
     // main  gc
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + " " + num);
    }
}

为了保证上卖弄的程序输出的值是2万,可以在add方法上加上synchronized,或者使用Lock锁,但是如何不加 synchronized解决?
在这里插入图片描述
JUC中解决:
使用原子类解决原子性问题:

import java.util.concurrent.atomic.AtomicInteger;

// volatile 不保证原子性
public class VDemo02 {
    
    

    // 原子类的 Integer
    private volatile static AtomicInteger num = new AtomicInteger();

    public static void add(){
    
    
        // num++; // 不是一个原子性操作
        num.getAndIncrement(); // AtomicInteger + 1 方法, CAS
    }

    public static void main(String[] args) {
    
    

        //理论上num结果应该为 2 万
        for (int i = 1; i <= 20; i++) {
    
    
            new Thread(()->{
    
    
                for (int j = 0; j < 1000 ; j++) {
    
    
                    add();
                }
            }).start();
        }
        // 存活的线程数量大于2(Java默认的两个线程),表示上面的线程没有执行完
        while (Thread.activeCount()>2){
    
     // main  gc
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + " " + num);
    }
}

CAS

CAS compareAndSet : 比较并交换

import java.util.concurrent.atomic.AtomicInteger;
/** * CAS : 比较并交换 compareAndSet *
 * * 参数:期望值,更新值 *
 public final boolean compareAndSet(int expect, int update) { *   
 return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
 } */
public class CASDemo02 {
    
    
    public static void main(String[] args) {
    
    
        AtomicInteger atomicInteger = new AtomicInteger(5);
        // 期望的是5,后面改为 2020 , 所以结果为 true,2020
        System.out.println(atomicInteger.compareAndSet(5, 2020)+"=>"+atomicInteger.get());
        // 期望的是5,后面改为 1024 , 所以结果为 false,2020
        System.out.println(atomicInteger.compareAndSet(5, 1024)+"=>"+atomicInteger.get());
    }
}

UnSafe类
UnSafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于 sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

atomicInteger.getAndIncrement();// 这里的自增 + 1怎么实现的?

分析:

atomicInteger.getAndIncrement(); // 分析源码,如何实现的 i++ 安全的问题
public final int getAndIncrement() {
    
     // 继续走源码    
     // this 当前对象    
     // valueOffset 内存偏移量,内存地址    
     // 1.固定写死    
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

再次点击进去,发现到了字节码文件:
在这里插入图片描述
需要去到JDK安装目录下的 rt.jar 包下寻找了!而且这个类中的方法大部分都是 native 的方法。
在这里插入图片描述
CAS(CompareAndSwap):比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止(自旋锁)。

ABA问题

比如说一个线程one从内存位置V中取出A,这个时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将 V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
在这里插入图片描述
代码演示

public class CASDemo02 {
    
    
    public static void main(String[] args) {
    
    
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        // 捣乱的线程
        atomicInteger.compareAndSet(2020, 2021);
        System.out.println(atomicInteger.get());
        atomicInteger.compareAndSet(2021, 2020);
        System.out.println(atomicInteger.get());

        // 期望的线程
        atomicInteger.compareAndSet(2020, 2021);
        System.out.println(atomicInteger.get());
    }
}

那么如何解决ABA问题呢? ->乐观锁

原子引用

要解决ABA问题,需要引入原子引用,思想是:乐观锁。
在这里插入图片描述

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class CASDemo {
    
    

    //AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
    // 正常在业务操作,这里面比较的都是一个个对象
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);

    // CAS  compareAndSet : 比较并交换!
    public static void main(String[] args) {
    
    

        new Thread(()->{
    
    
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("a1=>"+stamp);
            try {
    
    
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(1, 2,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println("a2=>"+atomicStampedReference.getStamp());
            System.out.println(atomicStampedReference.compareAndSet(2, 1,
                    atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println("a3=>"+atomicStampedReference.getStamp());
        },"a").start();
        // 跟乐观锁的原理相同!
        new Thread(()->{
    
    
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("b1=>"+stamp);

            try {
    
    
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(atomicStampedReference.compareAndSet(1, 6,
                    stamp, stamp + 1));
            System.out.println("b2=>"+atomicStampedReference.getStamp());
        },"b").start();

    }
}

在这里插入图片描述

公平锁+非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。–>不能插队
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比现申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。–>可以插队

Lock lock = new ReentrantLock();

底层默认就是非公平锁:,对于Synchronized而言,也是一种非公平锁。
在这里插入图片描述

可重入锁(递归锁)

指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
线程可以进入任何一个它已经拥有的锁,所同步着的代码块。 好比家里进入大门之后,就可以进入里面的房间了;
可重入锁最大的作用就是避免死锁

1.Synchronized版本

public class Demo01 {
    
    
    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");
    }
}

2.Lock锁

public class Demo02 {
    
    
    public static void main(String[] args) {
    
    
        Phone2 phone = new Phone2();

        new Thread(()->{
    
    
            phone.sms();
        },"A").start();


        new Thread(()->{
    
    
            phone.sms();
        },"B").start();
    }
}

class Phone2{
    
    
    Lock lock = new ReentrantLock();

    public void sms(){
    
    
        lock.lock(); // 细节问题:lock.lock(); lock.unlock(); // lock 锁必须配对,否则就会死在里面
        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "sms");
            call(); // 这里也有锁
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock();
            lock.unlock();
        }

    }

    public void call(){
    
    

        lock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "call");
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock();
        }
    }
}

自旋锁

是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

/**
 * 自旋锁
 */
public class SpinlockDemo {
    
    
    // int -> 默认是0
    // null -> 默认是null
    AtomicReference<Thread> atomicReference = new AtomicReference<>();


    // 加锁 -> 调用的时候thread不为null,里面的条件是while true
    public void myLock(){
    
    
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "==> mylock");
        // 自旋锁
        while (!atomicReference.compareAndSet(null,thread)){
    
    

        }
    }
    // 解锁
    public void myUnLock(){
    
    
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "==> myUnlock");
        atomicReference.compareAndSet(thread,null);
    }
}

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TestSpinLock {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
//        ReentrantLock reentrantLock = new ReentrantLock();
//        reentrantLock.lock();
//        reentrantLock.unlock();

        // 底层使用的自旋锁CAS
        SpinlockDemo lock = new SpinlockDemo();

        new Thread(()-> {
    
     // 休息3s解锁
            lock.myLock();
            try {
    
    
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.myUnLock();
            }

        },"T1").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()-> {
    
      // 卡在这里,等待T1线程执行完毕释放锁,T2才会执行
            lock.myLock();
            try {
    
    
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.myUnLock();
            }

        },"T2").start();

    }
}

当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_39182939/article/details/113758938