• java之ReentrantLock详解


    前言

    如果一个代码块被synchronized修饰了,当一个线程获取了相应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的释放,现在有这么一种情况,这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程只能干巴巴地等着,在这种情况下,非常影响程序执行效率

    所以Lock应运而生,可以不让等待的线程一直等待下去(比如只等待一定的时间或者能够响应中断)

    一、Lock接口

    (1)与synchronzed区别

    synchronized是JVM层面的内置锁,而Lock则是java层面的显示锁,Lock提供了一种可重入的、可轮询的、定时的以及可中断的锁获取操作。

    采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

    (2)lock接口源码详解

    public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    Condition newCondition();
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    }
    

    lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。 newCondition()方法返回一个Condition对象

    lock():此方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

    Lock lock = ...;
    lock.lock();
    try{
    //处理任务
    }catch(Exception ex){
     
    }finally{
        lock.unlock();   //释放锁
    }
    

    tryLock():此方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

    Lock lock = ...;
    if(lock.tryLock()) {
        try{
             //处理任务
         }catch(Exception ex){
         
         }finally{
             lock.unlock();   //释放锁
        } 
    }else {
    //如果不能获取锁,则直接做其他事情
    }
    

    tryLock(long time, TimeUnit unit):此方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

    lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

    二、ReentrantLock

    (1)模拟可中断的锁获取

    Lock中的lockInterruptibly() 可以在获得锁的同时保持对中断的响应,但是内置锁synchronized却很难实现这个功能。

    synchronized

    如下程序,创建一任务,假设该任务需要执行很长时间才能结束(使用死循环来模拟时长)。现在有两个线程竞争该资源的内置锁,在等待一段时间后,想要终止线程t2的锁获取等待操作,使用t2.interrupt(); 尝试中断线程t2。遗憾的是,此时t2根本不会响应这个中断操作,它会继续等待直到获得资源锁。

    public class InterruptedLockTest implements Runnable{
    public synchronized void doCount(){
        //使用死循环表示此操作要进行很长的一段时间才能结束
        while(true){}
    }
    
    @Override
    public void run() {
        doCount();
    }
    }
    
    
    public static void main(String[] args) throws InterruptedException {
        InterruptedLockTest test = new InterruptedLockTest();
    
        Thread t1 = new Thread(test);
        Thread t2 = new Thread(test);
    
        t1.start();
        t2.start();
    
          //等待两秒,尝试中断线程t2的等待
        TimeUnit.SECONDS.sleep(2);
        t2.interrupt();
    
        //等待1秒,让 t2.interrupt(); 执行生效
        TimeUnit.SECONDS.sleep(1);
        System.out.println("线程t1是否存活:" + t1.isAlive());
        System.out.println("线程t2是否存活:" + t2.isAlive());
    }
    
    console打印:
    线程t1是否存活:true
    线程t2是否存活:true
    

    Lock

    public class LockDemo implements Runnable {
    @Override
    public void run() {
        try {
            doCount();
        } catch (InterruptedException e) {
            System.out.println("被中断....");
        }
    }
    Lock lock = new ReentrantLock();
    
    public void doCount() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            while (true){
    
            }
        }catch (Exception e){
    
        }finally {
            lock.unlock();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        LockDemo lockDemo = new LockDemo();
    
        Thread t1 = new Thread(lockDemo);
        Thread t2 = new Thread(lockDemo);
    
        t1.start();
        t2.start();
    
        TimeUnit.SECONDS.sleep(2);
        t2.interrupt();
    
        //等待1秒,让 t2.interrupt(); 执行生效
        TimeUnit.SECONDS.sleep(1);
        System.out.println("线程t1是否存活:" + t1.isAlive());
        System.out.println("线程t2是否存活:" + t2.isAlive());
    }
    }
    
    console打印:
    被中断....
    线程t1是否存活:true
    线程t2是否存活:false
    

    (2)模拟可轮询(避免死锁)

    相比于synchronized内置锁的无条件锁获取模式,Lock提供了tryLock() 实现可定时和可轮询的锁获取模式,这也使Lock具有更完善的错误恢复机制。在内置锁中,死锁是一个很严重的问题,造成死锁的原因之一可能是,锁获取顺序不一致导致程序死锁。比如说,线程1持有A对象锁,正在等待获取B对象锁;线程2持有B对象锁,正在等待获取A对象锁。这样,两个线程都会由于获取不到想要的锁而陷入死锁的境地。解决办法可以是,两个线程要么同时获取两个锁,要么一个锁都不获取。Lock 的可定时和可轮询锁就可以很好的满足该条件,从而避免死锁的发生

    synchronized死锁问题

    public class TestDeadLock implements Runnable{  
    int flag ;
    static Object o1 = new Object();
    static Object o2 = new Object();
    
    @Override
    public void run() {
    	if(flag==0){
    		synchronized (o1) {
    			try {
    				Thread.sleep(500);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			synchronized (o2) {
    				System.out.println("flag==0");
    			}
    		}
    	}
    	else if (flag==1){
    		synchronized (o2) {
    			try {
    				Thread.sleep(500);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			synchronized (o1) {
    				System.out.println("flag==1");
    			}
    		}
    	}
    }
    
    
    public static void main(String[] args) {
    	TestDeadLock td1 = new TestDeadLock();
    	TestDeadLock td2 = new TestDeadLock();
    	td1.flag = 0;
    	td2.flag = 1;
    	Thread t1 = new Thread(td1);
    	Thread t2 = new Thread(td2);
    	t1.start();
    	t2.start();
    }
    
    }
    

    ReentrantLock可轮询

    // 资源类
    public class Resource {
        //资源总和
        private int resourceNum;
        // 显示锁
        public Lock lock = new ReentrantLock();
    
        public Resource(int resourceNum){
            this.resourceNum = resourceNum;
        }
        //返回此资源的总量
        public int getResourceNum(){
            return resourceNum;
        }
    }
    
    public class LockTest1 {
      //传入两个资源类和预期操作时间,在此期间内返回两个资源的数量总和
    public int getResource(Resource resourceA, Resource resourceB, long timeout, TimeUnit unit)
          throws InterruptedException {
        // 获取当前时间,算出操作截止时间
        long stopTime = System.nanoTime() + unit.toNanos(timeout);
    
        while(true){
            try {
                // 尝试获得资源A的锁
                if (resourceA.lock.tryLock()) {
                    try{
                        // 如果获得资源A的锁,尝试获得资源B的锁
                        if(resourceB.lock.tryLock()){
                            //同时获得两资源的锁,进行相关操作后返回
                            return getSum(resourceA, resourceB);
                        }
                    }finally {
                        resourceB.lock.unlock();
                    }
                }
            }finally {
                resourceA.lock.unlock();
            }
          
            // 判断当前是否超时,规定-1为错误标识
            if(System.nanoTime() > stopTime)
                return -1;
            
            //睡眠1秒,继续尝试获得锁
            TimeUnit.SECONDS.sleep(1);
        }
    }
    
    // 获得资源总和
    public int getSum(Resource resourceA,Resource resourceB){
        return resourceA.getResourceNum()+resourceB.getResourceNum();
    }
    }
    

    (3)公平锁

    背景

    CPU在调度线程的时候是在等待队列里随机挑选一个线程,由于这种随机性所以是无法保证线程先到先得的(synchronized控制的锁就是这种非公平锁)。但这样就会产生饥饿现象,即有些线程(优先级较低的线程)可能永远也无法获取cpu的执行权,优先级高的线程会不断的强制它的资源。那么如何解决饥饿问题呢,这就需要公平锁了。

    公平锁可以保证线程按照时间的先后顺序执行,避免饥饿现象的产生。但公平锁的效率比较地,因为要实现顺序执行,需要维护一个有序队列。

    // 也可以指定公平性
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    //默认创建非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    

    Demo

    package com.jalja.base.threadTest;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    public class LockFairTest implements Runnable{
    //创建公平锁
    private static ReentrantLock lock=new ReentrantLock(true);
    public void run() {
        while(true){
            lock.lock();
            try{
                System.out.println(Thread.currentThread().getName()+"获得锁");
            }finally{
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        LockFairTest lft=new LockFairTest();
        Thread th1=new Thread(lft);
        Thread th2=new Thread(lft);
        th1.start();
        th2.start();
    }
    }
    
    console打印:
    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    Thread-0获得锁
    Thread-1获得锁
    Thread-0获得锁
    

    分析结果可看出两个线程是交替执行的,几乎不会出现同一个线程连续执行多次。

    三、Synchronized与Lock的区别

    锁类型

    • 可重入锁:在执行对象中所有同步方法不用再次获得锁

    • 可中断锁:在等待获取锁过程中可中断

    • 公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利

    • 读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写

    区别

    类别 synchronized Lock
    存在层次 Java的关键字,在jvm层面上 是一个类
    锁的释放 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁
    锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
    锁状态 无法判断 可以判断
    锁类型 可重入 不可中断 非公平 可重入 可中断 可公平(两者皆可)
    性能 少量同步 大量同步

    关于读写锁

    我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。

  • 相关阅读:
    JavaScript面试题
    HTML&&css面试题
    nodejs面试题
    linux上安装apache
    linux上安装mysql
    linux基础命令学习(三)Vim使用
    linux基础命令学习(一)
    spring学习(六)注解方式实现AOP
    spring学习(五)详细介绍AOP
    spring学习(一)spring简介
  • 原文地址:https://www.cnblogs.com/sxkgeek/p/9401632.html
Copyright © 2020-2023  润新知