zoukankan      html  css  js  c++  java
  • 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);
    }
    
  • 相关阅读:
    20145105 《Java程序设计》第4周学习总结
    调查问卷
    20145105 《Java程序设计》第3周学习总结
    20145105 《Java程序设计》实验一总结
    20145105 《Java程序设计》第5周学习总结
    【linux配置】VMware安装Redhat6.5
    【eclipse】解决:eclipse或STS运行maven工程出现Missing artifact jdk.tools:jdk.tools:jar:1.7问题
    【linux配置】在VMware中为Redhat HAT配置本地yum源
    【linux配置】虚拟机配置静态IP地址
    【linux配置】Linux系统下安装rz/sz命令以及使用说明
  • 原文地址:https://www.cnblogs.com/kkzhilu/p/12859508.html
Copyright © 2011-2022 走看看