《实战Java高并发程序设计》笔记——同步工具类

一、可重入锁ReentrantLock

1.1 什么是可重入锁

  • 重入锁使用 java.util.concurrent.locks.ReentrantLock 类来实现
  • 重入锁使用的简单例子:
public class ReenterLock implements Runnable {
    
    private static ReentrantLock lock = new ReentrantLock();
    private static int i = 0;

    @Override
    public void run() {
        for (int j = 0; j < 100000; j++) {
            // 加锁
            lock.lock();
            try {
                i++;
            } finally {
                // 释放锁
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReenterLock demo = new ReenterLock();
        Thread t1 = new Thread(demo);
        Thread t2 = new Thread(demo);
        t1.start();
        t2.start();
        // 等待t1、t2运行完再运行主线程打印和
        t1.join();
        t2.join();
        System.out.println("sum: " + i);
    }
}

注意:在使用 lock() 方法时,一定要有相应的 unlock() 方法,并且放在 finally 块中,防止忘记释放锁,导致其它线程无法进入临界区

1.2 为什么叫做可重入

  • 可重入:即一个线程可以反复进入同一个锁
lock.lock();
lock.lock();
try{
    i++;
}finally {
    lock.unlock();
    lock.unlock();
}

注意:如果一个线程多次获取锁,那么在释放锁的时候,也必须释放相同次数的锁。

  • 如果释放次数多了,会得到一个异常
  • 如果释放次数少了,就会继续持有锁,这会导致其它线程无法进入临界区

1.3 可重入锁的高级功能

1.3.1 中断响应

  • 对于 synchronized 来说,一个线程等待锁只有两种情况
    • 获得锁并继续执行
    • 未获得锁继续等待
  • 而对于可重入锁而言,提供了被中断的功能,即:线程在等待锁的过程中,可以响应中断,取消对锁的请求
  • lockInterruptibly() 方法:等待锁的过程中可以响应中断
  • 代码示例:
/**
 * @Title: 可重入锁的中断响应
 * @Description: 两个线程启动后,分别占有一个锁,等待获取对方的锁;主线程被唤醒后,将线程“B”中断,线程"B"中断后释放锁并退出,而A获取锁
 * @author: QianYi
 * @date: 2020/9/9 - 21:13
 */
public class InterruptLock implements Runnable {

    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();

    private boolean flag;

    public InterruptLock(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        try {
            if (flag) {
                try {
                    // 先申请lock1(可对中断响应)
                    lock1.lockInterruptibly();
                    // 睡眠
                    Thread.sleep(1000);
                    // 再申请lock2(可对中断响应)
                    lock2.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + " get lock");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                try {
                    // 先申请lock2(可对中断响应)
                    lock2.lockInterruptibly();
                    // 睡眠
                    Thread.sleep(1000);
                    // 再申请lock2(可对中断响应)
                    lock1.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + " get lock");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            // 释放锁
            if (lock1.isHeldByCurrentThread()) {
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()) {
                lock2.unlock();
            }
            System.out.println(Thread.currentThread().getName() + " 线程退出");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        InterruptLock task1 = new InterruptLock(true);
        InterruptLock task2 = new InterruptLock(false);
        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");
        t1.start();
        t2.start();
        // 主线程睡眠2秒,等待线程t1、t2启动完毕
        Thread.sleep(2000);
        t2.interrupt();
    }
}

执行结果:
在这里插入图片描述

线程 t1 启动后,先请求 lock1,再请求 lock2;线程 t2 启动后,先请求 lock2,再请求 lock1。
main 线程启动后,处于休眠状态,t1、t2 处于死锁状态(此时 t1 占用 lock1,t2 占用 lock2)。
当对 t2 进行中断后,t2 会放弃请求 lock1,同时会释放 lock2,线程 t1 此时可以继续占有 lock2,从而继续执行下去。

1.3.2 限时等待

  • 概念:线程在等待锁时,可以给它指定一个等待时间,超过这个等待时间仍未获取锁,那就返回失败
  • tryLcok() 方法:可以进行限时等待
    • 带参数:两个参数,①等待时长;②计时单位。超过这个时间为获取锁返回 false ,否则返回 true
    • 不带参数:线程申请锁时,不会等待。若申请锁成功立即返回true,否则立即返回false
  • 代码(有参数、限时)
/**
 * @Title: 可重入锁的限时等待(限时)
 * @Description: A线程先拿到锁,睡4秒;而获取锁的时间只有3秒,所以B等待3秒后返回false
 * @author: QianYi
 * @date: 2020/9/9 - 21:25
 */
public class TimeLock implements Runnable {
    private static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        try {
            // 若获取到锁,睡眠4秒
            if (lock.tryLock(3, TimeUnit.SECONDS)) {
                System.out.println(Thread.currentThread().getName() + ":get lock success!");
                Thread.sleep(4000);
            } else {
                // 未获取到,输出信息
                System.out.println(Thread.currentThread().getName() + ":get lock failed!");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            // 这里要加判断,因为不持有锁的线程释放锁会报异常
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        TimeLock task = new TimeLock();
        new Thread(task, "A").start();
        new Thread(task, "B").start();

    }
}

获取锁的时间只有3秒,而获取锁后会睡眠4秒
A、B线程启动后,A先获取锁并睡眠4秒;所以B会等待获取锁,但只会等待3秒时间,所以B会申请锁失败
在这里插入图片描述

  • 代码(无参数、立即返回)
/**
 * @Title: 可重入锁的限时等待(立即返回)
 * @Description: 只要等待一段时间,就会成功
 * @author: QianYi
 * @date: 2020/9/9 - 21:32
 */
public class TryLock implements Runnable {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    private boolean flag;

    public TryLock(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            // 按顺序获取lock1、lock2
            while (true) {
                // 先获取lock1
                if (lock1.tryLock()) {
                    try {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 再获取lock2
                        if (lock2.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getName() + " is over!");
                                return;
                            } finally {
                                lock2.unlock();
                            }
                        }
                    } finally {
                        lock1.unlock();
                    }
                }
            }
        } else {
            // 按顺序获取lock2、lock1
            while (true) {
                // 先获取lock1
                if (lock2.tryLock()) {
                    try {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 再获取lock2
                        if (lock1.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getName() + " is over!");
                                return;
                            } finally {
                                lock1.unlock();
                            }
                        }
                    } finally {
                        lock2.unlock();
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        TryLock task1 = new TryLock(true);
        TryLock task2 = new TryLock(false);
        new Thread(task1, "A").start();
        new Thread(task2, "B").start();
    }
}

t1 先获取 lock1,t2 获取 lock2;然后 t1 申请 lock2,t2 申请 lock1。
一般情况下,这会导致 t1 和 t2 的相互等待,形成死锁。
但使用了 tryLock() 后,由于获取锁失败后会不断地进行尝试,直至成功
在这里插入图片描述

1.3.3公平锁

  • 概念:按照线程到达的先后顺序获取锁;它不会产生饥饿现象。
  • public ReentrantLock(boolean fair)
    • 当参数为false时,是非公平锁
    • 当参数为true时,是公平锁;实现公平锁需要维护一个有序队列,实现成本高,但是性能低下
/**
 * @Title: 可重入锁的公平锁
 * @Description: 设置可重入锁为公平锁,所以它们是交替获取锁的
 * @author: QianYi
 * @date: 2020/9/9 - 21:46
 */
public class FairLock implements Runnable {
    // 公平锁
    private static ReentrantLock fairLock = new ReentrantLock(true);
    // 非公平锁
    //    private static ReentrantLock fairLock = new ReentrantLock(false);

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                fairLock.lock();
                System.out.println(Thread.currentThread().getName() + " get lock!");
            } finally {
                fairLock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        FairLock task = new FairLock();
        new Thread(task, "A").start();
        new Thread(task, "B").start();
    }
}

1.4 相关方法

  • lock() :申请锁,若锁被占用则等待。
  • lockInterruptibly() :申请锁,等待锁的过程中优先响应中断。
  • tryLock() :尝试获取锁,不等待,立即返回。若成功,立即返回true;若失败,立即返回false。
  • tryLock(long time,TimeUnit unit) :在指定时间内尝试获取锁。若成功,立即返回true;若失败,立即返回false。
  • unlock() :释放锁

1.5 可重入的三要素

  • 原子状态:原子状态使用CAS操作来存储当前锁的状态,判断锁是否被别的线程持有了
  • 等待队列:没有获取锁的线程会进入一个等待队列,当有线程释放锁后,就可以从等待队列中唤醒一个线程继续工作
  • 阻塞原语park()和unpark():它们用来挂起和恢复线程。没有得到锁的线程会被挂起

二、 Condition重入锁搭档

2.1 介绍

Conditionwait() 方法和 notify() 方法大致相同。但是 wait() 方法和 notify() 方法是与关键字 synchronized 关键字组合使用的,而 Condition 是与重入锁相关联的

2.2 相关方法

  • void await() throws InterruptedException :使当前线程等待,同时释放锁,在等待锁的过程中可以响应中断
  • void awaitUninterruptibly() :和await()方法大致相同,不同点在于它在等待锁的过程中无法响应中断
  • boolean await(long time, TimeUnit unit) :使当前线程等待一段时间(需要设置时间单位)
  • long awaitNanos(long nanosTimeout) :使当前线程等待一段时间(时间单位默认:纳秒)
  • boolean awaitUntil(Date deadline) :使当前线程等待至某一时刻
  • void signal() :唤醒一个在等待中的线程
  • void signalAll() :唤醒所有在等待中的线程

2.3 代码示例

/**
 * @Title: Condition 的使用
 * @Description:
 * @author: QianYi
 * @date: 2020/9/9 - 23:26
 */
public class ReenterLockConditon implements Runnable {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    @Override
    public void run() {
        try {
            lock.lock();
            // 等待、释放锁
            condition.await();
            System.out.println("线程正在运行!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReenterLockConditon task = new ReenterLockConditon();
        Thread t1 = new Thread(task);
        t1.start();
        System.out.println("等待2秒...");
        Thread.sleep(2000);
        // 通知线程t1继续执行
        lock.lock();
        // 唤醒线程
        condition.signal();
        lock.unlock();
    }
}

三、Semaphore信号量

3.1 信号量和锁的区别

  • 内部锁synchronized和重入锁ReentrantLock,同时只允许一个线程访问一个资源
  • 信号量同一时刻可以允许多个线程访问一个资源

3.2 主要方法

  • public Semaphore(int permits) :构造函数,指定许可数量(默认非公平)
  • public Semaphore(int permits, boolean fair) :构造函数,指定许可数量,指定是否公平
  • public void acquire() :尝试获取一个许可。若无法获得,则会等待,直到有线程释放一个许可或者当前线程被中断
  • public void acquireUninterruptibly() :和acquire类似,但是不响应的中断
  • public boolean tryAcquire() :尝试获取一个许可,不等待,立即返回结果。若成功则立即返回false,若失败立即返回true
  • public boolean tryAcquire(long timeout, TimeUnit unit) :在一段时间内尝试获取一个许可。若成功则立即返回false,若失败立即返回true
  • public void release() :释放一个许可

3.3 代码示例

/**
 * @Title: 信号量Demo
 * @Description:
 * @author: QianYi
 * @date: 2020/9/9 - 23:40
 */
public class SemaphoreDemo implements Runnable {
    // 定义信号量
    private static Semaphore semaphore = new Semaphore(5);

    @Override
    public void run() {
        try {
            // 获取许可
            semaphore.acquire();
            // 操作
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " get it!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放许可
            semaphore.release();
        }
    }

    public static void main(String[] args) {
        SemaphoreDemo semaphoreDemo = new SemaphoreDemo();
        ExecutorService pool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 20; i++) {
            pool.submit(semaphoreDemo);
        }
        // 关闭线程池
        pool.shutdown();
    }
}

四、ReadWriteLock读写锁

4.1 读写锁和普通锁的区别

若线程A1、A2、A3进行写操作,线程B1、B2、B3进行读操作。如果使用重入锁ReentreantLock或内部锁synchronized,所有的读-读、读-写、写-写之间都是互斥的。然而读-读并不会破坏数据的完整性,所以读-读情况下的互斥不合理。这种情况下,可以使用读写锁,通过读写分离机制,使得读-读之间不再互斥

4.2 读写锁的访问约束情况

非互斥互斥
互斥互斥

4.3 适用情况

读次数>写次数。读次数远远大于写次数,则读写锁就可以发挥最大的作用

4.4 代码示例

/**
 * @Title: 读写锁的使用
 * @Description:
 * @author: QianYi
 * @date: 2020/9/9 - 23:55
 */
public class ReadWriteLockDemo {
    private static ReentrantLock lock = new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private int value;

    // 读操作
    public int read(Lock lock) {
        // 加锁
        lock.lock();
        try {
            // 睡眠2秒后打印值
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + " 的读操作: " + value);
            return value;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            lock.unlock();
        }
        return 0;
    }

    // 写操作
    public void write(Lock lock, int newValue) {
        lock.lock();
        try {
            // 睡眠2秒后修改值并打印
            Thread.sleep(2000);
            value = newValue;
            System.out.println(Thread.currentThread().getName() + " 的写操作: " + value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockDemo demo = new ReadWriteLockDemo();
        // 读任务
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                demo.read(readLock);
            }
        };
        // 写任务
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                demo.write(writeLock, new Random().nextInt());
            }
        };
        // 写线程
        for (int i = 0; i < 5; i++) {
            new Thread(r2).start();
        }
        // 读线程
        for (int i = 0; i < 20; i++) {
            new Thread(r1).start();
        }
    }
}

读写操作都会先睡眠2秒,然后再进行相关操作。
这段代码共启用5个写线程,20个读线程。
若使用普通锁,理论上需要50秒;但使用了读写锁,只需12秒

五、CountDownLatch:倒计数器

5.1 概念

CountDownLatch 可以让一个线程等待,直到倒计数器结束,这个线程才会继续执行

5.2 相关方法

  • public CountDownLatch(int count) :构造函数,接收一个整数作为参数,即计数器的计数个数
  • public void await() :直到倒计数器变为0 之前一直等待
  • public boolean await(long timeout, TimeUnit unit) :直到倒计数器变为0 之前一直等待或者时间耗尽
  • public void countDown() :当计数器的计数大于0时,减1

5.3 代码示例

/**
 * @Title: CountDownLatch 的使用
 * @Description:
 * @author: QianYi
 * @date: 2020/9/10 - 0:13
 */
public class CountDownLatchDemo implements Runnable {
    // 定义CountDownLatch,计数器设为 10
    private static final CountDownLatch LATCH = new CountDownLatch(10);

    @Override
    public void run() {
        try {
            // 操作
            Thread.sleep(500);
            System.out.println("完成!");
            // 计数器-1
            LATCH.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatchDemo demo = new CountDownLatchDemo();
        ExecutorService pool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            pool.submit(demo);
        }
        // 等待
        LATCH.await();
        System.out.println("全部完成!!!");
        // 关闭线程池
        pool.shutdown();
    }
}

六、CyclicBarrier:循环栅栏

6.1 概念

CyclicBarrier 可以理解为循环栅栏,意味着这个计数器可以反复使用。

比如:我们可以把这个计数器设置为10,那么凑齐第一批10 个线程后,计数器归0,然后继续凑齐下一批线程。

6.2 代码示例

/**
 * @Title: CyclicBarrier 的使用
 * @Description:
 * @author: QianYi
 * @date: 2020/9/10 - 0:30
 */
public class CyclicBarrierDemo {
    public static class Soldier implements Runnable {
        private String soldier;
        private CyclicBarrier cyclic;

        public Soldier(String soldier, CyclicBarrier cyclic) {
            this.soldier = soldier;
            this.cyclic = cyclic;
        }

        @Override
        public void run() {
            try {
                // 等待
                cyclic.await();
                doSomething();
                // 等待
                cyclic.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }

        public void doSomething() {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(soldier + "任务完成!");
        }
    }

    public static class BarrierRun implements Runnable {
        private boolean flag;
        private int N;

        public BarrierRun(boolean flag, int N) {
            this.flag = flag;
            this.N = N;
        }

        @Override
        public void run() {
            if (flag) {
                System.out.println("司令:[士兵" + N + "个,任务完成!]");
            } else {
                System.out.println("司令:[士兵" + N + "个,集合完毕!]");
                // 修改标志
                flag = true;
            }
        }
    }

    public static void main(String[] args) {
        final int N = 10;
        boolean flag = false;
        Thread[] allSoldiers = new Thread[N];
        CyclicBarrier cyclic = new CyclicBarrier(N, new BarrierRun(flag, N));
        System.out.println("集合队伍!");
        for (int i = 0; i < N; i++) {
            System.out.println("士兵" + i + "报道!");
            allSoldiers[i] = new Thread(new Soldier("士兵" + i, cyclic));
            allSoldiers[i].start();
            //            if (i == 5) allSoldiers[i].interrupt();
        }
    }
}

6.3 两个异常

  • InterruptException :在等待过程中,线程被中断
  • BrokenBarrierException :表示当前CyclicBarrier已经破损了,系统没办法等待所有线程到齐了

例如在73 行加入如下代码

if (i == 5) allSoldiers[i].interrupt();

会得到一个 InterruptException 和9 个 BrokenBarrierExceptionInterruptException 是被线程中断抛出的,而其它的 BrokenBarrierException 则是等待在当前 CyclicBarrier 上的线程抛出的,这个异常可以防止其它的线程进行永久且无谓的等待

七、 LockSupport:线程阻塞工具类

7.1 LockSupport与suspend()、wait()的对比

LockSupport 可以在线程内任意位置让线程阻塞

  • 相比于 Thread.suspend() ,它弥补了由于 resume() 方法先执行而导致线程无法继续执行的情况
  • 相比于 Objec.wait() ,它不需要先获得某个对象的锁,也不会抛出 InterruptException 异常

7.2 LockSupport的原理

LockSupport 类使用类似于信号量的机制。它为每个线程准备了一个许可,

  • 对于 park() 方法
    • 如果许可可用,那么就会消费这个许可(即把许可变为不可用),并立即返回
    • 如果许可不可用,那么就会阻塞
  • 对于 unpark() 方法,使得一个许可变为可用(和信号量不同,许可不能累加,你最多只能拥有一个许可)

7.3 park()和suspend()的区别

  • resume() 方法发生在 suspend() 方法前,系统可能就无法继续往下执行了;但即使 unpark() 方法操作发生在 park() 方法之前,它也可以使下一次得 park() 方法操作立即返回
  • suspend() 挂起线程会给出一个 Runnable 状态,而 park() 方法挂起线程会给出 WAITING 状态,还会标注是 park() 方法引起的

7.4 支持中断

LockSupport.park() 还能支持中断影响,但它不会抛出 InterrruptException 异常,只会默默返回,不过我们可以从 Thread.interrupted() 方法中获得中断标记

八、Guava和RateLimiter限流

8.1 漏桶算法

  • 基本思想:利用一个缓存区,当有请求进入系统时,无论请求得速率如何,都会在缓存区内保存,然后以固定得速率流出缓存区进行处理
  • 特点:无论请求得速率如何,漏桶算法总是以恒定得速率处理数据。漏桶得容积速率是该算法得两个重要参数
    在这里插入图片描述

8.2 令牌桶算法

基本思想:在令牌桶算法中,桶中存放得不是请求,而是令牌。处理程序只有拿到令牌后,才能对请求进行处理。如果没有令牌,处理程序要么丢弃请求,要么等待可用的令牌。为了限时流速,系统会在每个单位之间内产生一定量得令牌放入桶中。一般,桶的容量是有限的,令牌数量超过桶容量时就不再增加。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值