• Java多线程的同步控制记录


    Java多线程的同步控制记录

    一、重入锁

    重入锁完全可以代替 synchronized 关键字。在JDK 1.5 早期版本,重入锁的性能优于 synchronized。JDK 1.6 开始,对于 synchronized 做了大量优化,使得两者性能差距不大。
    

     代码示例:

        private static ReentrantLock lock = new ReentrantLock();
        @Override
        public void run() {
            lock.lock();    // 加锁
            try {
                // doSomething
            } finally {
                lock.unlock();  // 释放锁
            }
        }
    

    PS:因为重入锁需要我们手动加锁/释放锁,比较好实现对于逻辑的控制。

    注意事项:
    1. 加锁后记得释放锁,否则其他线程就没有机会访问(比如,临界区资源)。
    2. 一个线程多次获得锁(其实是同一把锁)。但加了多少次锁,就要释放多少次锁。同时,lock()方法会一直堵塞线程直到释放锁。
    3. 中断响应 : lockInterruptibly()方法是一个可以对中断进行响应的锁申请动作(在等待锁的过程中,可以响应中断)。
    4. 锁申请等待限时 : tryLock()方法给定一个等待时间,让线程自动放弃等待,避免死锁(PS:当前线程会尝试获得锁,如果锁未被其他线程占用,则申请成功返回true;否则返回false)。
    5. 公平锁:
      • 通过ReentrantLock(true)的构造函数来启用公平锁。
      • 按申请时间顺序来获得锁,不会产生饥饿现象。
      • 系统需要维护一个有序队列,实现成本较高且性能相对低下(一般情况下使用非公平锁)。
    ReentrantLock的几个重要方法
    • lock():获得锁,如果锁已经被占用,则等待。
    • lockInterruptibly():获得锁,但优先响应中断。
    • tryLock():立刻/给定时间内尝试获得锁(可以开启公平锁)。
    • unlock():释放锁。

    二、Condition条件

    Condition 与重入锁相关联。利用 Condition 可以让线程在合适的时间内等待,或者在某一个特定的时刻得到通知,继续执行。
    调用代码如下:

        private static ReentrantLock lock = new ReentrantLock();
        private static Condition condition = lock.newCondition();
    
    常用的方法:
    1. await() 当前线程等待,同时释放锁。其他线程可以使用 signal() 或 signalAll() 方法使线程获得锁并继续执行。或者线程中断时,也能跳出等待。
    2. awaitUninterruptibly() 方法与 await() 方法基本相同,,但等待过程中不会响应中断。
    3. signal() 方法唤醒一个在等待中的线程;signalAll() 方法唤醒所有在等待中的线程。
    用一个实例来理解 Condition 对象

    代码如下:

    public class MyThread implements Runnable {
        private static ReentrantLock lock = new ReentrantLock();    //定义重入锁
        private static Condition condition = lock.newCondition();   //定义Condition
    
        @Override
        public void run() {
            try {
                lock.lock();    //调用 await() 需要先获得锁
                condition.await();
                System.out.println("这条线程继续执行!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            MyThread myThread = new MyThread();
            Thread t1 = new Thread(myThread);
            t1.start();
            Thread.sleep(2000);
            //通知 t1 线程继续执行
            lock.lock();    //调用 signal() 需要先获得锁
            condition.signal();
            lock.unlock();  //记得释放锁,不然线程没有继续执行
        }
    }
    

     首先主线程开启副线程(t1)后,休眠2000毫秒。t1线程执行 run() 方法:获得锁后,调用 await() 释放锁并进入等待状态
    主线程被唤醒后,同样需要是获得锁,调用 signal() 唤醒线程但没有释放锁,所以要主动释放锁将其谦让给被唤醒的线程。
    一旦线程被唤醒了,它会尝试着获取与之绑定的重入锁,成功获取后就可以继续执行

    在JDK内部,重入锁和 Condition 对象被广泛地使用。
    

    三、允许多个线程同时访问:信号量(Semaphore)

    信号量是对锁的扩展,可以指定多个线程同时访问某个资源

    • 在构造信号量对象时,必须指定信号量的准入数。
    • 申请信号量使用 acquire() 操作,离开时必须使用 release() 释放信号量(PS:若申请了但没有释放,会使可以进入临界区的线程数量越来越少,直到所有的线程均不可访问)。

    四、倒计时器 CountDownLatch

    这个工具用来控制线程等待,可以让某个线程等待直到倒计时结束,再执行。(PS:就像王者农药一样,需要所有玩家都准备好了才能开始游戏)
    代码示例(来源于《实战Java高并发程序设计》,为了便于理解做了一点修改):

    public class MyThread implements Runnable {
        private static final CountDownLatch end = new CountDownLatch(5);
        private static final MyThread MY_THREAD = new MyThread();
        private static int count = 1;
    
        @Override
        public void run() {
            try {
                Thread.sleep(new Random().nextInt(5) * 1000);
                System.out.println("第 " + count + "条线程结束");
                count++;
                //倒计时减一
                end.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            ExecutorService executorService = Executors.newFixedThreadPool(5);
            for (int i=0; i<5; i++){
                //线程池加入线程
                executorService.submit(MY_THREAD);
            }
            System.out.println("主线程开始执行");
            //主线程等待倒计时结束
            end.await();
            System.out.println("主线程重新执行");
            //关闭线程池
            executorService.shutdown();
        }
    }
    

    注意注意:
    1. 这里的ExecutorService是线程池,在后面的学习中是非常重要的工具(有关其用法以后会学到,这里提前了解一下)。
    2. 这里的 count 其实是线程不安全的,但因为并发量不高影响不大,而且 count 变量只是为了展示而已,其意义也不大。

    输出结果如下:

    主线程开始执行
    第 1条线程结束
    第 2条线程结束
    第 3条线程结束
    第 4条线程结束
    第 5条线程结束
    主线程重新执行
    

    五、线程阻塞工具:LockSupport

    LockSupport 是用来创建锁和其他同步类的基本线程阻塞,其特点:

    • 可以在线程内任意位置堵塞线程。
    • 不需要想获得对象的锁就可以执行;不会抛出 InterruptedException 异常。
    • park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程(PS:不会引起 suspengd() 与 resume() 造成的死锁问题)。
    • park() 方法支持中断,但不会抛出异常。所以需要我们使用 Thread.interrupted() 等方法来获得中断的标记。
    为什么 park() 和 unpark() 不会引起 suspengd() 与 resume() 造成的死锁问题?

    答:
    首先,LockSupport 采用了类似于信号量的许可证机制。它为每一个线程准备了一个许可证(有且只有一个)。若 park() 可以获得许可证,会立刻返回,否则将处于堵塞状态。而 unpark() 会使一个许可证变为可用状态。所以说,即使 unpark() 发生在 park() 之前,也可以保证下一个 park() 操作立刻返回
    其次,处于 park() 挂起状态的线程,会明确地给出一个WAITING状态,还会标记是 park() 引起的。

    六、其他

      1. ReadWriteLock 读写锁:就是读读之间不阻塞(如果读操作次数远远大于写操作,读写锁就可以发挥最大功效,提升系统性能)。
      2. CyclicBarrier 循环栅栏:是 CountDownLatch 的升级版,可以接收一个参数 barrierAction,代表着一次计数完成后系统会执行的动作。
  • 相关阅读:
    Java8 Optional使用方式
    ABAC框架-casbin
    Java数据脱敏(手机号|邮箱号|身份证号|银行卡号)
    使用OpenOffice将office文件转为pdf
    在线审批流设计
    Java 将带逗号的字符串转为List
    Java8 lambda常用操作
    Markdown合并单元格
    本博客已搬迁至rcst.xyz
    涂色(题解)
  • 原文地址:https://www.cnblogs.com/MaxElephant/p/8109507.html
Copyright © 2020-2023  润新知