(十六)ReentrantLock可重入锁使用和介绍

2021-03-03 16:19:49 浏览数 (3)

1、ReentrantLock介绍

jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock

虽然在性能上ReentrantLocksynchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

两者的相同点:

1、ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。

但是实现上两者不同:

synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;

ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。

2、ReentrantLock和synchronized都是可重入的。

synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;

ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

不同点:

1、ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。

2、ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

3、ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

2、ReentrantLock的额外功能

公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。(保证)

非公平锁则随机分配这种使用权。

和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。

在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁

代码语言:javascript复制
//公平锁
ReentrantLock lock = new ReentrantLock(true);

上个例子:

代码语言:javascript复制
public class FairReentrantLock {
  //   static Lock lock = new ReentrantLock(true);
    static Lock lock = new ReentrantLock(false);
    public static void main(String[] args) {
        myThreadDemo[] threadDemos = new myThreadDemo[10];
        for (int i = 0; i < 5; i  ) {
            threadDemos[i] = new myThreadDemo(i);
        }
        for (int i = 0; i < 5; i  ) {
            threadDemos[i].start();
        }
    }

    static class myThreadDemo extends Thread {
        int threadId;

        myThreadDemo(int threadId) {
            this.threadId = threadId;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1 * 100);
                for (int i = 0; i < 3; i  ) {
                    lock.lock();
                    System.out.println("当前获得锁的线程--->>>"   threadId);
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出:

可以看到非公平锁,几乎是一个线程同时获取锁后再到下一个线程执行。

如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题。

如果换成公平锁:

代码语言:javascript复制
static Lock lock = new ReentrantLock(true);

可以看到,看起来就有那么一点打乱的顺序,系统会公平地分配资源给每个线程,而不是一个线程一直霸占着,线程几乎是轮流的获取到了锁。

3、ReentrantLock可响应中断问题

synchronized死锁例子:

代码语言:javascript复制
class SynchronizedDeadLock implements Runnable {
    private String lockA;
    private String lockB;

    public SynchronizedDeadLock(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName()   "t 自己持有:"   lockA   "t 尝试获得:"   lockB);
            //至关重要是这个sleep,因为这里睡眠是为了让 第二个线程有机会进来
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName()   "t 自己持有:"   lockB   "t 尝试获得:"   lockA);
            }
        }
    }
}

public class DeadLockDemo {

    public static void main(String[] args) throws InterruptedException {
        String lockA = "locka";
        String lockB = "lockb";
        new Thread(new SynchronizedDeadLock(lockA, lockB), "Thread1").start();
        new Thread(new SynchronizedDeadLock(lockB, lockA), "Thread2").start();
    }
}

ReentrantLock死锁例子:

代码语言:javascript复制
public class ReentrantLockDeadLock {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new DeadLockDemo(lock1, lock2), "Thread1");
        Thread thread2 = new Thread(new DeadLockDemo(lock2, lock1), "Thread2");
        thread1.start();
        thread2.start();
    }

    static class DeadLockDemo implements Runnable {
        Lock lockA;
        Lock lockB;

        public DeadLockDemo(Lock lockA, Lock lockB) {
            this.lockA = lockA;
            this.lockB = lockB;
        }

        @Override
        public void run() {
            try {
                lockA.lock();
//                lockA.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()   "t 自己持有:"   lockA   "t 尝试获得:"   lockB);
                TimeUnit.SECONDS.sleep(2);
                lockB.lock();
//                lockB.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()   "t 自己持有:"   lockB   "t 尝试获得:"   lockA);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lockA.unlock();
                lockB.unlock();
                System.out.println(Thread.currentThread().getName()   "正常结束!");
            }
        }
    }
}

以上这两个例子都会发生死锁,它们的资源竞争是这样的:

Thread1线程执行,首先获取lockA资源,上锁,然后休眠2秒…

Thread2线程执行,然后获取lockB资源,上锁,然后休眠2秒…

Thread1线程醒来,获取lockB资源,发现被锁住了,只能等待…

Thread2线程醒来,获取lockA资源,发现被锁住了,只能等待…

这样,死锁出现了…

ReentrantLock可响应中断:

ReentrantLock的优点还是有的,它提供了lockInterruptibly()方法,用于感知线程中断,从而退出程序。

代码语言:javascript复制
public class ReentrantLockDeadLock {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new DeadLockDemo(lock1, lock2), "Thread1");
        Thread thread2 = new Thread(new DeadLockDemo(lock2, lock1), "Thread2");
        thread1.start();
        thread2.start();

        Thread.sleep(5 * 1000);
        if (Thread.activeCount() >= 4) {
            thread1.interrupt();//让thread1线程中断
        }
    }

    static class DeadLockDemo implements Runnable {
        Lock lockA;
        Lock lockB;

        public DeadLockDemo(Lock lockA, Lock lockB) {
            this.lockA = lockA;
            this.lockB = lockB;
        }

        @Override
        public void run() {
            try {
//                lockA.lock();
                lockA.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()   "t 自己持有:"   lockA   "t 尝试获得:"   lockB);
                TimeUnit.SECONDS.sleep(2);
//                lockB.lock();
                lockB.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()   "t 自己持有:"   lockB   "t 尝试获得:"   lockA);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lockA.unlock();
                lockB.unlock();
                System.out.println(Thread.currentThread().getName()   "正常结束!");
            }
        }
    }
}

结果:

程序在休眠5秒后,假如Thread.activeCount() >= 4,主线程、守护线程、Thread1、Thread2 四个都在, 表示死锁发生了。

通过 thread1.interrupt(),中断了Thread1lockInterruptibly()方法 会释放lockAlockb的锁,即 lockA.unlock()lockB.unlock()Thread2感知了,就可以获取loackB的资源,即可以上锁,然后正常退出。

所以ReentrantLock相比synchronized的优势就是:无限等待获取锁的行为可以被中断

4、ReentrantLock锁限时等待

线程中断不是处理死锁特别好的方法,万一线程真的是执行了很久,而不是死锁了,如果贸然中断,可不是一个明智的处理方法。

ReentrantLock 提供了一个tryLock() 方法,可以指定获取锁的等待时间。

tryLock()如果拿到锁就返回true,否则返回false,不会像lock那样无限等待。

代码语言:javascript复制
if (!lockA.tryLock(2, TimeUnit.SECONDS)) {
    System.out.println(Thread.currentThread().getName()   " 正在等待锁......");
} else {
    System.out.println(Thread.currentThread().getName()   " 拿到了锁");
}

tryLock()也会有死锁的情况,所以为了避免死锁,一个线程不要获取多个锁。

5、Condition

synchronized可以结合Object进行线程之间的通信,比如说waitnotify实现线程的等待和唤醒。

ReentrantLock也有,Java提供了Condition 接口,可以实现ReentrantLock线程之间的通信。

eg:

代码语言:javascript复制
public class ConditionTest {
    static ReentrantLock lock = new ReentrantLock();
    //通过ReentrantLock创建Condition实例,并与之关联
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程执行ing...");
        new Thread(new AwaitThread()).start();
        try {
            Thread.sleep(2000);
            lock.lock();
            condition.signal();
        } finally {
            lock.unlock();
        }
        System.out.println("主线程执行结束");
    }

    static class AwaitThread implements Runnable {
        @Override
        public void run() {

            System.out.println("子线程执行ing...");
            lock.lock();
            try {
                System.out.println("子线程停止了");
                condition.await();
                System.out.println("子线程恢复执行了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

结果:

代码语言:javascript复制
主线程执行ing...
子线程执行ing...
子线程停止了
主线程执行结束
子线程恢复执行了

总结:

ReentrantLock比起synchronized功能更加丰富,支持公平锁和非公平锁,而且提供了响应中断。

而且提供了tryLock()锁限时等待,相比synchronized要更灵活。

参考:

  • https://www.cnblogs.com/takumicx/p/9338983.html

0 人点赞