1.线程安全
当多个线程访问某个类时,无论运行时环境采用哪种线程线程调度策略或者这些线程如何交替执行,而且在主调代码中无需任何额外的同步或协同,该类总是可以表现出正确的行为,那么我们称该类是线程安全的。
注:在线程安全类中封装了必要的同步机制,因此客户端无需进一步采取同步措施
影响线程安全的因素:
- 共享
- 可变
实现线程安全的方式:
- 不在线程间共享状态变量,特例:无状态的对象一定是线程安全的
- 状态变量不可变
- 对状态变量应用同步机制
1.1 不在线程间共享状态变量
如果仅在单线程内访问数据,那么就不需要同步机制,也能保证线程安全。这种技术也被称为线程封闭。
当某个对象封闭在一个线程中,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的!!!
线程封闭主要有三种实现:
- Ad-hoc封闭,即维护线程封闭的职责完全由程序实现来承担,不推荐使用
- 栈封闭:在栈封闭中,只能通过局部变量访问对象。在维护栈封闭时,程序员需要多做一些工作,以保证被引用的对象不会逃逸【Escape】。
- Thread-Local类:ThreadLocal提供了线程本地变量,使得线程中的某个值与保存值的对象关联起来。ThreadLocal对象通常用于防止对可变的单例变量或全局变量进行共享。每个Thread对象内存在一个threadLocals变量,类型为ThreadLocal.ThreadLocalMap,ThreadLocal更像是一个Facade,帮助我们以一个一致的接口,操作Thread的threadLocals变量。
1.2 状态变量不可变
不可变对象一定是线程安全的!
当满足一下条件时,对象才是不可变的:
- 对象创建后其状态不能修改
- 对象的所有域都是Final的(Final类型的域是不能修改的)
- 对象是正确创建的(在对象的创建期,this引用没有逃逸)
1.3 同步机制
Java提供的同步机制:
- 同步原语:synchronized
- 显式锁:Lock
- volatile
- 原子变量:AtomicXxxx
1.3.1 同步原语:synchronized
通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有守护变量的锁,都能以独占的方式来访问这些变量,并且对变量的任何修改对随后获得这个锁的其他线程都是可见的。
Java5之前,synchronized是一个重量级锁,Java5之后,为synchronized提供了偏向锁、轻量级锁、无锁等概念。
Java将锁与线程的映射关系,保存在对象的header中。Java规范规定,堆中的任何对象,必须有header,在底层实现中,所有的对象都是一个oop对象,oop是一个C++抽象类,对于实例对象,其对应的底层数据结构是一个instanceOop.
下面是JDK1.7中oop继承结构
oop (abstract base)
instanceOop (instance objects)
methodOop (representations of methods)
arrayOop (array abstract base)
symbolOop (internal symbol / string class)
klassOop (klass header) (Java 7 and before only)
markOop
instanceOop的内存布局以两个机器字头开始。标记字【Mark Wod】是其中的第一个,它是指向特定于实例的元数据的指针(通俗而言,即是一个关于对象的元数据的)。接下来是klass字【Klass Word】,它指向类的元数据。
在Java 7和以前的版本中,instanceOop中的klass字【Klass Word】指向一个称为PermGen的内存区域,它是Java堆的一部分。一般的规则是,Java堆中的任何东西都必须有一个对象头。在这些较早的Java版本中,我们将类的元数据称为klassOop。klassOop的内存布局很简单——header + klass元数据。
从Java 8开始,klass被放在Java的堆空间之外,即元数据区【Metadata Space】(但不是JVM进程的C堆之外),因此不再需要有header了。
KclassOop与Class<> 并不是一回事!!!
在下图中,我们可以看到区别:基本上klassOop包含类的虚函数表(vtable),而Class 对象包含用于反射调用的方法对象的引用数组(见图中的M0,M1等)。
Oops通常是机器字,所以在传统的32位机器上是32位,而在现代的处理器上是64位。然而,这可能会浪费大量的内存。为了缓解这种情况,HotSpot提供了一种称为压缩oops的技术。如果选择:
-XX:+UseCompressedOops
对象的实例字段紧跟在header的后面。
oop结构在运行时表示对象,一个指针指向类级元数据,另一个指针指向实例元数据
关于锁的记录,就记录在标记字【Mark Word】中,标记字【Mark Word】存储身份哈希码、分代年龄以及锁标记,为了能在有限的空间里存储下更多的数据,其存储格式是不固定的,在32位系统上各状态的格式如下:
下面是锁状态的转换示意图:
什么是偏向锁:在“偏向锁”中,一个对象被偏向于第一个锁定它的线程,然后这个线程获得更好的锁定性能。
当分配对象内存时,比如new:
(1) 如果JVM启动了偏向锁模式(JDK1.6默认开启),如果该对象所属的class没有关闭偏向锁模式,那么该对象的header里标记字【Mark Word】将是可偏向的,即上图图左,此时的32位分布如下:
- JavaThread* : 23 bits:线程ID
- Epoch : 2 bits
- Age : 4 bits
- Biased_lock : 1 bit
- Lock : 2 bits
此时,线程ID值为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)
然后,当想线程获取锁时候,发现是匿名偏向(anonymously biased),使用CAS指令,将其线程ID修改为当前线程的ID:
- ① 如果成功,那么偏向锁获取成功,就可以执行监视区内的代码了。
- 当该线程再次进入该同步代码块时,发现当前线程就是偏向线程,那么它会执行检查,然后在栈中插入一条Lock Record,转而继续执行监视区代码,因为栈是线程私有的,并不需要CAS指令。由此可见,偏向锁使得被偏向的线程再次获得锁时,仅仅几个简单的操作即可,性能开销很低
- ② 如果失败,这其实表示锁被其他线程捷足先登了,首先撤销偏向锁,撤销的操作一般是在safepoint中查看偏向的线程是否存活,如果该线程存活,且还在监视区中,那么进行锁升级,偏向线程继续持有锁,而新线程则走入锁升级的逻辑里;如果偏向线程已不存活或者不在监视区中,那么将header中的标记字【Mark Word】修改为无锁状态【Unlocked】,然后升级为轻量级锁。当在轻量级锁中发生了锁竞争,那么就会膨胀为重量级锁,重量级锁即我们所说的监视器,其利用操作系统底层的同步机制去实现Java中的线程同步。重量级锁的状态下,对象的标记字【Mark Word】为指向一个堆中的monitor对象的指针。一个监视器有入口集【Entry Set】、等待集【Wait Set】、持有者【Owner】。
(2) 如果对象所属的class关闭了偏向锁模式,图的右侧说明了标准的锁定过程。对象一开始均处于无锁状态【Unlocked】。只要对象未被锁定,最后两位的值就是01。当方法对对象进行同步时,头字和指向对象的指针存储在当前堆栈帧中的锁记录中。然后,VM尝试通过比较和交换操作,在对象的头字中安装一个指向锁记录的指针。如果成功,则当前线程随后拥有锁。由于锁记录总是在字边界处对齐,因此头字的最后两位是00并标识
重量级锁的缺点:Java中的监视器锁monitor需要操作系统的互斥量(mutex)和条件变量(condition variable)的支持,进行系统调用涉及到用户态内核态切换,导致开销比较大。
1.3.2 显式锁:Lock
Java5引入的Lock机制,并不是替代内置锁的方法,而是当内置加锁机制不适当时,作为一种可选的高级功能。
与内置加锁机制不同,显式锁Lock提供了一种无条件的、可轮询的、可定时的、可中断的锁获取模式,所有加锁与解锁都是显式的。
Lock实现提供了与同步原语(内置锁)相同的内存可见性保证!!
public interface Lock {
//获取锁
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
ReentrantLock实现了Lock接口,并提供与synchronized相同的互斥性和内存可见性保证。
从内存语义的角度看:
- 获取ReentrantLock = 进入同步代码块
- 释放ReentrantLock = 退出同步代码块
ReentrantLock同样实现了synchronized所具备的可重入性。
显式锁的优点:
- 可定时性,例如Lock.tryLock(long time, TimeUnit unit)
- 如果锁可用,那么获取锁并立即返回
- 如果锁不可用,那么当前线程处于线程调度的目的而被禁用,并处于休眠状态,直到下面三个条件发生:
- 1 锁被当前线程获取到
- 2 其他线程通过Thread#interrupt中断了该线
- 3 时间到了
- 可中断性,例如Lock.lockInterruptibl(),
- 如果锁可用,则获取锁并立即返回。
- 如果锁不可用,那么当前线程将出于线程调度的目的被禁用,并处于休眠状态,直到发生以下两种情况之一:
- 1 锁被当前线程获取到
- 2 其他线程通过Thread#interrupt中断了该线程
- 多条件队列,synchronized只能通过Object的 wait/notify支持单一条件队列,而ReentrantLock支持多条件队列
- 公平-非公平性,synchronized仅仅支持非公平锁,而ReentrantLock可以支持公平锁
- 非阻塞,ReentrantLock的tryLock()支持非阻塞性,而synchronized必须只能是阻塞的
显式锁的缺点:
- 必须显式lock & unlock
无条件锁
Lock#lock()方法提供了无条件锁,它与同步原语基本一致,提供了互斥性与内存可见性。
轮询锁与定时锁
可定时的与可轮训的锁获取模式是由Lock#tryLock()方法实现的。
在内置锁中,死锁是一个严重的问题,而恢复程序的唯一方式就是重启,而防止死锁的唯一方法就是在构造程序时避免出现不一致的锁顺序。
在可定时的且可轮训的锁中,仅当锁在调用时处于空闲状态时才获取锁。当获取锁时,锁如果可用,那么获取锁并返回true;而如果锁不可用,那么直接返回false。
public static <T> Optional<Future<T>> action(Lock l1,
Lock l2,
long timeOut,
Callable<T> task){
LocalTime endline = LocalTime.now().plusNanos(timeOut);
while (true){
try {
if (l1.tryLock()){
try {
if (l2.tryLock()){
Future<T> future = pool.submit(task);
return Optional.of(future);
}
} finally {
l2.unlock();
}
}
} finally {
l1.unlock();
}
if (LocalTime.now().isAfter(endline)){
return Optional.empty();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
定时锁的使用,是使用Lock#tryLock(long time, TimeUnit unit)这个重载版本实现的。
定时锁同样具有可中断性,如果获取锁时,锁不可用,那么当前线程会进入休眠,直到下面三种事情之一发生:
- 当前线程获取到锁
- 其他线程通过Thread#interrupt中断了该线程
- 指定的等待时间已经过了
如果锁被获取,则返回true,否则返回false,如果线程被中断,则抛出InterruptedException
public static <T> Optional<Future<T>> action(Lock lock,
long timeOut,
TimeUnit unit,
Callable<T> task){
try {
if (!lock.tryLock(timeOut,unit)){
return Optional.empty();
}
} catch (InterruptedException e){
return Optional.empty();
}
// Acquired Lock
try {
return Optional.of(pool.submit(task));
} finally {
lock.unlock();
}
}
可中断锁
Lock#lockInterruptibly()实现了可中断的锁获取模式。
公平锁
ReentrantLock提供了公平策略,默认是非公平锁,可以通过构造器参数fair来配置自定义的公平策略。
公平锁:线程将按照他们发出请求的顺序来获取锁
非公平锁:允许插队
注:公平锁的性能显著低于非公平锁,因此如果不必要的话,不要为公平付出性能的代价
读写锁
互斥是一种保守的加锁策略,虽然可以避免“写-写”或者“写-读”冲突,但是他同样也避免了“读-读”冲突。
读写锁,一个资源可以被多个读操作访问,或者被一个写操作访问,但是不可以二者兼得!!!
public interface ReadWriteLock {
/**
* 返回用于读的锁.
*/
Lock readLock();
/**
* 返回用于写的锁.
*/
Lock writeLock();
}
Java默认的实现是ReentrantReadWriteLock,它在读写锁的基础上,又提供了可重入性。
StampedLock
在Java 8中引入了StampedLock。它还支持读锁和写锁。但是,锁获取的方法返回一个用于释放锁或检查锁是否仍然有效的戳记:
public class StampedLockDemo {
Map<String,String> map = new HashMap<>();
private StampedLock lock = new StampedLock();
public void put(String key, String value){
long stamp = lock.writeLock();
try {
map.put(key, value);
} finally {
lock.unlockWrite(stamp);
}
}
public String get(String key) throws InterruptedException {
long stamp = lock.readLock();
try {
return map.get(key);
} finally {
lock.unlockRead(stamp);
}
}
}
StampedLock提供的另一个特性是乐观锁。大多数情况下,读操作不需要等待写操作完成,因此不需要完全的读锁。
相反,我们可以升级到读锁:
public String readWithOptimisticLock(String key) {
// 获取乐观锁,其实并没加锁,它假定不会有write操作
long stamp = lock.tryOptimisticRead();
String value = map.get(key);
// 验证stamp之后,是否有write操作
if(!lock.validate(stamp)) {
// 由乐观锁升级到读锁
stamp = lock.readLock();
try {
return map.get(key);
} finally {
// 释放锁
lock.unlock(stamp);
}
}
return value;
}
StampedLock还支持读锁升级写锁,即通过tryConvertToWriteLock方法,如果升级成功,则返回新的stamp,否则返回0
StampedLock性能很高,推荐使用
协作性
监视器需要支持两种线程同步:
- ①互斥:Java虚拟机通过对象锁来支持互斥,以允许多个线程独立地操作共享数据,而不会相互干扰。
- ②协作:在Java虚拟机中,通过Object类的wait和notify方法支持协作,这使线程能够一起工作,以实现一个共同的目标。
条件队列:它使得一组线程(称之为等待集)能通过某种方式,等待特定的条件变为True.
注:在传统的队列中,元素通常是数据,而在条件队列中的元素,却是一个个等待相关条件的线程
每个对象可以作为一个锁(对象锁),同时,每个对象还可以作为一个条件队列,且Java通过Object中声明的wait/notify方法,构成了内部条件队列的API。
注意:内置锁仅可以有一个相关联的条件队列,而一个条件队列可以支持多个条件谓词!!
当使用条件等待时,比如Object#wait或Condition#await:
- 通常都存在一个条件谓词—其中包括对一些对象状态变量的验证,在线程执行正式逻辑代码前必须通过该谓词测试
- 在调用wait之前测试条件谓词,并且从wait中返回时需要再次验证条件谓词,因此wait通常处于一个while循环中,while语句的条件表达式,即是条件谓词
- 确保使用与条件队列相关联的锁,来保护构成条件谓词的各个状态变量
- 当调用wait/notify/notifyAll时,必须持有与条件队列相关的锁
- 在正式逻辑处理结束后再释放锁,不要在条件谓词之后以及逻辑处理之前释放锁
// 模拟有界的缓存
public class OuterPrediectQueueBuffer<V> extends BaseBoundedBuffer<V> {
final Lock lock = new ReentrantLock();
// 不得Empty,空了不得take
final Condition nonEmptyCond = lock.newCondition();
//不得Full,满了不得put
final Condition nonFullCond = lock.newCondition();
protected OuterPrediectQueueBuffer(int capacity) {
super(capacity);
}
public synchronized void put(V v) throws InterruptedException {
lock.lock(); // lock是条件队列相关联的锁
try {
while(isFull()){ // isFull是条件谓词,被唤醒后,while循环中再验证是否满足了条件谓词
nonFullCond.wait(); // 条件等待
}
doPut(v); // 正式的逻辑
nonEmptyCond.signalAll();// 通知
return;
} finally {
lock.unlock(); // 逻辑处理完成后释放锁
}
}
public synchronized V take() throws InterruptedException {
lock.lock();
try {
while (isEmpty()){
nonEmptyCond.wait();
}
V v = doTake();
nonFullCond.signalAll();
return v;
} finally {
lock.unlock();
}
}
}
Java5引入的Condition,解决了内置锁仅可以有一个相关联的条件队列的限制
一个Condition与一个Lock关联
一个条件队列与一个内置锁(对象锁)关联
对于每个Lock,它可以有人以数量的Coondition对象。而且Condiftion会继承Lock的公平策略,对于公平锁,线程会按照FIFO的顺序从Condition#await中释放。
Object与Condition中等待、通知的方法对应关系为:
Object | Condition |
---|---|
wait | await |
notify | signal |
notifyAll | signalAll |
1.3.3 volatile
volatile关键字实现:
- 原子性:保证64位变量(long,double)的原子性读写,但是volatile并不支持复合操作的原子性,比如:++
- 可见性:Java内存模型中规定了volatile的happen-before效果,对volatile变量的写操作happen-before于后续的读。这样volatile变量能够确保一个线程的修改对其他线程可见
- 有序性:防止指令重排序,依赖于内存屏障
1.3.4 原子变量:AtomicXxxx
CAS(Compare-And-Swap)的缩写,比较并交换。CAS指令在Intel CPU上称为CMPXCHG指令,它的作用是将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为指令中提供的新值,如果不相等,则更新失败。
CAS是一种乐观锁的技术,它希望能成功的更新操作,并且如果另一个线程在最近一次检查后更新了该变量,那么CAS需要检测到这个错误。
CAS的典型使用模式:首先从内存取出值,并根据该值计算出新值,然后调用CAS更新。
CAS使用无锁的方式,实现了读-改-写操作序列。
自Java5之后,Java提供了AtomicXxx的,支持CAS操作。
CAS中的ABA问题:假设内存V中的值由A -> B -> A,在某些情况下,仍然认为发生了变化,当此时执行CAS应该失败。
解决ABA的最简单的方式,仍然是使用乐观锁,即并非更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。
Java通过AtomicStampedReference 来解决了ABA问题。
2. 基础构建块
2.1 同步容器
HashTable、Vector是古老的线程安全类,他们通过对Map加锁,保证对所有的的操作都是线程安全的。
注:迭代会发生ConcurrentModificationException,这是Fail-Fast机制
2.2 并发容器
Java 5引入了ConcurrentHashMap,用来替换HashMap,引入了CopyOnWriteArrayList,用于在迭代为主的情况下替换List。
ConcurrentHashMap
ConcurrentHashMap也是一个基于散列的Map,它是它使用了一种完全不同的加锁策略来提供更高的并发性和伸缩性。
ConcurrentHashMap并不是像HashTable那样在每一个方法上加对象锁,而是使用了分段锁,来实现细粒度的加锁机制。
ConcurrentHashMap提供了弱一致性,通过牺牲了一点一致性,换得了并发性。
ConcurrentHashMap并不会出现Fail-Fast,因此对ConcurrentHashMap进行迭代不会触发ConcurrentModificationException,弱一致性的迭代器可以容忍并发修改。
PS:因为分段锁并不是基于Map对象的,因此我们无法通过客户端加锁,实现对ConcurrentHashMap的独占访问。
ConcurrentHashMap提供了很多有用的原子操作,比如putIfAbsent(key,value)、remove(key,value),他们是基于CAS的。
分段锁:ConcurrentHashMap通过其DEFAULT_CONCURRENCY_LEVEL指定了其初始的并发等级,也就是16。假设当前的容量大小为16.,那么以10个元素为一个段,每个段由一个特定于该段的锁进行互斥。此外,某些方法如size()和isEmpty()根本不受保护。虽然这允许更大的并发性,但这意味着它们不是强一致的(它们不会反映并发变化的状态)
CopyOnWriteArrayList
它是ArrayList的线程安全版本
仅仅当遍历操作远大于修改操作时,CopyOnWriteArrayList才是最高效的
CopyOnWriteArrayList的设计使用了一种有趣的技术,使它成为线程安全的,而不需要同步。当我们使用任何修改方法时——例如add()或remove()——CopyOnWriteArrayList的整个内容都会被复制到新的内部副本中。
由于这个简单的事实,我们可以以一种安全的方式遍历列表,即使并发修改正在发生。
当我们调用CopyOnWriteArrayList上的iterator()方法时,我们会得到一个由CopyOnWriteArrayList内容的不可变快照备份的迭代器。
当创建迭代器时,其实是创建了一个COWIterator类型的迭代器对象,该迭代器对象持有对当前ArrayList中array字段的引用。与此同时,即使其他线程向列表中添加或删除了某个元素,该修改先加锁,然后拷贝array,然后执行添加/删除操作,并将array的引用更新为指向该拷贝。
注:CopyOnWriteArrayList中读不加锁,写加锁!!
这种数据结构的特点使得它在迭代/遍历比修改更频繁的情况下特别有用。如果在我们的场景中添加/修改元素是一种常见的操作,那么CopyOnWriteArrayList将不是一个好的选择——因为额外的拷贝肯定会导致性能变低。
Queue
Queue实现了非阻塞操作,并提供了几种实现,比如:
- ConcurrentLinkedQueue,只是一个传统的先进先出的队列
- PriorityQueue,这是一个非并发的优先队列
如果在Queuue上执行add或者offer,那么当队列满的时候,会立即执行返回fasle
如果在Queuue上执行poll,那么当队列为空的时候,会立即执行返回fasle
BlockingQueue
BlockingQueue拓展了Queuue,增加了可阻塞API:
- 阻塞插入:put
- 阻塞获取:take
BlockingQueue多用于生产者-消费者模式
2.3 同步工具类
2.3.1 闭锁 Latch
闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。当闭锁到达终态,将不会再改变状态。
闭锁可以用于确保某些活动直到其他活动完成后才继续执行,比如:
- 确保某个计算在其需要的所有资源被初始化后才继续执行
- 确保某个服务在其锁依赖的所有服务都启动之后才启动
CountDownLatch
CountDownLatch不可重用,不可逆
FutureTask
FutureTask也可以作为闭锁,它可以处于一下三种状态:
- 等待运行
- 正在运行
- 运行完成
当FutureTask抵达终态,就会永远停留在这个状态上
我们通过CAS + FutureTask,可以实现Go语言中提供的Once机制
public class Once<V> {
private FutureTask<V> task;
private Once(FutureTask<V> task) {
this.task = task;
}
public static <V> Once of(Callable<V> callable){
return new Once<V>(
new FutureTask<V>(callable)
);
}
public Pair<V,Boolean> get(){
Pair<V,Boolean> vo = new Pair<>(null,false);
try {
vo =new Pair<>(task.get(),true);
} catch (Exception e) {
e.printStackTrace();
}
return vo;
}
}
2.3.2 屏障/栅栏
栅栏类似于闭锁,他能阻塞一组线程,直到某个时间发生。
栅栏与闭锁的区别在于:
- 所有线程必须同时到达栅栏为止,才可以继续执行。
- 闭锁无法重用,不可逆,而屏障/栅栏是可以重用的,且可逆
闭锁用于等待事件,而栅栏用于等待其他线程
eg:公司组织团建,并约定早上公司集合,那么只有当所有的同事集合后,大巴车才会触发去团建地点
CyclicBarrier
屏障/栅栏允许一组线程彼此等待到达一个共同的屏障点。
CyclicBarrier在包含固定大小的线程的程序中非常有用,这些线程有时必须彼此等待。
CyclicBarrier被称为循环屏障,因为它可以在等待的线程被释放后重新使用。所以在正常的使用中,一旦所有的线程都抵达屏障点,屏障将会被打破,然后它就会重置自己,可以再次被使用。
CyclicBarrier中有一个reset()方法,他会强制重置屏障为其初始状态。如果有线程在屏障点处await,他们将返回一个BrokenBarrierException异常。
因此,reset会导致当前正在等待的线程抛出一个BrokenBarrierException并立即唤醒。reset是当你想要“打破”屏障时才会使用。
Exchanger
Exchanger是一个特殊屏障,它是两个互相等待线程,在屏障点处交互数据,通俗而言,A线程(持有数据D1)与B线程(持有数据D2)在屏障点相互等待,当均抵达屏障点时,则通过exchange方法交换数据,交换后,A线程可以持有D2,而B则可以持有D1。
2.3.3 信号量
信号量用于控制同时地访问某个特定资源的操作数量,或者同时执行某个特定操作的数量。
Semaphore
Semaphore维护了一组虚拟的许可【permit】,许可的初始数量由构造器传入,在执行受限的操作时,需要先获取许可【permit】,操作结束后需要释放许可【permit】。
许可数为1的信号量,可以实现互斥的功能。
信号量是有界、阻塞的。
public class BoundedCache<K,V> {
public ConcurrentHashMap<K,V> cache = new ConcurrentHashMap<>();
public Semaphore semaphore;
public static final int BOUND = 256;
public BoundedCache() {
this(BOUND);
}
public BoundedCache(Integer max) {
semaphore = new Semaphore(max);
}
public boolean put(K key,V value) throws InterruptedException {
boolean flag = false;
try {
semaphore.acquire();
flag = true;
cache.put(key,value);
return true;
} finally {
if (!flag)
semaphore.release();
}
}
public V remove(K key){
V removed = cache.remove(key);
if (removed != null){
semaphore.release();
}
return removed;
}
}
3.线程池
任务:一组逻辑工作单元
线程:使任务异步执行的机制
虽然Executor是一个简单的接口,但是它为灵活且强大的异步任务执行框架提供了基础。它提供了一种标准的方法,将任务的提交与任务的执行解耦。
Executor基于生产者-消费者模式,提交任务的操作相当于生产者(生产待完成的工作单元),执行任务的线程则相当于消费者(执行完成这些工作单元)。
public class ThreadPoolExecutor extends AbstractExecutorService {
private volatile int corePoolSize;
private volatile int maximumPoolSize;
// 工作队列
private final BlockingQueue<Runnable> workQueue;
private final ReentrantLock mainLock = new ReentrantLock();
// 线程池中的所有工作者线程,访问由mainLock互斥锁定
private final HashSet<Worker> workers = new HashSet<Worker>();
// 创建新线程的线程工厂
private volatile ThreadFactory threadFactory;
// 当Executor终止时或者工作队列饱和时,饱和策略开始发挥作用
// Java提供了RejectedExecutionHandler 的各种实现,包括AbortPolicy、CallerRunsPolicy等
private volatile RejectedExecutionHandler handler;
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
}