大厂之路一由浅入深、并行基础、源码分析一 “J.U.C.L”之重入锁(ReetrantLock)、公平锁、非公平锁及Condition的应用

本文详细探讨了Java并发编程中ReentrantLock的特性,包括其作为可重入互斥锁的性质,公平锁与非公平锁的区别,以及如何通过Condition实现线程间的精确控制。ReentrantLock提供了比synchronized更丰富的功能,如可中断锁等待、定时锁等待和多路通知。此外,文章还通过实例展示了如何避免死锁并实现线程的高效协作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


关键字synchronized的功能扩展:重入锁ReentrantLock

  • ReentrantLock是一个 可重入 的互斥锁,又被称为“ 独占锁 ”。
    • 互斥锁: 在同一个时间点只能被一个线程持有;
    • 可重入: 单个线程能多次获得相同的锁;
  • ReentrantLock分为“公平锁”和“非公平锁”。
    • 区别:体现在获取锁的机制上是否公平
    • ReentraantLock是通过一个 FIFO的等待队列 来管理获取该锁所有线程的。
    • 在“公平锁”的机制下,线程依次排队获取锁
    • “非公平锁”在锁是可获取状态时,按抢占机制,都可去请求锁
  • 在JDK5.0的早期版本中,重入锁的性能远远优于关键字synchronized,但从JDK6.0开始,JDK在关键字synchronized上做了大量的优化,使得两者的性能差距并不大;

重入锁ReentrantLock的功能

  • ① 基本的加锁功能 、可以反复加锁、以及 加锁是对什么加锁?
package com.wwj.lockdemos;

import javax.sound.sampled.FloatControl;
import java.util.concurrent.locks.ReentrantLock;

public class ReenTrantLockDemo implements Runnable {
    ReentrantLock lock = new ReentrantLock();
    public static int count = 0 ;

    @Override
    public void run() {
        for(int i=0 ; i<1000 ; i++){
            try{
                lock.lock();
                lock.lock();
                System.out.print(lock.getHoldCount()+" ");
                count++;
            }finally {
                lock.unlock();
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

//        Thread t1 = new Thread(new ReenTrantLockDemo());   结果是1994,所以说明lock也是对象锁,和synchronized一样,但是底层原理不同
//        Thread t2 = new Thread(new ReenTrantLockDemo());

        ReenTrantLockDemo td = new ReenTrantLockDemo();
        Thread t1 = new Thread(td);
        Thread t2 = new Thread(td);
        t1.start();
        t2.start();
        t1.join(); t2.join();
        System.out.println(count);

    }

}
//结果1:19861 (for循环为10000,因为1000太小了)
//结果2:2 2 2 2 2 2 2... 2000
  • 分析:
    • 通过结果2000可以得到,ReentrantLock实现了基本的加锁功能;
    • 通过2可以得到,ReentrantLock实现了反复加锁(重加锁)
    • 通过19861可以得到,实现的是对象锁
    • 重点:对于19861,这个程序代码,如果把ReentrantLock lock = new ReentrantLock(); 前面加上static,则结果就是20000,因为这就相当于synchronized修饰类方法一样
  • 注意:
    • ReentrantLock虽然持有对象监视器,但是和synchronized持有的对象监视器不同。
    • 手动释放锁unlock()👏👏👏👏👏👏👏👏👏
  • ② 可中断功能(防止死锁)(避免死锁的第一种方法):
package com.wwj;

import java.util.concurrent.locks.ReentrantLock;

public class LockInterruptibilyDemo implements  Runnable{
    public  static ReentrantLock lock1 = new ReentrantLock();
    public  static ReentrantLock lock2 = new ReentrantLock();
    int lock;
    /*
         @lock 控制加锁顺序,方便构造死锁
     */
    public LockInterruptibilyDemo(int lock){
        this.lock = lock;
    }

    @Override
    public void run() {
        try{
            if(lock == 1){
               lock1.lockInterruptibly();
               try{
                   Thread.sleep(500);
               }catch (InterruptedException e){ }
               lock2.lockInterruptibly();
            }else{
               lock2.lockInterruptibly();
               try {
                   Thread.sleep(500);
               }catch (InterruptedException e){ }
               lock1.lockInterruptibly();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            if(lock1.isHeldByCurrentThread())
                lock1.unlock();
            if(lock2.isHeldByCurrentThread())
                lock2.unlock();
            System.out.println(Thread.currentThread().getId() + ":线程退出");
        }
    }

    public static void main(String[] args) throws InterruptedException{

        LockInterruptibilyDemo l1 = new LockInterruptibilyDemo(1);
        LockInterruptibilyDemo l2 = new LockInterruptibilyDemo(2);
        Thread t1 = new Thread(l1);
        Thread t2 = new Thread(l2);
        t1.start();t2.start();
        Thread.sleep(1000);
        t2.interrupt();
    }
}
结果:
java.lang.InterruptedException
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:958)
	at java.base/java.util.concurrent.locks.ReentrantLock$Sync.lockInterruptibly(ReentrantLock.java:161)
	at java.base/java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:372)
	at com.wwj.LockInterruptibilyDemo.run(LockInterruptibilyDemo.java:32)
	at java.base/java.lang.Thread.run(Thread.java:832)
16:线程退出
15:线程退出
  • 程序讲解:
    • 线程 t1和t2启动后,t1先占用lock1,再占用lock2;t2先占用lock2,再占用lock1,这样很容易形成t1和t2之间的相互等待。
    • 程序中的 Thread.sleep(1000); 说明主线程main处于休息状态,此时这两个线程处于死锁的状态
    • t2.interrupt();此时让t2先释放锁,这样就不满足死锁条件了,然后让t1获得锁执行完操作再退出。
  • 锁申请等待限时,防止一直等待锁(避免死锁的第二种方法)
package com.wwj.lockdemos;

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

public class TimeLockDemo implements  Runnable{
    ReentrantLock  lock = new ReentrantLock();
    @Override
    public void run() {
        try {
            if(lock.tryLock(5, TimeUnit.SECONDS)){
                Thread.sleep(6000);
            }else{
                System.out.println("没有得到锁");
            };
        }catch (InterruptedException e){
            e.printStackTrace();
        } finally{
            if(lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        TimeLockDemo td = new TimeLockDemo();
        Thread t1 = new Thread(td);
        Thread t2 = new Thread(td);
        t1.start();t2.start();
    }
}
  • 程序讲解:
    • 这个程序表明如果超过5秒还没有得到锁,就会返回flase,如果成功得到锁,则返回true。
    • 因此让占用锁的线程持有锁长达6秒,故另一个线程无法在5秒内获得锁,因此获得锁失败
    • 再看一个程序:
package com.wwj;

import java.util.concurrent.locks.ReentrantLock;

public class LockInterruptibilyDemo implements  Runnable{
    public  static ReentrantLock lock1 = new ReentrantLock();
    public  static ReentrantLock lock2 = new ReentrantLock();
    int lock;
    /*
         @lock 控制加锁顺序,方便构造死锁
     */
    public LockInterruptibilyDemo(int lock){
        this.lock = lock;
    }

    @Override
    public void run() {
        if (lock == 1) { //是为了让他们死锁用,保证先后调用得顺序
            while (true) {
                if (lock1.tryLock()) {
                    try {
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                        }
                        if (lock2.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() + "我的工作完成");
                                return;
                            } finally {
                                lock2.unlock();
                            }
                        }
                    } finally {
                        lock1.unlock();
                    }
                }
            }
        } else {
            while (true) {
                if (lock2.tryLock()) {
                    try {
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                        }
                        if (lock1.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() + "我的工作完成");
                                return;
                            } finally {
                                lock1.unlock();
                            }
                        }
                    } finally {
                        lock2.unlock();
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException{

        LockInterruptibilyDemo l1 = new LockInterruptibilyDemo(1);
        LockInterruptibilyDemo l2 = new LockInterruptibilyDemo(2);
        Thread t1 = new Thread(l1);
        Thread t2 = new Thread(l2);
        t1.start();t2.start();
        Thread.sleep(1000);
        t2.interrupt();
    }
}
结果: 16我的工作完成
	   15我的工作完成
  • 程序讲解:上述代码非常容易造成死锁,但是通过tryLock()方法后,这种情况大大改善了。由于线程不能傻傻得等,而是不停地尝试。

  • 公平锁和非公平锁的实现

    • ReentrantLock的一个很好的功能就是 指定锁是否是公平的 。其实 ReentrantLock内部都维持着一个同步队列 ,公平锁表示线程获取锁的顺序是按照线程排队的顺序来唤醒的,而非公平锁是随机唤醒的,是一个抢占机制。
    • synchronized就是一种非公平锁
    • 非公平锁可能造成线程的”饥饿“问题,涉及到了线程的优先级问题。后续会分析源码,现在只展示应用。
package com.wwj.lockdemos;

import java.util.concurrent.locks.ReentrantLock;

public class IsFairSycDemo {
    public static void main(String[] args) {
        Thread_0 do1 = new Thread_0();
        Thread[] threads = new Thread[5];
        for(int i=0 ; i<5 ; i++){
            threads[i] = new Thread(do1);
        }
        for(int i=0 ; i<5 ; i++){
            threads[i].start();
        }
    }
}

class Thread_0 implements  Runnable{
    DoSomeThing td = new DoSomeThing();
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getId()+"----->运行了");
        td.doSomeThing();
    }
}

class DoSomeThing {
    ReentrantLock lock = new ReentrantLock();
    public void  doSomeThing(){
        try{
           lock.lock();
           System.out.println(Thread.currentThread().getId()+"获得锁");
        }finally {
           lock.unlock();
        }
    }
}

结果:
17----->运行了
16----->运行了
19----->运行了
18----->运行了
15----->运行了
17获得锁
18获得锁
15获得锁
16获得锁
19获得锁
  • 结果分析:
    • 由线程的运行和获得锁的顺序可以得知,这并不是公平锁。
    • 将ReentrantLock的构造器来改成公平锁:
    • ReentrantLock lock = new ReentrantLock(true);
19----->运行了
17----->运行了
18----->运行了
15----->运行了
16----->运行了
19获得锁
17获得锁
15获得锁
16获得锁
18获得锁
  • 由线程的运行和获得锁的顺序可以得知,这是公平锁。
  • 别的方法就不一一介绍了。

Condition

  • synchronized与wait()和nitofy()/notifyAll()方法相结合可以实现等待/通知模型,那么ReentrantLock可以嘛?
    • ReentrantLock同样可以,但是需要借助“Condition",且Condition有更好的灵活性,具体体现在:
    • 一个Lock里面 可以创建多个Condition实例,实现多路通知(典型的就是生产消费者模式);
    • notify()方法进行通知时,被通知的线程是JVM随机选择的,但是ReentrantLock结合Condition可以实现有选择性地通知
  • Condition的作用:
    • 对锁进行更精确的控制。
  • Condition中await()、signal()、singnalAll()和同步锁中的部分方法的对比:
    • Condition中的await()方法相当于Object的wait()方法;Condition中的signal()方法相当于Object的notify()方法;Condition中的signalAll()相当于Object的notifyAll()方法。
    • 不同的是,Object中的wait(),notify(),notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的;而Condition是需要与 “互斥锁”/“共享锁” 捆绑使用的
  • 程序1:Condition的基本使用(通过synchronized也可以做到):
package com.wwj.lockdemos;

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

public class ReentrantLockConditionDemo implements Runnable {
    public  static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();
    @Override
    public void run() {
        try{
            lock.lock();
            System.out.println("持有锁,并等待");
            condition.await();
            System.out.println("唤醒锁");
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new ReentrantLockConditionDemo());
        t1.start();
        Thread.sleep(2000);
        
        lock.lock();
        condition.signal();
        lock.unlock();
    }
}
//结果:
//持有锁,并等待
//唤醒锁
  • 程序分析:
    • condition.await(); 要求线程在Condition对象上进行等待
    • main()中的lock.lock();------ lock.unlock(); :指的是由main发出通知,告知等待在Condition上的线程可以继续执行了。
    • 与Object.wait()方法和notify()一样,当线程使用Condition.await()方法时,要求线程持有相关的重入锁,在Condition.await()调用后,这个线程会释放这把锁。
    • 同理, 当线程执行condition.signal()时,要求线程持有相关的重入锁,执行signal()后,系统会从当前Condition对象的等待队列中唤醒一个线程。一旦线程被唤醒,它会重新尝试获得与 之绑定的重入锁。
    • 因此,在signal()方法调用后,一般要释放相关的锁,让给被唤醒的线程,也就是t1;可以这样想,signal()、await()必须在lock()后执行,然后执行完还要有相应的unlock(),就如Object下的wait()和notify()执行必须在sychronized下执行一个道理。
  • 程序2:Condition的精妙使用、精确控制:
package com.wwj.lockdemos;

import java.io.InputStream;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class ProConBuffer{
    final ReentrantLock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();
    final Object[] buffers = new Object[5];
    int putIdx , takeIdx , count;  //putIdx , takeIdx 是为了防止超出边界
    //放入方法
    public  void put(Object x) throws InterruptedException{
        try{
            lock.lock();
            //如果缓冲满了则等待
            if(count == buffers.length)
                notFull.await();
            //不满则放入
            buffers[putIdx++] = x;
            if(putIdx == buffers.length)
                putIdx = 0;
            count++;
            notEmpty.signal();
            System.out.println(Thread.currentThread().getName()+" put :" + (Integer)x);
        }finally {
            lock.unlock();// 释放锁
        }
    }
    //输出方法
    public Object take() throws  InterruptedException{
        try{
            lock.lock();
            if(count == 0)
                notEmpty.await();
            //非空则可以取出
            Object y = buffers[takeIdx++];
            if(takeIdx == buffers.length)
                takeIdx = 0;
            // 将“缓冲”数量-1
            count--;
            // 唤醒put线程,因为put线程通过notFull.await()等待
            notFull.signal();
            // 打印取出的数据
            System.out.println(Thread.currentThread().getName()+" take:" + (Integer)y);
            return y;
        }finally {
            lock.unlock();
        }
    }
}
public class ConditionBufferDemo {
    private  static ProConBuffer proConBuffer = new ProConBuffer();
    public static void main(String[] args) {
        for(int i=0 ; i<10 ; i++){
            new PutThread("p"+i , i).start();
            new TakeThread("t"+i).start();
        }
    }
    static class PutThread extends  Thread{
        private int num;
        public  PutThread(String name ,int num ){
            super(name);
            this.num = num;
        }

        @Override
        public void run() {
            try{
                Thread.sleep(1);
                proConBuffer.put(num);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    static  class TakeThread extends Thread{
        public TakeThread(String name){
            super(name);
        }

        @Override
        public void run() {
            try{
                Thread.sleep(10);
                Integer num = (Integer) proConBuffer.take();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
结果:
p0 put :0
p4 put :4
p7 put :7
p5 put :5
p3 put :3
t5 take:0
p6 put :6
t1 take:4
t2 take:7
t3 take:5
t6 take:3
t4 take:6
p9 put :9
p2 put :2
p1 put :1
p8 put :8
t7 take:9
t9 take:2
t8 take:1
t0 take:8
  • 结果分析:
    • Buffers 是容量为5的缓冲,缓冲中存储的是Object对象,支持多线程的读/写缓冲。多个线程操作“一个Buffers对象”时,它们通过互斥锁lock对缓冲区items进行互斥访问;而且同一个Buffers对象下的全部线程共用“notFull”和“notEmpty”这两个Condition。
    • notFull用于控制写缓冲;
    • notEmpty用于控制读缓冲;
    • 当缓冲已满的时候,调用put的线程会执行notFull.await()进行等待;
    • 当缓冲区不是满的状态时,就将对象添加到缓冲区并将缓冲区的容量count+1;
    • 最后,调用notEmpty.signal()缓冲notEmpty上的等待线程(调用notEmpty.await的线程);
    • 简言之,notFull控制“缓冲区的写入”,当往缓冲区写入数据之后会唤醒notEmpty上的等待线程。
    • 同理,notEmpty控制“缓冲区的读取”,当读取了缓冲区数据之后会唤醒notFull上的等待线程。
    • 在ConditionTest2的main函数中,启动10个“写线程”,向BoundedBuffer中不断的写数据(写入0-9);同时,也启动10个“读线程”,从BoundedBuffer中不断的读数据.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值