Java并发—— Java中的显式锁

538 阅读9分钟

 在Java里面,互斥同步最简单的手段就是synchronized内置锁,还有一种就是一种是concurrent包下的lock接口显式加锁

独占锁ReentrantLock

             在Java5.0之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile。 Java5.0增加了一种新的机制:ReentrantLock。它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。

基本方法


Lock的API

        ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。此外,与synchronized一样,ReentrantLock还提供了可重入的加锁语义。 ReentrantLock 支持在Lock接口中定义的所有获取锁模式,并且与synchronized相比,它还为处理锁的不可用性问题提供了更高的灵活性。 

        必须在finally块中释放锁。否则,如果在被保护的代码中抛出了异常,那么这个锁永远都无法释放。当使用加锁时,还必须考虑在try块中抛出异常的情况,如果可能使对象处于某种不一致的状态,那么就需要更多的try-catch或try-finally代码块。

Lock lock = new ReentrantLock();
lock.lock();
try {
//更新对象状态
// 捕获异常,并在必要时恢复不变性条件
} finally {
	lock.unlock();
}

ReentrantLock获取锁

       void lock()方法当一个线程调用该方法时,说明该线程希望获取该锁。如果锁当前没有被其他线程占用并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置AQS的状态值为1,然后直接返回。如果当前线程之前已经获取过该锁,则这次只是简单地把AQS的状态值加1后返回。如果该锁已经被其他线程持有,则调用该方法的线程会被放入AQS队列后阻塞挂起。

//非公平锁
static final class NonfairSync extends Sync {
	final void lock() {
		//CAS设置状态值
		if (compareAndSetState(0, 1))
			setExclusiveOwnerThread(Thread.currentThread());
		else
		//调用AQS acquire()方法
			acquire(1);
	}
}	
//公平锁
static final class FairSync extends Sync {

	final void lock() {
	//调用AQS acquire()方法
		acquire(1);
	}
}						

       根据创建ReentrantLock构造函数选择sync的实现是NonfairSync还是FairSync,这个锁是一个非公平锁或者公平锁。因为默认AQS的状态值为0,所以第一个调用Lock的线程会通过CAS设置状态值为1,CAS成功则表示当前线程获取到了锁,然后setExclusiveOwnerThread 设置该锁持有者是当前线程。 如果这时候有其他线程调用lock方法企图获取该锁,CAS会失败,然后会调用AQS的acquire方法。

public final void acquire(int arg) {
	//调用ReentrantLock重写的tryAcquire方法
	if (!tryAcquire(arg) &&
			//tryAcquire 返回false会把当前线程放入AQS阻塞队列
		acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg))
		selfInterrupt();
}

AQS并没有提供可用的tryAcquire方法,tryAcquire方法需要子类自己定制化

//非公平锁的实现NonfairSync
protected final boolean tryAcquire(int acquires) {
	return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	//当前AQS状态为0
	if (c == 0) {
		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;
}

当前锁的状态值是否为0,为0则说明当前该锁空闲,那么就尝试CAS获取该锁,将AQS的状态值从0设置为1,并设置当前锁的持有者为当前线程然后返回,true。如果当前状态值不为0则说明该锁已经被某个线程持有,查看当前线程是否是该锁的持有者,如果当前线程是该锁的持有者,则状态值加1,然后返回true,这里需要注意,nextc<0说明可重入次数溢出了。如果当前线程不是锁的持有者则返回false,然后其会被放入AQS阻塞队列。

//公平锁的实现FairSync
protected final boolean tryAcquire(int acquires) {
	........
	//当前AQS状态为0
	if (c == 0) {
		if (!hasQueuedPredecessors() &&
				compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	.......
}

公平的tryAcquire方法与非公平的类似,不同之处在于,在设置CAS前添加了hasQueuedPredecessors方法,该方法是实现公平性的核心代码

//实现公平性的核心代码
public final boolean hasQueuedPredecessors() {
   
	Node t = tail; // Read fields in reverse initialization order
	Node h = head;
	Node s;
	return h != t &&
		((s = h.next) == null || s.thread != Thread.currentThread());
}

       如果当前线程节点有前驱节点则返回true,否则如果当前AQS队列为空或者当前线程节点是AQS的第一个节点则返回false。其中如果h==t则说明当前队列为空,直接返回false;如果h!=t并且s==null则说明有一个元素将要作为AQS的第一个节点入队列,那么返回true,如果h!=t并且sl=nul和s.thread!=Thread.currentThread)则说明队列里面的第一个元素不是当前线程,那么返回true。

ReentrantLocks释放锁

        void unlock()方法尝试释放锁,如果当前线程持有该锁,则调用该方法会让该线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。 如果当前线程没有持有该锁而调用了该方法则会抛出llegalMonitorStateException异常

public void unlock() {
	sync.release(1);
}
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);
	}
	//设置可重入次数为原始值-1
	setState(c);
	return free;
}

ReentrantLocks中断锁

       可中断的锁获取操作同样能在可取消的操作中使用加锁。lockInterruptibly()方法能够在获得锁的同时保持对中断的响应,并且由于它包含在Lock中,因此无须创建其他类型的不可中断阻塞机制。定时的tryLock()同样能响应中断,因此当需要实现一个定时的和可中断的锁获取操作时,可以使用tryLock()方法。

public void lockInterruptibly() throws InterruptedException {
	sync.acquireInterruptibly(1);
}

public final void acquireInterruptibly(int arg)
		throws InterruptedException {
	//如果当前线程中断抛出异常
	if (Thread.interrupted())
		throw new InterruptedException();
	//尝试获取资源
	if (!tryAcquire(arg))
		//调用AQS可被中断方法
		doAcquireInterruptibly(arg);
}

该方法与lock()方法类似,它的不同在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的interrupt()方法,则当前线程会抛出InterruptedException异常,然后返回。 

轮询锁与定时锁

在内置锁中,死锁是一个严重的问题,恢复程序的唯一方法是重新启动程序,而防止死锁的唯一方法就是在构造程序时避免出现不一致的锁顺序。可定时的与可轮询的锁提供了另一种选择:避免死锁的发生。可定时的与可轮询的锁获取模式是由tryLock()方法实现的,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。

// 如果不能在指定时间内完成,代码就会失败。
// 定时的tryLock能够在这种带有时间限制的操作中实现独占加锁行为。
if (lock.tryLock(10, TimeUnit.MINUTES)) {
	return false;
}
try {
	return true;
} finally {
	lock.unlock();
}

公平性

        在ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁。

ReentrantLock lock = new ReentrantLock(true);

在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。在公平的锁中,如果有另一个线程持有这个锁或者有其他线程在队列中等待这个锁,那么新发出请求的线程将被放入队列中。在非公平的锁中, 只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。

        当执行加锁操作时,公平性将由于在挂起线程和恢复线程时存在的开销而极大地降低性能。在实际情况中,统计上的公平性保证——确保被阻塞的线程能最终获得锁,通常已经够用了,并且实际开销也小得多。在大多数情况下,非公平锁的性能要高于公平锁的性能。

 在synchronized和ReentrantLock之间进行选择

        ReentrantLock在加锁和内存上提供的语义与与内置锁相同,此外它还提供了一些其他功能,包括定时的锁等待、可中断的锁等待、公平性,以及实现非块结构的加锁。

在内置锁中,锁的获取和释放等操作都是基于代码块的——释放锁的操作总是与获取锁的操作处于同一个代码块,而不考虑控制权如何退出该代码块。自动的锁释放操作简化了对程序的分析,提供灵活的加锁规则。

ReentrantLock在性能上似乎优于内置锁。 与显式锁相比,内置锁仍然具有很大的优势。内置锁为许多开发人员所熟悉,并且简洁紧凑。ReentrantLock的危险性比同步机制要高,如果忘记在finally块中调用unlock,那么虽然代码表面上能正常运行,但实际上已经埋下了一颗定时炸弹,并很有可能伤及其他代码。仅当内置锁不能满足需求时,才可以考虑使用ReentrantLock。 在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。

读写锁ReentrantReadWriteLock

        在许多情况下,数据结构上的操作都是“读操作”,此时,如果能够允许多个执行读操作的线程同时访问数据结构,那么将提升程序的性能。只要每个线程都能确保读取到最新的数据,并且在读取数据时不会有其他的线程修改数据,那么就不会发生问题。在这种情况下就可以使用读/写锁:一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。 读-写锁是一种性能优化措施,在一些特定的情况下能实现更高的并发性。

ReadWriteLock中特性

         在读-写锁实现的加锁策略中,允许多个读操作同时进行,但每次只允许一个写操作。与Lock一样,ReadWriteLock可以采用多种不同的实现方式,这些方式在性能、调度保证、获取优先性、公平性以及加锁语义等方面可能有所不同。 

  • ReentrantReadWriteLock为这两种锁都提供了可重入的加锁语义。
  • 与ReentrantLock类似,ReentrantReadWriteLock在构造时也可以选择是一个非公平的锁(默认)还是一个公平的锁。 
  • 在公平的锁中,等待时间最长的线程将优先获得锁。如果这个锁由读线程持有,而另一个线程请求写入锁,那么其他读线程都不能获得读取锁,直到写线程使用完并且释放了写入锁。
  •  在非公平的锁中,线程获得访问许可的顺序是不确定的。写线程降级为读线程是可以的,但从读线程升级为写线程则是不可以的(这样做会导致死锁)。 与ReentrantLock类似的是,ReentrantReadWriteLock中的写入锁只能有唯一的所有者,并且只能由获得该锁的线程来释放。

读写锁的接口

          ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()方法,而其实现——ReentrantReadWriteLock,除了接口方法之外,还提供了一些便于外界监控其内部工作状态的方法。

                                 ReentrantReadWriteLock展示内部工作状态的方法

读写锁的使用方式

static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
// 获取一个key对应的value
public static final Object get(String key) {
	r.lock();
	try {
		return map.get(key);
	} finally {
		r.unlock();
	}
}
// 设置key对应的value,并返回旧的value
public static final Object put(String key, Object value) {
	w.lock();
	try {
		return map.put(key, value);
	} finally {
		w.unlock();
	}
}

上述示例中,在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作put(String key,Object value)方法,在更新 HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放之后,其他读写操作才能继续。使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性。

读写锁的实现

        读写锁的内部维护了一个ReadLock和一个WriteLock,它们依赖同步器(AbstractQueuedSynchronizer)实现具体功能。读写状态就是其AQS中同步状态,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

       如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写

写锁的获取与释放

         写锁是个独占锁,某时只有一个线程可以获取该锁。如果当前没有线程获取到读锁和写锁,则当前线程可以获取到写锁然后返回。如果当前已经有线程获取到读锁和写锁,则当前请求写锁的线程会被阻塞挂起。另外,写锁是可重入锁,如果当前线程已经获取了该锁,再次获取只是简单地把可重入次数加1后直接返回。     

  在ReentrantReadWriteLock中写锁使用WriteLock来实现。

写锁lock()内部调用了AQS的acquire方法,其中tryAcquire是ReentrantReadWriteLock内部的sync类重写的

//写锁获取tryAcquire 源码
protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            //(1) c!=0说明读锁或者写锁已经被某线程获取
            if (c != 0) {
                (2//w=0说明已经有线程获取了读锁或者w!=0并且当前线程不是写锁拥有者,则返回false
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
               (3//说明某线程获取了写锁,判断可重入个数
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");

               (4// 设置可重入数量(1)
                setState(c + acquires);
                return true;
            }

           (5//第一个写线程获取写锁
            if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
   }

该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如 果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

写锁unlock()内部调用了release()方法

//写锁释放的源码
public final boolean release(int arg) {
	//调用ReentrantReadWriteLock中sync实现的tryRelease方法
	if (tryRelease(arg)) {
		//激活阻塞队列里面的一个线程
		Node h = head;
		if (h != null && h.waitStatus != 0)
			unparkSuccessor(h);
		return true;
	}
	return false;
}
protected final boolean tryRelease(int releases) {
   //(6) 看是否是写锁拥有者调用的unlock
	if (!isHeldExclusively())
		throw new IllegalMonitorStateException();
   //(7)获取可重入值,这里没有考虑高16位,因为写锁时候读锁状态值肯定为0
	int nextc = getState() - releases;
	boolean free = exclusiveCount(nextc) == 0;
   //(8)如果写锁可重入值为0则释放锁,否者只是简单更新状态值。
	if (free)
		setExclusiveOwnerThread(null);
	setState(nextc);
	return free;
 }

 写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0 时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。

读锁的获取与释放

          读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问 (或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

ReentrantReadWriteLock中的读锁是使用ReadLock来实现的。

          读锁的lock方法调用了AQS的aquireShared方法,内部调用了 ReentrantReadWriteLock 中的 sync 重写的 tryAcquireShared 方法。

//读锁获取的源码
protected final int tryAcquireShared(int unused) {

   //(1)获取当前状态值
    Thread current = Thread.currentThread();
    int c = getState();

    //(2)判断是否写锁被占用
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;

    //(3)获取读锁计数
    int r = sharedCount(c);
    //(4)尝试获取锁,多个读线程只有一个会成功,不成功的进入下面fullTryAcquireShared进行重试
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        //(5)第一个线程获取读锁
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        //(6)如果当前线程是第一个获取读锁的线程
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            //(7)记录最后一个获取读锁的线程或记录其它线程读锁的可重入数
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != current.getId())
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    //(8)类似tryAcquireShared,但是是自旋获取
    return fullTryAcquireShared(current);
}

如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS的状态值state的高16位的值会增加1,然后方法返回。否则如果其他一个线程持有写锁,当前线程会被阻塞。

读锁unlock()释放

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    //如果当前线程是第一个获取读锁线程
    if (firstReader == current) {
        //如果可重入次数为1
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else//否者可重入次数减去1
            firstReaderHoldCount--;
    } else {
        //如果当前线程不是最后一个获取读锁线程,则从threadlocal里面获取
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != current.getId())
            rh = readHolds.get();
        //如果可重入次数<=1则清除threadlocal
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        //可重入次数减去一
        --rh.count;
    }

    //循环直到自己的读计数-1 cas更新成功
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))

            return nextc == 0;
    }
}

        查看当前AQS状态值是否为0,为0则说明当前已经没有读线程占用读锁。然后会调用doReleaseShared方法释放一个由于获取写锁而被阻塞的线程,如果当前AQS状态值不为0,则说明当前还有其他线程持有了读锁,如果CAS更新AQS状态值失败,则自重试直到成功。

锁降级

        锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读 锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

public void processData() {
	readLock.lock();
	if (!update) {
	  // 必须先释放读锁
		readLock.unlock();
	   // 锁降级从写锁获取到开始
		writeLock.lock();
		try {
			if (!update) {
	  // 准备数据的流程(略)
				update = true;
			}
			readLock.lock();
		} finally {
			writeLock.unlock();
		}
	// 锁降级完成,写锁降级为读锁
	}
	try {
		// 使用数据的流程(略)
	} finally {
		readLock.unlock();
	}
}

        当前线程获取写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级。 锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降 的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

StampedLock锁

          StampedLock是并发包里面JDK8版本新增的一个锁,该锁提供了三种模式的读写控制,当调用获取锁的系列函数时,会返回一个long型的变量,我们称之为戳记(stamp),这个戳记代表了锁的状态。其中try系列获取锁的函数,当获取锁失败后会返回为0的stamp值。当调用释放锁和转换锁的方法时需要传入获取锁时返回的stamp值。

 StampedLock读写模式

          StampedLock提供的三种读写模式的锁分别如下。 

  • 写锁writeLock:类似于ReentrantReadWriteLock的写锁(不同的是这里的写锁是不可重入锁)。请求该锁成功后会返回一个stamp变量用来表示该锁的版本,当释放该锁时需要调用unlockWrite方法并传递获取锁时的stamp参数。并且它提供了非阻塞的tryWriteLock方法。
  • 悲观读锁readLock:类似于ReentrantReadWriteLock的读锁(不同的是这里的读锁是不可重入锁)。 这里说的悲观是指在具体操作数据前其会悲观地认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据加锁,这是在读少写多的情况下的一种考虑。请求该锁成功后会返回一个stamp变量用来表示该锁的版本,当释放该锁时需要调用unlockRead方法并传递stamp参数。并且它提供了非阻塞的tryReadLock方法。
  • 乐观读锁tryOptimisticRead:它是相对于悲观锁来说的,在操作数据前并没有通过CAS设置锁的状态,仅仅通过位运算测试。如果当前没有线程持有写锁,则简单地返回一个非0的stamp版本信息。获取该stamp后在具体操作数据前还需要调用validate 方法验证该stamp是否已经不可用,也就是看当调用tryOptimisticRead返回stamp后到当前时间期间是否有其他线程持有了写锁,如果是则validate会返回0,否则就可以使用该stamp版本的锁对数据进行操作。由于tryOptimisticRead并没有使用CAS设置锁状态,所以不需要显式地释放该锁。该锁的一个特点是适用于读多写少的场景,因为获取读锁只是使用位操作进行检验,不涉及CAS操作,所以效率会高很多,但是同时由于没有使用真正的锁,在保证数据一致性上需要复制一份要操作的变量到方法栈,并且在操作数据时可能其他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。

 StampedLock还支持这三种锁在一定条件下进行相互转换。tryConvertToWriteLock(long stamp)期望把stamp标示的锁升级为写锁,这个函数会在下面几种情况下返回一个有效的stamp(也就是晋升写锁成功):

  • 当前锁已经是写锁模式了。
  • 当前锁处于读锁模式,并且没有其他线程是读锁模式
  • 当前处于乐观读模式,并且当前写锁可用。 

 另外,StampedLock的读写锁都是不可重入锁,所以在获取锁后释放锁前不应该再调用会获取锁的操作,以避免造成调用线程被阻塞。当多个线程同时尝试获取读锁和写锁时,谁先获取锁没有一定的规则,完全都是尽力而为,是随机的。并且该锁不是直接实现Lock 或ReadWriteLock接口,而是其在内部自己维护了一个双向阻塞队列。

小结

StampedLock 提供的读写锁与ReentrantReadWriteLock类似,只是前者提供的是不可重入锁。但是前者通过提供乐观读锁在多线程多读的情况下提供了更好的性能,这是因为获取乐观读锁时不需要进行CAS操作设置锁的状态,而只是简单地测试状态。

LockSupport工具

        当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应 工作。LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。

LockSupport的使用

LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread) 方法来唤醒一个被阻塞的线程。

LockSupport和wait/notify的区别

LockSupport功能和wait,notify有些相似,但是LockSupport比起wait,notify功能更强大,也更加方便。

主要有两点:

  • wait和notify都是Object中的方法,在调用这两个方法前必须先获得锁对象,但是park不需要获取某个对象的锁就可以锁住线程。
  • notify只能随机选择一个线程唤醒,无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。

Condition接口

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、 wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

与Object的监视器方法的对比

Condition的方法以及描述

Condition接口与示例

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到 Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
	lock.lock();
	try {
		condition.await();
	} finally {
		lock.unlock();
	}
}
public void conditionSignal() throws InterruptedException {
	lock.lock();
	try {
		condition.signal();
	} finally {
		lock.unlock();
	}
}

获取一个Condition必须通过Lock的newCondition()方法。一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会 释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。

Condition的实现分析

ConditionObject是同步器AQS的内部类。每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。

1.等待队列

等待队列是一个FIFO(先进先出)的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类 AbstractQueuedSynchronizer.Node。 一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点 (lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列


等待队列的基本结构

Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter 指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用 await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的 Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列

2.等待

调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。

//ConditionObject的await方法
public final void await() throws InterruptedException {
	if (Thread.interrupted())
		throw new InterruptedException();
   // 当前线程加入等待队列
	Node node = addConditionWaiter();
  // 释放同步状态,也就是释放锁
	int savedState = fullyRelease(node);
	int interruptMode = 0;
	while (!isOnSyncQueue(node)) {
		LockSupport.park(this);
		if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
			break;
	}
	if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
		interruptMode = REINTERRUPT;
	if (node.nextWaiter != null)
		unlinkCancelledWaiters();
	if (interruptMode != 0)
		reportInterruptAfterWait(interruptMode);
}

调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前 线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当 前线程会进入等待状态。 当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过 其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出 InterruptedException。

3.通知

调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。

//ConditionObject的signal方法
public final void signal() {
	if (!isHeldExclusively())
		throw new IllegalMonitorStateException();
	AbstractQueuedSynchronizer.Node first = firstWaiter;
	if (first != null)
		doSignal(first);
}
private void doSignalAll(Node first) {
	lastWaiter = firstWaiter = null;
	do {
		Node next = first.nextWaiter;
		first.nextWaiter = null;
		transferForSignal(first);
		first = next;
	} while (first != null);
}
final boolean transferForSignal(Node node) {

	if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
		return false;

	Node p = enq(node);
	int ws = p.waitStatus;
	if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
		LockSupport.unpark(node.thread);
	return true;
}
private Node enq(final Node node) {
	for (;;) {
		Node t = tail;
		if (t == null) { // Must initialize
			if (compareAndSetHead(new Node()))
				tail = head;
		} else {
			node.prev = t;
			if (compareAndSetTail(t, node)) {
				t.next = node;
				return t;
			}
		}
	}
}

调用该方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了 isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节 点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。

通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。 被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法 返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状 态的竞争中。 成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此 时该线程已经成功地获取了锁。 Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效 果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

       与内置锁相比,显式的Lock提供了一些扩展功能,在处理锁的不可用性方面有着更高的灵活性,并且对队列行有着更好的控制。但ReentrantLock不能完全替代synchronized,只有在synchronized无法满足需求时,才应该使用它。 读一写锁允许多个读线程并发地访问被保护的对象,当访问以读取操作为主的数据结构时,它能提高程序的可伸缩性。

参考

Java并发编程实战
Java并发编程艺术
Java并发编程之美