JUC可重入锁ReentrantLock

1.概述

java.util.concurrent.locks.ReentrantLock,即可重入锁,是Lock接口的一个实现类,可以实现和synchronized一样的功能

以往我们都是使用synchronized解决线程的安全问题,在JUC中,java.util.concurrent.locks.Lock接口提供了比synchronized更多的功能,可以使用Lock接口实现手动上锁和释放锁,并且可以更加精准的设置条件(Condition)控制等待唤醒,它的特点以及和synchronized的区别是:

  1. synchronized是Java中的关键字,而Lock则是Java中的一个类。
  2. synchronized的上锁和释放锁都是自动完成,而使用Lock时必须手动释放锁,否则会造成死锁现象。
  3. Lock可以让等待锁的线程中断,而synchronized却不能,使用synchronized时,未得到锁的线程会一直等待下去不会中断。
  4. 通过Lock可以知道是否成功获取到锁,而synchronized却不能。
  5. Lock可以提高多个线程进行读操作的效率,在大量线程同时竞争时,效率远胜于synchronized。

2.获取锁和释放锁

使用lock.lock();方法实现上锁,用lock.unlock();方法释放锁,一般情况下,lock.unlock();的代码总是要写在finally中,以保证方法出现异常后也能释放锁,避免死锁。

例:10个线程卖票,每次只能有一个线程对票数进行修改,JUC中所有的例子都可以写成“线程操作资源类”的形式,本例创建10个线程共同操作资源类TicketTask,调用ticket()方法卖票

package example.juc.test2;

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

public class TestLock {

    public static void main(String[] args) {

        TicketTask ticketTask = new TicketTask();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {

                ticketTask.ticket();

            }, "线程" + i).start();
        }
    }
}


class TicketTask {

    private int number = 500;
    private Lock lock = new ReentrantLock();

    public void ticket() {

        while (true) {
            lock.lock();
            try {
                if (number > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    number--;
                    System.out.println(Thread.currentThread().getName() + "完成售票:" + number);
                } else {
                    break;
                }
            } finally {
                lock.unlock();
            }

        }

    }
}

3.公平与非公平

synchronized默认是非公平的,ReentrantLock也是默认非公平锁,比如以下例子运行结果会出现卖的票被同一个线程大量抢占的情况

package example.juc.test2;

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

public class TestLock {

    public static void main(String[] args) {

        TicketTask ticketTask = new TicketTask();


        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                ticketTask.ticket();
            }

        }, "线程A").start();

        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                ticketTask.ticket();
            }

        }, "线程B").start();

        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                ticketTask.ticket();
            }

        }, "线程C").start();

    }
}


class TicketTask {

    private int number = 30;
    private Lock lock = new ReentrantLock();

    public void ticket() {

        lock.lock();
        try {
            if (number > 0) {
                try {
                    Thread.sleep(10);
                }
                catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                number--;
                System.out.println(Thread.currentThread().getName() + "完成售票:" + number);
            }
        } 
        finally {
            lock.unlock();
        }

    }

}

出现这种情况的原因就在于,ReentrantLock默认是非公平的

java.util.concurrent.locks.ReentrantLock

public ReentrantLock() {
    sync = new NonfairSync();
}

......

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

如果想要实现公平的效果,创建对象时需要加上一个初始化参数

private Lock lock = new ReentrantLock(true);

公平锁的优点在于“公平”,缺点在于效率较低

4.可重入

可重入锁指的是,一个线程在一个方法外层获取了锁 ,进入方法内层会自动获取锁。

和synchronized一样,ReentrantLock也是可重入的,但是需要注意加锁和释放次数要匹配,否则其他线程无法获得锁,例如:

package example.juc3;

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

public class TestReentrantLock {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("外层");
                lock.lock();
                try {
                    System.out.println("内层");
                } finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }

        },"t1").start();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("2");
            } finally {
                lock.unlock();
            }
        },"t2").start();

    }
}

5.响应中断

synchronized是无法支持响应中断的,一个线程获取不到锁,就会一直等着,程序无法结束,造成死锁。

而ReentrantLock支持通过tryLock()设置一个时间参数,到时间后自动放弃获取锁,如果不设置时间参数代表立即返回获取锁的结果,通过这个可以避免死锁现象

例1:派发1000个线程,每个为变量加1,结果抢不到的直接放弃,所以变量被加的远远不到1000

package example.juc2.test;

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

public class TestTryLock {
    
    private static final Lock lock = new ReentrantLock();
    private static int sum = 0;

    public static void main(String[] args) {

        for (int i = 0; i < 1000; i++) {

            new Thread(() -> {

                if (lock.tryLock()) {
                    try {
                        sum ++;
                        System.out.println(Thread.currentThread().getName() +" - "+ sum);
                    }
                    catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                    finally {
                        lock.unlock();
                    }

                }

            }).start();
        }
    }


}

例2:每个抢到锁的线程1秒钟才能执行完成,每个抢不到锁的线程最多等待5秒否则直接放弃争抢锁,因此程序只能打印5次左右

package example.juc2.test;

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

public class TestTryLock {
    
    private static final Lock lock = new ReentrantLock();
    private static int sum = 0;

    public static void main(String[] args) {

        for (int i = 0; i < 1000; i++) {

            new Thread(() -> {

                try {
                    if (lock.tryLock(5, TimeUnit.SECONDS)) {
                        try {
                            sum ++;
                            System.out.println(Thread.currentThread().getName() +" - "+ sum);
                            TimeUnit.SECONDS.sleep(1);
                        }
                        catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                        finally {
                            lock.unlock();
                        }

                    }
                }
                catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }).start();
        }
    }

}

6.精准唤醒

如果说Lock代替了synchronized的使用,Condition则是替代了Object的wait()notify()notifyAll()方法,java.util.concurrent.locks.Condition是一个接口,通过调用Lock对象的newCondition()方法获取具体Condition对象,将Condition绑定在Lock上,通过Condition的await()signal()signalAll()实现线程之间的通信,与传统Object作为同步监视器一样,Condition的await()也要总是出现在循环中,实现二次条件判断。

例:实现一个资源类AirConditioner,然后新建4个线程,每个线程对AirConditioner中的变量number交替改成0和1

package example.juc2.test;

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

public class TestConditionWaitNotify {
    
    public static class AirConditioner {
        
        private int number = 0;
        
        private Lock lock = new ReentrantLock();
        private Condition condition = lock.newCondition();

        public void increment() {
            lock.lock();
            try {
                while (number != 0) {
                    condition.await();
                }
                number ++;
                System.out.println(Thread.currentThread().getName()+" 修改为 "+number);
                condition.signalAll();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
            
        }

        public void decrement() {
            lock.lock();
            try {
                while (number == 0) {
                    condition.await();
                }
                number --;
                System.out.println(Thread.currentThread().getName()+" 修改为 "+number);
                condition.signalAll();
            } 
            catch (InterruptedException e) {
                throw new RuntimeException(e);
            } 
            finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        AirConditioner airConditioner = new AirConditioner();

        new Thread(() -> {
            for (int i=0;i<10;i++) {
                airConditioner.increment();
            }
        }, "T1").start();

        new Thread(() -> {
            for (int i=0;i<10;i++) {
                airConditioner.decrement();
            }
        }, "T2").start();

        new Thread(() -> {
            for (int i=0;i<10;i++) {
                airConditioner.increment();
            }
        }, "T3").start();

        new Thread(() -> {
            for (int i=0;i<10;i++) {
                airConditioner.decrement();
            }
        }, "T4").start();

    }
}

运行结果:

T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0
T3 修改为 1
T2 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T1 修改为 1
T4 修改为 0
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0

上面的例子中,线程对资源类的操作是无序的,和加了synchronized的效果一样,而Condition有一个比synchronized更强大的地方,那就是能精确的控制等待唤醒。如果希望几个线程有序的交替执行,就可以获取多个监视器Condition绑定同一个Lock,实现精确的定制化通信。

例:三个线程ABC,按A-B-C顺序执行资源类ShareResource中的打印任务,并循环交替3轮(ABC-ABC-ABC),A执行时打印5次,B10次,C15次,通过lock.newCondition()为三个线程分别创建3个同步监视器:conditionA,conditionB, conditionC,再通过标志位变量flag判断当前该谁执行了,开始默认为A。

程序执行,A线程先获得执行权,执行第一轮,执行完成唤醒B,执行第一轮,B线程执行完成唤醒C,C也执行第一轮,完成再唤醒A,A开始执行第二轮,以此类推直到三轮任务全部完成,实现每一轮都是按照A-B-C的顺序执行。。

package example.juc2.test;

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

public class TestThreadOrderAccess {
    
    public static class ShareResource {
        
        private char flag = 'A';
        
        private final Lock lock = new ReentrantLock();
        
        private final Condition conditionA = lock.newCondition();
        private final Condition conditionB = lock.newCondition();
        private final Condition conditionC = lock.newCondition();
        
        public void doA() {
            lock.lock();
            try {
                while (flag != 'A') {
                    conditionA.await();
                }

                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + "\t" +i);
                }
                
                flag = 'B';
                conditionB.signal();
            } 
            catch (InterruptedException e) {
                throw new RuntimeException(e);
            } 
            finally {
               lock.unlock(); 
            }
        }

        public void doB() {
            lock.lock();
            try {
                while (flag != 'B') {
                    conditionB.await();
                }

                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + "\t" +i);
                }

                flag = 'C';
                conditionC.signal();
            } 
            catch (InterruptedException e) {
                throw new RuntimeException(e);
            } 
            finally {
                lock.unlock();
            }
        }

        public void doC() {
            lock.lock();
            try {
                while (flag != 'C') {
                    conditionC.await();
                }

                for (int i = 0; i < 15; i++) {
                    System.out.println(Thread.currentThread().getName() + "\t" +i);
                }

                System.out.println("*********");

                flag = 'A';
                conditionA.signal();
            } 
            catch (InterruptedException e) {
                throw new RuntimeException(e);
            } 
            finally {
                lock.unlock();
            }
        }
        
    }

    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        
        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                shareResource.doA();
            }
        }, "A").start();
        
        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                shareResource.doB();
            }
        }, "B").start();
        
        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                shareResource.doC();
            }
        }, "C").start();
    }
}

最终运行结果:

A	0
A	1
A	2
A	3
A	4
B	0
B	1
B	2
B	3
B	4
B	5
B	6
B	7
B	8
B	9
C	0
C	1
C	2
C	3
C	4
C	5
C	6
C	7
C	8
C	9
C	10
C	11
C	12
C	13
C	14
*********
A	0
A	1
A	2
A	3
A	4
B	0
B	1
B	2
B	3
B	4
B	5
B	6
B	7
B	8
B	9
C	0
C	1
C	2
C	3
C	4
C	5
C	6
C	7
C	8
C	9
C	10
C	11
C	12
C	13
C	14
*********
A	0
A	1
A	2
A	3
A	4
B	0
B	1
B	2
B	3
B	4
B	5
B	6
B	7
B	8
B	9
C	0
C	1
C	2
C	3
C	4
C	5
C	6
C	7
C	8
C	9
C	10
C	11
C	12
C	13
C	14
*********

7.总结

Lock和synchronized都是独占锁,可重入锁,但是synchronized获取/释放锁是JVM自动完成,Lock是需要开发者手动完成。synchronized不能响应中断,Lock可以响应中断,synchronized无法精准唤醒,Lock可以实现精准唤醒。


"如果文章对您有帮助,可以请作者喝杯咖啡吗?"

微信二维码

微信支付

支付宝二维码

支付宝


JUC可重入锁ReentrantLock
https://blog.liuzijian.com/post/java/2022/09/23/juc-lock.html
作者
Liu Zijian
发布于
2022年9月23日
许可协议