zoukankan      html  css  js  c++  java
  • Java并发编程之重入锁(ReentrantLock)

    简介

    ReentrantLock重入锁,由于它的加锁和解锁操作需要手动来完成,所以也称为显式锁

    ReentrantLock是基于AQS独占模式实现的独占锁,同时只能有一个线程能获取到该锁,获取不到锁的线程将被放入该锁的AQS阻塞队列中等待。

    ReentrantLock的简单使用

    一个ReentrantLock使用示例,对临界资源进行加锁,当线程访问临界资源时需要先获得锁,如果锁已经被其他线程所持有,则需要等待其他线程释放锁后再重新获取锁,直到获取锁之后才能继续执行。

    public class ReentrantLockDemo {
        public static void main(String[] args) {
            ReentrantLock reentrantLock = new ReentrantLock();
    
            Runnable runnable = () -> {
                try{
                    reentrantLock.lock();
                    System.out.println(Thread.currentThread().getName() + "获得了锁");
                    Thread.sleep(3000);
                }catch (InterruptedException ex){
                    ex.printStackTrace();
                }finally {
                    reentrantLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了锁");
                }
            };
    
            Thread thread1 = new Thread(runnable);
            Thread thread2 = new Thread(runnable);
            thread1.start();
            thread2.start();
        }
    }
    
    /*
     * 输出结果:
     * Thread-1获得了锁
     * Thread-1释放了锁
     * Thread-0获得了锁
     * Thread-0释放了锁
     */
    

    可重入性

    可重入性是指线程获得锁之后,没有释放锁,当再次需要获得同一把锁的时候仍然可以成功。如果锁是不可重入的,第二次获得锁的时候就会导致死锁。

    state变量记录了锁的重入次数,每次lock()方法的调用会使state加1,每次unlock()方法的调用会使state减1,当state为0时锁被释放,将锁的持有者设置为null。

    如下示例,多次调用lock()方法是可以的,但是对应的unlock()方法也要调用对应次数。

    public class ReentrantLockDemo {
        public static void main(String[] args) {
            ReentrantLock reentrantLock = new ReentrantLock();
    
            Runnable runnable = () -> {
                try{
                    reentrantLock.lock();
                    reentrantLock.lock();
                    System.out.println(Thread.currentThread().getName() + "获得了锁");
                    Thread.sleep(3000);
                }catch (InterruptedException ex){
                    ex.printStackTrace();
                }finally {
                    reentrantLock.unlock();
                    reentrantLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了锁");
                }
            };
    
            Thread thread1 = new Thread(runnable);
            Thread thread2 = new Thread(runnable);
            thread1.start();
            thread2.start();
        }
    }
    
    /*
     * 输出结果:
     * Thread-1获得了锁
     * Thread-1释放了锁
     * Thread-0获得了锁
     * Thread-0释放了锁
     */
    

    当unlock()方法次数少于lock()方法调用次数时,state变量值大于0,相当于当前线程还持有该锁,其他线程依然不能获取到锁,只能继续等待。如下所示:

    public class ReentrantLockDemo {
        public static void main(String[] args) {
            ReentrantLock reentrantLock = new ReentrantLock();
    
            Runnable runnable = () -> {
                try{
                    reentrantLock.lock();
                    reentrantLock.lock();
                    System.out.println(Thread.currentThread().getName() + "获得了锁");
                    Thread.sleep(3000);
                }catch (InterruptedException ex){
                    ex.printStackTrace();
                }finally {
                    reentrantLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了锁");
                }
            };
    
            Thread thread1 = new Thread(runnable);
            Thread thread2 = new Thread(runnable);
            thread1.start();
            thread2.start();
        }
    }
    
    /*
     * 输出结果:
     * Thread-1获得了锁
     * Thread-1释放了锁
     */
    

    当unlock()方法次数大于lock()方法调用次数时,相当于锁已经被释放,锁的持有者不再是当前线程,此时再次调用unlock()方法将抛出IllegalMonitorStateException异常。

    public class ReentrantLockDemo {
        public static void main(String[] args) {
            ReentrantLock reentrantLock = new ReentrantLock();
    
            Runnable runnable = () -> {
                try{
                    reentrantLock.lock();
                    System.out.println(Thread.currentThread().getName() + "获得了锁");
                    Thread.sleep(3000);
                }catch (InterruptedException ex){
                    ex.printStackTrace();
                }finally {
                    reentrantLock.unlock();
                    reentrantLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了锁");
                }
            };
    
            Thread thread1 = new Thread(runnable);
            Thread thread2 = new Thread(runnable);
            thread1.start();
            thread2.start();
        }
    }
    

    输出结果:

    Thread-0获得了锁
    Thread-1获得了锁
    Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
    	at java.base/java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:149)
    	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1302)
    	at java.base/java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:439)
    	at com.buhe.demo.demos.reen.ReentrantLockDemo.lambda$main$0(ReentrantLockDemo.java:18)
    	at java.base/java.lang.Thread.run(Thread.java:834)
    Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
    	at java.base/java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:149)
    	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1302)
    	at java.base/java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:439)
    	at com.buhe.demo.demos.reen.ReentrantLockDemo.lambda$main$0(ReentrantLockDemo.java:18)
    	at java.base/java.lang.Thread.run(Thread.java:834)
    

    公平策略

    使用无参构造函数创建的ReentrantLock默认是非公平锁,带参数的构造函数可以指定公平策略,true表示公平锁,false表示非公平锁。

    公平锁是指先请求锁的线程一定先获得锁,反之非公平锁则是随机的。

    示例:

    public class ReentrantLockDemo {
        public static void main(String[] args) {
            ReentrantLock reentrantLock = new ReentrantLock();
    
            Runnable runnable = () -> {
                for(int i = 0; i < 2; i++) {
                    try {
                        reentrantLock.lock();
                        System.out.println(Thread.currentThread().getName() + "获得了锁");
                        Thread.sleep(3000);
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    } finally {
                        reentrantLock.unlock();
                    }
                }
            };
    
            Thread thread1 = new Thread(runnable);
            Thread thread2 = new Thread(runnable);
            Thread thread3 = new Thread(runnable);
            Thread thread4 = new Thread(runnable);
            Thread thread5 = new Thread(runnable);
            thread1.start();
            thread2.start();
            thread3.start();
            thread4.start();
            thread5.start();
        }
    }
    

    锁的获取是随机的,即使刚释放锁的线程也有可能立即获得锁。

    输出结果:

    Thread-0获得了锁
    Thread-0获得了锁
    Thread-1获得了锁
    Thread-1获得了锁
    Thread-2获得了锁
    Thread-2获得了锁
    Thread-3获得了锁
    Thread-4获得了锁
    Thread-4获得了锁
    Thread-3获得了锁
    

    修改ReentrantLock的创建方式,改为公平锁

    ReentrantLock reentrantLock = new ReentrantLock(true);
    

    先请求锁的线程一定先获得锁,锁的获取是有序的。

    输出结果:

    Thread-2获得了锁
    Thread-1获得了锁
    Thread-0获得了锁
    Thread-3获得了锁
    Thread-4获得了锁
    Thread-2获得了锁
    Thread-1获得了锁
    Thread-0获得了锁
    Thread-3获得了锁
    Thread-4获得了锁
    

    使用公平锁不会产生饥饿现象,由于要维护线程的顺序,使公平锁的效率低下,所以没特殊情况应该使用非公平锁。

    和synchronized的比较

    synchronized和ReentrantLock都属于独占锁,两者性能相差不大,但是在使用上,ReentrantLock要更灵活。

    • synchronized的加锁和解锁是自动进行的,易于操作,但不够灵活。ReentrantLock的加锁和解锁是手动进行的,不易操作,但更灵活。

    • ReentrantLock支持公平锁和非公平锁,而synchronized只支持非公平锁。

    • ReentrantLock可以关联多个条件队列,而synchronized最多只能关联一个条件队列。

    • 在等待锁的过程中,使用ReentrantLock能使等待的线程响应中断,而使用synchronized时,线程只能保持等待直到获取到锁后继续执行。

    • ReentrantLock支持锁申请等待超时,当在给定的时间内还没有获取到锁,则主动放弃获取锁。

    • ReentrantLock支持尝试获取锁,如果锁没有被其他线程占用,则获得锁成功并立即返回true,如果锁被其他线程所占用,则锁获取失败直接返回false,不会进行等待。

    • ReentrantLock和synchronized都是可重入锁。

    源码分析

    ReentrantLock和AQS的关系如下图:

    ReentrantLock的内部类Sync继承自AQS,它的子类NonfairSync和FairSync分别实现了非公平锁和公平锁策略。

    通过ReentrantLock的构造函数参数来指定公平策略:

    public ReentrantLock(boolean fair) {
    	sync = fair ? new FairSync() : new NonfairSync();
    }
    

    下面以非公平锁为例分析下加锁和解锁的过程。

    加锁

    非公平锁加锁调用的是NonfairSync的lock方法,默认state为0,通过CAS将state设置为1,设置成功则表示加锁成功,并设置当前线程为锁的持有者。设置失败则调用AQS的acquire方法。

    //java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
    final void lock() {
    	//CAS设置状态值
    	if (compareAndSetState(0, 1))
    		//设置当前线程为锁的持有者
    		setExclusiveOwnerThread(Thread.currentThread());
    	else
    		//调用AQS的acquire方法
    		acquire(1);
    }
    

    acquire方法,当tryAcquire方法返回false时,会把当前线程放入AQS阻塞队列。

    //java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
    public final void acquire(int arg) {
    	if (!tryAcquire(arg) &&
    		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    		selfInterrupt();
    }
    

    tryAcquire方法由AQS的子类实现,这里就是Sync以及它的子类来实现。

    tryAcquire方法中会判断state值,如果state为0,则会尝试获取锁,使用CAS设置state的值,设置成功返回true,设置失败返回false。

    如果state大于0,则判断持有锁的线程是不是当前线程,如果是则需要更新重入次数,重新设置state的值后返回true,反之则表示锁已经被其他线程所持有,直接返回false。

    //java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
    protected final boolean tryAcquire(int acquires) {
    	return nonfairTryAcquire(acquires);
    }
    
    //java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
    final boolean nonfairTryAcquire(int acquires) {
    	final Thread current = Thread.currentThread();
    	//获取锁的状态值
    	int c = getState();
    	//判断锁的状态,为0则表示锁没有被任何线程所持有
    	if (c == 0) {
    		//CAS设置状态值
    		if (compareAndSetState(0, acquires)) {
    			/设置当前线程为锁的持有者
    			setExclusiveOwnerThread(current);
    			return true;
    		}
    	}
    	//判断持有锁的线程是否为当前线程
    	else if (current == getExclusiveOwnerThread()) {
    		int nextc = c + acquires;
    		if (nextc < 0) // overflow
    			throw new Error("Maximum lock count exceeded");
    		setState(nextc);
    		return true;
    	}
    	return false;
    }
    
    解锁

    非公平锁和公平锁的解锁过程是一致的,解锁unlock方法调用的是AQS的release方法,release方法会调用tryRelease方法尝试释放锁,释放锁成功之后的处理由AQS完成。同tryAcquire方法,tryRelease方法也是需要AQS子类实现的。

    //java.util.concurrent.locks.AbstractQueuedSynchronizer#release
    public final boolean release(int arg) {
    	if (tryRelease(arg)) {
    		Node h = head;
    		if (h != null && h.waitStatus != 0)
    			unparkSuccessor(h);
    		return true;
    	}
    	return false;
    }
    
    //java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
    protected final boolean tryRelease(int releases) {
    	//减少重入次数
    	int c = getState() - releases;
    	//如果不是锁的持有者调用unlock方法则抛出异常
    	if (Thread.currentThread() != getExclusiveOwnerThread())
    		throw new IllegalMonitorStateException();
    	boolean free = false;
    	//如果重入次数为0,则清空锁的持有线程
    	if (c == 0) {
    		free = true;
    		setExclusiveOwnerThread(null);
    	}
    	setState(c);
    	return free;
    }
    

    如果当前持有锁的线程不是当前线程则直接抛出异常,否则继续判断state的值,如果state为0,则清空锁的持有线程,如果state不为0,则减少重入次数。

    参考资料:
    《Java并发编程之美》
    一文彻底理解ReentrantLock可重入锁的使用

  • 相关阅读:
    利用模板方法模式导出Excel文件
    利用模板方法模式实现导出CSV文件功能
    抽象类多实现,省略this关键字调用实现类方法
    Spring工具类
    DateUtils工具类
    Java获取时间戳
    springMVC接受类型为Map,ajax请求参数类型为字符串将导致的请求400错误
    Set转逗号分隔字符串
    html标签中自定义属性
    编译安装MySQL
  • 原文地址:https://www.cnblogs.com/seve/p/14663940.html
Copyright © 2011-2022 走看看