ReentrantLock 使用

简介

ReentrantLock的实现不仅可以替代隐式的synchronized关键字,而且能够提供超过关键字本身的多种功能。
这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定被先满足,那么这个锁是公平的,反之,是不公平的,也就是说等待时间最长的线程最有机会获取锁,也可以说锁的获取是有序的。ReentrantLock这个锁提供了一个构造函数,能够控制这个锁是否是公平的。
而锁的名字也是说明了这个锁具备了重复进入的可能,也就是说能够让当前线程多次的进行对锁的获取操作,这样的最大次数限制是Integer.MAX_VALUE,约21亿次左右。
事实上公平的锁机制往往没有非公平的效率高,因为公平的获取锁没有考虑到操作系统对线程的调度因素,这样造成JVM对于等待中的线程调度次序和操作系统对线程的调度之间的不匹配。对于锁的快速且重复的获取过程中,连续获取的概率是非常高的,而公平锁会压制这种情况,虽然公平性得以保障,但是响应比却下降了,但是并不是任何场景都是以TPS作为唯一指标的,因为公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。

ReentrantLock是一个互斥的同步器,其实现了接口Lock,里面的功能函数主要有:
1. ‍lock() -- 阻塞模式获取资源
2. ‍lockInterruptibly() -- 可中断模式获取资源
3. ‍tryLock() -- 尝试获取资源
4. tryLock(time) -- 在一段时间内尝试获取资源
5. ‍unlock() -- 释放资源

公平性和非公平性对比测试

package demo;

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

import org.junit.Test;

public class ReentrantLockTest {

    private static Lock fairLock = new ReentrantLock(true);
    private static Lock unfairLock = new ReentrantLock();

    @Test
    public void fair() {

        System.out.println("fair version");
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Job(fairLock));
            thread.setName("" + i);
            thread.start();
        }
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    @Test
    public void unfair() {

        System.out.println("unfair version");
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Job(unfairLock));
            thread.setName("" + i);
            thread.start();
        }
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 
    private static class Job implements Runnable {

        private Lock lock;
        public Job(Lock lock) {
            this.lock = lock;
        }

        @Override
        public void run() {

            for (int i = 0; i < 5; i++) {
                lock.lock();
                try {
                    System.out.println("Lock by:" + Thread.currentThread().getName());
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}
测试用例
unfair version
Lock by:1
Lock by:1
Lock by:1
Lock by:1
Lock by:1
Lock by:0
Lock by:0
Lock by:3
Lock by:3
Lock by:3
Lock by:3
Lock by:3
Lock by:0
Lock by:0
Lock by:0
Lock by:2
Lock by:2
Lock by:2
Lock by:2
Lock by:2
Lock by:4
Lock by:4
Lock by:4
Lock by:4
Lock by:4

fair version
Lock by:0
Lock by:0
Lock by:0
Lock by:0
Lock by:0
Lock by:1
Lock by:4
Lock by:2
Lock by:3
Lock by:1
Lock by:4
Lock by:2
Lock by:3
Lock by:1
Lock by:4
Lock by:2
Lock by:3
Lock by:1
Lock by:4
Lock by:2
Lock by:3
Lock by:1
Lock by:4
Lock by:2
Lock by:3
测试结果

仔细观察返回的结果(其中每个数字代表一个线程),非公平的结果一个线程连续获取锁的情况非常多,而公平的结果连续获取的情况基本没有。

可中断模式测试

public class Buffer {

    private Object lock;

    public Buffer() {
        lock = this;
    }

    public void write() {
        
        synchronized (lock) {
            long startTime = System.currentTimeMillis();
            System.out.println("开始往这个buff写入数据…");
            // 模拟要处理很长时间    
            for(;;) {
                if(System.currentTimeMillis() - startTime > Integer.MAX_VALUE) {
                    break;
                }
            }
            System.out.println("终于写完了");
        }
    }

    public void read() {
        synchronized(lock) {
            System.out.println("从这个buff读数据");
        }
    }
    
    public static void main(String[] args) {
        
        Buffer buff = new Buffer();

        final Writer writer = new Writer(buff);
        final Reader reader = new Reader(buff);

        writer.start();
        reader.start();

        new Thread(new Runnable() {

            @Override
            public void run() {
                long start = System.currentTimeMillis();
                for (;;) {
                    // 等5秒钟去中断读    
                    if(System.currentTimeMillis() - start > 5000) {
                        System.out.println("不等了,尝试中断");
                        reader.interrupt();
                        break;
                    }
                }
            }
        }).start();
    }
}

class Writer extends Thread {

    private Buffer buff;

    public Writer(Buffer buff) {
        this.buff = buff;
    }

    @Override
    public void run() {
        buff.write();
    }
}

class Reader extends Thread {

    private Buffer buff;

    public Reader(Buffer buff) {
        this.buff = buff;
    }

    @Override
    public void run() {

        buff.read();//这里估计会一直阻塞    

        System.out.println("读结束");

    }
}
synchronized加锁
开始往这个buff写入数据…
不等了,尝试中断
测试结果

我们期待“读”这个线程能退出等待锁,可是事与愿违,一旦读这个线程发现自己得不到锁,就一直开始等待了,就算它等死,也得不到锁,因为写线程要21亿秒才能完成,即使我们中断它,它都不来响应下,看来真的要等死了。

这个时候,ReentrantLock给了一种机制让我们来响应中断, 让“读”能伸能屈,勇敢放弃对这个锁的等待。我们来改写Buffer这个类,就叫InterruptiblyBuffer吧,可中断缓存。

public class InterruptiblyBuffer {

    private ReentrantLock lock = new ReentrantLock();

    public void write() {
        lock.lock();
        try {
            long startTime = System.currentTimeMillis();
            System.out.println("开始往这个buff写入数据…");
            // 模拟要处理很长时间    
            for (;;) {
                if(System.currentTimeMillis() - startTime > Integer.MAX_VALUE) {
                   break;
                }
            }
            System.out.println("终于写完了");
        }finally {
            lock.unlock();
        }
    }

    public void read() throws InterruptedException {
        // 可中断模式获取资源
        lock.lockInterruptibly();
        try {
            System.out.println("从这个buff读数据");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String args[]) {
        InterruptiblyBuffer buff = new InterruptiblyBuffer();

        final Writer writer = new Writer(buff);
        final Reader reader = new Reader(buff);

        writer.start();
        reader.start();

        new Thread(new Runnable() {

            @Override
            public void run() {
                long start = System.currentTimeMillis();
                for (;;) {
                    if(System.currentTimeMillis() - start > 5000) {
                        System.out.println("不等了,尝试中断");
                        reader.interrupt();
                        break;
                    }
                }
            }
        }).start();

    }
}

class Reader extends Thread {

    private InterruptiblyBuffer buff;

    public Reader(InterruptiblyBuffer buff) {
        this.buff = buff;
    }

    @Override
    public void run() {
        // 可以收到中断的异常,从而有效退出 
        try {
            buff.read();
        } catch (InterruptedException e) {
            System.out.println("我不读了");
        }
        System.out.println("读结束");
    }
}

class Writer extends Thread {

    private InterruptiblyBuffer buff;

    public Writer(InterruptiblyBuffer buff) {
        this.buff = buff;
    }

    @Override
    public void run() {
        buff.write();
    }
}
ReentrantLock加锁
开始往这个buff写入数据…
不等了,尝试中断
我不读了
读结束
测试结果

 

可重入锁的使用场景

场景1:如果发现该操作已经在执行中则不再执行(有状态执行)

  1. 用在定时任务时,如果任务执行时间可能超过下次计划执行时间,确保该有状态任务只有一个正在执行,忽略重复触发。
  2. 用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。

  以上两种情况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操作等)

public class Demo {
    
    private static ReentrantLock lock = new ReentrantLock();
    
    public static void main(String[] args) {
        // 如果已经被lock,则立即返回false不会等待,达到忽略操作的效果 
        if(lock.tryLock()) {  

            try {

               //操作

            } finally {
                lock.unlock();
            }

        }
    }
}
代码用例

 

场景2:如果发现该操作已经在执行,等待一个一个执行(同步执行,类似synchronized)

  这种比较常见大家也都在用,主要是防止资源使用冲突,保证同一时间内只有一个操作可以使用该资源。
  但与synchronized的明显区别是性能优势(伴随jvm的优化这个差距在减小)。同时Lock有更灵活的锁定方式,公平锁与不公平锁,而synchronized永远是公平的。

  这种情况主要用于对资源的争抢(如:文件操作,同步消息发送,有状态的操作等)

  ReentrantLock默认情况下为不公平锁

public class Demo {
    
    private static ReentrantLock lock = new ReentrantLock();
    
    public static void main(String[] args) {
        try {
            // 如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
            lock.lock(); 

           //操作

        } finally {
            lock.unlock();
        }
    }
}
代码用例

 

场景3:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)

  这种其实属于场景2的改进,等待获得锁的操作有一个时间的限制,如果超时则放弃执行。
  用来防止由于资源处理不当长时间占用导致死锁情况(大家都在等待资源,导致线程队列溢出)。

public class Demo {
    
    private static ReentrantLock lock = new ReentrantLock();
    
    public static void main(String[] args) {
        try {
            // 如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行
            if(lock.tryLock(5, TimeUnit.SECONDS)) {
                try {

                    //操作

                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            // 当前线程被中断时(interrupt),会抛InterruptedException
            e.printStackTrace();                  
        }
    }
}
代码用例

 

场景4:如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作。 

  synchronized与Lock在默认情况下是不会响应中断(interrupt)操作,会继续执行完。lockInterruptibly()提供了可中断锁来解决此问题。(场景2的另一种改进,没有超时,只能等待中断或执行完毕)

  这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞)

public class Demo {
    
    private static ReentrantLock lock = new ReentrantLock();
    
    public static void main(String[] args) {
        
        try {
            lock.lockInterruptibly();
            //操作

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        
    }
}
代码用例

 

ps:可重入概念

若一个程序或子程序可以“安全的被并行执行(Parallel computing)”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,可以再次进入并执行它(并行执行时,个别的执行结果,都符合设计时的预期)。可重入概念是在单线程操作系统的时代提出的。

 

参考资料:

摘自: 并发编程网 – ifeve.com本文链接地址: ReentrantLock(重入锁)以及公平性

摘自:http://www.cnblogs.com/jianwei-dai/p/6004657.html

摘自:https://my.oschina.net/noahxiao/blog/101558

 

posted @ 2017-06-06 14:46  戏子诺  阅读(162)  评论(0)    收藏  举报