锁
synchronized
synchronized 是根据 Monitor Object 模式来实现的,在这个模式下有4个角色:
- 监视对象(Monitor Object)
- 同步方法(临界区)
- 监视锁
- 监视条件 (什么时候决定 wait 和 notify)
当线程需要进入临界区,首先线程需要获取临界区的锁。
再具体点,对象设置markword里面锁的地址(线程地址)
每个对象只能被一个线程获取,一个线程可以获取多个锁(类似嵌套synchronized)
监视条件则提供了线程可以恰当的时候进行阻塞/唤醒,阻塞的线程会被放入阻塞队列中,等待唤醒(对应的就是 监视对象的 wait() 和 notify() 方法)
所以这正好验证了别人说的:java从最初设计出来就适合多线程编程。
Lock接口
java 中提供了两种方式来给代码加锁,一种是隐式加锁synchronized,当然他的解锁也是隐式的。
另一种就是显示加锁 Lock ,他的解锁需要放到finally 里面去解锁,以保证可以在异常的时候正常解除锁。
Lock lock = new ReentrantLock();
lock.lock();
try {
}finally {
lock.unlock();
}
lock 比 synchronized 的好处
- 获取锁可以被中断
- 公平或者非公平锁(ReentrantLock)
- 多条件
- 超时获取锁
队列同步器
队列同步器(AbstractQueuedSynchronizer)(AQS)
他的内部维护了一给双向列表,并且符合FIFO的规则。
在尝试获取锁的时候,失败了就会以节点的方式被添加进同步器里面的列表内。
源码介绍:
// 获取锁
public final void acquire(int arg) {
// 进行一次尝试获取
if (!tryAcquire(arg) &&
// 如果获取失败的话,就添加到队列中 addWaiter()
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
以上是获取锁的代码,方法套方法,接下来一个个解释。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
尝试获取锁,此方法需要被重写,尝试获取锁,可以是 循环cas的方法,可以参考ReentrantLock 里面的方法。
// 做为新节点添加进 队列中
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 进行一次快速的尝试,如果失败了则进入 enq()方法 进行 循环+CAS的方式添加
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果快速尝试失败了 就进入 循环CAS
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;
// jvm的时候说过cas是硬件支持的只有一条指令方法,native方法由unsafe类提供
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
如果获取锁失败的话,创建节点,并进行自旋
// 节点被添加进入后就进入自旋
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;
}
// 如果不是头节点,或者 获取锁失败类
// 现判断前节点的status,如果不是-1,则设置成-1 (SIGNAL),如果大于0则将节点取消
// parkAndCheckInterrupt,使用LockSupport.part()将线程挂起。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
节点创建成功后,会进行第一次自旋,失败后会挂起。
这是独占锁获取的流程。
图为共享锁的访问。当有一个获取共享锁的时候,其他需要共享锁的都可以进入,独占的不行。
如果独占锁占着资源,共享和其他独占的都需要等待。代码和独占的大同小异,就不贴了。
重入锁
- 相同的线程可以重复得到锁。
- 获得几次,退出几次,锁才会被最终释放
获取锁过程(code)
下面给ReentrantLock的代码注释下。
public boolean tryLock() {
// 获取锁的请求 +1
return sync.nonfairTryAcquire(1);
}
// 非公平获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取现在的状态,重入的个数
int c = getState();
// 第一给抢到的锁
if (c == 0) {
// cas设置 state值 = 1
if (compareAndSetState(0, acquires)) {
// 成功,设置锁的主人
setExclusiveOwnerThread(current);
return true;
}
}
// 如果重入个数>=1,且是锁的主人
else if (current == getExclusiveOwnerThread()) {
// 设置 重入个数++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置state,返回
setState(nextc);
return true;
}
return false;
}
以上是请求锁的过程,包括了重入的情况。
protected final boolean tryRelease(int releases) {
// 得到状态--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果重入的个数是0了,那就是没有人占着锁了
if (c == 0) {
free = true;
// 锁主人为 null
setExclusiveOwnerThread(null);
}
// 设置state
setState(c);
return free;
}
上面是释放锁的时候,会对重入数做减法。
公平和非公平的区别
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;
}
他会去判断队列同步器是否存在节点,如果有存在同步队列则会从同步队列的头开始取。
开始可能会疑惑,非公平的好像也是从同步队列头开始取,然后头结束了再去取下一个节点。
在竞争情况下,所有线程可能同时在竞争锁,失败的会进入队列。然后在下一刻,又有许多线程开始竞争,这是可能是外部的线程和队列中的线程同时在抢,如果外部的抢锁成功,那么同步队列的头会等待下次的竞争。所以不公平就不公平在同步队列会跟外部的其他线程竞争,而公平就是有竞争就进队列,一定让头开始先获得锁
。
读写锁(ReentrantReadWriteLock)
特点:读锁可以被共享,但是写锁只能是一个线程在写。
ReentrantReadWriteLock 对 读和写读状态 是另外设计的。
int 32位
高16位给 读锁使用,低16位给 写锁使用。
取读锁的state的时候,state>>>16 无符号右移16位
取写锁的state的时候,state & 0x0000FFFF 与操作
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
// 得到状态
int c = getState();
// exclusiveCount 是否存在写锁,1则是
// getExclusiveOwnerThread 独占的线程owner
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
// 得到共享的count
int r = sharedCount(c);
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// cas 添加读锁数
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// cachedHoldCounter 每个线程可能会有再次重入读的情况
// 记录的是 每个线程获取锁的次数
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return true;
}
}
}
读请求锁过程,没有写锁,那就是会获取锁成功。
LockSupport工具
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
System.out.println(System.currentTimeMillis()+ " park start!!!");
// 挂起线程
LockSupport.park();
System.out.println(System.currentTimeMillis() + " end!!!");
} finally {
}
});
Thread t2 = new Thread(() -> {
try {
System.out.println(System.currentTimeMillis()+ " start");
Thread.sleep(3000);
// 唤醒指定线程
LockSupport.unpark(t);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
});
t.start();
t2.start();
}
输出结果:
1547304943153 park start!!!
1547304943153 start
1547304946158 end!!!
Condition接口
之前说着比较 lock 和 synchronized 的有点就是有个是:可以多条件
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
private void conditionAwait() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
private void conditionSignal() {
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
这只是单条件的实现方式。
多条件可以参考生产者和消费者来编写。
condition的实现
单方向链表的等待队列。
如果是object的监视者模型的话,一个对象有一个同步队列和一个等待队列。
但是lock的话是一个同步队列和多个等待队列。
如果有新的线程需要等待,如果被通知的话就会从等待队列进入同步队列。