Java并发之同步 —— 锁对象与条件对象(ReentrantLock 、ReentrantReadWriteLock、Condition)

I.锁对象

有两种机制防止代码受并发访问的干扰。Java语言提供一个 synchronized 关键字达 到这一目的,并且 Java SE 5.0 引入了 ReentrantLock 类。

i.ReentrantLock 重入锁

private ReentrantLock lock = new ReentrantLock();

public void transfer(){
    lock.lock();
    try{
    ...
    }finally{
        lock.unlock();//;// make sure the lock is unlocked even if an exception is thrown
    }    
}

当不同线程调用同一个对象的transfer()方法时,其中线程A得到锁对象,线程B在执行到lock.lock() 无法获得锁对象,线程B会阻塞直到线程A释放锁资源。当然ReentrantLock默认是非公平锁。线程C也可以和线程B争夺线程A释放的锁资源

1.构造方法:

RentrantLock()

构建一个可以被用来保护临界区的可重入锁(线程可以重复的获得已经持有的锁,锁保存一个持有技术器来跟踪对lock方法的嵌套调用,线程每一次调用lock都要使用unlock来释放锁,由于这一特性,被一个锁保护的代码可以调用另一个相同锁的方法)

ReentrantLock(boolean fair)

构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的锁,但这一公平将大大降低性能。即时使用公平锁,也无法确保线程调度器是公平的,如果线程调度器忽略了一个线程。

2.锁测试与超时

线程在调用 lock 方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。tryLock 方法试图申请一个锁, 在成功获得锁后返回 true, 否则, 立即返回 false, 而且线程可以立即离开去做其他事情。不会阻塞当前线程。 

1)java1.5 Lock接口定义


package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;


public interface Lock {

    
    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();
}

2)说明

• void lock( ) 获取这个锁;如果锁同时被另一个线程拥有则发生阻塞。

• void unlock( ) 释放这个锁。

• boolean tryLock()
尝试获得锁而没有发生阻塞;如果成功返回真。这个方法会抢夺可用的锁, 即使该锁有公平加锁策略, 即便其他线程已经等待很久也是如此。

• boolean tryLock(long time, TimeUnit unit)
尝试获得锁,阻塞时间不会超过给定的值;如果成功返回 true。

• void lockInterruptibly()
获得锁, 但是会不确定地发生阻塞。如果线程被中断, 抛出一个 InterruptedException异常。从而避免发生死锁。

3)使用

tryLock 方法试图申请一个锁, 在成功获得锁后返回 true, 否则, 立即返回false, 而且线程可以立即离开去做其他事情。

if (myLock.tryLock())
{
// now the thread owns the lock
try { . . . }
finally { myLock.unlock(); }
}
else
// do something else

4)lock和tryLock 比较

lock 方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之 前一直处于阻塞状态。如果出现死锁, 那么,lock 方法就无法终止。 然而,如果调用带有用超时参数的 tryLock, 那么如果线程在等待期间被中断,将抛出 InterruptedException 异常。这是一个非常有用的特性,因为允许程序打破死锁。 也可以调用 locklnterruptibly 方法。它就相当于一个超时设为无限的 tryLock 方法。

ii.ReentrantReadWriteLock 读写锁

用读/写锁:一个资源可以被多个读操作访问,或者被 一个写操作访问,但两者不能同时进行。

1.算法特性:

  • 读线程插队:在非公平模式下,读线程无条件插队
  • 重入性 :同ReentrantLock
  • 降级:一个线程持有写入锁,在不释放的锁的情况下,“降级”为读取锁(一个线程拥有写入锁时,也可以降级为读取锁,但一个线程拥有读取锁时,必须等待读取锁释放才能获取写入锁)

2.原理:

    通过一个state状态变量控制,高16位控制读取锁,低16位控制写入锁

   ReentrantReadWriteLock源码中,使用位运算方式 ,sharedCount获得高16位,exclusiveCount获得低16位

        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** Returns the number of shared holds represented in count  */
        /** 返回计数中表示的共享持有数 */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** Returns the number of exclusive holds represented in count  */
        /** 返回计数中表示的独占保持数 */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

II.Condition 条件对象

通常, 线程进人临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对 象来管理那些已经获得了一个锁但是却不能做有用工作的线程。

现在来细化银行转账程序。需要避免没有足够资金进行转账,需要加判断

if(bank.getBalance(from) >= amount)

//thread might be deactivated at this point 

 bank,transfer(from,to,amount);   

 当前线程完全有可能在成功的完成测试,且在tansfer方法前中断。线程在次运行时,可能账户余额已经低于提取金额。
通过使用锁来保护检查和转账这一动作。(还是有弊端,没办法保证判断和转账的原子性操作?-所有线程对这个账户的操作都使用 bankLock排他)

public void transfer(int from,int to,int amount){
    bankLock.lock();
    try{
        while(accounts[from]<amount){
              //wait;
        }
        // transfer funds
    
    }finally{
        bankLock.unlock();
    }

}

现在,当账户中没有足够余额时,会一直while循环等待,另一个线程向账户中注入了足够资金。但是这个线程刚刚获得了bankLock的排他性访问,因此别的线程没有进行存款操作的机会,因此我们需要引入一个条件对象,让此线程暂时阻塞并释放锁。

class Bank{

    private Condition sufficientFunds;
    ...
    public Bank(){
       
    ..
        sufficientFunds = bankLock.newCondition(); //    
    }

    public void transfer(int from,int to,int amount){
    bankLock.lock();
    try{
        while(accounts[from]<amount){
              sufficientFunds.await()//wait;
        }
        // transfer funds
        sufficientFunds.signalAll();  
    }finally{
        bankLock.unlock();
    }

}
}

 如果transfer方法发现余额不足,调用

sufficientFunds.await();

 当前线程现在被阻塞了,并释放锁,其他线程可以获得锁,并进行余额增加的操作。

III.测试

package com.java.future;

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

public class ConditionAndLock {
	private ReentrantLock lock = new ReentrantLock();
	private Condition condition = lock.newCondition();
	
	public void method1(){
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "进入线程");
			Thread.sleep(3000);
			System.out.println(Thread.currentThread().getName() + "释放锁,阻塞进入等待");
			condition.await();//线程会释放当前占用的锁,并进入等待
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName() + "被唤醒");
		System.out.println(Thread.currentThread().getName() + "离开线程,并解锁");
		lock.unlock();
	}
	
	public void method2(){
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + "进入线程 ");
			Thread.sleep(500);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName() + "随机唤醒等待队列中的一个线程");
		condition.signal();//随机唤醒等待队列中的一个线程
		System.out.println(Thread.currentThread().getName() + "离开线程,并解锁");
		lock.unlock();
	}
	
	public static void main(String[] args) {
		ConditionAndLock c = new ConditionAndLock();
		Thread t  =new Thread(new Runnable() {
			@Override
			public void run() {
				c.method1();
				
			}
		});
		Thread t2  =new Thread(new Runnable() {
			@Override
			public void run() {
				c.method2();
				
				
			}
		});
		t.start();
		t2.start();
	}
}

 结果

Thread-0进入线程
Thread-0释放锁,阻塞进入等待
Thread-1进入线程 
Thread-1随机唤醒等待队列中的一个线程
Thread-1离开线程,并解锁
Thread-0被唤醒
Thread-0离开线程,并解锁

 

IV.小结

锁与条件的关键之处:

  1. 锁用来保护代码片段, 任何时刻只能有一个线程执行被保护的代码。 
  2. 锁可以管理试图进入被保护代码段的线程。 
  3. 锁可以拥有一个或多个相关的条件对象。
  4. 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。

参考 : java核心技术 卷I (第9版) 第14章 

猜你喜欢

转载自blog.csdn.net/zl_momomo/article/details/81198407