美文网首页android
重入锁:ReentrantLock

重入锁:ReentrantLock

作者: topshi | 来源:发表于2019-05-05 16:39 被阅读7次

synchronized的功能扩展:重入锁

同步控制是并发程序必不可少的手段,它决定了一个线程是否可以访问临界区资源。synchronized关键字就是一个简单的同步控制方式,而重入锁ReentrantLock可以完全替代synchronized关键字。在jdk 5.0的早期版本中,重入锁的性能远远好于synchronized;jdk 6.0开始,synchronized得到了大量优化,使得两者性能差距并不大。
ReentrantLock使用java.util.concurrent.locks.ReentrantLock实现,不同于synchronized,它支持显式地获得锁和释放锁,中断响应、锁申请等待限时、公平锁。

ReentrantLock使用示例

package reenterLock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @Time : 2019/05/05 下午 02:07
 * @Author : xiuc_shi
 **/
public class ReenterLock implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;
    public void run() {
        for(int j = 0;j < 10000000;j++){
            lock.lock();
            try{
                i++;
            }finally {
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ReenterLock tl = new ReenterLock();
        Thread t1 = new Thread(tl);
        Thread t2 = new Thread(tl);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

重入锁之所以叫重入锁,它允许同一个线程两次获得同一把锁,其实synchronized也支持。

lock.lock();
lock.lock();
try{
    i++;
}finally {
    lock.unlock();
    lock.unlock();
}

获得多少次就得释放多少次。

  • 获得次数 > 释放次数:线程没有完成释放锁,还持有着锁。
  • 获得次数 < 释放次数:抛出java.lang.IllegalMonitorStateException异常。

重入锁显式加锁,释放锁的特点使得它的灵活性很高,除此之外,它还提供了一些高级的功能,这是它区别于synchronized的地方。

中断响应

对于synchronized,如果一个线程在等待锁,那么它要么获得锁后继续执行,要么保持等待。而重入锁则有第三种选择,那就是中断该线程,这对处理死锁有一定的帮助。

以下代码制造死锁状态,并中断线程2以释放锁lock2,让线程1可以获得锁lock2继续执行。

package reenterLock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Time : 2019/05/05 下午 03:13
 * @Author : xiuc_shi
 **/
public class IntLock implements Runnable {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;
    public IntLock(int lock){
        this.lock = lock;
    }

    /**
     * 通过lock控制线程申请lock1和lock2的顺序,从而制造死锁的情况
     */
    public void run() {
        try{
            if(lock == 1){
                lock1.lockInterruptibly();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) { }

                lock2.lockInterruptibly();
                System.out.println("线程1执行完成");
            }else{
                lock2.lockInterruptibly();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) { }

                lock1.lockInterruptibly();
                System.out.println("线程2执行完成");
            }
        }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 {
        IntLock r1 = new IntLock(1); //先申请lock1,再申请lock2
        IntLock r2 = new IntLock(2); //先申请lock2,再申请lock1
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        t2.interrupt(); //中断线程2
    }
}

>>>>>结果
线程1执行完成
java.lang.InterruptedException
10:线程退出
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
9:线程退出
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at reenterLock.IntLock.run(IntLock.java:36)
    at java.lang.Thread.run(Thread.java:748)

t2.interrupt()执行后,线程2的lock2.lockInterruptibly()响应中断从而释放lock2.
lockInterruptibly()方法可以对中断进行响应的锁申请动作,即在等待锁时可以响应中断。

锁申请等待限时

除了等待外部通知,限时等待锁也可以避免死锁。通常我们无法判断线程为何迟迟未能获得锁,或死锁,或饥饿。因此给定一个等待时间,在该时间内未能获得锁则让线程自动放弃。

以下代码线程对锁的等待时长为3秒,线程1获得锁后持有锁5秒钟,线程2不可能在3秒内获得锁,因此放弃。

package reenterLock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
 * @Time : 2019/05/05 下午 03:50
 * @Author : xiuc_shi
 **/
public class TimeLock implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    public void run() {
        try {
            if(lock.tryLock(3,TimeUnit.SECONDS)){
                Thread.sleep(5000);
            }else{
                System.out.println("获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            if(lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        TimeLock tl = new TimeLock();
        Thread t1 = new Thread(tl);
        Thread t2 = new Thread(tl);
        t1.start();
        t2.start();
    }
}

tryLock()方法接受两个参数:1.等待时长 2.计时单位。
该例线程对锁的等待时长为3秒,线程获得锁后会持有5秒,故当一个线程获得锁后,另一个线程不可能在3秒内获得该锁,因此请求锁失败。
如果使用tryLock()方法的无参数版本,当线程尝试获得锁,若锁未被占用,则申请锁成功,返回true,否则不等待直接返回false

公平锁

大多数情况下,锁的申请都是非公平的。即,线程1先申请锁A,接着线程2也申请锁A。那么当锁A可用时,系统会从这个锁的等待队列钟随机挑选一个线程获得锁A。而公平锁则讲究先来后到,按申请先后给予锁。所以公平锁不会产生饥饿现象。只要排队,最终还是会获得锁的。
重入锁通过如下有参构造函数设置锁的公平与否:

public ReentrantLock(boolean fair);

当参数fair为true时,表示锁是公平的。
公平锁需要维护一个有序队列,因此实现成本比较高,性能相对非常低下,因此默认情况下锁是非公平的。

小结

  • ReentrantLock需要显式加锁、释放锁,支持中断响应,锁申请等待限时,公平锁。
  • ReentrantLock的几个重要方法整理:
    • lock():获得锁,若锁被占用,则等待。
    • lockInterruptibly():获得锁,但优先响应中断。
    • tryLock():尝试获得锁,成功返回true,失败返回false,不等待。
    • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。
    • unlock():释放锁。
  • 重入锁实现的三个要素:
    • 原子状态。使用CAS操作来存储当前锁的状态,判断锁是否已被其他线程占用。
    • 等待队列。所有未请求到锁的线程会进入等待队列。待锁释放,系统从等待队列中唤醒一个线程获得锁继续运行。
    • 阻塞原语park()unpark(),用来挂起和恢复线程。

相关文章

网友评论

    本文标题:重入锁:ReentrantLock

    本文链接:https://www.haomeiwen.com/subject/psxhoqtx.html