并发编程(第九章 共享模型之工具 - J.U.C)

9 篇文章 0 订阅

JUC

一、AQS原理(重要)

1、概述

全称是AbstractQueueSynchronizer,是阻塞式锁和相关的同步器工具的框架。

特点:

  • 用state属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
    – getState - 获取state状态
    – setState - 设置state状态
    – compareAndSetState - cas机制设置state状态
    – 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源

  • 提供来基于FIFO的等待队列,类似于 Monitor 的 EntryList

  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

子类主要实现这样的一些方法(默认抛出 UnsupportedOperationException)

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively

获取锁的姿势

// 如果获取锁失败
if (!tryAcquire(arg)) {
	// 入队, 可以选择阻塞当前线程 park unpark 
}

释放锁的姿势

// 如果释放锁成功
if (tryRelease(arg)) {
	// 让阻塞线程恢复运行 
}
2、实现不可重入锁

自定义同步器

final class MySync extends AbstractQueuedSynchronizer { 

	@Override
	protected boolean tryAcquire(int acquires) { 
		if (acquires == 1){
			if (compareAndSetState(0, 1)) { 
				setExclusiveOwnerThread(Thread.currentThread()); 
				return true;
			} 
		}
		return false; 
	}
	@Override
	protected boolean tryRelease(int acquires) { 
		if(acquires == 1) {
			if(getState() == 0) {
				throw new IllegalMonitorStateException();
			} 
			setExclusiveOwnerThread(null); 
			setState(0);
			return true;
		}
		return false; 
	}
	
	protected Condition newCondition() { 
		return new ConditionObject();
	}
	@Override
	protected boolean isHeldExclusively() { 
		return getState() == 1;
	} 
}

自定义锁
有了自定义同步器,很容易复用AQS,实现一个功能完备的自定义锁

class MyLock implements Lock {
	static MySync sync = new MySync();
	@Override
	// 尝试,不成功,进入等待队列 
	public void lock() {
		sync.acquire(1); 
	}
	@Override
	// 尝试,不成功,进入等待队列,可打断
	public void lockInterruptibly() throws InterruptedException {
		sync.acquireInterruptibly(1); 
	}
	@Override
	// 尝试一次,不成功返回,不进入队列 
	public boolean tryLock() {
		return sync.tryAcquire(1); 
	}
	@Override
	// 尝试,不成功,进入等待队列,有时限
	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
		return sync.tryAcquireNanos(1, unit.toNanos(time)); 
	}
	@Override
	// 释放锁
	public void unlock() {
		sync.release(1); 
	}
	@Override
	// 生成条件变量
	public Condition newCondition() {
		return sync.newCondition(); 
	}
}

测试一下

MyLock lock = new MyLock();
new Thread(() -> { 
	lock.lock();
	try { 
		log.debug("locking..."); 
		sleep(1);
	} finally { 
		log.debug("unlocking..."); 
		lock.unlock();
	} 
},"t1").start();

new Thread(() -> { 
	lock.lock();
	try { 
		log.debug("locking...");
	} finally { 
		log.debug("unlocking..."); 
		lock.unlock();
	} 
},"t2").start();

输出:
在这里插入图片描述

不可重入测试
如果改为下面代码,会发现自己也会被挡住(只会打印一次locking)

lock.lock(); 
log.debug("locking..."); 
lock.lock(); 
log.debug("locking...");
3、心得

3.1 起源

早期程序员会自己通过一种同步器去实现另一种相近的同步器,例如用可重入锁去实现信号量,或反之。这显然不够优雅,于是在JSR166(java规范提案)中创建了AQS,提供了这种通用的同步器机制。

3.2 目标

AQS要实现的功能目标

  • 阻塞版本获取锁acquire和非阻塞的版本尝试获取锁tryAcquire
  • 获取锁超时机制
  • 通过打断取消机制
  • 独占机制及共享机制
  • 条件不满足时的等待机制

要实现的性能目标

  • Instead, the primary performance goal here is scalability: to predictably maintain efficiency even, or especially, when synchronizers are contended.

3.3 设计

AQS的基本思想其实很简单

1、获取锁的逻辑

while(state 状态不允许获取) { 
	if(队列中还没有此线程) {
		入队并阻塞
	} 
}
当前线程出队

2、释放锁的逻辑

if(state 状态允许了) { 
	恢复阻塞的线程(s)
}

要点

  • 原子维护state状态
  • 阻塞及恢复线程
  • 维护队列

1)state设计

  • state使用了volatile配合cas保证其修改时的原子性
  • state使用了32bit int来维护同步状态,因为当时使用log在很多平台下测试的结果并不理想

2)阻塞恢复设计

  • 早期的控制线程暂停和恢复的api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的resume,那么suspend 将感知不到。
  • 解决方法是使用park & unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先unpark 再park 也没问题
  • park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
  • park 线程还可以通过 interrupt 打断

3)队列设计

  • 使用了FIFO 先入先出队列,并不支持优先级队列
  • 设计时借鉴了CLH队列,它是一种单向无锁队列

在这里插入图片描述
队列中有 head 和 tail 两个指针节点,都用 volatile 修饰配合cas使用,每个节点有 state 维护节点状态

  • 入队伪代码,只需要考虑 tail 赋值的原子性
    在这里插入图片描述
  • 出队伪代码
    在这里插入图片描述

CLH 好处:

  • 无锁,使用自旋
  • 快速,无阻塞

AQS 在一些方面改进了CLH

private Node enq(final Node node) { 
	for (;;) {
		Node t = tail;
		// 队列中还没有元素 tail 为 null 
		if (t == null) {
			// 将 head 从 null -> dummy
			if (compareAndSetHead(new Node()))
				tail = head; 
		} else {
			// 将 node 的 prev 设置为原来的 tail 
			node.prev = t;
			// 将 tail 从原来的 tail 设置为 node 
			if (compareAndSetTail(t, node)) {
				// 原来 tail 的 next 设置为 node 
				t.next = node;
				return t;
			} 
		}
	} 
}

主要用到AQS的并发工具类
在这里插入图片描述

二、ReentrantLock原理(重要)

1、非公平锁锁实现原理

1.1 加锁解锁流程

先从构造器开始看,默认为非公平锁实现

public ReentrantLock() {
	sync = new NonfairSync();
}

NofairSync 继承自 AQS
1、没有竞争时
在这里插入图片描述
2、第一个竞争出现时
在这里插入图片描述

Thread - 1 执行了
1、CAS尝试将state由0改为1,结果失败
2、进入tryAcquire逻辑,这时state已经是1,结果仍然失败
3、接下来进入addWaiter逻辑,构造Node队列

  • 图中黄色三角表示该Node的waitStatus状态,其中0为默认正常状态
  • Node的创建是懒惰的
  • 其中第一个Node成为Dummy(哑元)或哨兵,用来占位,并不关联线程
    在这里插入图片描述

当前线程进入acquireQueued逻辑
1、acquireQueued会在一个死循环中不断尝试获取锁,失败后进入park阻塞
2、如果自己是紧邻着head(排第二位),那么再次tryAcquire尝试获取锁,当然这时state仍为1,失败
3、进入shouldParkAfterFailedAcquire逻辑,将前驱 node ,即head的waitStatus改为 - 1,这次返回false
在这里插入图片描述
4、shouldParkAfterFailedAcquire执行完毕回到acquireQueued,再次tryAcquire尝试获取锁,当然这时state仍为1,失败
5、当再次进入shouldParkAfterFailedAcquire时,这时因为其前驱node的waitStatus已经是 - 1,这次返回true
6、进入parkAndCheckInterrupt,Thread - 1 park(灰色表示)
在这里插入图片描述
再次有多个线程经历上述过程竞争失败,变成这个样子
在这里插入图片描述
Thread - 0 释放锁,进入tryRelease流程,如果成功

  • 设置exclusiveOwnerThread为null
  • state = 0

在这里插入图片描述
当前队列不为null,并且head的waitStatus = - 1,进入unparkSuccessor流程
找到队列中离head最近的一个Node(没取消的),unpark恢复其运行,本例中即为Thread - 1
回到Thread - 1的acquireQueued流程
在这里插入图片描述
如果加锁成功(没有竞争),会设置

  • exclusiveOwnerThread为Thread -1,state = 1
  • head指向刚刚Thread - 1所在的Node,该Node清空Thread
  • 原本的head因为从链表断开,而可被垃圾回收

如果这时候有其它线程来竞争(非公平的体现),例如这时有Thread - 4来了
在这里插入图片描述
如果不巧又被Thread - 4 占了先

  • Thread - 4 被设置为 exclusiveOwnerThread,state = 1
  • Thread - 1 再次进入acquireQueued流程,获取锁失败,重新进入park阻塞。

1.2 加锁源码

这里是引用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
注意:

  • 是否需要unpark是由当前节点的前驱节点的waitStatus == Node.SIGNAL来决定,而不是本节点的waitStatus决定

1.3 解锁源码

这里是引用
在这里插入图片描述

2、可重入原理

这里是引用
在这里插入图片描述

3、可打断原理

3.1、不可打断模式

在此模式下,即使它被打断,仍会驻留在AQS队列中,一直要等到获得锁后方能得知自己被打断了
在这里插入图片描述
在这里插入图片描述

3.2、可打断模式

这里是引用

4、公平锁实现原理

这里是引用
在这里插入图片描述

5、条件变量实现原理

每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

5.1 await 流程

  • 开始Thread - 0 持有锁,调用await,进入 ConditionObject的addConditionWaiter流程
  • 创建新的Node状态为 -2 (Node.CONDITION),关联Thread-0,加入等待队列尾部
    在这里插入图片描述
  • 接下来进入AQS的fullyRelease流程,释放同步器上的锁
    在这里插入图片描述
  • unpark AQS队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么Thread - 1 竞争成功
    在这里插入图片描述
  • park阻塞Thread - 0
    在这里插入图片描述

5.2 signal流程

  • 假设Thread - 1要来唤醒Thread - 0
    在这里插入图片描述
  • 进入 ConditonObject的doSignal流程,取得等待队列中第一个Node,即Thread - 0 所在Node
    在这里插入图片描述
  • 执行transferForSignal流程,将该Node加入AQS队列尾部,将Thread- 0的waitStatus改为0,Thread - 3的waitStatus改为 -1
    在这里插入图片描述
  • Thread - 1释放锁,进入unlock流程,略

源码

这里是引用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三、读写锁

1、ReentrantReadWriteLock
  • 当读操作远远高于写操作时,这时候使用读写锁读-读可以并发,提高性能。类似于数据库中的select ... from ... lock in share mode
  • 提供一个数据容器类内部分别使用读锁保护数据的read()方法,写锁保护数据的write()方法
class DataContainer {
	private Object data;
	private ReentrantReadWriteLock rw = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock r = rw.readLock(); private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
	public Object read() { 
		log.debug("获取读锁..."); 
		r.lock();
		try {
			log.debug("读取"); sleep(1);
			return data;
		} 
		finally { 
			log.debug("释放读锁...");
			r.unlock(); 
		}
	}
	public void write() { 
		log.debug("获取写锁..."); 
		w.lock();
		try {
			log.debug("写入");
			sleep(1); 
		} finally {
			log.debug("释放写锁...");
			w.unlock(); 
		}
	} 
}

测试读锁-读锁可以并发

DataContainer dataContainer = new DataContainer(); 
new Thread(() -> {
	dataContainer.read(); 
}, "t1").start();

new Thread(() -> { 
	dataContainer.read();
}, "t2").start();

输出结果,从这里可以看到Thread - 0锁定期间,Thread - 1的读操作不受影响:
在这里插入图片描述

测试读锁-写锁相互阻塞

DataContainer dataContainer = new DataContainer(); 
new Thread(() -> {
	dataContainer.read(); 
}, "t1").start();

Thread.sleep(100); new Thread(() -> {
	dataContainer.write(); 
}, "t2").start();

输出结果:
在这里插入图片描述

写锁-写锁也是相互阻塞的,这里就不测试了。

注意事项:

  • 读锁不支持条件变量
  • 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
    在这里插入图片描述
  • 重入时降级支持:即持有写锁的情况下去获取读锁
    在这里插入图片描述
    在这里插入图片描述
2、应用之缓存(重要)

2.1 缓存更新策略

更新时,是先清缓存还是先更新数据库
1、先清缓存
在这里插入图片描述
2、先更新数据库
在这里插入图片描述
3、补充一种情况,假设查询线程A查询数据时恰好缓存数据由于时间到期失效,或是第一次查询
在这里插入图片描述
这种情况的出现几率非常小,见facebook论文。

2.2 读写锁实现一致性缓存

使用读写锁实现一个简单的按需加载缓存

class GenericCachedDao<T> {
	// HashMap 作为缓存非线程安全, 需要保护 
	HashMap<SqlPair, T> map = new HashMap<>();
	ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); GenericDao genericDao = new GenericDao();
	public int update(String sql, Object... params) { 
	SqlPair key = new SqlPair(sql, params);
	// 加写锁, 防止其它线程对缓存读取和更改
	lock.writeLock().lock(); 
	try {
		int rows = genericDao.update(sql, params); 
		map.clear();
		return rows;
	} 
	finally { 
		lock.writeLock().unlock();
	} 
	}
	
	public T queryOne(Class<T> beanClass, String sql, Object... params) { 
		SqlPair key = new SqlPair(sql, params);
		// 加读锁, 防止其它线程对缓存更改
		lock.readLock().lock();
		try {
			T value = map.get(key); 
			if (value != null) {
				return value; 
			}
		} finally { 
			lock.readLock().unlock();
		}
		// 加写锁, 防止其它线程对缓存读取和更改 lock.writeLock().lock();
		try {
			// get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据 
			// 为防止重复查询数据库, 再次验证
			T value = map.get(key);
			if (value == null) {
				// 如果没有, 查询数据库
				value = genericDao.queryOne(beanClass, sql, params); 
				map.put(key, value);
			}
			return value; 
		} 
		finally {
			lock.writeLock().unlock(); 
		}
	}
	
	// 作为 key 保证其是不可变的 
	class SqlPair {
		private String sql; 
		private Object[] params;
		public SqlPair(String sql, Object[] params) { 
			this.sql = sql;
			this.params = params; 
		}
		@Override
		public boolean equals(Object o) { 
			if (this == o) {
				return true; 
			}
			if (o == null || getClass() != o.getClass()) { 
				return false;
			}
			SqlPair sqlPair = (SqlPair) o; 
			return sql.equals(sqlPair.sql) && Arrays.equals(params, sqlPair.params);
		}
		
		@Override
		public int hashCode() {
			int result = Objects.hash(sql);
			result = 31 * result + Arrays.hashCode(params); 
			return result;
		} 
	}
}

注意:

  • 以上实现体现的是读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑
    – 适合读多写少,如果写操作比较频繁,以上实现性能低
    – 没有考虑缓存容量
    – 没有考虑缓存过期
    – 只适合单机
    – 并发性还是低,目前只会用一把锁
    – 更新方法太过简单粗暴,清空了所有key(考虑按类型分区或重新设计key)

  • 乐观锁实现:用CAS去更新

3、读写锁原理(重要)

3.1 图解流程

读写锁用的是一个Sycn同步器,因此等待队列,state等也是同一个

t1 w.lock , t2 r.lock
1)t1成功上锁,流程与ReentrantLock加锁相比没有特殊之处,不同是写锁状态占了state的低16位,而读锁使用的是state的高16位
在这里插入图片描述
2)t2执行r.lock,这时进入读锁的sync.acquireShared(1)流程,首先会进入tryAcquireShared流程。如果有写锁占据,那么tryAcquireShared返回 -1表示失败

  • tryAcquireShared返回值表示
    – -1表示失败
    – 0表示成功,但后继节点不会继续唤醒
    – 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回1

在这里插入图片描述
3) 这时会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
在这里插入图片描述
4)t2 会看看自己的节点是不是老二,如果是,还会再次调用tryAcquireShared(1) 来尝试获取锁
5)如果没有成功,在 doAcquireShared 内 for (;😉 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;😉 循环一 次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park
在这里插入图片描述

t3 r.lock,t4 w.lock
这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子
在这里插入图片描述

t1 w.unlock

  • 这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子
    在这里插入图片描述
  • 接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
  • 这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一
    在这里插入图片描述
  • 这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
    在这里插入图片描述
  • 事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
    在这里插入图片描述
  • 这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一
    在这里插入图片描述
  • 这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
    在这里插入图片描述
  • 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点

t2 r.unlock,t3 r.unlock

  • t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零
    在这里插入图片描述
  • t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即
    在这里插入图片描述
  • 之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是老二,并且没有其他 竞争,tryAcquire(1) 成功,修改头结点,流程结束
    在这里插入图片描述

3.2 源码分析

1、写锁上锁流程
在这里插入图片描述
在这里插入图片描述

2、写锁释放流程
在这里插入图片描述
在这里插入图片描述

3、读锁上锁流程
在这里插入图片描述 在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4、读锁释放流程
在这里插入图片描述
在这里插入图片描述

4、StampedLock

该类自JDK8加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用

1、加锁读锁

 long stamp = lock.readLock(); 
 lock.unlockRead(stamp);

2、加解写锁

 long stamp = lock.writeLock(); 
 lock.unlockWrite(stamp);

3、乐观锁,StampedLock支持tryOptimisticRead()方法(乐观读),读取完毕后需要做一次戳校验如果娇艳通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

long stamp = lock.tryOptimisticRead(); 
// 验戳
if(!lock.validate(stamp)){
	// 锁升级 
}

4、提供一个数据容器类 内部分别使用读锁保护数据的read()方法,写锁保护数据的write()方法

class DataContainerStamped { 
	private int data;
	private final StampedLock lock = new StampedLock();
	public DataContainerStamped(int data) { 
		this.data = data;
	}
	
	public int read(int readTime) {
		long stamp = lock.tryOptimisticRead(); 
		log.debug("optimistic read locking...{}", stamp); 
		sleep(readTime);
		if (lock.validate(stamp)) {
			log.debug("read finish...{}, data:{}", stamp, data);
			return data; 
		}
		// 锁升级 - 读锁
		log.debug("updating to read lock... {}", stamp); 
		try {
			stamp = lock.readLock();
			log.debug("read lock {}", stamp);
			sleep(readTime);
			log.debug("read finish...{}, data:{}", stamp, data); 
			return data;
		} finally {
			log.debug("read unlock {}", stamp); 
			lock.unlockRead(stamp);
		} 
	}
	
	public void write(int newData) {
		long stamp = lock.writeLock(); 
		log.debug("write lock {}", stamp); 
		try {
			sleep(2);
			this.data = newData; 
		} finally {
			log.debug("write unlock {}", stamp);
			lock.unlockWrite(stamp); 
		}
	} 
}

测试读-读可以优化

public static void main(String[] args) {
	DataContainerStamped dataContainer = new DataContainerStamped(1); 
	new Thread(() -> {
		dataContainer.read(1); 
	}, "t1").start(); 
	
	sleep(0.5);
	
	new Thread(() -> {
		dataContainer.read(0); 
	}, "t2").start();
}

输出结果,可以看到实际没有加读锁
在这里插入图片描述

测试读-写时优化读补加读锁

public static void main(String[] args) {
	DataContainerStamped dataContainer = new DataContainerStamped(1); 
	new Thread(() -> {
		dataContainer.read(1); 
	}, "t1").start(); sleep(0.5);
	new Thread(() -> {
		dataContainer.write(100); 
	}, "t2").start();
}

输出结果
在这里插入图片描述

注意:

  • StampedLock不支持条件变量
  • StampedLock不支持可重入

四、Semaphore

1、基本使用

[ˈsɛməˌfɔr] 信号量,用来限制能同时访问共享资源的线程上限。

public static void main(String[] args) {
	// 1. 创建 semaphore 对象
	Semaphore semaphore = new Semaphore(3);
	// 2. 10个线程同时运行
	for (int i = 0; i < 10; i++) {
		new Thread(() -> { 
			// 3. 获取许可
			try { 
				semaphore.acquire();
			} catch (InterruptedException e) { 
				e.printStackTrace();
			} 
			try {
				log.debug("running..."); 
				sleep(1); 
				log.debug("end...");
			} finally {
				// 4. 释放许可
				semaphore.release(); 
			}
		}).start(); 
	}
}

输出:
在这里插入图片描述

2、Semaphore应用(重要)

semaphore实现

  • 使用Semaphore限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比Tomcat LimitLatch的实现)
  • 用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好, 注意下面的实现中线程数和数据库连接数是相等的
@Slf4j(topic = "c.Pool") class Pool {
	// 1. 连接池大小
	private final int poolSize;
	// 2. 连接对象数组
	private Connection[] connections;
	// 3. 连接状态数组 0 表示空闲, 1 表示繁忙 
	private AtomicIntegerArray states;
	private Semaphore semaphore;
	// 4. 构造方法初始化
	public Pool(int poolSize) {
		this.poolSize = poolSize;
		// 让许可数与资源数一致
		this.semaphore = new Semaphore(poolSize); 
		this.connections = new Connection[poolSize];
		this.states = new AtomicIntegerArray(new int[poolSize]); 
		for (int i = 0; i < poolSize; i++) {
			connections[i] = new MockConnection("连接" + (i+1)); 
		}
	}
	
	// 5. 借连接
	public Connection borrow() {
		// t1, t2, t3
		// 获取许可 
		try {
			semaphore.acquire(); 
			// 没有许可的线程,在此等待 
		} catch (InterruptedException e) {
			e.printStackTrace(); 
		}
		for (int i = 0; i < poolSize; i++) { 
			// 获取空闲连接
			if(states.get(i) == 0) {
				if (states.compareAndSet(i, 0, 1)) {
					log.debug("borrow {}", connections[i]);
					return connections[i]; 
				}
			} 
		}
		// 不会执行到这里
		return null; 
	}
	// 6. 归还连接
	public void free(Connection conn) {
		for (int i = 0; i < poolSize; i++) { 
			if (connections[i] == conn) {
				states.set(i, 0); 
				log.debug("free {}", conn); 
				semaphore.release(); 
				break;
			} 
		}
	} 
}
3、Semaphore原理(重要)

1、加锁解锁流程

  • Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后 停车场显示空余车位减一
  • 刚开始,permits(state)为 3,这时 5 个线程来获取资源
    在这里插入图片描述
  • 假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列 park 阻塞
    在这里插入图片描述
  • 这时 Thread-4 释放了 permits,状态如下
    在这里插入图片描述
  • 接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接 下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
    在这里插入图片描述

2、源码分析

这里是引用
在这里插入图片描述
在这里插入图片描述

五、CountdownLatch

  • 用来进行线程同步协作,等待所有线程完成倒计时。
  • 其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一
public static void main(String[] args) throws InterruptedException { 
	CountDownLatch latch = new CountDownLatch(3);
	new Thread(() -> {
		log.debug("begin...");
		sleep(1);
		latch.countDown(); 
		log.debug("end...{}", latch.getCount());
	}).start();
	
	new Thread(() -> {
		log.debug("begin...");
		sleep(2);
		latch.countDown(); 
		log.debug("end...{}", latch.getCount());
	}).start();
	
	new Thread(() -> {
		log.debug("begin...");
		sleep(1.5);
		latch.countDown(); 
		log.debug("end...{}", latch.getCount());
	}).start();
	
	log.debug("waiting..."); 
	latch.await(); 
	log.debug("wait end...");
}

输出:
在这里插入图片描述

可以配合线程池使用,改进如下

public static void main(String[] args) throws InterruptedException { 
	CountDownLatch latch = new CountDownLatch(3);
	ExecutorService service = Executors.newFixedThreadPool(4); 
	service.submit(() -> {
		log.debug("begin...");
		sleep(1);
		latch.countDown(); 
		log.debug("end...{}", latch.getCount());
	});
	
	service.submit(() -> {
		log.debug("begin...");
		sleep(1.5);
		latch.countDown(); 
		log.debug("end...{}", latch.getCount());
	});
	
	service.submit(() -> {
		log.debug("begin...");
		sleep(2);
		latch.countDown(); 
		log.debug("end...{}", latch.getCount());
	}); 
	
	service.submit(()->{
		try { 
			log.debug("waiting..."); 
			latch.await(); 
			log.debug("wait end...");
		} catch (InterruptedException e) { 
			e.printStackTrace();
		} 
	});
}

输出:
在这里插入图片描述

1、应用之同步等待多线程准备完毕
AtomicInteger num = new AtomicInteger(0);
ExecutorService service = Executors.newFixedThreadPool(10, (r) -> {
	return new Thread(r, "t" + num.getAndIncrement()); 
});
CountDownLatch latch = new CountDownLatch(10); 
String[] all = new String[10];
Random r = new Random();
for (int j = 0; j < 10; j++) {
	int x = j; 
	service.submit(() -> {
		for (int i = 0; i <= 100; i++) { 
			try {
				Thread.sleep(r.nextInt(100));
			} catch (InterruptedException e) {
			}
			all[x] = Thread.currentThread().getName() + "(" + (i + "%") + ")"; 
			System.out.print("\r" + Arrays.toString(all));
		}
		latch.countDown(); 
	});
}
latch.await(); 
System.out.println("\n游戏开始..."); 
service.shutdown();

中间输出:
在这里插入图片描述
最后输出:
在这里插入图片描述

2、应用之同步等待多个线程调用结束
@RestController
public class TestCountDownlatchController {

	@GetMapping("/order/{id}")
	public Map<String, Object> order(@PathVariable int id) {
		HashMap<String, Object> map = new HashMap<>(); 
		map.put("id", id);
		map.put("total", "2300.00");
		sleep(2000);
		return map; 
	}
	
	@GetMapping("/product/{id}")
	public Map<String, Object> product(@PathVariable int id){
		HashMap<String, Object> map = new HashMap<>(); 
		if (id == 1) {
			map.put("name", "小爱音箱");
			map.put("price", 300); 
		} else if (id == 2) {
			map.put("name", "小米手机");
			map.put("price", 2000); 
		}
		map.put("id", id); sleep(1000); 
		return map;
	}
	
	@GetMapping("/logistics/{id}")
	public Map<String, Object> logistics(@PathVariable int id) {
		HashMap<String, Object> map = new HashMap<>(); 
		map.put("id", id);
		map.put("name", "中通快递");
		sleep(2500);
		return map; 
	}
	
	private void sleep(int millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace(); 
		}
	} 
}

rest 远程调用

RestTemplate restTemplate = new RestTemplate(); 
log.debug("begin");

ExecutorService service = Executors.newCachedThreadPool(); 
CountDownLatch latch = new CountDownLatch(4); 
Future<Map<String,Object>> f1 = service.submit(() -> {
	Map<String, Object> r = restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1);
	return r; 
});

Future<Map<String, Object>> f2 = service.submit(() -> { 
	Map<String, Object> r = restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1); 
	return r;
});

Future<Map<String, Object>> f3 = service.submit(() -> {
	Map<String, Object> r = restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2);
	return r; 
});

Future<Map<String, Object>> f4 = service.submit(() -> { 
	Map<String, Object> r = restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1);
	return r;
});

System.out.println(f1.get()); 
System.out.println(f2.get()); 
System.out.println(f3.get()); 
System.out.println(f4.get()); 
log.debug("执行完毕"); 
service.shutdown();

执行结果:
在这里插入图片描述

六、CyclicBarrier

[ˈsaɪklɪk ˈbæriɚ] 循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执 行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行

CyclicBarrier cb = new CyclicBarrier(2); // 个数为2时才会继续执行

new Thread(()->{ 
	System.out.println("线程1开始.."+new Date()); 
	try {
		cb.await(); // 当个数不足时,等待
	} catch (InterruptedException | BrokenBarrierException e) {
		e.printStackTrace(); 
	}
	System.out.println("线程1继续向下运行..."+new Date()); 
}).start();

new Thread(()->{
	System.out.println("线程2开始.."+new Date());
	try { 
		Thread.sleep(2000); 
	} catch (InterruptedException e) { 
	} 
	try {
		cb.await(); // 2 秒后,线程个数够2,继续运行
	} catch (InterruptedException | BrokenBarrierException e) {
		e.printStackTrace(); 
	}
	System.out.println("线程2继续向下运行..."+new Date()); 
}).start();

注意: CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比 喻为『人满发车』

七、线程安全集合类概述

在这里插入图片描述

线程安全集合类可以分为三大类:

  • 遗留的线程安全集合如HashtableVector
  • 使用Collections装饰的线程安全集合,如:
    Collections.synchronizedCollection
    Collections.synchronizedList
    Collections.synchronizedMap
    Collections.synchronizedSet
    Collections.synchronizedNavigableMap
    Collections.synchronizedNavigableSet
    Collections.synchronizedSortedMap
    Collections.synchronizedSortedSet
  • java.util.concurrent.*

重点介绍java.util.concurrent.*下线程安全集合类,可以发现它们有规律,里面包含三类关键词:Blocking、CopyOnWrite、Concurrent

  • Blocking大部门实现基于锁,并提供用来阻塞的方法
  • CopyOnWrite之类容器修改开销相对较重
  • Concurrent 类型的容器
    – 内部很多操作使用cas优化,一般可以提供较高吞吐量
    – 弱一致性
  • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的
  • 求大小弱一致性,size操作未必是100%准确
  • 读取弱一致性

遍历时如果发生了修改,对于非安全容器来讲,使用 fail - fast 机制也就是让遍历立刻失败,抛出ConcurrentModificationException,不再继续遍历

八、ConcurrentHashMap

1、练习:单词计数

生成测试数据

static final String ALPHA = "abcedfghijklmnopqrstuvwxyz";

public static void main(String[] args) {
	int length = ALPHA.length();
	int count = 200;
	List<String> list = new ArrayList<>(length * count); 
	for (int i = 0; i < length; i++) {
		char ch = ALPHA.charAt(i);
		for (int j = 0; j < count; j++) { 
			list.add(String.valueOf(ch));
		} 
	}
	
	Collections.shuffle(list); 
	for (int i = 0; i < 26; i++) {
		try (PrintWriter out = new PrintWriter( 
			new OutputStreamWriter(
				new FileOutputStream("tmp/" + (i+1) + ".txt")))) {
			String collect = list.subList(i * count, (i + 1) * count).stream()
				.collect(Collectors.joining("\n")); 
			out.print(collect);
		} catch (IOException e) {
		} 
	}
}

模版代码,模版代码中封装了多线程读取文件的代码

private static <V> void demo(Supplier<Map<String,V>> supplier,
 BiConsumer<Map<String,V>,List<String>> consumer) { //BiConsumer是操作接口
	Map<String, V> counterMap = supplier.get(); 
	List<Thread> ts = new ArrayList<>();
	for (int i = 1; i <= 26; i++) {
		int idx = i;
		Thread thread = new Thread(() -> {
			List<String> words = readFromFile(idx);
			consumer.accept(counterMap, words); 
		});
		ts.add(thread); 
	}

	ts.forEach(t->t.start()); ts.forEach(t-> {
		try { 
			t.join();
		} catch (InterruptedException e) { 
			e.printStackTrace();
		} 
	});
	System.out.println(counterMap); 
}

public static List<String> readFromFile(int i) {
	ArrayList<String> words = new ArrayList<>();
	try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("tmp/" + i +".txt")))) { 
	while(true) {
		String word = in.readLine(); 
		if(word == null) {
			break; 
		}
		words.add(word); 
	}
	return words;
	} catch (IOException e) {
		throw new RuntimeException(e); 
	}
}

你要做的是实现两个参数

  • 一是提供一个map集合,用来存放每个单词的计数结果,key为单词,value为计数
  • 二是提供一组操作,保证计数的安全性,会传递map集合以及单词List

正确结果输出应该是每个单词出现200次
在这里插入图片描述

下面的实现为:

demo(
	// 创建 map 集合
	// 创建 ConcurrentHashMap 对不对?
	() -> new HashMap<String, Integer>(),
	
	// 进行计数
	(map, words) -> {
		for (String word : words) {
			Integer counter = map.get(word);
			int newValue = counter == null ? 1 : counter + 1; 
			map.put(word, newValue);
		} 
	}
);

有没有问题?请改进

参考解答1

demo(
	() -> new ConcurrentHashMap<String, LongAdder>(), (map, words) -> {
		for (String word : words) {
			// 注意不能使用 putIfAbsent,此方法返回的是上一次的 value,首次调用返回 null 
			map.computeIfAbsent(word, (key) -> new LongAdder()).increment();
		} 
	}
);

参考解答2

demo(
	() -> new ConcurrentHashMap<String, Integer>(), 
	(map, words) -> {
		for (String word : words) {
			// 函数式编程,无需原子变量 
			map.merge(word, 1, Integer::sum);
		} 
	}
);
2、ConcurrentHashMap原理(重要)
2.1 JDK7 HashMap并发死链

1、测试代码

注意:

  • 要在JDK 7 下运行,否则扩容机制和hash的计算方法都变了
  • 以下测试代码是精心准备的,不要随便改动
public static void main(String[] args) {
	// 测试 java 7 中哪些数字的 hash 结果相等 
	System.out.println("长度为16时,桶下标为1的key"); 
	for (int i = 0; i < 64; i++) {
		if (hash(i) % 16 == 1) { 
			System.out.println(i);
		} 
	}
	System.out.println("长度为32时,桶下标为1的key"); 
	for (int i = 0; i < 64; i++) {
		if (hash(i) % 32 == 1) { 
			System.out.println(i);
		} 
	}
	// 1, 35, 16, 50 当大小为16时,它们在一个桶内
	final HashMap<Integer, Integer> map = new HashMap<Integer, Integer>(); 
	// 放 12 个元素
	map.put(2, null);
	map.put(3, null);
	map.put(4, null);
	map.put(5, null);
	map.put(6, null);
	map.put(7, null);
	map.put(8, null);
	map.put(9, null);
	map.put(10, null);
	map.put(16, null);
	map.put(35, null);
	map.put(1, null);
	System.out.println("扩容前大小[main]:"+map.size()); 
	
	new Thread() {
		@Override
		public void run() {
			// 放第 13 个元素, 发生扩容
			map.put(50, null); System.out.println("扩容后大小[Thread-0]:"+map.size());
		} 
	}.start();
	
	new Thread() { 
		@Override
		public void run() {
			// 放第 13 个元素, 发生扩容
			map.put(50, null); System.out.println("扩容后大小[Thread-1]:"+map.size());
		} 
	}.start();
}

final static int hash(Object k) { 
	int h = 0;
	if (0 != h && k instanceof String) {
		return sun.misc.Hashing.stringHash32((String) k);
	}
	h ^= k.hashCode();
	h ^= (h >>> 20) ^ (h >>> 12); 
	return h ^ (h >>> 7) ^ (h >>> 4);
}

2、死链复现

  • 测试工具使用idea
  • 在HashMap源码590行加断点
int newCapacity = newTable.length;

断电的条件如下,目的是让HashMap在扩容为32时,并且线程为Thread - 0或Thread - 1时停下来

newTable.length==32 && 
(
	Thread.currentThread().getName().equals("Thread-0")||
	Thread.currentThread().getName().equals("Thread-1") 
)
  • 断点暂停方式选择Thread,否则在调试Thread - 0时,Thread - 1无法恢复运行
  • 运行代码,程序在预料的断电位置停了下来,输出
    在这里插入图片描述
  • 接下来进入扩容流程调试
  • 在HashMap源码594行加断电
 Entry<K,V> next = e.next; // 593 
 if (rehash) // 594 
 // ...
  • 这是为了观察e节点和next 节点的状态,Thread - 0单步执行到594行,再594处再添加一个断电(条件Thread.currentThread().getName().equals(“Thread-0”))
  • 这时可以在Variables 面板观察到 e 和next 变量,使用view as -> Object查看节点状态
    在这里插入图片描述
  • 在Threads面板选中 Thread - 1恢复运行,可以看到控制台输出新的内容如下,Thread - 1扩容已完成:
    在这里插入图片描述
  • 这时Thread - 0还停在594处,Variables面板变量的状态已经变化为:
    在这里插入图片描述
  • 为什么呢,因为Thread - 1扩容时链表也是后加入的元素放入链表头,因此链表头就倒过来了,但Thread - 1虽然结果正确,但它结束后Thread - 0 还要继续运行
  • 接下来就可以单步调试(F8)观察死链的产生了
  • 下一轮循环到594,将e搬迁到newTable链表头
    在这里插入图片描述
  • 下一轮循环到594,将e搬迁到newTable链表头
    在这里插入图片描述
  • 再看看源码
    在这里插入图片描述

3、源码分析

1、HashMap的并发死链发生在扩容时
在这里插入图片描述
在这里插入图片描述
2、假设map中初始元素是:
在这里插入图片描述

4、小结

  • 究其原因,是因为在多线程环境下使用了非线程安全的map集合
  • JDK8虽然讲扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)
2.2 JDK8 ConcurrentHashMap

1、重要属性和内部类

// 默认为 0
// 当初始化时, 为 -1
// 当扩容时, 为 -(1 + 扩容线程数)
// 当初始化或扩容完成后,为 下一次的扩容的阈值大小 
private transient volatile int sizeCtl;

// 整个 ConcurrentHashMap 就是一个 Node[]
static class Node<K,V> implements Map.Entry<K,V> {}

// hash 表
transient volatile Node<K,V>[] table;

// 扩容时的 新 hash 表
private transient volatile Node<K,V>[] nextTable;

// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点 
static final class ForwardingNode<K,V> extends Node<K,V> {}

// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node 
static final class ReservationNode<K,V> extends Node<K,V> {}

// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}

// 作为 treebin 的节点, 存储 parent, left, right 
static final class TreeNode<K,V> extends Node<K,V> {}

2、重要方法

 // 获取 Node[] 中第 i 个 Node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)

// cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)

// 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)

3、构造器分析

可以看到实现了懒惰初始化,在构造方法中仅仅计算了table的大小,以后在第一次使用时才会真正创建:
在这里插入图片描述

4、get流程

这里是引用

5、put流程

以下数组简称(table),链表简称(bin)
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

6、size计算流程

size计算实际发生在put,remove改变集合元素的操作之中

  • 没有竞争发生,向baseCount累加计数
  • 有竞争发生,新建counterCells,向其中的一个cell累加计数
    – counterCells初始有两个cell
    – 如果计数竞争比较激烈,会创建新的cell来累加计数
    在这里插入图片描述

Java 8 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)

  • 初始化,使用 cas 来保证并发安全,懒惰初始化 table
  • 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程 会用 synchronized 锁住链表头
  • put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素 添加至 bin 的尾部
  • get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新 table 进行搜索
  • 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可 做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中
  • size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加 即可

源码分析: http://www.importnew.com/28263.html
其它实现: Cliff Click’s high scale lib

2.3 JDK7 ConcurrentHashMap

它维护了一个 segment 数组,每个 segment对应一把锁

  • 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的
  • 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化

1、构造器分析
在这里插入图片描述

构造完成,如下图所示:
在这里插入图片描述

  • 可以看到 ConcurrentHashMap 没有实现懒惰初始化,空间占用不友好
  • 其中 this.segmentShift 和 this.segmentMask 的作用是决定将 key 的 hash 结果匹配到哪个 segment
  • 例如,根据某一 hash 值求 segment 位置,先将高位向低位移动 this.segmentShift 位
    在这里插入图片描述
  • 结果再与 this.segmentMask 做位于运算,最终得到 1010 即下标为 10 的 segment
    在这里插入图片描述

2、put流程

这里是引用
segment 继承了可重入锁(ReentrantLock),它的 put 方法为
在这里插入图片描述
在这里插入图片描述

3、rehash流程

发生在 put 中,因为此时已经获得了锁,因此 rehash 时不需要考虑线程安全
在这里插入图片描述
在这里插入图片描述
附,调试代码:
在这里插入图片描述
在这里插入图片描述

4、get流程

get 时并未加锁,用了 UNSAFE 方法保证了可见性,扩容过程中,get 先发生就从旧表取内容,get 后发生就从新表取内容
在这里插入图片描述

5、size计算流程

  • 计算元素个数前,先不加锁计算两次,如果前后两次结果如一样,认为个数正确返回
  • 如果不一样,进行重试,重试次数超过 3,将所有 segment 锁住,重新计算个数返回
    在这里插入图片描述

九、LinkedBlockingQueue原理

1、基本的入队出队

这里是引用
在这里插入图片描述

  • 初始化链表 last = head = new Node<E>(null); Dummy 节点用来占位,item 为 null
    在这里插入图片描述
  • 当一个节点入队 last = last.next = node;
    在这里插入图片描述
  • 再来一个节点入队 last = last.next = node;
    在这里插入图片描述
  • 出队
    在这里插入图片描述
  • h = head
    在这里插入图片描述
  • first = h.next
    在这里插入图片描述
  • h.next = h
    在这里插入图片描述
  • head = first
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

2、加锁分析

高明之处在于用了两把锁和 dummy 节点

  • 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
  • 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
    – 消费者与消费者线程仍然串行
    – 生产者与生产者线程仍然串行

线程安全分析

  • 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是 head 节点的线程安全。两把锁保证了入队和出队没有竞争
  • 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
  • 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞
    在这里插入图片描述

put操作
在这里插入图片描述
在这里插入图片描述

take操作
在这里插入图片描述在这里插入图片描述

由 put 唤醒 put是为了避免信号不好

3、性能比较

主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较

  • Linked 支持有界,Array 强制有界
  • Linked 实现是链表,Array 实现是数组
  • Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
  • Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的 Linked 两把锁,Array 一把锁

十、ConcurrentLinkedQueue

ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是

  • 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
  • dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
  • 只是这【锁】使用了 cas 来实现
  • 事实上,ConcurrentLinkedQueue 应用还是非常广泛的
  • 例如之前讲的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了 ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用
    在这里插入图片描述

十一、CopyOnWriteArrayList

CopyOnWriteArraySet 是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更 改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。 以新增为例:
在这里插入图片描述

这里的源码版本是 Java 11,在 Java 1.8 中使用的是可重入锁而不是 synchronized

其它读操作并未加锁,例如:
在这里插入图片描述

适合『读多写少』的应用场景

1、get 弱一致性
在这里插入图片描述

不容易测试,但问题确实存在

2、迭代器弱一致性

CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(); 
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iter = list.iterator(); 
new Thread(() -> {
	list.remove(0);
	System.out.println(list); 
}).start();

sleep1s();
while (iter.hasNext()) {
	System.out.println(iter.next()); 
}

不要觉得弱一致性就不好

  • 数据库的 MVCC 都是弱一致性的表现
  • 并发高和一致性是矛盾的,需要权衡
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值