StampedLock 应用及原理详解

  从synchronized到ReentrantLock,从ReentrantLock到ReentrantReadWriteLock,都在尽量减少锁带来的负面影响。

  · synchronized:语言级别支持,使用简单,但缺少灵活性,且不可中断。

  · ReentrantLock:JUC提供,使用相对复杂,但是可灵活控制锁的获得和释放,可中断。

  · ReentrantReadWriteLock:实现了锁的分离,分为读锁和写锁,写写、写读互斥,读读可并发使用。

  虽然synchronized、ReentrantLock、ReentrantReadWriteLock已经提供相对丰富的解决方案来供锁的使用,但这些锁在宏观来看的话,都属于悲观锁。ReentrantReadWriteLock进行了锁的分离,但读锁和写锁之间的获取依然是悲观的,在读多写少的极限情况下,写锁可能出现永远无法获得锁的情况。

  JDK1.8引入了StampedLock,StampedLock提供了三种锁模式:

  · 写锁:写锁是一个独占锁,属于悲观锁。

  · 读锁:读锁是一个共享锁,当和写锁出现竞争时,若当前已获取读锁,禁止写入,属于悲观锁。

  · 乐观读锁:乐观读锁乐观地估计读的过程中大概率不会有写入,因此被称为乐观读锁。读取完成后,会进行版本号的校验,若读取过程中发生写入,则重新读取。

  演示示例:

  读锁与写锁:

  读锁与写锁之间是互斥的,读锁被线程持有时,为了防止出现类似脏读问题,其它线程是无法获取写锁的,只有读锁没有线程持有时,写锁才可以被获取。

  首先,新建一个读取线程类和一个写入线程类,用于模拟多线程环境:

package com.securitit.serialize.locks;

import java.util.List;
import java.util.concurrent.locks.StampedLock;

public class StampedReadLockThread extends Thread {
	// 锁实例.
	private StampedLock lock;
	// 模拟数据存储.
	private List<String> dataList;

	// 构造方法.
	public StampedReadLockThread(StampedLock lock, List<String> dataList) {
		this.lock = lock;
		this.dataList = dataList;
	}

	@Override
	public void run() {
		long stamped = -1;
		try {
			stamped = lock.readLock();
			System.out.println(Thread.currentThread().getName() + ":读锁获得锁.");
			for (String data : dataList) {
				System.out.println(Thread.currentThread().getName() + ":读锁读数据.");
			}
			Thread.sleep(2000);
		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			System.out.println(Thread.currentThread().getName() + ":读锁释放锁.");
			lock.unlockRead(stamped);
		}
	}

}
package com.securitit.serialize.locks;

import java.util.List;
import java.util.concurrent.locks.StampedLock;

public class StampedWriteLockThread extends Thread {
	// 锁实例.
	private StampedLock lock;
	// 模拟数据存储.
	private List<String> dataList;

	// 构造方法.
	public StampedWriteLockThread(StampedLock lock, List<String> dataList) {
		this.lock = lock;
		this.dataList = dataList;
	}

	@Override
	public void run() {
		long stamped = -1;
		try {
			stamped = lock.writeLock();
			System.out.println(Thread.currentThread().getName() + ":写锁获得锁.");
			dataList.add("写锁写入数据");
			Thread.sleep(2000);
		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			System.out.println(Thread.currentThread().getName() + ":写锁释放锁.");
			lock.unlockWrite(stamped);
		}
	}

}

  在测试类中逐个调用读取和写入线程:

package com.securitit.serialize.locks;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.StampedLock;

public class StampedLockTester {

	// StampedLock实例.
	private static StampedLock lock = new StampedLock();
	// 模拟数据存储.
	private static List<String> dataList = new ArrayList<String>();

	public static void main(String[] args) {
		new StampedReadLockThread(lock, dataList).start();
		new StampedWriteLockThread(lock, dataList).start();
		new StampedReadLockThread(lock, dataList).start();
		new StampedWriteLockThread(lock, dataList).start();
	}

}

  输出结果:

Thread-0:读锁获得锁.
Thread-2:读锁获得锁.
Thread-0:读锁释放锁.
Thread-2:读锁释放锁.
Thread-3:写锁获得锁.
Thread-3:写锁释放锁.
Thread-1:写锁获得锁.
Thread-1:写锁释放锁.

  无论运行几次,上面的结果读锁获取和释放、写锁获取和释放都是成对出现的,说明读锁和写锁不存在交叉操作。

  乐观读锁与写锁:

  乐观读锁不会阻塞写锁的获取和释放,乐观读锁获取成功后,可以通过validate(stamped)来验证读锁持有过程中是否有写入发生。

  首先,新建一个乐观读取线程类和一个写入线程类,用于模拟多线程环境:

package com.securitit.serialize.locks;

import java.util.List;
import java.util.concurrent.locks.StampedLock;

public class StampedOptimisticReadLockThread extends Thread {
	// 锁实例.
	private StampedLock lock;
	// 模拟数据存储.
	private List<String> dataList;

	// 构造方法.
	public StampedOptimisticReadLockThread(StampedLock lock, List<String> dataList) {
		this.lock = lock;
		this.dataList = dataList;
	}

	@Override
	public void run() {
		long stamped = -1;
		try {
			stamped = lock.tryOptimisticRead();
			System.out.println(Thread.currentThread().getName() + ":乐观读锁获得锁.");
			for (String data : dataList) {
				Thread.sleep(500);
				System.out.println(Thread.currentThread().getName() + ":乐观读锁读数据.");
			}
			if (!lock.validate(stamped)) {
				stamped = lock.readLock();
				System.out.println(Thread.currentThread().getName() + ":读锁获得锁.");
				for (String data : dataList) {
					Thread.sleep(500);
					System.out.println(Thread.currentThread().getName() + ":读锁读数据.");
				}
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			System.out.println(Thread.currentThread().getName() + ":乐观读锁释放锁.");
			lock.unlockRead(stamped);
		}
	}

}
package com.securitit.serialize.locks;

import java.util.List;
import java.util.concurrent.locks.StampedLock;

public class StampedWriteLockThread extends Thread {
	// 锁实例.
	private StampedLock lock;
	// 模拟数据存储.
	private List<String> dataList;

	// 构造方法.
	public StampedWriteLockThread(StampedLock lock, List<String> dataList) {
		this.lock = lock;
		this.dataList = dataList;
	}

	@Override
	public void run() {
		long stamped = -1;
		try {
			stamped = lock.writeLock();
			System.out.println(Thread.currentThread().getName() + ":写锁获得锁.");
			dataList.add("写锁写入数据");
			Thread.sleep(2000);
		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			System.out.println(Thread.currentThread().getName() + ":写锁释放锁.");
			lock.unlockWrite(stamped);
		}
	}

}

  在测试类中逐个调用读取和写入线程:

package com.securitit.serialize.locks;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.StampedLock;

public class StampedLockTester {

	// StampedLock实例.
	private static StampedLock lock = new StampedLock();
	// 模拟数据存储.
	private static List<String> dataList = new ArrayList<String>();

	public static void main(String[] args) {
		new StampedOptimisticReadLockThread(lock, dataList).start();
		new StampedWriteLockThread(lock, dataList).start();
		new StampedOptimisticReadLockThread(lock, dataList).start();
		new StampedWriteLockThread(lock, dataList).start();
	}

}

  输出结果:

Thread-0:乐观读锁获得锁.
Thread-1:写锁获得锁.
Thread-2:乐观读锁获得锁.
Thread-2:乐观读锁读数据.
Thread-1:写锁释放锁.
Thread-3:写锁获得锁.
Thread-3:写锁释放锁.
Thread-0:读锁获得锁.
Thread-2:读锁获得锁.
Thread-0:读锁读数据.
Thread-2:读锁读数据.
Thread-2:读锁读数据.
Thread-0:读锁读数据.
Thread-0:乐观读锁释放锁.
Thread-2:乐观读锁释放锁.

  通过上面输出结果可以看出,使用乐观读锁时,不会限制写锁获取和释放,但乐观读锁进行validate验证时,可能验证失败的结果,此时有两种选择:一是重新获取乐观读锁进行数据操作;二是获取读锁进行数据操作;频繁发生validate验证失败的可能会浪费CPU资源。一般情况下,首先获取乐观读锁尝试进行读取,若验证失败,则获取读锁进行读取。

猜你喜欢

转载自blog.csdn.net/securitit/article/details/106922889