现已全部整理完,其他两篇
并发整理(一)—Java并发底层原理
并发整理(三)— 并发集合类与线程池
本篇主要讲锁实现涉及到点线程
线程相关
优先级
通过setPriority(int newPriority)
来设定,范围为1-10,默认是5。
更高优先级的线程优先运行,优先的意思是只是在分配cpu时间段的时候,得到的概率高一些。
当在某个线程创建一个新的线程,这个线程有与创建线程相同的优先级。
线程优先级不能作为程序正确性的依赖,因为部分操作系统不一定会理会Java线程对于优先级的设定
Daemon线程
一种支持线程,主要用作程序中后台调度以及支持性工作。
通过setDaemon(boolean on)
必须在start之前设置,不能在启动之后设置。
JVM中只剩下Daemon线程,JVM会退出,不会支持其运行。
所以,在构造Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
常用方法
各种状态
Jvm中的线程有以下状态(不是操作系统)
NEW :初始状态,还未调用start的线程
RUNNABLE :运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地成为"运行中"
BLOCKED : 阻塞状态,被锁阻塞
-
WAITING :等待状态, 线程在这个状态下等待其他线程执行特定操作,通常一下操作后:
Object.wait
,
Thread.join
,
LockSupport.park
-
TIMED_WAITING :超时等待状态,不同于等待,这个状态会在一定时间自行返回,通常一下操作后:
Thread.sleep(long)
,
Object.wait(long)
,
Thread.join(long)
,
LockSupport.parkNanos
,
LockSupport.parkUntil
TERMINATED :终止状态,线程已结束
锁相关
关于CAS
CAS是一种无锁并发技术,是并发中很重要的技术,用来原子的更新数据
简单说就是CAS(Compare and Swap)比较并替换,先获取旧值,然后修改,再替换,在替换的过程中如果发现旧值和原来不一样,则说明其他线程也在修改,自己已经脏读,所以本次失败
隐式锁
Java中的指的就是synchronized。是一种可重入/非公平/悲观/独占锁。
它是由JVM实现的,基于面向对象中Monitor Object设计模式来实现的
- 使用synchronized获得对象锁,来保证互斥
- notify/notifyAll/wait 方法来协同不同线程之间的工作
- 将条件值储存在对象头中
关于这个模式可以看
对象头
Java对象头中的MarkWord储存锁标记位
锁升级
很多人把synchronized叫重量锁,但是jdk6之后进行优化了其性能,通过不同状态来采取策略。可以看到总共有四种状态由低到高:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。并且采取只能升级不降级的策略。
锁升级如下:
-
当一个线程访问同步块,先看记录,如果没有储存线程ID则用CAS替换MarkWord并且置偏向锁。
之后每次同一个线程进入只要检查MarkWord状态就行,没有其他额外开销。
如果有其他线程进入,检测MarkWord是否有自己线程ID,发现没有采用CAS替换并且失败了,检查是否偏向线程还活着,不活着就置无锁,活动则遍历锁记录,然后决定是否升级。
-
到轻量锁后在锁记录添加,并且CAS替换指向锁指针,如果成功则没有竞争,并获得锁。
失败则说明存在竞争,所以升级。
-
重量级锁就会用到Monitor
显式锁
虽然synchronized可以获取锁,但是其将锁固化了,有时不够灵活,所以JDK5后新增了Lock接口并且相关实现类,来方便我们实现更高的需求。
Lock接口
所有的锁实现都要实现这个接口符合规范
public interface Lock {
//只有当锁获得后才会从该方法返回,否则阻塞
void lock();
//提供synchronized做不到的可中断获取锁
void lockInterruptibly() throws InterruptedException;
//尝试获取锁,不会阻塞
boolean tryLock();
//提供synchronized做不到的超时获取锁,可被中断
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
//获取锁的协同条件变量,如obj.wait一样使用
Condition newCondition();
}
AQS
AQS(AbstractQueuedSynchronizer)可以称作同步器,每个锁都会有一个其实现并作为内部类,用来管理线程来获取锁。
结构
public abstract class AbstractQueuedSynchronizer{
private volatile int state;//控制同步的状态量
//设计者希望其能够简化锁的实现,所以封装了大量的操作。主要提供了共享与非共享的同步状态管理。
//采用模板方法的设计模式,其模板方法主要如下:
//独占式获取同步状态的几个方法
public final void acquire(int arg) {...}
public final void acquireInterruptibly(int arg){...}//可中断
public final boolean tryAcquireNanos(int arg, long nanosTimeout){...}//超时获取
public final boolean release(int arg) {...}
//共享式获取同步状态的几个方法
public final void acquireShared(int arg) {...}
public final void acquireSharedInterruptibly(int arg){...}
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout){...}
public final boolean releaseShared(int arg) {...}
//还有一些工具方法,返回等待线程情况
public final Collection<Thread> getQueuedThreads() {...}
//模板中调用的核心方法希望我们在子类中实现
//重写的时候修改Status时要用AQS给我们提供的compareAndSetState方法来CAS设置
protected boolean tryAcquire(int arg) {}
protected boolean tryRelease(int arg) {}
protected int tryAcquireShared(int arg) {}//共享式获取返回值>0代表成功
protected boolean tryReleaseShared(int arg) {}
protected boolean isHeldExclusively() {}//表示是否被当前线程独占
//以下部分是AQS内部维护的CLH同步队列
private transient volatile Node head;
private transient volatile Node tail;
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
...
}
//锁内的条件变量也封装在AQS中
public class ConditionObject implements Condition{...}
}
同步队列管理
可以看到AQS就是封装了这部分,这也是锁最重要的部分,因为其封装了独占式和共享式的同步方式,两者略有不同,现在看一下其实现。
锁获取
acquire
、acquireShared
就是这两个方法,源码很少可以自己看
- 不论独占式还是共享式获取锁的时候,首先调用
tryAcquire
或tryAcquireShared
来尝试CAS改变Status - 如果失败则通过
addWaiter
来创建一个Node,并加入队列(也是CAS),如果加入失败则一直循环添加知道成功 - 唯一区别在于,共享式创建的Node的nextWaiter是一个SHARED常量来标志,而独占式标志是一个NULL节点
队列中等待
- 进入队列后就开始进入一个死循环自旋,不过不是无脑循环,通过
LockSupport.park()
来暂停线程,直到被唤醒再转,每次转每次判断 - 被唤醒并且判断到一个节点的上一个节点是一个头结点了,然后通过
tryAcquire
或tryAcquireShared
再来CAS一次 - 独占式如果改变Status成功则就直接设置为头结点就返回了,但是共享式会进行判断,如果是SHARED并且Status>=0则会设置头结点并且直接将下一个节点也unpark唤醒以便传递下去
锁释放
- 通过调用
release
或releaseShared
来CAS改变Status,并且LockSupport.unpark()
来唤醒下一个节点,然后返回 - 这里共享式不同于独占式区别在于共享式由于一直传递可能存在CAS失败,所以采用一直循环来保证Status更新成功安全释放
支持超时获取同步状态
- AQS让线程不仅可以支持中断获取,还支持超时且可中断获取,并且做了优化
- 优化就是不是无脑自旋判断时间,而是通过spinForTimeoutThreshold(1000ns)来作为分界线,大于分界值就采用定时
park
到分界值,小于就进入快速自旋做到精确时间
AbstractQueuedSynchronizer的介绍和原理分析
可重入锁
可重入锁ReentrantLock就是典型的Lock接口实现,继承了AQS并且实现tryAcquire
还添加了nofairTryAcquire
来支持公平与非公平锁的选择。代码非常少,可以自己看。
主要注意的:
- 之所以叫可重入锁,就是因为
tryAcquire
与nofairTryAcquire
都对是否是当前线程进行了判断,如果是则不会阻塞,也不会加入节点,只是CAS改变Status -
tryAcquire
与nofairTryAcquire
两个方法唯一的区别就是在申请的时候公平锁增加了一个判断当前节点是否有前驱节点来保证FIFO队列实现公平,而nofair则没有这个判断,所以支持闯入机制,可能会被一个刚来的线程抢走,所以是不公平的
Condition
可以看到ConditionObjective是AQS的内部类
与Obj监视器方法类似,通过Lock.newCondition()
方法能够创建与Lock绑定的Condition实例。
await()
对应于Object.wait()
signal()
对应于Object.notify()
signalAll()
对应于Object.notifyAll()
。
当然这几个方法与synchronized
一样,需要先Lock.lock()
获取锁。
注意的是:
- 在Obj的监视器模型上,一个对象拥有一个同步队列和一个等待队列。但是AQS中有一个上面说的同步队列,然后每一个ConditionObjective都对应一个等待队列
- 当调用方法的线程获取了锁,也就是成为了同步队列的头结点,
await
会将新生成一个节点加入等待队列,然后释放锁,唤醒同步队列中的下一个节点 - 节点加入等待队列后就会进行循环判断,如果没有被
signal
唤醒,也就是没有加入到同步队列则会park
自己 -
signal
在获取锁的情况下,将节点重新CAS移到同步队列,并且将节点线程unpark
,然后节点线程就会从await
返回
ReentrantReadWriteLock
同样实现Lock接口,包含AQS,只是比ReentrantLock再复杂一些,是一个可重入共享锁,能够做到比排它锁更好的并发性和吞吐量
- 其包含一个ReadLock和一个WriteLock,允许读读,不允许读写,写写并发,并且支持锁降级
- 其将AQS中的Status拆分,高16位给读锁,低16位给写锁,这样来满足两个锁记录
- 其AQS同样分别写了公平与非公平两种支持
工具类
CountDownLatch
可以实现类似计数器的功能,做到允许一个或多个线程等待其他线程完成操作
- 构造方法传入计数,计数器等于0就不会阻塞,并且不能重新初始化与修改计数
- 通过简单2个方法
await
,countDown
来控制,一个线程调用countDown
方法happen-before另外一个线程调用await
CyclicBarrier
是一种可循环使用的屏障,做到一组线程到达一个屏障是被阻塞,知道最后一个线程到达屏障,才会开门。常用于Fork/Join操作。
其与CountDownLatch最大区别在于,如果计算发生错误,可以重置计数器,让线程重新执行,所以用途更广泛
Semaphore
与操作系统中的信号量类似,通过协调各个线程,来保证合理的访问限定的资源数
其内部依然实现AQS来管理各个线程的同步状态
简单通过semp.acquire()
,semp.release()
来进行许可请求和释放