❤ 爆肝JUC 包中的 API 解读

Posted 经理,天台风好大

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了❤ 爆肝JUC 包中的 API 解读相关的知识,希望对你有一定的参考价值。

文章目录

一、JUC包中的锁应用?

1.1 Lock接口及ReentrantLock对象分析及应用?

并发编程领域,有两大核心问题:
① 一个是互斥,即同一时刻只允许一个线程访问共享资源;
② 另一个是同步,即线程之间如何通信、协作。
这两大问题,在Java SDK 并发包可通过 Lock 和 Condition 两个接口来实现,其中Lock 用于解决互斥问题,Condition 用于解决同步问题。Java SDK 并发包里的 Lock 接口中,不仅有支持类似 synchronized 的隐式加锁方法,还支持超时、非阻塞、可中断的方式获取锁, 这三种方式为我们编写更加安全、健壮的并发程序提供了很大的便利。

我们来一起看看Lock接口常用方法,关键方法如下:

  • void lock():获取锁对象,优先考虑是锁的获取,而非中断;
  • void lockInterruptibly():获取锁,但优先响应中断而非锁的获取;
  • boolean tryLock():试图获取锁;
  • boolean tryLock(long timeout, TimeUnit timeUnit):试图获取锁,并设置等待时长;
  • void unlock():释放锁对象。

ReentrantLock实现了Lock接口,是一个可重入的互斥锁(“独占锁”), 同时提供了”公平锁”和”非公平锁”的支持。所谓公平锁和非公平锁其含义如下:

  • 公平锁:在多个线z程争用锁的情况下,公平策略倾向于将访问权授予等待时间最长的线程。也就是说,相当于有一个线程等待队列,先进入等待队列的线程后续会先获得锁。
  • 非公平锁:在多个线程争用锁的情况下,能够最终获得锁的线程是随机的(由底层OS调度)。

ReetrantLock简易应用如下(默认为非公平策略)

class Counter
	ReentrantLock lock = new ReentrantLock();
	int count = 0;
	void increment() 
	lock.lock();
	try 
		count++;
	 finally 
		lock.unlock();
	

其中,这里的锁通过 lock() 方法获取锁,通过 unlock() 方法释放锁。重要的是将代码包装成try/finally块,以确保在出现异常时解锁。这个方法和synchronized关键字修饰的方法一样是线程安全的。在任何给定的时间,只有一个线程可以持有锁。

ReetrantLock对象在构建时,可以基于ReentrantLock(boolean fair)构造方法参数,设置对象的”公平锁”和”非公平锁”特性。其中fair的值true表示“公平锁”。这种公平锁,会影响其性能,但是在一些公平比效率更加重要的场合中公平锁特性就会显得尤为重要。关键代码示例如下:

public void performFairLock()
	//...
	ReentrantLock lock = new ReentrantLock(true);
	try 
		//Critical section here
	 finally 
		lock.unlock();
	
	//...

ReetrantLock对象在基于业务获取锁时,假如希望有等待时间,可以借助tryLock实现,关键代码示例如下:

public void performTryLock()
	//...
	boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
	if(isLockAcquired) 	
		try 
			//Critical section here
		 finally 
			lock.unlock();
		
	
	//...

“锁”是为了保护竞争资源,防止多个线程同时操作线程而出错,ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待);ReentrantLock是通过一个FIFO(先进先出)的等待队列来管理获取该锁所有线程的。在“公平锁”的应用机制下,线程依次排队获取锁;而“非公平锁”,在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。

参考代码

package com.cy.pj.juc.lock;

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

/**
 * @author archibald
 * @date 2021/9/2
 * @apiNote
 */
public class ReentrantLockTests 
	
	public static void main(String[] args) throws InterruptedException 
		//doTestGetLock();
		Counter c=new Counter();
		for(int i=0;i<1000;i++)
			new Thread(()->c.increment()).start();
		
		TimeUnit.SECONDS.sleep(1);
		System.out.println(c.count);
	
	static class Counter
		int count;
		ReentrantLock lock=new ReentrantLock();
		void increment() 
			lock.lock();
			//lock.lockInterruptibly();
			try 
				count++;
			finally 
				lock.unlock();
			
		
	

	private static void doTestGetLock() throws InterruptedException 
		ReentrantLock lock=new ReentrantLock();
		lock.lock();
		Thread t1=new Thread() 
			@Override
			public void run() 
				//lock.lock();//获取锁,拿不到锁会一直阻塞,不响应中断
				//lock.lockInterruptibly();//获取锁,但会优先响应中断
				//if(lock.tryLock()) //尝试获取锁,当拿不到锁,不阻塞
				try 
					if(lock.tryLock(5,TimeUnit.SECONDS)) 
						System.out.println("thread.execute");
					else 
						System.out.println("do not get lock");
					
				catch (InterruptedException e) 
					e.printStackTrace();
				
			;
		;
		t1.start();
		TimeUnit.SECONDS.sleep(1);//休眠一秒钟
		t1.interrupt();
		//lock.unlock();
	


1.2 Condition接口对象分析与应用?

JUC包中提供的Condition接口对象,实现了对Object对象中的wait()、notify()、notifyAll()方法功能的增强。我们可以借助Condition接口对象实现线程之间更加高效的通讯。

Condition相关方法介绍:
(1)await()方法相当于Object的wait()方法。
(2)signal()方法相当于Object的notify()方法。
(3)signalAll()方法相当于Object的notifyAll()方法。

案例分析:基于Condition实现阻塞式栈(Stack)

关键代码分析:

class BlockingStack
	LinkedList<String> stack = new LinkedList<>();
	int CAPACITY = 3;
	ReentrantLock lock = new ReentrantLock();
	Condition pushCondition = lock.newCondition();
	Condition popCondition = lock.newCondition();
	public void pushToStack(String item) throws InterruptedException
		try 
			lock.lock();
			while(stack.size() == CAPACITY) 
				pushCondition.await();
			
			stack.push(item);
			popCondition.signalAll();
		 finally 
			lock.unlock();
		
	
	public String popFromStack() throws InterruptedException
		try 
			lock.lock();
			while(stack.size() == 0) 
				popCondition.await();
			
			String e=stack.pop();
			pushCondition.signalAll();
			return e;
		 finally 
			lock.unlock();
		
	

Condition的强大之处在于它可以为多个线程间建立不同的Condition。我们知道对于栈而言:
① 假设栈中数据已经满了,那么阻塞的肯定是写线程,唤醒的肯定是读线程;
② 相反,阻塞的肯定是读线程,唤醒的肯定是写线程;

那么假设只有一个Condition会有什么效果呢,缓存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。

参考代码

package com.cy.pj.juc.lock;

import com.sun.org.apache.bcel.internal.generic.POP;

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

/**
 * @author archibald
 * @date 2021/9/3
 * @apiNote
 */

//阻塞式栈结构实现
class BlockingStack 
    //stack结构
    private LinkedList<String> stack = new LinkedList<>();

    //capacity,默认容量
    private int capacity = 3;

    //lock object
    private ReentrantLock lock = new ReentrantLock();

    //Condition
    Condition pushCondition = lock.newCondition();
    Condition popCondition = lock.newCondition();

    public BlockingStack() 
    public BlockingStack(int capacity) 
        this.capacity = capacity;
    

    //入栈
    public void push(String element) throws InterruptedException 
        //1.获取锁
        lock.lockInterruptibly();
        try 
            //2.检测stack是否已满,满了则阻塞生产者线程
            while (stack.size() == capacity) 
                pushCondition.await();
            
            //3.放数据
            stack.push(element);
            //4.通知消费者线程取数据
            popCondition.signalAll();
         finally 
            lock.unlock();
        
    

    //出栈
    public String pop() throws InterruptedException 
        //1.获取锁对象
        lock.lockInterruptibly();
        try 
            //2.检测stack是否为空,为空则等待
            while (stack.size() == 0) 
                popCondition.await();
            
            //3.取数据
            String temp = stack.pop();
            //4.通知生产者线程继续放数据
            pushCondition.signalAll();
            return temp;
         finally 
            lock.unlock();
        
    


public class ConditionTests 
    public static void main(String[] args) throws InterruptedException 
        BlockingStack stack = new BlockingStack();
        stack.push("A");
        stack.push("B");
        stack.push("C");
        //stack.push("D");

        stack.pop();
        stack.pop();
        stack.pop();
        //stack.pop();
    

1.3 ReadWriteLock接口及实现类分析与应用?

ReadWriteLock是一个是读写锁接口。其中“读锁”又称“共享锁”,能同时被多个线程获取。“写锁”又称独占锁,只能被一个线程获取。
读写锁与互斥锁的一个重要区别就是读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少的场景下性能优于互斥锁的关键。
但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作的。

案例分析:ReentrantReadWriteLock 对象应用

构建一个简易的线程安全的MapCache对象,并允许多个线程同时从cache读数据。

class MapCache
	private Map<String,Object> map=new HashMap<>();
	private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
	public void writeObject(String key,Object value)
		readWriteLock.writeLock().lock();
		try 
			map.put(key, value);
		finally 
			readWriteLock.writeLock().unlock();
		
	
	public Object readObject(String key) 
		readWriteLock.readLock().lock();
		try 
			return map.get(key);
		finally 
			readWriteLock.readLock().unlock();
		
	

ReentrantReadWriteLock可以让多个读线程可以同时持有读锁(只要写锁未被占用),而写锁是独占的。但是,假如读写锁使用不当,很容易产生“饥饿”问题:比如在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。

ReadWriteLock 锁应用的升降级应用

ReadWriteLock中的锁不支持升级操作,比方说我们在读锁内获取写锁,这个过程我们通常理解为锁的升级。代码分析如下:

static void doMethod01() 
	ReadWriteLock rtLock = new ReentrantReadWriteLock();
	rtLock.readLock().lock();
	System.out.println("get readLock.");
	rtLock.writeLock().lock();
	System.out.println("get writeLock");

ReadWriteLock中的锁虽不支持升级操作,但支持降级操作,比方说我们在写锁内获取读锁,这个过程我们通常理解为锁的降级。代码分析如下

static void doMethod02() 
	ReadWriteLock rtLock = new ReentrantReadWriteLock();
	rtLock.writeLock().lock();
	System.out.println("get writeLock.");
	rtLock.readLock().lock();
	System.out.println("get readLock");

练习:分析如下代码检查是否存在问题?

	r.lock();        
	try 
		v = m.getObject(key);
		if (v == null) 
			w.lock();
			try 
				//更新缓存(代码省略)
			 finally
				w.unlock();
			
		
	 finally
		r.unlock();
	

对于如上代码,看上去好像是没有问题的,先是获取读锁,然后再升级为写锁。可惜 ReadWriteLock 并不支持这种升级。在上面的代码示例中,读锁还没有释放,此时获取写锁,会导致写锁永久等待,最终导致相关线程都被阻塞,永远也没有机会被唤醒。那如何修改呢?参考代码如下:

	r.lock();
	try 
		v = m.getObject(key);
		if (v == null) 
			r.unlock();
			w.lock();
			try 
				//假如缓存没有从数据库或一级缓存查询
				//然后更新缓存(代码省略)
				r.lock();
			 finally
				w.unlock();
			
		
	 finally
		r.unlock();
	

参考代码

package com.cy.pj.juc.lock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author archibald
 * @date 2021/9/3
 * @apiNote
 */

class MapCache
    //HashMap作为缓存数据的对象
    private Map<String,Object> cache = new HashMap<>();
    //写锁ReadWriteLock
    private ReadWriteLock readWriteLock  = new ReentrantReadWriteLock();
    //writeObject() 向缓存写数据,写锁,互斥锁
    public void writeObject(String key,Object value) 
        //加锁
        readWriteLock.writeLock().lock();
        //测试
        System.out.println(Thread.currentThread().getName() +"->writeObject");
        try 
            cache.put(key,value);
            try 
                Thread.sleep(3000);
             catch (InterruptedException e) 
                e.printStackTrace();
            
         finally 
            System.out.println(Thread.currentThread().getName() +"->writeObjectFinally");
            //释放锁
            readWriteLock.writeLock().unlock以上是关于❤ 爆肝JUC 包中的 API 解读的主要内容,如果未能解决你的问题,请参考以下文章

JUC包中的锁框架

(大厂必备)厂长熬夜爆肝万字之多线程高并发JUC编程⭐学妹已收藏

(大厂必备)厂长熬夜爆肝万字之多线程高并发JUC编程⭐学妹已收藏

JUC并发编程 共享模式之工具 JUC CyclicBarrier(循环栅栏 与CountdownLatch最大的不同是可以重值倒计时) -- CyclicBarrier介绍使用注意事项

JUC包中的CountDownLatch源码实现分析

十JUC包中的锁