Java锁机制
1.Android的实际开发过程中,虽然高并发的场景不是很多,但是掌握Java中的各种锁机制对于理解高并发的场景,对于资源操作的处理尤为重要。Java并发中锁虽然众多,但是还是有一定的规矩可言的。
2.按照大致类型区分:
悲观锁VS乐观锁
1.无论是悲观锁还是乐观锁都是广义上的概念。是对同一个问题从不同角度看待(当然程序中是指的对资源或数据的操作)。
2.悲观锁:认为操作一份数据一定会有其他线程来同时修改数据,故操作数据之前,先加锁,确保其他线程阻塞等待直到锁被释放。synchronized(JVM实现)
与Lock(接口)
都是悲观锁的实现(显式的加锁操作)。1
2
3
4
5
6
7
8
9
10
11
12
13
14private synchronized void methodOne() {
//直接对方法加锁,悲观锁
}
private void methodThree() {
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
//先加锁
try {
System.out.println("do something");
} finally {
reentrantLock.unlock();
}
}
3.乐观锁:认为操作一份数据,不会有其他线程对数据进行修改,因此不会加锁。但是会在更新内存中数据时去判断之前是否有线程对数据进行过修改,如果没有其他线程对数据进行操作,则将自己自身修改的值更新到内存。如果判断有其他线程对数据进行了修改,根据不同的实现逻辑,要么重试操作或者直接放弃提交。乐观锁在Java中是实现原理是CAS(Compare And Swap)
也即比较交换。其中的原子操作类AtomicInteger
的自增操作是基于CAS
算法自旋实现的。
4.什么是CAS,比较交换算法,是一种无锁自旋算法。主要操作三个值,需要操作的内存数据Value
,需要对比的值CopyValue
,更新操作后的值BValue
。需要明确的是“比较+交换”是被打包成了一个原子操作,如果比较的结果是Value
与CopyValue
的值是不相等的,那么证明有其他线程对Value
进行了修改。则重试(自旋),直到Value
与CopyValue
相同那么更新内存中的Value
为BValue
。通过过程中我们大概会发现一些问题,显然,没有加锁,少了线程的阻塞与线程的切换带来了效率上提升。但是CAS是无法判断ABA
问题的(有其他线程对原始内存中数据做了修改若干次虽然中间值不为A但是最终结果为A)。那么比较时自然会认为没有其他线程对Value
修改。第二点,对于没有加锁,那么要是其他线程一直不停的修改Value
会导致比较一直不通过,会存在一直自旋的问题,这必然会拉高CPU
的负荷。得不偿失。第三点,只能保证一个共享变量的原子操作。
5.ABA问题,对与ABA问题的解决,即增加版本号的判断,每次对Value
的操作对应的修改版本号,增加了一层判断。而JDK
从1.5开始利用AtomicStampedReference
检查引用是否相同来确定后序操作。对于只能对一个共享变量实现原子操作,同样的从1.5开始增加的AtomicReference
将多个变量保存在一个对象中来保证原子操作。回忆线程池中对线程数量与状态保存就是利用了这种思想(将线程的状态保存在int数值的高3位,线程数量保存在低29位)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
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;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
6.通过分析可以发现,悲观锁更加适合,写操作更多的场景确保数据的正确性。而乐观锁则更适合读操作更多的场景,未加锁,可以大大提过读操作的性能。当然这仅仅是理论,具体的实际运用还是需要根据具体的业务场景。
自旋锁VS适应性自旋锁
1.为什么要自旋锁?对于线程的调度(阻塞或唤醒)都是依赖CPU的时间片的切换,这自然消耗的是CPU的性能。可是有时候我们的同步方法过于简单,此时CPU的对与线程状态的切换所带来的性能损失可能远大于同步代码执行的时间。这显然起到了反作用,这个时候就需要引入自旋锁。让线程不放弃CPU的执行时间,自旋,如果自旋结束恰好之前的线程释放了锁,那么就可以直接获得锁并获得同步资源。从而达到避免线程切换带来的开销-自旋锁(基于CAS原理实现)。
2.如果仔细思考就会发现,自旋锁也是有缺点的,自旋锁不能代替阻塞。试想如果前一个线程占用锁的时间太长,那么自旋会一直操作(早期的JDK默认是自旋10次)。如果自旋达到阀值仍然没有获得锁,此时还是要将线程挂起。需要明白的是,自旋过程中是一直占用CPU时间的,自旋失败后挂起,这段时间CPU的开销是被白白浪费的。或许自旋锁应该更智能一点,JDK1.6引入了-适应性自旋锁。
3.既然是自适应,那么自旋的次数是不在固定了。变的更加智能,对于对同一个锁的获取上,如果自旋锁通过自旋操作成功的获得了锁。并且持有锁的线程正在运行,那么虚拟机会认为再次自旋依然会成功获得锁(获得锁的概率大),则增大自旋的时间。这个很好理解,比如对于掷骰子我们要的结果是小于6,而小于6的概率为5/6,显然这个概率很大,随着掷的次数增多等于6的概率为1/6^n。也即我们增加摇的次数成功的概率会增大,同样的虚拟机此时增大了自旋的时间,尽可能增大再次成功的可能性。相反的,如果自旋并没有获得锁,虚拟机则会减少自旋的时间或者取消自旋直接阻塞(反正成功的机会不大,不会浪费太多资源,稍微给点时间意思一下,碰碰运气。。。虚拟机也这么现实)。
公平锁VS非公平锁
1.公平锁:顾名思义当多个线程需要申请锁来访问同步资源时,按照申请的顺序来依次获得执行的时机。按照队列的排序,当任务被提交会被压入到队列中(先进先出)。非公平锁:同样的,非公平锁与公平锁的区别在于,可以插队,当任务被提交会先尝试获取对象锁。此时恰好锁被释放,那么就直接获得并执行任务,如果没有获得则入队尾排列。因为有几率会直接获得锁,省去了CPU对线程的切换唤醒操作,因此性能要高于公平锁的,整体吞吐量也相对较优秀。但是因为“插队”的情况,那么那些老实排队的线程可能很长时间无法获得锁而导致任务得不到执行。(另外synchronized是非公平锁)
2.Synchronized与ReentrantLock,ReentrantLock内实现了公平锁和非公平锁,而默认的是非公平锁,源码中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71//非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
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;
}
//公平锁,与非公平锁的唯一区别hasQueuedPredecessors
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;
}
//逻辑很简单就是判断任务是否是队列head
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
可重入锁VS非可重入锁
1.Synchronized
与ReentrantLoc
都是可重入锁,非公平锁。可重入锁(递归锁),指的是同一个线程在外层方法获得锁的时候,进入到方法内部会自动获得锁(锁对象是相同的)。可重入锁在一定的程度上可以避免死锁得产生:1
2
3
4
5
6
7
8
9//两个同步方法,需要明确的是,当方法执行完,锁才会被释放,当进入到methodOne中,需要执行methodTwo方法,此时锁还未被释放(方法未执行完毕)
//如果是非可重入锁,那么methodTwo需要获得锁,但是锁还未释放,此时就会造成死锁的情况。
private synchronized void methodOne() {
System.out.println("enter the methodOne");
methodTwo();
}
private synchronized void methodTwo() {
System.out.println(Thread.currentThread().getName() + "enter the methodTwo");
}
2.以生活中的实际例子来说说可重入锁与非重入锁,可重入锁:排队就餐,生活中,按照一定的顺序选择菜品并由打菜员打取。一般都是一个人打一份,某天你的同事有bug需要修改,让你帮忙打一份,这很好办,一般店员是不会阻拦你打两份的。同样的你也不会轴到自己的打完再次去排队给同事打,那么这个帮忙给同事打饭的操作只是属于你的一个附属操作。只是稍微耽误一点时间,等两份等打完,自然会轮到下一位,这是可重入锁。食堂发现这种情况越来越多(帮人代打,并且是好几份浪费时间),于是禁止帮人代打。但是你并没有看到食堂出的通知,你的同事还是让你代打。这次打菜员不乐意了,因为老板规定,涉及到扣绩效,于是你跟他理论希望通融一次。可是无论你怎么说都不行,一直堵在那里,导致后面的人都在等待。没办法就一直僵持。。。。可就是非可重入锁,并且已经导致了死锁。同时也可发现,非可重入锁的死锁导致死锁情况很严重。
3.看ReentrantLoc
可重入锁的对立NonReentrantLock
(没找到这个类),先看可重入锁:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前的线程已经占有了锁,设置state(AQS中维护的状态) + 1,这个很好理解,当前线程获得了锁,并有多个子流程(同步方法)需要执行
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 tryRelease(int releases) {
int c = getState() - releases;
//判断当前线程是否占有锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//当state==0时才真正的释放锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
4.非可重入锁尝试直接获取锁,而在释放锁时,是直接将state设置为0。