多线程基础(八):ReentrantLock的使用及与synchronized的区别

[toc]

前面部分,我们着重讨论了synchronized的使用和wait、notify及notifyAll方法等并发的基础部分。今天,我们来学习另外一种解决方案。

1.交替打印数字和阻塞队列

还记得,前面一篇文章《什么?面试官让我用ArrayList实现一个阻塞队列?》中,描述了一个关于实现两个线程交替打印以及实现阻塞队列的例子,那么今天,我们来看看另外一种解决办法---ReentrantLock。
代码如下:

package com.dhb.reentrantlocktest;

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

public class ReentrantLockTest {


    private static final ReentrantLock lock = new ReentrantLock(true);

    private static int count = 0;

    public static final int MAX = 100;

    public static void main(String[] args) {

        new Thread(() -> {
            while (count <= MAX) {
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName()+" "+count++);
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        },"T1").start();


        new Thread(() -> {
            while (count <= MAX) {
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName()+" "+count++);
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        },"T2").start();
    }
}

上述代码执行之后如下:

T1 0
T2 1
T1 2
T2 3
T1 4
T2 5
T1 6
T2 7
T1 8
T2 9
T1 10
T2 11
T1 12
T2 13
T1 14
T2 15
T1 16
T2 17
T1 18
T2 19
T1 20
T2 21
T1 22
T2 23
T1 24
T2 25
T1 26
T2 27
T1 28
T2 29
T1 30
T2 31
T1 32
T2 33
T1 34
T2 35
T1 36
T2 37
T1 38
T2 39
T1 40
T2 41
T1 42
T2 43
T1 44
T2 45
T1 46
T2 47
T1 48
T2 49
T1 50
T2 51
T1 52
T2 53
T1 54
T2 55
T1 56
T2 57
T1 58
T2 59
T1 60
T2 61
T1 62
T2 63
T1 64
T2 65
T1 66
T2 67
T1 68
T2 69
T1 70
T2 71
T1 72
T2 73
T1 74
T2 75
T1 76
T2 77
T1 78
T2 79
T1 80
T2 81
T1 82
T2 83
T1 84
T2 85
T1 86
T2 87
T1 88
T2 89
T1 90
T2 91
T1 92
T2 93
T1 94
T2 95
T1 96
T2 97
T1 98
T2 99

可以看到用两个线程交替打印的功能很快就实现了。
那么阻塞队列,貌似也可以这样实现:

package com.dhb.reentrantlocktest;

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

public class BlockQuene {

    private static final int CAPACITY = 10;

    private static final ReentrantLock lock = new ReentrantLock(true);

    private static final List queue = new ArrayList<>();

    private static int number = 0;

    public static void main(String[] args) {
        new Thread(new Producer(),"P1").start();
        new Thread(new Consumer(),"C1").start();
    }

    private static class Producer implements Runnable{
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    if(queue.size() < CAPACITY) {
                        int num = number++;
                        queue.add(num);
                        System.out.println(Thread.currentThread().getName()+" Producer : "+num);
                        TimeUnit.SECONDS.sleep(1);
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }

        }
    }

    private static class Consumer implements Runnable {
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    if(queue.size() > 0) {
                        int num = queue.remove(0);
                        System.out.println(Thread.currentThread().getName()+" Consumer : "+num);
                    }
                }finally {
                    lock.unlock();
                }
            }
        }
    }
}

输出:

P1 Producer : 0
C1 Consumer : 0
P1 Producer : 1
C1 Consumer : 1
P1 Producer : 2
C1 Consumer : 2
P1 Producer : 3
C1 Consumer : 3
P1 Producer : 4
C1 Consumer : 4

这就是本文今天要介绍的主角,ReentrantLock。
上面两个案例实际上都是利用了ReentrantLock的公平锁来完成了业务需求,所谓公平锁,就是说被阻塞的队列中,获取锁的顺序不是随机的,而是依赖于进入等待队列的顺序,满足先进先出。这一点比较像我们之前学习的WaitSet中notify之后出队的状态。因此notify也是具有公平性的。
以上就是对ReentrantLock第一个特性的应用。这个地方在构造函数中,我们传入了true。如果不传入true,则默认创建非公平锁,这就与synchronized的方式类似了。

//公平锁
ReentrantLock lock1 = new ReentrantLock(true);
//非公平锁
ReentrantLock lock2 = new ReentrantLock();

2.可以被Interrupt

我们前面聊过,当线程在获取锁的时候,如果此时锁被其他线程获取,那就进入cxq和EntryList的阻塞队列,此时处于BLOCK状态,除非其他线程释放锁,否则这个状态是不能被Interrupt的。我们可以通过如下方式实验:

package com.dhb.reentrantlocktest;

import java.util.concurrent.TimeUnit;

public class SynchronizedInterruptTest {

    public static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException{
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            try {
                synchronized (lock) {
                    TimeUnit.SECONDS.sleep(60);
                }
            }catch (InterruptedException e){
                System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
                e.printStackTrace();
            }
        },"T1");

        Thread it1 = new Thread(() -> {
            try {
                synchronized (lock) {
                    TimeUnit.SECONDS.sleep(60);
                }
            }catch (InterruptedException e){
                System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
                e.printStackTrace();
            }
        },"IT1");

        t1.start();
        it1.start();

        TimeUnit.SECONDS.sleep(1);
        it1.interrupt();
        long cost = System.currentTimeMillis()-start;
        t1.join();
        it1.join();
    }
}

执行结果:

IT1 被打断Interrupted。 cost:60061
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:340)
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    at com.dhb.reentrantlocktest.SynchronizedInterruptTest.lambda$main$1(SynchronizedInterruptTest.java:25)
    at java.lang.Thread.run(Thread.java:748)

可以看到,IT1到60秒之后才被打断。这说明,在T1没有释放锁的时候,IT1处于BLOCK状态是不能被打断的。
我们再来试试ReentrantLock。ReentrantLock提供了两种方式,当使用lockInterruptibly的时候,可以被打断。

package com.dhb.reentrantlocktest;

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

public class ReentrantLockInterruptTest {

    public static final ReentrantLock lock = new ReentrantLock();


    public static void main(String[] args) throws InterruptedException{
        long start = System.currentTimeMillis();

        Thread t1 = new Thread(() -> {
            try {
                lock.lock();
                TimeUnit.SECONDS.sleep(60);
            }catch (InterruptedException e){
                System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        },"T1");

        Thread it1 = new Thread(() -> {
            try {
                lock.lock();
                TimeUnit.SECONDS.sleep(60);
            }catch (InterruptedException e){
                System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        },"IT1");
        Thread it2 = new Thread(() -> {
            try {
                lock.lockInterruptibly();
                TimeUnit.SECONDS.sleep(60);
            }catch (InterruptedException e){
                System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        },"IT2");


        t1.start();
        it1.start();
        it2.start();

        TimeUnit.SECONDS.sleep(1);
        it1.interrupt();
        it2.interrupt();

    }
}

上述代码执行结果:

"D:\Program Files\Java\jdk1.8.0_231\bin\java.exe" "-javaagent:D:\Program Files\ideaIU-2020.2.1.win\lib\idea_rt.jar=58930:D:\Program Files\ideaIU-2020.2.1.win\bin" -Dfile.encoding=UTF-8 -classpath "D:\Program Files\Java\jdk1.8.0_231\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\deploy.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\javaws.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\management-agent.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\plugin.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\rt.jar;D:\workspace-git\MyProject\target\classes;D:\m2\log4j\log4j\1.2.17\log4j-1.2.17.jar;D:\m2\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;D:\m2\org\slf4j\slf4j-log4j12\1.7.25\slf4j-log4j12-1.7.25.jar;D:\m2\org\apache\commons\commons-lang3\3.9\commons-lang3-3.9.jar;D:\m2\org\openjdk\jol\jol-core\0.10\jol-core-0.10.jar" com.dhb.reentrantlocktest.ReentrantLockInterruptTest
IT2 被打断Interrupted。 cost:1061
java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at com.dhb.reentrantlocktest.ReentrantLockInterruptTest.lambda$main$2(ReentrantLockInterruptTest.java:39)
    at java.lang.Thread.run(Thread.java:748)
Exception in thread "IT2" java.lang.IllegalMonitorStateException
    at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
    at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
    at com.dhb.reentrantlocktest.ReentrantLockInterruptTest.lambda$main$2(ReentrantLockInterruptTest.java:45)
    at java.lang.Thread.run(Thread.java:748)
IT1 被打断Interrupted。 cost:60059
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:340)
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    at com.dhb.reentrantlocktest.ReentrantLockInterruptTest.lambda$main$1(ReentrantLockInterruptTest.java:29)
    at java.lang.Thread.run(Thread.java:748)

可以看到,IT1被打断需要61秒,而IT2被打断只需要1秒。这说明,对于ReentrantLock而言。等待锁的过程中,是支持Interrupt的。

3.支持超时

对,Reentrant还支持超时。可以通过tryLock方法返回是否能获得锁的状态。

package com.dhb.reentrantlocktest;

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

public class ReentrantLockTimeOutTest {


    private static final ReentrantLock lock = new ReentrantLock(true);


    public static void main(String[] args) throws Exception{
        long start = System.currentTimeMillis();

        new Thread(() -> {
            lock.lock();
            try {

                TimeUnit.SECONDS.sleep(60);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName()+" 执行完毕。 cost:"+(System.currentTimeMillis()-start));
            }
        },"T1").start();


        new Thread(() -> {
            boolean isLock = false;
            try {
                isLock = lock.tryLock(10,TimeUnit.SECONDS);
                System.out.println(Thread.currentThread().getName()+" 执行完毕。 cost:"+(System.currentTimeMillis()-start));
            } catch (Exception e) {
                System.out.println(Thread.currentThread().getName()+" 出现异常。 cost:"+(System.currentTimeMillis()-start));
                e.printStackTrace();
            }finally {
                if(isLock) {
                    lock.unlock();
                }
            }
        },"T2").start();
    }
}

上述代码执行结果:

T2 执行完毕。 cost:10059
T1 执行完毕。 cost:60057

T2等待了10秒之后,没有获得锁,就结束执行了。这也是synchronized无法提供的功能。

4.配合Condition

我们再回到一开始的面试题,两个线程交替打印的问题,现在将题目改一下,每个线程分别各自打印10次。不再用之前公平锁的方式怎么实现?对,就是Condition可以解决这个问题:

package com.dhb.reentrantlocktest;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;

public class ConditionTest {

    public static final ReentrantLock lock = new ReentrantLock();

    public static final Condition con1 = lock.newCondition();

    public static final Condition con2 = lock.newCondition();

    public static  int count = 0;

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    for(int i=0;i<10;i++) {
                        System.out.println(Thread.currentThread().getName() +" "+count++);
                        TimeUnit.SECONDS.sleep(1);
                    }
                    con2.signal();
                    con1.await();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        },"T1").start();

        new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    for(int i=0;i<10;i++) {
                        System.out.println(Thread.currentThread().getName() +" "+count++);
                        TimeUnit.SECONDS.sleep(1);
                    }
                    con1.signal();
                    con2.await();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        },"T2").start();


    }

}

上述代码执行结果:

T1 0
T1 1
T1 2
T1 3
T1 4
T1 5
T1 6
T1 7
T1 8
T1 9
T2 10
T2 11
T2 12
T2 13
T2 14
T2 15
T2 16
T2 17
T2 18
T2 19
T1 20
T1 21
T1 22
T1 23
T1 24
T1 25
T1 26
T1 27
T1 28
T1 29
T2 30
T2 31
T2 32
T2 33
T2 34
T2 35
T2 36
T2 37
T2 38
T2 39
T1 40
T1 41
T1 42
T1 43
T1 44
T1 45
T1 46
T1 47
T1 48
T1 49
T2 50
T2 51
T2 52
T2 53
T2 54
T2 55
T2 56
T2 57
T2 58
T2 59
T1 60
T1 61
T1 62
T1 63
T1 64
T1 65
T1 66
T1 67
T1 68
T1 69

可以看到Condition可以根据lock产生条件变量,之后通过await进入等待队列,再通过signal可以唤醒。这与wait和notify机制非常像。但是Condition的优势就在于,可以指定多个条件变量。上面的案例中我们就指定了2个条件变量。我们可以根据需要设置我们所需要的条件。这就比wait和notify机制要灵活得多。

5.可重入性

现在对ReentrantLock和Synchronized进行重入性测试:

package com.dhb.reentrantlocktest;

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

public class ReentTest {

    private static final ReentrantLock lock = new ReentrantLock();

    public static final Object lock1 = new Object();


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

        new Thread(() -> {
                try {
                    lock.lock();
                    TimeUnit.SECONDS.sleep(1);
                    for(int i=0;i<5;i++) {
                        lock.lock();
                        TimeUnit.SECONDS.sleep(1);
                        System.out.println(Thread.currentThread().getName()+"重入:" + i);
                        lock.unlock();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
        },"T1").start();

        TimeUnit.SECONDS.sleep(10);

        new Thread(() -> {
                try {
                    synchronized (lock1){
                        TimeUnit.SECONDS.sleep(1);
                        for(int i=0;i<5;i++) {
                            synchronized (lock1) {
                                TimeUnit.SECONDS.sleep(1);
                                System.out.println(Thread.currentThread().getName()+"重入:" + i);
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

        },"T2").start();
    }
}

执行结果:

T1重入:0
T1重入:1
T1重入:2
T1重入:3
T1重入:4
T2重入:0
T2重入:1
T2重入:2
T2重入:3
T2重入:4

可见,两种锁都是支持重入的。

6.总结

本文介绍了ReentrantLock的用法以及和synchronized进行比较,其对比如下:

  • 1.公平/非公平锁:ReentrantLock可以支持公平和非公平两种锁。而synchronized是非公平的。
  • 2.reentrantLock再BLOCK状态的时候可以被Interrupt,而synchronized处于BLOCK状态的时候不支持Interrupt。
  • 3.ReentrantLock可以通过tryLock设置获取锁的时间,而synchronized不支持。
  • 4.ReentrantLock可以配合Conditaion,实现多个条件变量。而synchronized只能够wait/notify实现一个条件。
  • 5.两种锁都是支持重入的。
    以上就是对这两种锁的总结,总的来说,ReentrantLock会更加灵活。但是在通常情况下,如果我们并不需要这些灵活配置,那么使用synchronized,尤其是在jdk1.8之后,jvm进行过专门的优化,可能效率会更好。

你可能感兴趣的:(多线程基础(八):ReentrantLock的使用及与synchronized的区别)