并发
1 JVM相关
1.1 volatile
volatile的作用:保证内存可见性,禁止指令重排。
-
保证内存可见性:在JMM层面,主内存的变量由各线程共享,但是各线程并不会直接操作主内存,而是先将主内存中的数据调入工作内存,在工作内存中操作结束后再写回主内存。在CPU层面,CPU不会直接操作内存,而是先将内存中的数据调入缓存,之后有寄存器操作后写入缓存,最后才会写回内存。因此在多线程工作时就有可能发生如下情况:A线程对变量a做了修改但还没有来得及写回主线程就发生了上下文切换到了B线程,此时B线程就会读取到错误的数据。而volatile关键字可以保证单读和单写的原子性。具体解释就是,当线程A对变量a做了修改之后JVM保证被修改的变量会被立刻写回主内存而不能被打断。而当线程B对变量a进行读取时,即便工作内存中有a变量也会在主内存中重新读取a变量。
-
禁止指令重排:指令重排是JVM为了优化指令、提高程序运行效率,在不影响单线程程序执行结果的前提下对指令顺序进行的修改。但在多线程中可能产生一些问题。在下面的代码中,将writer()方法和reader()方法分别放在A线程和B线程中运行。A线程将a修改为1并将flag置为true。B线程会先检查flag是否为true,是的话将a自乘2并且退出循环。在我们的期望中,两个线程运行结束之后a的值应该为2。但是,对于JMM而言,语句1和语句2没有数据依赖性,因此将语句1和语句2就有可能被颠倒顺序,此时代码将无法按照我们的预期运行。
class A { int a = 0; boolean flag = false; public void writer() { a = 1; // 1 flag = true; // 2 } public void reader() { while(true) { if (flag) { a *= 2; break; } } } }
1.2 synchronized
1.2.1 修饰目标
修饰代码块:使用程序员指定的对象作为锁。
修饰成员方法:使用当前对象(this)作为锁。
修饰类方法:使用类对象(XXX.class)作为锁。
1.2.2 sleep(),wait()和yield()
sleep():调用方法为Thread.sleep()。调用此方法后,当前线程不会失去锁。
wait():调用方法为object.wait()(object是指锁对象)。调用此方法后,当前线程会失去锁。
yield():调用方法为Thread.sleep()。调用此方法后,当前线程会让出CPU。不常用。
1.2.3 锁
JDK6对锁进行了优化,引入了“偏向锁”和“轻量级锁”。锁的信息存储在对象头的Mark Word中。
偏向锁:偏向锁的获取过程:访问Mark Word中的锁标志位(2bit)是否设置成1。如果不是1,就需要更高级的锁。如果是1,则测试线程ID是否指向当前线程,如果是,获取锁成功。如果线程ID并未指向当前线程,则通过CAS操作竞争锁(偏向锁不会主动释放锁,因此当线程ID指向其他线程时并不意味着有其他线程正在使用该锁)。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,获取锁成功;如果竞争失败,当到达全局安全点时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁。
轻量级锁:自旋获取锁,响应快但会自旋会无意义的消耗CPU。适合同步代码块执行快的场合。
重量级锁:阻塞,等待唤醒。相应慢。适合同步代码块执行时间长的场合。
1.3 双重检验单例模式
public class Singleton {
private static volatile Singleton instance = null; // volatile禁止指令重排
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检验,当对象已经被初始化就没有必要获取锁
synchronized (Singleton.class) {
if (instance == null) { // 第二次检验,避免在初始化时被阻塞的线程在获取锁之后再次进行初始化
instance = new Singleton();
}
}
}
return instance;
}
}
2. LOCK
2.1 读写锁
- 读锁:可以由多个线程同时获得。当读锁已被获得则可以再获得读锁,但如果已获得写锁则不可以再获得读锁。
- 写锁:只能由一个线程获得。不论读锁还是写锁已被获得都将无法再获得写锁。
2.2 悲观锁和乐观锁
- 悲观锁总是假设最坏的情况,因此每次获取数据时都要保证其他线程无法操作数据即加锁。
- 乐观锁总是假设最好的情况,读取数据时不会加锁,只有在写数据时才会加锁,可以使用CAS实现。
2.3 可重入锁和不可重入锁
- 可重入锁:该锁可以被同一线程多次获取。可以简化编程。
- 不可重入锁:当持有该锁的线程再次获得该锁时,会死锁。
3. 源码分析
3.1 AbstractQueuedSynchronizer
可重写的方法
protected boolean tryAcquire(int arg); // 这是独占式获取同步状态的方法,将会被acquire(int arg)方法调用
protected boolean tryRelease(int arg); // 这是独占式释放同步状态的方法,将会被release(int arg)方法调用
protected int tryAcquireShared(int arg); // 这是共享式获取同步状态的方法,将会被acquireShared(int arg)方法调用
protected boolean tryReleaseShared(int arg); // 这是共享式释放同步状态的方法,将会被releaseShared(int arg)方法调用
protected boolean isHeldExclusively(); // 这个方法用来判断当前同步器是否在独占模式下被线程占用
同步器提供的模板方法
public final void acquire(int arg); // 独占式获取同步状态
public final void acquireInterruptibly(int arg); // 与acquire(int arg)相同,但在同步队列中可响应中断
public final boolean tryAcquireNanos(int arg, long nanosTimeout); // 在acquireInterruptibly(int arg)添加了超时限制
public final void acquireShared(int arg); // 共享式获取同步状态
public final void acquireSharedInterruptibly(int arg); // acquireShared(int arg)相同,可响应中断
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout); // 在acquireSharedInterruptibly(int arg)添加了超时限制
public final boolean release(int arg); // 独占式释放同步状态
public final boolean releaseShared(int arg); // 共享式获取释放状态
public final Collection getQueuedThreads(); // 获取同步队列上的线程集合
获取同步状态:调用acquire(int arg)获取同步状态,在acquire(int arg)中会调用tryAcquire(int arg)方法尝试获取同步状态,若获取成功结束方法,执行同步方法。若获取失败首先将当前线程添加到同步队列中。之后在acquireQueued(final Node node, int arg)中尝试获取同步状态。
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 若返回true直接退出方法获取到同步状态
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 先将当前线程添加到同步队列,之后再自旋尝试获取同步状态
selfInterrupt();
}
将线程添加至同步队列:首先将当前线程封装为Node节点,将该节点的pred节点指向尾节点,之后将尾节点的next节点指向该节点(此时可能有多个线程都在尝试将尾节点的next节点指向自己所在的节点,因此需要cas,而之前将该节点的pred节点指向尾节点的操作只会影响到自己,即便失败,只需要将该节点指向新的尾节点即可因此不需要cas)。若尾节点为空(同步队列为空)或插入失败。将会进入 enq(final Node node)方法自旋地插入。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 将当前线程封装为Node节点
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) { // 如果同步队列为空,交给enq(final Node node)方法处理
node.prev = pred; // 若插入失败,即其他线程插入并修改尾节点,将prev节点更新即可
if (compareAndSetTail(pred, node)) { // 若不使用cas,可能导致节点丢失
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;
}
}
}
}
队列中的线程获取同步状态:首先判断当前线程所在的节点的prev节点是否是首节点。若是,尝试获取同步状态并将此节点设置为首节点,否则,检查前驱节点是否已经singal。若是停放(park)。
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) && // 若前节点等待状态为singal(当前线程已就绪),
// 若前节点已取消,跳过所有已取消的节点
parkAndCheckInterrupt()) // 进入停放状态(阻塞)使用LockSupport.park()
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
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.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev; // 跳过被取消的节点
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
释放同步状态:调用release(int arg)方法会在内部调用tryRelease(int arg)方法,当释放同步状态后若首节点为就绪状态唤醒首节点(其实唤醒的并不是首节点中的线程,具体见“唤醒线程”)。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
唤醒线程:若首节点的下一个节点未被取消,则唤醒首节点的下一个节点,否则唤醒离首节点最近的未被取消的节点。该节点被唤醒之后会在acquireQueued(final Node node, int arg)方法中跳过该节点之前被取消的节点。
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);
}
3.2 ReentrantLock
ReentrantLock底层使用AQS:在公平锁实现中,当状态为0时查看同步队列是否为空,若满足通过cas设置状态值,若成功,获取同步状态并将当前锁的拥有线程指向当前线程。若状态不为0查看当前线程是否是该锁的拥有线程,若是设置状态值,否则获取失败。
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;
}
释放锁:修改状态值若状态值为0,则将锁的拥有线程置为空。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
3.3 ReentrantReadWriteLock
以后再写。。。
4. 阻塞队列
-
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
-
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
-
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
-
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
-
SynchronousQueue:一个不存储元素的阻塞队列。
-
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
-
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
boolean add(E e); // 添加,当队列满时抛出异常 boolean remove(Object o); // 移除,当队列为空时抛出异常 boolean offer(E e); // 添加,当队列满时返回false E poll() throws InterruptedException; // 移除,当队列为空时返回false void put(E e) throws InterruptedException; // 添加,当队列满时一直阻塞直到添加成功 E take() throws InterruptedException; // 移除,当队列为一直阻塞直到移除成功 boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; // 添加,当队列满时一直阻塞直到添加成功或超时 E poll(long timeout, TimeUnit unit) throws InterruptedException; // 移除,当队列为空时一直阻塞直到移除成功或超时
5. 线程池
private volatile ThreadFactory threadFactory; // 用于创建线程的线程工厂,线程池中的工作线程都由此工厂创建
private volatile RejectedExecutionHandler handler; // 拒绝策略
private final BlockingQueue<Runnable> workQueue; // 工作队列
private volatile long keepAliveTime; // 空闲存活时间 单位为枚举类型TimeUnit
private volatile int corePoolSize; // 基本(核心)线程数
private volatile int maximumPoolSize; // 最大线程数
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // integer高三位存储线程池运行状态,其余位用于记录工作线程数
private final HashSet<Worker> workers = new HashSet<Worker>(); // 用于存放工作线程
// 线程池的运行状态
private static final int RUNNING = -1 << COUNT_BITS; // 运行
private static final int SHUTDOWN = 0 << COUNT_BITS; // 关闭
private static final int STOP = 1 << COUNT_BITS; // 停止
private static final int TIDYING = 2 << COUNT_BITS; // 整理
private static final int TERMINATED = 3 << COUNT_BITS; // 结束
线程池的工作过程:
执行(提交)任务:当提交任务时,首先检查当前线程数若小于基本线程数则直接添加一个核心任务。若核心线程池已满,则尝试将任务放入工作队列中。若工作队列已满而工作线程数小于最大线程数则创建非核心任务()。若工作线程数已等于最大线程数则采取拒绝策略(handler)。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
任务结束:当线程中正在执行的任务结束之后,当前线程会调用getTask()从工作队列中获取任务。以此来保证线程复用。并且在完成所有任务(getTask()方法无法取出任务)后结束当前线程。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock();
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
线程空闲死亡:当允许核心线程死亡,或者工作线程数已经大于核心线程数时,从工作队列中取任务就会从take()方法(一直阻塞直到取出为止)变为poll(long timeout, TimeUnit unit)(阻塞直到取出或超时),未取出任务会进入下一轮循环,并在下一轮循环中返回unll。
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
6. 并发工具类
6.1 等待多线程完成的 CountDownLatch
CountDownLatch中有一个计数器(在构造时传入),调用countDown()方法会使计数器减一,调用await()方法的线程会被阻塞,直至CountDownLatch中有一个计数器为0。
6.2 同步屏障CyclicBarrier
CyclicBarrier中有也有一个计数器(在构造时传入),调用await()方法的线程会被阻塞并且计数器会减一,当计数器为0时,唤醒所有被此CyclicBarrier阻塞的线程。
6.3 控制并发线程数的Semaphore(信号量)
内部使用AQS,当参数会传给state。在获取锁时,若state大于1,可以将state减一并获取锁。可以控制同时访问指定资源的线程数量。