• 显式锁(二)Lock接口与显示锁介绍


    一、显式锁简介

       显式锁,这个叫法是相对于隐式锁synchronized而言的,加锁和解锁都要用户显式地控制。显示锁Lock是在Java5中添加到jdk的,同synchronized一样,这也是一种协调共享对象访问的机制。但是它不是用来替代内置锁的,而是一种可选择的高级功能。

    1、Lock接口提供了synchronized关键字不具备的主要特性:

    • 尝试非阻塞获取锁:当前线程尝试获取锁,如果这一时刻,锁没有被其他线程占有,那么成功获取锁并返回。
    • 能被中断地获取锁:当线程正在等待获取锁,则这个线程能够 响应中断,即当中断来了,线程不会阻塞等待获取锁,抛出中断异常。
    • 超时获取锁:在指定的截止时间前获取锁,如果截止时间到了仍旧无法获取锁,则返回;

    关于Lock与synchronized的区别,请参考我的上一篇博文。

    2、两种显式锁

    JDK中提供了两种显式锁,即Lock的实现方式有两种:ReentrentLock(重入锁)、ReentrantReadWriteLock.ReadLock 和 ReentrantReadWriteLock.WriteLock(这两个锁是由其父类ReentrantReadWriteLock 控制使用,可视为一体,称为读写锁)。具体的Lock接口的继承结构,可参考下图:

    这里写图片描述


    二、Lock接口的API

    方法名称 描述
    void lock( ) 阻塞地获取锁,直到获取到锁才返回,而且是不可中断的。
    void lockInterruptibly( ) throws InterruptedException 可中断地获取锁,与lock()方法的不同之处,在于该方法在阻塞等待锁的过程中会 响应中断。
    boolean tryLock( ) 尝试非阻塞地获取锁,即调用该方法后,立刻返回,成功获取锁,返回true,失败则返回false。
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException 可中断的超时获取锁,在以下3种情况下会返回:
    1. 当前线程在指定时间内获得了锁;
    2. 当前线程在指定时间内被中断;
    3. 指定时间结束(超时结束),返回false;
    void unlock( ) 释放锁。
    Condition newCondition() 等待通知组件,当前线程只有获得了锁,才能调用该组件的wait()方法,调用后,线程将会释放锁

    三、ReentrentLock重入锁 详解

      ReentrentLock 是Lock接口的常用的实现类,是一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。更加灵活(实现了显示锁的特性)。下面将由ReentrentLock 来介绍体验显示锁的特性:

    1、可中断锁与不可中断锁 - - lockInterruptibly( )、lock( )

      Lock接口不仅提供了不可中断锁(synchronized是不可中断的),还有可中断锁。在某些应用场景下,可中断锁的用处很大:当检测到线程等待锁的时间过长,不能继续等待,需要进行下一步操作;或者某个任务已经完成了,则中断其他等待锁来完成这个任务的线程。
       显式锁的加锁和解锁都是由用户来操作,所以用户一旦忘记释放锁了,很可能就会造成线程讥饿。正确的用是使用 try-finally 确保锁能被正确释放。

       public void m() { 
         lock.lock();  // block until condition holds
         try {
           // ... method body
         } finally {
           lock.unlock()
         }
       }
    

    如果是可中断方式获取锁 lockInterruptibly,则要 try-finally 要处于 捕获中断异常的 try-catch 块间,或者在方法上抛出中断异常。

    public void method() throws InterruptedException {//抛出中断异常
        lock.lockInterruptibly();
        try {  
         //方法体.....
        }
        finally {
            lock.unlock();
        }  
    }
    
       public void m() { 
               try {
    				lock.lockInterruptibly();
    				try{
    					// ... method body
    				}finally{
    					lock.unlock();
    				}
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    	}
    

    中断锁的例子:

    //静态变量
    static Lock lock =new ReentrantLock();
    	
    public static void main(String[] args) {
    	
    	Thread A = new Thread("A"){
    
    		@Override
    		public void run() {
    			//不可中断锁,在等待获取锁的过程,忽略中断
    			lock.lock();
    			try {
    				System.out.println("线程"+getName()+"成功获取锁");
    			} finally {
    				lock.unlock();
    			}
    		}
    	};
    	
    	Thread B = new Thread("B"){
    
    		@Override
    		public void run() {
    			try {
    				//可中断锁,在等待获取锁的过程中,如果有中断到来,将会停止获取锁,并抛出中断异常
    				lock.lockInterruptibly();
    				try{//
    					System.out.println("线程"+getName()+"成功获取锁");
    				}finally{
    					lock.unlock();
    				}
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			
    		}
    	};
    	
    	//mian线程保持着锁时,再启动A、B线程,确保中断A、B线程时,A、B线程在等待获取锁
    	lock.lock();
    	try{
    		A.start();
    		B.start();
    		System.out.println("中断A、B线程");
    		A.interrupt();
    		B.interrupt();
    	}finally{
    		lock.unlock();
    	}
      
    }
    

    运行结果:
    这里写图片描述


    2、非阻塞获取锁 - - tryLock( )

    调用此方法后,无论是否成功获取锁,都将立刻返回,成功获取锁,返回true,否则,返回false;

    public static void main(String[] args) throws InterruptedException {
    	
    	Lock lock = new ReentrantLock();
    	Thread A = new Thread("A"){
    		@Override
    		public void run() {
    			if(lock.tryLock()){//尝试非阻塞获取锁
    				try{
    					System.out.println(getName()+"成功获取锁");
    				}finally {//释放锁
    					lock.unlock();
    				}
    			}else{
    				System.out.println(getName()+"获取锁失败!");
    			}
    		}
    	};
    	
       if(lock.tryLock()){//main线程成功获取锁后,启动线程A
    	   try{
    			A.start();
    			System.out.println(Thread.currentThread().getName()+"启动线程A");
    			//sleep可以保持锁,模拟main线程还要运行1秒
    			TimeUnit.SECONDS.sleep(1);
    		}finally {
    			lock.unlock();
    		}
       }else{
    	   System.out.println("程序结束!");
       }
    }
    

    运行结果:

    main启动线程A
    A获取锁失败!


    3、超时获取锁 - - tryLock(long time, TimeUnit unit)

    与tryLock( )相比,除了不是立刻返回,而是超时等待外,tryLock(long time, TimeUnit unit)还是可以被中断的。
    改造一下上面的例子,将tryLock()换成 tryLock(1,TimeUnit.SECONDS):

    public static void main(String[] args) throws InterruptedException {
    	
    	Lock lock = new ReentrantLock();
    	Thread A = new Thread("A"){
    		@Override
    		public void run() {
    			try {
    				if(lock.tryLock(1,TimeUnit.SECONDS)){//超时等待获取锁
    					try{
    						System.out.println(getName()+"成功获取锁");
    					}finally {
    						lock.unlock();
    					}
    				}else{
    					System.out.println(getName()+"获取锁失败!");
    				}
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	};
    	
       if(lock.tryLock()){//main线程成功获取锁后,启动线程A
    	   try{
    			A.start();
    			System.out.println(Thread.currentThread().getName()+"启动线程A");
    			//sleep可以保持锁,模拟main线程还要运行1秒
    			TimeUnit.SECONDS.sleep(1);
    		}finally {
    			lock.unlock();
    		}
       }else{
    	   System.out.println("程序结束!");
       }
    }
    

    运行结果:

    main启动线程A
    A成功获取锁

    上面的3小点是 显式锁的区别与隐式锁 synchronize的特性,接下来的几点,则是ReentrantLock的方法讲解,不是 显式锁的特性;


    4、可重入的锁

    可重入的锁:是指线程持有了某个锁,便可以进入任意的该锁同步着的代码块。
    不可重入的锁:线程进入任何一个同步的代码块都必须获取锁,即使这些代码块是同一个锁;
      使用可重入锁,可以很大程度地避免死锁,所以不可重入锁的应用场景很少,JDK提供的锁(synchronize、ReentrentLock、ReentrantReadWriteLock)都是可重入的锁。当线程获取重入锁时,先判断线程是不是已经持有该锁,如果是,那么重入计数器加一,否则去获取该锁。ReentrentLock中,提供了锁被线程重入的次数的方法 - - getHoldCount()。

    //静态变量
    static ReentrantLock lock = new ReentrantLock();
    static int num = 5;
    public static void main(String[] args)
       Thread B = new Thread("B"){
    	@Override
    	public void run() {
    		lock.lock();
    		try{
    			int aa = 5*5;
    			//countNumber里面也要获取同步锁,而且与当前线程所拥有的锁是同一个
    			countNumber(aa);  //可重入,意味着不需要再次去等待获取锁
    			System.out.println("num的值是:"+num);
    		}finally {
    			lock.unlock();
    		}
    	}
       };
       B.start();
    }
    
    public static void countNumber(int a){ //包含同步代码块
    	lock.lock(); //如果是重入,则重入计数器加一
    	try{
    		num+=a;
    		System.out.println("锁lock被当前线程重入的次数:"+lock.getHoldCount());
    	}finally {
    		lock.unlock();//如果是重入,则重入计数器减一
    	}
    }
    

    运行结果:

    锁lock被当前线程重入的次数:2
    num的值是:30


    5、公平锁 和 非公平锁

    ReentrantLock 支持公平锁,可以在构造方法中传入参数设置,默认为非公平锁;
    ReentrantLock(boolean fair): 创建一个具有给定公平策略的 ReentrantLock。此类的构造方法接受一个可选的公平 参数。当设置为 true 时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程,即先来先获取锁。否则此锁将无法保证任何特定访问顺序。
    关于公平锁的注意以下三点:

    • 与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小
    • 公平锁不能保证线程调度的公平性。公平锁保证的是获取锁的顺序。
    • 未定时的 tryLock 方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。
    public class MyTest{
    	//设置成公平锁模式
    	static ReentrantLock lock = new ReentrantLock(true);
    	
        public static void main(String[] args) {
        	
    		Thread threadA = new Thread(new MyRunable4(),"threadA"); 
    		Thread threadB = new Thread(new MyRunable4(),"threadB"); 
    		Thread threadC = new Thread(new MyRunable4(),"threadC");
    		Thread threadD = new Thread(new MyRunable4(),"threadD");
    		//启动四个线程
    		threadA.start();
    		threadB.start();
    		threadC.start();
    		threadD.start();
    	}
        
        public static void countNumber(){
        	//
        	lock.lock();
        	try{
        		System.out.println("线程锁"+Thread.currentThread().currentThread().getName()+"成功获取了锁!");
        	}finally{
        		lock.unlock();
        	}	
        }
    
    }
    
    class MyRunable4 implements Runnable{
    
    	@Override
    	public void run() {
    		while(true){
    			MyTest.countNumber();
    		}
    		
    	}
    }
    

    运行结果:
      下面的结果尽管不是按照启动的顺序来执行(这是因为调用start( )方法后是进入就绪队列,公平锁无法保证线程的调度,因此4个线程谁被先调度就先去获取锁),但是却是一直按照特定的顺序来执行的(C->B->A->D);

    线程锁threadC成功获取了锁!
    线程锁threadB成功获取了锁!
    线程锁threadA成功获取了锁!
    线程锁threadD成功获取了锁!
    线程锁threadC成功获取了锁!
    线程锁threadB成功获取了锁!
    线程锁threadA成功获取了锁!
    线程锁threadD成功获取了锁!
    线程锁threadC成功获取了锁!
    线程锁threadB成功获取了锁!
    线程锁threadA成功获取了锁!
    线程锁threadD成功获取了锁!
    线程锁threadC成功获取了锁!
    线程锁threadB成功获取了锁!
    线程锁threadA成功获取了锁!
    .......


    6、ReentrentLock的其他API方法

    ReentrentLock 除了实现Lock接口外,还提供了对检测和监视可能很有用的方法,包括三个protected方法。

    boolean hasQueuedThread(Thread thread):
    查询给定线程是否正在等待获取此锁。注意,因为随时可能发生取消,所以返回 true 并不保证此线程将获取此锁。此方法主要用于监视系统状态。
    boolean hasQueuedThreads( ):
    查询是否有些线程正在等待获取此锁。注意,因为随时可能发生取消,所以返回 true 并不保证有其他线程将获取此锁。此方法主要用于监视系统状态。
    boolean hasWaiters(Condition condition):
    查询是否有些线程正在等待与此锁有关的给定条件。注意,因为随时可能发生超时和中断,所以返回 true 并不保证将来某个 signal 将唤醒线程。此方法主要用于监视系统状态。
    boolean isLocked( ):
    查询此锁是否由任意线程保持。此方法用于监视系统状态,不用于同步控制。
    boolean isFair( ): 如果此锁的公平设置为 true,则返回 true。
    boolean isHeldByCurrentThread( ): 查询当前线程是否保持此锁。

    int getHoldCount( ): 查询当前线程保持此锁的次数。
    int getQueueLength( ):
    返回正等待获取此锁的线程估计数。该值仅是估计的数字,因为在此方法遍历内部数据结构的同时,线程的数目可能动态地变化。此方法用于监视系统状态,不用于同步控制。
    int getWaitQueueLength(Condition condition):
    返回等待与此锁相关的给定条件的线程估计数。注意,因为随时可能发生超时和中断,所以只能将估计值作为实际等待线程数的上边界。此方法用于监视系统状态,不用于同步控制。

    三个protected方法,提供给用户在继承ReentrentLock时,拥有更多的监控方法:

    protected Thread getOwner( ):
    返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。当此方法被不是拥有者的线程调用,返回值反映当前锁状态的最大近似值。例如,拥有者可以暂时为 null,也就是说有些线程试图获取该锁,但还没有实现。此方法用于加快子类的构造速度,提供更多的锁监视设施。
    protected Collection getQueuedThreads( ):
    返回一个 collection,它包含可能正等待获取此锁的线程。因为在构造此结果的同时实际的线程 set 可能动态地变化,所以返回的 collection 仅是尽力的估计值。所返回 collection 中的元素没有特定的顺序。此方法用于加快子类的构造速度,以提供更多的监视设施。
    protected Collection getWaitingThreads(Condition condition):
    返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。因为在构造此结果的同时实际的线程 set 可能动态地变化,所以返回 collection 的元素只是尽力的估计值。所返回 collection 中的元素没有特定的顺序。此方法用于加快子类的构造速度,提供更多的条件监视设施。

    重点介绍以下两个方法:
    1、boolean isHeldByCurrentThread( ): 查询当前线程是否保持此锁。
    与内置监视器锁的 Thread.holdsLock(java.lang.Object) 方法类似,此方法通常用于调试和测试。例如,只在保持某个锁时才应调用的方法可以声明如下:

    class X {
       ReentrantLock lock = new ReentrantLock();
       // ...
    
       public void m() { 
          //在保持某个锁的条件下才进入,
           assert lock.isHeldByCurrentThread();
           // ... method body
       }
     }
     还可以用此方法来确保某个重入锁是否以非重入方式使用的,例如: 
    
     class X {
       ReentrantLock lock = new ReentrantLock();
       // ...
    
       public void m() { 
           assert !lock.isHeldByCurrentThread();
           lock.lock();
           try {
               // ... method body
           } finally {
               lock.unlock();
           }
       }
     }
    

    2、public int getHoldCount( ):查询当前线程保持此锁的次数。
    对于与解除锁操作不匹配的每个锁操作,线程都会保持一个锁。

    保持计数信息通常只用于测试和调试。例如,如果不应该使用已经保持的锁进入代码的某一部分,则可以声明如下:

     class X {
       ReentrantLock lock = new ReentrantLock();
       // ...     
       public void m() { 
         assert lock.getHoldCount() == 0;
         lock.lock();
         try {
           // ... method body
         } finally {
           lock.unlock();
         }
       }
     }
    

  • 相关阅读:
    Java并发编程:同步容器
    poj 1961 Period
    html与JacaScript中的重要思想:预留后路、向后兼容、js分离
    SQL从头開始
    android帧动画,移动位置,缩放,改变透明度等动画解说
    COCOS学习笔记--Cocod2dx内存管理(三)-Coco2d-x内存执行原理
    构建基于Javascript的移动CMS——生成博客(二).路由
    Oracle 单表选择率
    刚接触Joomla,写一下瞎折腾的初感受~
    Android学习笔记之ProgressBar案例分析
  • 原文地址:https://www.cnblogs.com/jinggod/p/8491242.html
Copyright © 2020-2023  润新知