多线程和锁

177 阅读11分钟

欢迎大家关注 github.com/hsfxuebao/j… ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

5、多线程各种锁

公平锁/非公平锁/可重入锁/递归锁/自旋锁,谈谈你的理解?请手写一个自旋锁

5.1、公平锁与非公平锁

是个啥玩意儿?

  1. 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。

  2. 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象

  3. 并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    
    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    

公平锁与非公平锁的区别

公平锁

  1. 公平锁:Threads acquire a fair lock in the order in which they requested it
  2. 公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己

非公平锁

  1. 非公平锁:a nonfair lock permits barging:threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested.
  2. 非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

题外话

  1. Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
  2. 对于Synchronized而言,也是一种非公平锁

5.2、可重入锁

可重入锁(又名递归锁)是啥玩意儿?

  1. 可重入锁(也叫做递归锁)指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
  2. 就像有了家门的锁,厕所、书房、厨房就为你敞开了一样
  3. 也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块
  4. ReentrantLock,synchronized 就是一个典型的可重入锁
  5. 可重入锁的最大作用就是避免死锁

可重入锁的代码示例

synchronized 示例

  • 代码

    /*

    • 可重入锁(也就是递归锁)
    • 指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,
    • 在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
    • 也就是说,线程可以进入任何一个它已经拥有的锁所有同步着的代码块。
    • t1 invoked sendSMS() t1线程在外层方法获取锁的时候
    • t1 invoked sendEmail() t1在进入内层方法会自动获取锁
    • t2 invoked sendSMS()
    • t2 invoked sendEmail()

    */ public class RenenterLockDemo { public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "t1").start();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
    

    }

    class Phone { public synchronized void sendSMS() throws Exception { System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()"); sendEmail(); }

    public synchronized void sendEmail() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t invoked sendEmail()");
    }
    

    }

  • 程序运行结果

    t1 invoked sendSMS() t1 invoked sendEmail() t2 invoked sendSMS() t2 invoked sendEmail() 1234

ReentrantLock 示例

  • 代码

    /*

    • 可重入锁(也就是递归锁)
    • 指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,
    • 在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
    • 也就是说,线程可以进入任何一个它已经拥有的锁所有同步着的代码块。

    */ public class RenenterLockDemo { public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { try { phone.get(); } catch (Exception e) { e.printStackTrace(); } }, "t1").start();

        new Thread(() -> {
            try {
                phone.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
    

    }

    class Phone implements Runnable { //Reentrant TEST Lock lock = new ReentrantLock();

    @Override
    public void run() {
        get();
    }
    
    public void get() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "get()");
            set();
        } finally {
            lock.unlock();
        }
    }
    
    public void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "set()");
        } finally {
            lock.unlock();
        }
    }
    

    }

  • 程序运行结果

    t1 get() t1 set() t2 get() t2 set() 1234

锁两次,释放两次

  • 代码

    class Phone implements Runnable { //Reentrant TEST Lock lock = new ReentrantLock();

    @Override
    public void run() {
        get();
    }
    
    public void get() {
        lock.lock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "get()");
            set();
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }
    
    public void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "set()");
        } finally {
            lock.unlock();
        }
    }
    

    }

  • 程序运行结果:正常执行

    t1 get() t1 set() t2 get() t2 set() 1234

锁两次,释放一次

  • 代码

    class Phone implements Runnable { //Reentrant TEST Lock lock = new ReentrantLock();

    @Override
    public void run() {
        get();
    }
    
    public void get() {
        lock.lock();
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "get()");
            set();
        } finally {
            lock.unlock();
        }
    }
    
    public void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "set()");
        } finally {
            lock.unlock();
        }
    }
    

    }

  • 程序运行结果:由于 t1 线程未释放 lock 锁,程序卡死(死锁)

image-20200808132913472

结论:锁几次,就释放几次

5.3、自旋锁

什么是自旋锁?

自旋锁(SpinLock)

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
12345678

自旋锁代码示例

  • 代码:使用 AtomicReference 封装 Thread ,通过 CAS算法实现线程的自旋锁

    /**

    • 写一个自旋锁
    • 自旋锁的好处:循环比较获取直到成功为止,没有类似wait的阻塞。
    • 通过CAS操作完成自旋锁:
    • A线程先进来调用myLock方法自已持有锁5秒钟
    • B随后进来后发现当前有线程持有锁,不是null,
    • 所以只能通过自旋等待,直至A释放锁后B随后抢到

    */ public class SpinLockDemo { // 泛型为 Thread AtomicReference atomicReference = new AtomicReference<>();

    public void myLock() {
        // 获取当前线程
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in ");
        /*
         自旋:
            期望值为 null 表示当前没有线程
            新值为 thread ,即 Thread.currentThread()
          */
        while (!atomicReference.compareAndSet(null, thread)) {
    
        }
    }
    
    public void myUnLock() {
        // 获取当前线程
        Thread thread = Thread.currentThread();
        // 解锁当前线程
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "\t invoked myUnLock()");
    }
    
    public static void main(String[] args) {
        // 原子引用线程
        SpinLockDemo spinLockDemo = new SpinLockDemo();
    
        new Thread(() -> {
            spinLockDemo.myLock(); // 加锁
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnLock(); // 解锁
        }, "AA").start();
    
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        new Thread(() -> {
            spinLockDemo.myLock(); // 加锁
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnLock(); // 解锁
        }, "BB").start();
    }
    

    }

  • 程序运行结果:核心为 CAS 算法

    • 线程 A 先执行,此时期望值为 null ,线程 A 将获得锁,并将期望值设置为线程 A 自身
    • 线程 B 尝试获取锁,发现期望值并不是 null ,就在那儿原地自旋
    • 线程 A 释放锁之后,将期望值设置为 null ,此时线程 B 获得锁,将期望值设置为线程 B 自身
    • 最后线程 B 释放锁

    AA come in BB come in AA invoked myUnLock() BB invoked myUnLock() 1234

5.4、读写锁

独占锁(写锁)、共享锁(读锁)、互斥锁

  1. 独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和synchronized而言都是独占锁
  2. 共享锁:指该锁可以被多个线程所持有
  3. 对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。
  4. 读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

读写锁的代码示例

  • 代码:使用 ReentrantReadWriteLock 完成读锁、写锁分离

    /**

    • 多个线程同时读一个资源类没有问题,所以为了满足并发量,读取共享资源应该可以同时进行。
    • 但是写资源只能有一个线程。
    • 写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断。
    • 小总结:
    • 读-读能共存
    • 读-写不能共存
    • 写-写不能共存
    • 写操作:原子性+独占,整个过程必须是一个完整的统一体,中间不许被分隔,被打断

    public class ReadWriteLockDemo { public static void main(String[] args) { MyCache myCache = new MyCache(); for (int i = 1; i <= 5; i++) { int tempInt = i; new Thread(() -> { myCache.put(tempInt + "", tempInt + ""); }, String.valueOf(i)).start(); }

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

    }

    class MyCache { // 凡缓存,一定要用 volatile 修饰,保证内存可见性 private volatile Map<String, Object> map = new HashMap<>(); // ReentrantReadWriteLock:读写锁 private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {
        reentrantReadWriteLock.writeLock().lock(); // 加写锁
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
            try {
                TimeUnit.MILLISECONDS.sleep(300); // 模拟网络传输,暂停线程一会儿
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t 写入完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantReadWriteLock.writeLock().unlock(); // 释放写锁
        }
    }
    
    public void get(String key) {
        reentrantReadWriteLock.readLock().lock(); // 加读锁
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读取:" + key);
            try {
                TimeUnit.MILLISECONDS.sleep(300); // 模拟网络传输,暂停线程一会儿
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t 读取完成" + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            reentrantReadWriteLock.readLock().unlock(); // 释放读锁
        }
    }
    

    }

  • 程序运行结果:写操作没有被打断

    2 正在写入:2 2 写入完成 3 正在写入:3 3 写入完成 1 正在写入:1 1 写入完成 4 正在写入:4 4 写入完成 5 正在写入:5 5 写入完成 1 正在读取:1 2 正在读取:2 3 正在读取:3 4 正在读取:4 5 正在读取:5 2 读取完成2 4 读取完成4 1 读取完成1 5 读取完成5 3 读取完成3 1234567891011121314151617181920

  • 在ReentrantReadWriteLock中分别维护了一把读锁和一把写锁

    public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { private static final long serialVersionUID = -6992448646407690164L; /** Inner class providing readlock / private final ReentrantReadWriteLock.ReadLock readerLock; /* Inner class providing writelock / private final ReentrantReadWriteLock.WriteLock writerLock; /* Performs all synchronization mechanics */ final Sync sync; 123456789

6、线程通信

CountDownLatch、CyclicBarrier、Semaphore使用过吗?

6.1、CountDownLatch

CountDownLatch 的用法

  1. 让一些线程阻塞,直到另一些线程完成一系列操作后才被唤醒
  2. CountDownLatch 维护了一个计数器,有两个核心方法:countDown()await()
    • 调用 countDown() 方法会将计数器减一
    • 当计数器的值不为零时,线程调用 await() 方法时,会被阻塞
    • 当计数器的值变为0时,因调用 await() 方法被阻塞的线程会被唤醒,继续执行

CountDownLatch 代码示例

  • 代码

    public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { leaveClassroom(); }

    private static void leaveClassroom() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6); // 初始化次数为 6
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t上完自习,离开教室");
                countDownLatch.countDown(); // 计数器减 1
            }, String.valueOf(i)).start();
        }
        countDownLatch.await(); // 等待上述线程执行完成(等待计数减为 0)
        System.out.println(Thread.currentThread().getName() + "\t ******班长最后关门走人");
    }
    

    } 123456789101112131415161718192021222324

  • 程序运行结果:班长等待所有同学都完成自习后,再锁门

    2 上完自习,离开教室 1 上完自习,离开教室 3 上完自习,离开教室 4 上完自习,离开教室 5 上完自习,离开教室 6 上完自习,离开教室 main ******班长最后关门走人 1234567

CountDownLatch + 枚举类的使用

  • 定义枚举类:可以通过 CountryEnum.ONE 获得齐国对应的 CountryEnum 对象

    public enum CountryEnum { ONE(1, "齐"), TWO(2, "楚"), THREE(3, "燕"), FOUR(4, "赵"), FIVE(5, "魏"), SIX(6, "韩");

    private Integer retCode;
    private String retMsg;
    
    CountryEnum(Integer retCode, String retMsg) {
        this.retCode = retCode;
        this.retMsg = retMsg;
    }
    
    public Integer getRetCode() {
        return retCode;
    }
    
    public void setRetCode(Integer retCode) {
        this.retCode = retCode;
    }
    
    public String getRetMsg() {
        return retMsg;
    }
    
    public void setRetMsg(String retMsg) {
        this.retMsg = retMsg;
    }
    
    public static CountryEnum list(int idx) {
        // 获取枚举类中的所有值
        CountryEnum[] countryEnums = CountryEnum.values();
        for (CountryEnum countryEnum : countryEnums) {
            if (idx == countryEnum.getRetCode()) {
                return countryEnum;
            }
        }
        return null;
    }
    

    }

  • 秦灭六国,后一统华夏

    public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { county(); }

    private static void county() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6); // 初始化次数为 6
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 国被灭");
                countDownLatch.countDown(); // 计数器减 1
            }, CountryEnum.list(i).getRetMsg()).start();
        }
        countDownLatch.await(); // 等待上述线程执行完成(等待计数减为 0)
        System.out.println(Thread.currentThread().getName() + "\t ******秦国一统华夏");
    }
    

    } 123456789101112131415161718192021222324

  • 程序运行结果

    齐 国被灭 魏 国被灭 燕 国被灭 楚 国被灭 韩 国被灭 赵 国被灭 main ******秦国一统华夏 1234567

6.2、CyclicBarrier

CyclicBarrier 的使用

  1. CyclicBarrier 字面意思是可循环使用的屏障。它要做的事情是,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续干活
  2. 通过 CyclicBarrie r的 await() 方法,使线程进入屏障
  3. CountDownLatch 是减,而 CyclicBarrier 是加,理解了CountDownLatch,CyclicBarrier 就很容易
  4. 一句话总结:集齐7颗龙珠召唤神龙

CyclicBarrier 代码示例

  • 代码

    public class CyclicBarrierDemo { public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { System.out.println("召唤神龙"); // 龙珠收集完成,召唤神龙 });

        for (int i = 1; i <= 7; i++) {
            final int tempInt = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 收集到第:" + tempInt + "龙珠");
                try {
                    cyclicBarrier.await(); // 等待龙珠收集完成
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
    
            }, String.valueOf(i)).start();
        }
    }
    

    } 1234567891011121314151617181920212223242526272829

  • 程序运行结果(感觉像是线程完成后的回调函数)

    2 收集到第:2龙珠 3 收集到第:3龙珠 1 收集到第:1龙珠 6 收集到第:6龙珠 5 收集到第:5龙珠 4 收集到第:4龙珠 7 收集到第:7龙珠 召唤神龙 12345678

6.3、Semaphore

Semaphore 的使用

  1. Semaphore 即信号量,信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制
  2. 构造器 Semaphore(int) 用于指定共享资源的数目,如果设定为 1 ,则 Semaphore 信号量退化为 Lock 锁或者 synchronized 锁
  3. 调用 semaphore.acquire() 方法获取对共享资源的使用,调用 semaphore.release() 释放对共享资源的占用
  4. 一句话讲明白:抢车位

Semaphore 代码示例

  • 代码:使用 Semaphore 完成对共享资源的并发控制

    public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3); // 模拟3个车位 for (int i = 1; i <= 6; i++) { // 模拟6部车 new Thread(() -> { try { semaphore.acquire(); // 尝试抢车位(获取信号量) System.out.println(Thread.currentThread().getName() + "\t抢到车位"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t停车3秒后离开车位"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); // 释放车位(释放信号量) } }, String.valueOf(i)).start(); } } } 123456789101112131415161718192021222324252627282930

  • 程序运行结果

    2 抢到车位 1 抢到车位 3 抢到车位 3 停车3秒后离开车位 2 停车3秒后离开车位 1 停车3秒后离开车位 5 抢到车位 6 抢到车位 4 抢到车位 4 停车3秒后离开车位 6 停车3秒后离开车位 5 停车3秒后离开车位