Java多线程之线程安全(3)Lock(ReentrantLock)

#一.概况
前面的synchronised方式属于Java内部锁机制,Java SE5之后java.util.concurrent类库中增加了显式的互斥机制Lock。前者使用简便,后者稍微复杂(Lock锁必须显式声明,并在合适的位置释放锁),但是更加灵活。
一般情况下使用synchronized,因为它简单不容易出错。在一些特殊条件下我们才是用Lock,例如用synchronized关键字不能尝试着获取锁且最终获取锁会失败,或者尝试获取锁一段时间,然后放弃他。内置锁实际上是一种阻塞锁。而新增的Lock锁机制则是一种非阻塞锁。
#二.Lock接口和它的实现类
##2.1先看一下Lock的源码如下

public interface Lock {
	//无条件获取锁
    void lock();
    //获取可响应中断的锁
    //在获取锁的时候可响应中断,中断的时候会抛出中断异常
    void lockInterruptibly() throws InterruptedException;
    //轮询锁。如果不能获得锁,则采用轮询的方式不断尝试获得锁
    boolean tryLock();
    //定时锁。如果不能获得锁,则每隔unit的时间就会尝试重新获取锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    //释放获得锁
    void unlock();
    //获取绑定的Lock实例的条件变量。在等待某个条件变量满足的之
    //前,lock实例必须被当前线程持有。调用Condition的await方法
    //会自动释放当前线程持有的锁
    Condition newCondition();
}

可以看出Lock锁机制新增的可响应中断锁和使用公平锁是内置锁机制锁没有的。
Lock是一个接口,实现类主要是ReentrantLock,ReentrantReadWriteLock。下面看一下ReentrantLock

##2.2ReentrantLock
下面我们对比synchronized来看ReentrantLock,因为我们对前者相对来说更加熟悉,这样便于我们记忆。
####synchronized与ReentrantLock实现策略的比较
###2.2.1悲观并发策略
synchronized使用的这种并发策略,下面来解释一下悲观并发策略。
synchronized使用的是互斥锁机制,当多个线程需要获取同一把锁的时候只能通过阻塞同步的方式等待已经获得锁的线程自动释放锁。这个过程涉及线程的阻塞和线程的唤醒,这个过程需要在操作系统从用户态切换到内核态完成。那么问题来了,多个线程竞争同一把锁的时候,会引起CPU频繁的上下文切换,效率很低,系统开销也很大。这种策略就叫悲观并发策略。
###2.2.2基于冲突检测的乐观并发策略
ReentrantLock使用的这种并发策略,以自旋的方式获得锁。“自旋的方式获得锁”指的是,如果需要获得锁不存在争用的情况,那么获取成功;如果锁存在争用的情况,那么使用失败补偿措施(jdk 5之后到目前的jdk 8使用的是不断尝试重新获取,直到获取成功)解决争用的矛盾。由于自旋发生在线程内部,所以不用阻塞其他的线程,也就是实现了非阻塞同步。这种策略就叫做乐观并发模式。
##2.3ReentrantLock和synchronized比较的优势
#####1.可响应中断的锁。
当在等待锁的线程如果长期得不到锁,那么可以选择不继续等待而去处理其他事情,而synchronized的互斥锁则必须阻塞等待,不能被中断
#####2.可实现公平锁。
所谓公平锁指的是多个线程在等待锁的时候必须按照线程申请锁的时间排队等待。而非公平性锁则保证每个线程都有获得锁的机会。synchronized的锁和ReentrantLock使用的默认锁都是非公平性锁,但是ReentrantLock支持公平性的锁,在构造函数中传入一个boolean变量指定为true实现的就是公平性锁。不过一般而言,使用非公平性锁的性能优于使用公平性锁。
#####3.每个synchronized只能支持绑定一个条件变量
这里的条件变量是指线程执行等待或者通知的条件,而ReentrantLock支持绑定多个条件变量,通过调用lock.newCondition()可获取多个条件变量。不过使用多少个条件变量需要依据具体情况确定。
#三.如何在ReentrantLock和synchronized之间进行选择
在实际开发中,最长考虑的就是用哪个,哪个更适合我们的应用场景。正如本文概述中说的synchronized用不了,才使用ReentrantLock。下面就是选择的原则
######在一些synchronized无法满足一些高级功能的时候才考虑使用ReentrantLock。这些高级功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则还是应该优先使用synchronized。
####为什么前者优点这么多这么明显还是优先用后者呢?
原因如下:
1.使用内置锁dump线程信息可以帮助分析哪些调用帧获得了哪些锁,并且能够帮助检测和识别发生死锁的线程。这点是ReentrantLock无法做到的(有那么一点说服力了)。
2.synchronized未来还将继续优化,目前的synchronized已经进行了自适应、自旋、锁消除、锁粗化、轻量级锁和偏向锁等方面的优化,在线程阻塞和线程唤醒方面的性能已经没有那么大了。
3.ReentrantLock的性能可能就止步于此,未来优化的可能性很小。由于synchronized是JVM的内置属性,执行synchronized优化是必须的,毕竟是亲儿子。
#四.验证可中断性
##4.1先看synchronized,看synchronized代码块运行期间是否可中断
下面是代码,因为是AndroidStudio项目测试的,所以放在了Activity中了,自己用Java的main方法中测试也可以。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        try {
            test();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

	//下面是关键代码
    static DateFormat dateFormat;
    private void test() throws InterruptedException {
        dateFormat = new SimpleDateFormat("HH:mm:ss");

        //读写工具类实例化
        SynInterruptTest st = new SynInterruptTest();
        //写线程和读线程实例化
        Thread threadWriter = new Thread(new Writer(st),"writer");
        Thread threadReader = new Thread(new Reader(st),"reader");

        //写线程开始
        threadWriter.start();
        //读线程开始
        threadReader.start();

        //运行线程睡5s
        TimeUnit.SECONDS.sleep(5);
        //5s之后打印读线程它不想等了,并调用中断方法
        System.out.println(threadReader.getName() +":I don't want to wait anymore at " + dateFormat.format(new Date()));
        threadReader.interrupt();
    }

    //写线程
    static class Writer implements Runnable{
        AttemptLocking al;
        public Writer(AttemptLocking al){
            this.al = al;
        }

        @Override
        public void run() {
            al.write();
        }
    }
    
    //读线程
    static class Reader implements Runnable{
        AttemptLocking al;
        public Reader(AttemptLocking al){
            this.al = al;
        }

        @Override
        public void run() {
            al.read();
            //打印读结束时间
            System.out.println(Thread.currentThread().getName() + ":finish read data at "
                    + dateFormat.format(new Date()));
        }
    }
}

下面是读写打断测试工具类

public class SynInterruptTest{
    //锁对象
    private static Object lock = new Object();
    //日期格式器
    private static DateFormat format = new SimpleDateFormat("HH:mm:ss");

    //写操作
    public void write(){
        //加同步代码块
        synchronized (lock){
            //打印写开始时间
            System.out.println(Thread.currentThread().getName()+":start write date at "+format.format(new Date()));
            long start = System.currentTimeMillis();
            //模拟耗时操作,运行15s
            for (;;){
                if (System.currentTimeMillis() - start >15*1000){
                    break;
                }
            }
            //打印写结束时间
            System.out.println(Thread.currentThread().getName()+":finish write date at "+format.format(new Date()));
        }
    }

    //读操作
    public void read(){
        //加同步代码块
        synchronized (lock){
            //打印读开始时间
            System.out.println(Thread.currentThread().getName()+":start read date at "+format.format(new Date()));
        }
    }
}

先看log如下:

07-20 16:34:32.262 31541-31574/test.gong.com.myapplication I/System.out: writer:start write date at 16:34:32
07-20 16:34:37.261 31541-31541/test.gong.com.myapplication I/System.out: reader:I don't want to wait anymore at 16:34:37
07-20 16:34:47.263 31541-31574/test.gong.com.myapplication I/System.out: writer:finish write date at 16:34:47
07-20 16:34:47.264 31541-31575/test.gong.com.myapplication I/System.out: reader:start read date at 16:34:47
    reader:finish read data at 16:34:47

#####测试流程设计如下:
1.同时开始写和读,在写和读中加一个同步锁,写默认执行15s。
2.让运行线程睡5s,此时调用读线程的中断方法。看log情况,具体log的主要是时间,为了观察各个线程的执行情况。

#####下面分析log结果
从结果可以看到,尝试在读线程运行5秒后中断它,发现无果,因为写线程需要运行15秒,sleep5秒后过了10秒(sleep的5秒加上10刚好是写线程的15秒)读线程才显示中断的信息,意味着在写线程释放锁之后才响应了主线程的中断事件,也就是说在synchronized代码块运行期间不允许被中断,这点也验证了上面对synchronized的讨论。
##4.2使用ReentrantLock试一下
设计的测试流程和上面是一样的。下面是测试代码。
下面是Activity

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        try {
            test();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static DateFormat dateFormat;

    private void test() throws InterruptedException {
        dateFormat = new SimpleDateFormat("HH:mm:ss");

        //读写工具类实例化
        RetInterruptText rt = new RetInterruptText();
        //写线程和读线程实例化
        Thread threadWriter = new Thread(new Writer(rt),"writer");
        Thread threadReader = new Thread(new Reader(rt),"reader");

        //写线程开始
        threadWriter.start();
        //读线程开始
        threadReader.start();

        //运行线程睡5s
        TimeUnit.SECONDS.sleep(5);
        //5s之后打印读线程它不想等了,并调用中断方法
        System.out.println(threadReader.getName() +":I don't want to wait anymore at " + dateFormat.format(new Date()));
        threadReader.interrupt();
    }

    //写线程
    static class Writer implements Runnable{
        RetInterruptText rt;
        public Writer(RetInterruptText al){
            this.rt = al;
        }

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

    //读线程
    static class Reader implements Runnable{
        RetInterruptText rt;
        public Reader(RetInterruptText al){
            this.rt = al;
        }

        @Override
        public void run() {
            try {
                rt.read();
                //打印读结束时间
                System.out.println(Thread.currentThread().getName() + ":finish read data at "
                        + dateFormat.format(new Date()));
            } catch (Exception e) {
                System.out.println(Thread.currentThread().getName() + ":interrupt read at "
                        + dateFormat.format(new Date()));
            }
            System.out.println(Thread.currentThread().getName() + ":end read data at "
                    + dateFormat.format(new Date()));
        }
    }
}

下面是关键类RetInterruptText

public class RetInterruptText {
    //锁对象
    private ReentrantLock lock = new ReentrantLock();
    //日期格式器
    private static DateFormat format = new SimpleDateFormat("HH:mm:ss");

    //写操作
    public void write(){
        lock.lock();
        try {
            //打印写开始时间
            System.out.println(Thread.currentThread().getName()+":start write date at "+format.format(new Date()));
            long start = System.currentTimeMillis();
            //模拟耗时操作,运行15s
            for (;;){
                if (System.currentTimeMillis() - start >15*1000){
                    break;
                }
            }
            //打印写结束时间
            System.out.println(Thread.currentThread().getName()+":finish write date at "+format.format(new Date()));
        } finally {
            lock.unlock();
        }
    }

    //读操作
    public void read() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            //打印读开始时间
            System.out.println(Thread.currentThread().getName()+":start read date at "+format.format(new Date()));
        } finally {
            lock.unlock();
        }
    }
}

先看一下log

07-20 17:34:32.044 9606-9628/test.gong.com.myapplication I/System.out: writer:start write date at 17:34:32
07-20 17:34:37.043 9606-9606/test.gong.com.myapplication I/System.out: reader:I don't want to wait anymore at 17:34:37
07-20 17:34:37.045 9606-9629/test.gong.com.myapplication I/System.out: reader:interrupt read at 17:34:37
07-20 17:34:37.046 9606-9629/test.gong.com.myapplication I/System.out: reader:end read data at 17:34:37
07-20 17:34:47.045 9606-9628/test.gong.com.myapplication I/System.out: writer:finish write date at 17:34:47

读线程正常响应了我们的中断,因为读线程输出了中断信息,即使写线程写完数据后,读线程也没有输出结束读数据的信息,说明读线程没有开始读,就中断了。验证了可中断锁的分析。
#五.ReentrantLock实现公平锁
方法很简单,只要如下在构造方法中传入true参数。

private ReentrantLock lock = new ReentrantLock(true);

猜你喜欢

转载自blog.csdn.net/gongxiaoou/article/details/81170546