1、进程
一个正在执行中的程序就是一个进程,系统会为这个进程发配独立的【内存资源】。进程是程序的一次执行过程,它有自己独立的生命周期,它会在启动程序时产生,运行程序时存在,关闭程序时消亡。
2、线程
线程是由进程创建的,是进程的一个实体,是具体干活的人,一个进程可能有多个线程。线程不独立分配内存,而是共享进程的内存资源,线程可以共享cpu的计算资源。
3、上下文切换
从任务管理器中我们可以看到,这台电脑上运行着304个进程,6207个线程,但是我只有32个逻辑内核,这足以证明对于每一个逻辑内核他在执行的过程当中也是按照时间片执行不同的线程的。
但是这里有几个问题:
- 我们的进程可以直接创建、调度线程吗?QQ运行了一会说我累了,不想执行了,微信你来吧!这显然是不合理的。
- QQ执行了一会,不执行了,那等其他线程执行完成之后,又轮上QQ了,QQ还能记得刚才运行到哪里了吗?
针对第一个问题,任何一个用户的线程是不允许调度其他的线程的,所有的线程调用都由一个大管家统一调度,这个大管家就是系统内核。
第二个问题,下一个执行时想要知道上一次的执行结果,就必须在上一次执行之后,讲运行时的数据进行保存,那么整个过程就出来了。
其中,用户线程执行的过程我们称之为【用户态】,内核调度的状态称之为【内核态】,每一个线程运行时产生的数据我们称之为【上下文】,线程的每次切换都需要进行用户态到内核态的来回切换,同时伴随着上下文的切换,是一个比较消耗资源的操作,所以一个计算机当中不是线程越多越好,线程如果太多也是有可能拖垮整个系统的。
4、synchronized原理分析
JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。
JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。
锁升级中涉及的四把锁:
-
无锁:不加锁
-
偏向锁:不锁锁,只有一个线程争夺时,偏心某一个线程,这个线程来了不加锁。
-
轻量级锁:少量线程来了之后,先尝试自旋,不挂起线程。
注:挂起线程和恢复线程的操作都需要转入内核态中完成这些操作,给系统的并发性带来很大的压力。在许多应用上共享数据的锁定状态,只会持续很短的一段时间,为了这段时间去挂起和恢复现场并不值得,我们就可以让后边请求的线程稍等一下,不要放弃处理器的执行时间,看看持有锁的线程是否很快就会释放,锁为了让线程等待,我们只需要让线程执行一个盲循环也就是我们说的自旋,这项技术就是所谓的【自旋锁】。
-
重量级锁:排队挂起线程
JVM一般是这样使用锁和Mark Word的:
1,当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
2,当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
3,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
4,当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
5,偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在【当前线程】的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,也叫所记录(lock record),同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
6,轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7,自旋默认10次。
7,自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞排队。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等)进入阻塞状态,等待将来被唤醒。就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源。
5、LockSupport
LockSupport
是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。
接下来我来看看LockSupport
有哪些常用的方法。主要有两类方法:park
和unpark
。
public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);
为什么叫park呢,park英文意思为停车。我们如果把Thread看成一辆车的话,park就是让车停下,unpark就是让车启动然后跑起来。
我们写一个例子来看看这个工具类怎么用的。
public class LockSupportTest {
public static final Object MONITOR = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable runnable = ()->{
synchronized (MONITOR) {
System.out.println("线程【" + Thread.currentThread().getName() + "】正在执行。");
LockSupport.park();
if (Thread.currentThread().isInterrupted()) {
System.out.println("被中断了");
}
System.out.println("继续执行");
}
};
Thread t1 = new Thread(runnable,"线程一");
Thread t2 = new Thread(runnable,"线程二");
t1.start();
Thread.sleep(1000L);
t2.start();
Thread.sleep(3000L);
t1.interrupt();
LockSupport.unpark(t2);
t1.join();
t2.join();
}
}
//运行的结果如下:
//线程【线程一】正在执行。
//被中断了
//继续执行
//线程【线程二】正在执行。
//继续执行
这儿park
和unpark
其实实现了wait
和notify
的功能,不过还是有一些差别的。
park
不需要获取某个对象的锁- 因为中断的时候
park
不会抛出InterruptedException
异常,所以需要在park
之后自行判断中断状态,然后做额外的处理
我们在park线程的时候可以传递一些信息,给调用者看,这个object什么都能传递。
小结:
park和unpark
可以实现类似wait和notify
的功能,但是并不和wait和notify
交叉,也就是说unpark
不会对wait
起作用,notify
也不会对park
起作用。park和unpark
的使用不会出现死锁的情况- blocker的作用是看到阻塞对象的信息
6、lock锁的原理cas和aqs
CAS
CAS,compare and swap的缩写,中文翻译成比较并交换,我发现jdk11以后改成了compare and set。
它的思路其实很简单,就是给一个元素赋值的时候,先看看内存里的那个值到底变没变,如果没变我就修改,变了我就不改了,其实这是一种无锁操作,不需要挂起线程,无锁的思路就是先尝试,如果失败了,进行补偿,也就是你可以继续尝试。这样在少量竞争的情况下能很大程度提升性能。
AQS
抽象队列同步器,用来解决线程同步执行的问题。
AQS解决问题的思路如下:
我们可以在AQS中看到这样的代码:
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled. */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking. */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition. */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate.
*/
static final int PROPAGATE = -3;
//CANCELLED(1):取消状态,当线程不再希望获取锁时,设置为取消状态
//SIGNAL(-1):当前节点的后继者处于等待状态,当前节点的线程如果释放或取消了同步状态,通知后继节点
//CONDITION(-2):等待队列的等待状态,当调用signal()时,进入同步队列
//PROPAGATE(-3):共享模式,同步状态的获取的可传播状态
//0:初始状态
volatile int waitStatus;
volatile Node prev;
volatile Node next;
/**
* The thread that enqueued this node. Initialized on
* construction and nulled out after use.
*/
volatile Thread thread;
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
/** Establishes initial head or SHARED marker. */
Node() {}
/** Constructor used by addWaiter. */
Node(Node nextWaiter) {
this.nextWaiter = nextWaiter;
THREAD.set(this, Thread.currentThread());
}
}
private transient volatile Node head;
private transient volatile Node tail;
从这段代码中我们看到AQS中维护了一个队列,这个队列是个双向队列,里边保存了一个线程,还有一个状态。
简单的聊聊这个队列,他叫【CLH队列】,这种队列有什么特性:
1、它是一个双向链表
2、CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next)等信息。
我们以ReentrantLock
来分析其中的过程:
有一个sunc
abstract static class Sync extends AbstractQueuedSynchronizer
两个
static final class FairSync extends Sync
static final class NonfairSync extends Sync
我们发现不传值是非公平锁,传入true
是公平锁,有啥区别咱们慢慢看:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
加锁:(获取锁)acquire就是获取的意思
// NonfairSync 非公平的加锁动作一上来就抢一下,这是非公平锁的第一次抢锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// FairSync 公平锁直接调用acquire(1)
final void lock() {
acquire(1);
}
sync.acquire(1)方法:
public final void acquire(int arg) {
if ( !tryAcquire(arg)
&&
acquireQueued( addWaiter(Node.EXCLUSIVE), arg) )
selfInterrupt();
}
将if语句拆开了,会有以下三个步骤:
- !tryAcquire(arg)
- addWaiter(Node.EXCLUSIVE), arg)
- acquireQueued( addWaiter(Node.EXCLUSIVE), arg)
首先,!tryAcquire(arg) 尝试获取锁,公平锁和非公平锁的差别就在这里:
非公平锁的获取锁方式
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 直接设置状态,并将当前的锁持有者改成自己,第二次自旋获取,非公平锁有两次抢锁的机会
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁的获取锁方式
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 先看看有没有排队的节点,再尝试获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
公平锁 会看看有没有队列,有队列就排队,而非公平锁根本不管有无队列都直接抢锁。
入队:如果没有获得锁,就排队,addWaiter(Node.EXCLUSIVE) 添加一个节点到队列
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 插入了一个空节点,就是一个哨兵,因为每一个真实的线程节点都会坚挺前一个节点的状态
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
阻塞:入队完成之后再判断一次当前是否有可能获得锁,也就是前一个节点是head的话,前一个线程有可能已经释放了,再获取一次,如果获取成功,设置当前节点为头节点,整个获取过程完成。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 不死心,进了队伍了,发现我是第二个,还要尝试获取一下
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 真正的挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
获取失败的话先将之前的节点等待状态设置为SIGNAL,如果之前的节点取消了就向前一直找。
// 就是要将我的前一个节点的等待状态改为SIGNAL
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
* 前驱节点已经设置了SIGNAL,闹钟已经设好,现在我可以安心睡觉(阻塞)了。
* 如果前驱变成了head,并且head的代表线程exclusiveOwnerThread释放了锁,
* 就会来根据这个SIGNAL来唤醒自己
*/
return true;
if (ws > 0) {
/*
* 发现传入的前驱的状态大于0,即CANCELLED。说明前驱节点已经因为超时或响应了中断,
* 而取消了自己。所以需要跨越掉这些CANCELLED节点,直到找到一个<=0的节点
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 进入这个分支,ws只能是0或PROPAGATE。
* CAS设置ws为SIGNAL
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
上一个条件完成之后,我就可以安心的阻塞了,然后一直等待直到被唤醒
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
上面就是获取锁并等待的过程,总结起来就是:
当lock()
执行的时候:
- 先快速获取锁,当前没有线程执行的时候直接获取锁
- 尝试获取锁,当没有线程执行或是当前线程占用锁,可以直接获取锁
- 将当前线程包装为node放入同步队列,设置为尾节点
- 前一个节点如果为头节点,再次尝试获取一次锁
- 将前一个有效节点设置为SIGNAL
- 然后阻塞直到被唤醒
释放锁: 当ReentrantLock进行释放锁操作时,调用的是AQS的release(1)
操作
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
在tryRelease(arg)
中会将锁释放一次,如果当前state是1,且当前线程是正在占用的线程,释放锁成功,返回true,否则因为是可重入锁,释放一次可能还在占用,应一直释放直到state为0为止
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 如果没有下一个节点,或者下个节点的状态被取消了,就从尾节点开始找,找到最前面一个可用的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 唤醒下一个节点
if (s != null)
LockSupport.unpark(s.thread);
}
在tryRelease(arg)
中会将锁释放一次,如果当前state是1,且当前线程是正在占用的线程,释放锁成功,返回true,否则因为是可重入锁,释放一次可能还在占用,应一直释放直到state为0为止
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 如果没有下一个节点,或者下个节点的状态被取消了,就从尾节点开始找,找到最前面一个可用的节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 唤醒下一个节点
if (s != null)
LockSupport.unpark(s.thread);
}
然后优先找下一个节点,如果取消了就从尾节点开始找,找到最前面一个可用的节点
7、JUC并发编程包
基本类型
使用原子的方式更新基本类型
- AtomicInteger:整形原子类
- AtomicLong:长整型原子类
- AtomicBoolean:布尔型原子类
数组类型
使用原子的方式更新数组里的某个元素
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray:引用类型数组原子类
引用类型
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新引用类型里的字段原子类
- AtomicMarkableReference :原子更新带有标记位的引用类型
对象的属性修改类型**
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,以及解决使用 CAS 进行原子更新时可能出现的 ABA 问题
AtomicInteger
的使用
public final int get(); // 获取当前的值
public final int getAndSet(int newValue); // 获取当前的值,并设置新的值
public final int getAndIncrement(); // 获取当前的值,并自增
public final int getAndDecrement(); // 获取当前的值,并自减
public final int getAndAdd(int delta); // 获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update); // 如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue); // 最终设置为 newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
AtomicInteger
类原理
以 AtomicInteger
类为例,以下是部分源代码:
该类维护一个volatile修饰的int,保证了可见性和有序性:
private volatile int value;
所有的方法都是使用cas
保证了原子性,所以这几个类都是线程安全的:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
发现原子类中的任何操作都没有上锁,是无锁操作。
8、线程同步
这些类为JUC包,它们都起到线程同步作用
1、CountDownLatch (倒计时器)
这个类常常用于等待,等多个线程执行完毕,再让某个线程执行。
CountDownLatch的典型用法就是:某一线程在开始运行前等待n个线程执行完毕。
使用方法如下:
-
将 CountDownLatch 的计数器初始化为n :new CountDownLatch(n),
-
每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,
在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newCachedThreadPool();
CountDownLatch countDownLatch = new CountDownLatch(3);
Runnable task1 = () -> {
ThreadUtils.sleep(new Random().nextInt(5000));
System.out.println("计算山西分公司的账目");
countDownLatch.countDown();
};
Runnable task2 = () -> {
ThreadUtils.sleep(new Random().nextInt(5000));
System.out.println("计算北京分公司的账目");
countDownLatch.countDown();
};
Runnable task3 = () -> {
ThreadUtils.sleep(new Random().nextInt(5000));
System.out.println("计算上海分公司的账目");
countDownLatch.countDown();
};
pool.submit(task1);
pool.submit(task2);
pool.submit(task3);
countDownLatch.await();
System.out.println("计算总账!");
}
}
CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
2、CyclicBarrier(循环栅栏)
CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
public class CyclicBarrierTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newCachedThreadPool();
// 计算总账的主线程
Runnable main = () -> System.out.println("计算总账!");
CyclicBarrier cyclicBarrier = new CyclicBarrier(3,main);
Runnable task1 = () -> {
ThreadUtils.sleep(new Random().nextInt(5000));
System.out.println("计算山西分公司的账目");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
};
Runnable task2 = () -> {
ThreadUtils.sleep(new Random().nextInt(5000));
System.out.println("计算北京分公司的账目");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
};
Runnable task3 = () -> {
ThreadUtils.sleep(new Random().nextInt(5000));
System.out.println("计算上海分公司的账目");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
};
pool.submit(task1);
pool.submit(task2);
pool.submit(task3);
// 重复利用
ThreadUtils.sleep(5000);
cyclicBarrier.reset();
System.out.println("-------------reset-----------");
pool.submit(task1);
pool.submit(task2);
pool.submit(task3);
}
}
CyclicBarrier与CountDownLatch的区别
至此我们难免会将CyclicBarrier与CountDownLatch进行一番比较。这两个类都可以实现一组线程在到达某个条件之前进行等待,它们内部都有一个计数器,当计数器的值不断的减为0的时候所有阻塞的线程将会被唤醒。
有区别的是CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制,在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值。
另外,CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截。一般来说用CyclicBarrier可以实现CountDownLatch的功能,而反之则不能。总之,这两个类的异同点大致如此,至于何时使用CyclicBarrier,何时使用CountDownLatch,还需要自行拿捏
3、Semaphore(信号量)
java.util.concurren
t包中有Semaphore
的实现,可以设置参数,控制同时访问的个数。
下面的Demo中申明了一个只有5个许可的Semaphore,而有20个线程要访问这个资源,通过acquire()和release()获取和释放访问许可。
public class SemaphoreTest {
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(5);
ExecutorService exec = Executors.newCachedThreadPool();
for (int index = 0; index < 100; index++) {
Runnable run = () -> {
try {
// 获取许可
semaphore.acquire();
System.out.println("开进一辆车...");
Thread.sleep((long) (Math.random() * 5000));
// 访问完后,释放
semaphore.release();
System.out.println("离开一辆车...");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
exec.execute(run);
}
exec.shutdown();
}
}
最后的结果是开始五辆车全部进入,因为停车场是空的,后边就是出一辆进一辆了。