J.U.C体系进阶(二):juc-locks 锁框架

Java - J.U.C体系进阶

作者:Kerwin

邮箱:806857264@qq.com

说到做到,就是我的忍道!

juc-locks 锁框架

接口说明

Lock接口

类型 名称
void lock()
void lockInterruptibly ()
Condition newCondition()
boolean tryClock()
boolean tryClock(Long time, TimeUnit unit)
void unlock()

lock()方法类似于使用synchronized关键字加锁,如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。

lockInterruptibly()方法顾名思义,就是如果锁不可用,那么当前正在等待的线程是可以被中断的,这比synchronized关键字更加灵活。

Condition接口

可以看做是Obejct类的wait()、notify()、notifyAll()方法的替代品,与Lock配合使用

核心方法 -> awit() signal() signalAll()

ReadWriteLock接口

核心方法 -> readLock() writeLock() 获取读锁和写锁,注意除非使用Java8新锁,否则读读不互斥,读写是互斥的

ReentrantLock类使用


ReentrantLock的使用非常简单,Demo如下:

/***
 * TestDemo
 * @author 柯贤铭
 * @date   2019年4月22日
 * @email  806857264@qq.com
 */
public class ReadWriteLockTest {
	
	private static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
	private static final WriteLock writeLock = readWriteLock.writeLock();
	private static final ReadLock readLock = readWriteLock.readLock();
	private static final ExecutorService pool = Executors.newFixedThreadPool(50);

	private static int surplusTickets = 100;// 余票量
	private static int surplusThread = 500;// 统计进程执行量,在进程都执行完毕后才关闭主线程

	/**
	 * 运行多线程,进行模拟抢票,并计算执行时间
	 */
	public static void main(String[] args) {
		Date beginTime = new Date();
		for (int i = 0; i < surplusThread; i++) {
			final int runNum = i;
			pool.execute(new Runnable() {
				public void run() {
					boolean getted = takeTicket();

					String gettedMsg = "";
					if (getted) {
						gettedMsg = "has getted";
					} else {
						gettedMsg = "not getted";
					}
					System.out.println("thread " + runNum + " " + gettedMsg + ", remain: " + surplusTickets
							+ ", line up:" + surplusThread + "..");
				}
			});
		}

		while (surplusThread >= 30) {
			sleep(100);
		}
		
		Date overTime = new Date();
		System.out.println("take times:" + (overTime.getTime() - beginTime.getTime()) + " millis.");
	}

	/**
	 * 查询当前的余票量
	 */
	private static int nowSurplus() {
		readLock.lock();
		int s = surplusTickets;
		sleep(30);// 模拟复杂业务
		readLock.unlock();
		return s;
	}

	/**
	 * 拿出一张票
	 */
	private static boolean takeTicket() {
		writeLock.lock();
		boolean result = false;

		if (nowSurplus() > 0) {
			surplusTickets -= 1;
			result = true;
		}

		surplusThread -= 1;
		writeLock.unlock();
		return result;
	}

	/**
	 * 睡觉觉
	 */
	private static void sleep(int millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

说明: 关键点就在获取其读锁和写锁上,为什么要区分?

因为读写互斥,读读不互斥,所以如果不分清楚的话就会让只读操作性能大大下降

另外: 在频繁互斥情况下,其实Lock的性能和synchronized是一样的

但这仅限于在PC端(用新型编译器和虚拟机),如果是在安卓端,synchronized会慢十几倍

LockSupport工具类


Doug Lea 的神作concurrent包是基于AQS (AbstractQueuedSynchronizer)框架,AQS框架借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)。因此,LockSupport可谓构建concurrent包的基础之一。理解concurrent包,就从这里开始。

归根结底,LockSupport调用的Unsafe中的native代码:

public native void unpark(Thread jthread); 
public native void park(boolean isAbsolute, long time); 

两个函数声明清楚地说明了操作对象:

park函数是将当前Thread阻塞,而unpark函数则是将另一个Thread唤醒。

与Object类的wait/notify机制相比,park/unpark有两个优点:

  • 以thread为操作对象更符合阻塞线程的直观定义;
  • 操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性

举个例子,假设现在需要实现一种FIFO类型的独占锁,可以把这种锁看成是ReentrantLock的公平锁简单版本,且是不可重入的,就是说当一个线程获得锁后,其它等待线程以FIFO的调度方式等待获取锁 :

public class FIFOMutex {
    private final AtomicBoolean locked = new AtomicBoolean(false);
    private final Queue<Thread> waiters = new ConcurrentLinkedQueue<Thread>();
 
    public void lock() {
        Thread current = Thread.currentThread();
        waiters.add(current);
 
        // 如果当前线程不在队首,或锁已被占用,则当前线程阻塞
        // NOTE:这个判断的意图其实就是:锁必须由队首元素拿到
        while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
            LockSupport.park(this);
        }
        waiters.remove(); // 删除队首元素
    }
 
    public void unlock() {
        locked.set(false);
        LockSupport.unpark(waiters.peek());
    }
}

测试代码:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        FIFOMutex mutex = new FIFOMutex();
        MyThread a1 = new MyThread("a1", mutex);
        MyThread a2 = new MyThread("a2", mutex);
        MyThread a3 = new MyThread("a3", mutex);
 
        a1.start();
        a2.start();
        a3.start();
 
        a1.join();
        a2.join();
        a3.join();
 
        assert MyThread.count == 300;
        System.out.print("Finished");
    }
}
 
class MyThread extends Thread {
    private String name;
    private FIFOMutex mutex;
    public static int count;
 
    public MyThread(String name, FIFOMutex mutex) {
        this.name = name;
        this.mutex = mutex;
    }
 
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            mutex.lock();
            count++;
            System.out.println("name:" + name + "  count:" + count);
            mutex.unlock();
        }
    }
}

park方法的调用一般要方法一个循环判断体里面。
如上述示例中的:

while (waiters.peek() != current || !locked.compareAndSet(false, true)) {
    LockSupport.park(this);
}

之所以这样做,是为了防止线程被唤醒后,不进行判断而意外继续向下执行,这其实是一种的多线程设计模式-Guarded Suspension

AbstractQueuedSynchronizer抽象类

AbstractQueuedSynchronizer抽象类是整个JUC体系的核心,一两句话说不清,如果仅限于使用JUC的话,其实也不用看,如果想知道源码层的话,推荐以下几个博文:

核心:抽象类采用模板方法模式主要解决何时,何线程,在何状态下 -> acquire和release的问题 获取资源与释放资源

StampedLock Java8新型锁

ReentrantReadWriteLock锁具有读写锁,问题在于ReentrantReadWriteLock使得多个读线程同时持有读锁(只要写锁未被占用),而写锁是独占的 ,很容易造成写锁获取不到资源

解决的必要问题:读锁采用乐观锁机制,非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程,但是API稍微复杂,因此使用时需要注意

StampedLock的主要特点概括一下,有以下几点:
所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
StampedLock有三种访问模式:
①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
③Optimistic reading(乐观读模式):这是一种优化的读模式。
StampedLock支持读锁和写锁的相互转换
我们知道RRW中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。
StampedLock提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。
无论写锁还是读锁,都不支持Conditon等待

/***
 * TestStampedLock
 * @author 柯贤铭
 * @date   2019年4月22日
 * @email  806857264@qq.com
 */
public class TestStampedLock {
	
	// StampedLock锁
	private static final StampedLock sLock = new StampedLock();

	// 模拟500张票
	private static Integer total  = 500;
	
	// 模拟100个人
	private static Integer person = 100;
	
	private static final ExecutorService pool = Executors.newFixedThreadPool(person);
	
	private static final CountDownLatch LATCH = new CountDownLatch(person);
	
	public static void main(String[] args) throws InterruptedException {
		Long start = System.currentTimeMillis();
		for (int i = 0; i < person; i++) {
			final Integer index = i;
			pool.execute(new Runnable() {
				@Override
				public void run() {
					Integer sheng = TestStampedLock.read();
					if (sheng >= 1) {
						try {
							boolean flag = TestStampedLock.buy();
							if (flag) {
								System.out.println("线程 " + index + "买到了");
							} else {
								System.out.println("线程 " + index + "no no no");
							}
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					} else {
						System.out.println("线程 " + index + "no no no");
					}
					LATCH.countDown();
				}
			});
		}
		LATCH.await();
		Long end = System.currentTimeMillis();
		System.out.println("一共耗时: " + (end - start));
	}
	
	/**
	 * 读剩余还有几张票
	 * @return
	 */
	private static Integer read () {
		Long stamp = sLock.tryOptimisticRead();
		Integer piao = total;
		if (!sLock.validate(stamp)) {
			stamp = sLock.readLock();
			try {
				piao = total;
			} finally {
				sLock.unlockRead(stamp);
			}
		}
		return piao;
	}
	
	/***
	 * 买票
	 * @throws InterruptedException 
	 */
	private static boolean  buy () throws InterruptedException {
		Long stamp = sLock.writeLock();
		// 模拟复杂操作
		Thread.sleep(30);
		try {
			if (total >= 1) {
				total--;
				return true;
			}
		} finally {
			sLock.unlockWrite(stamp);
		}
		return false;
	}
}

StampedLock乐观锁操作必要步骤:

// 注意:StampedLock的必要操作流程
// 唯一需要注意的地方就是乐观读锁的地方 - 官方Demo
double distanceFormOrigin() {//只读方法
    //试图尝试一次乐观读 返回一个类似于时间戳的邮戳整数stamp
    long stamp = s1.tryOptimisticRead();  
    //读取x和y的值,这时候我们并不确定x和y是否是一致的
    double currentX = x, currentY = y;
    //判断这个stamp是否在读过程发生期间被修改过,如果stamp没有被修改过,责任无这次读取时有效的,因此就可以直接return了,反之,如果stamp是不可用的,则意味着在读取的过程中,可能被其他线程改写了数据,因此,有可能出现脏读,如果如果出现这种情况,我们可以像CAS操作那样在一个死循环中一直使用乐观锁,知道成功为止
    if (!s1.validate(stamp)) {
        //也可以升级锁的级别,这里我们升级乐观锁的级别,将乐观锁变为悲观锁, 如果当前对象正在被修改,则读锁的申请可能导致线程挂起.
        stamp = s1.readLock();
        try {
            currentX = x;
            currentY = y;
        } finally {
            s1.unlockRead(stamp);//退出临界区,释放读锁
        }
    }
    return Math.sqrt(currentX * currentX + currentY * currentY);
}
posted @ 2019-04-26 10:59  Super-Kerwin  阅读(123)  评论(0编辑  收藏  举报