解析锁——悲观|乐观锁、自旋|互斥锁、公平|非公平锁
悲观锁
总认为最坏的情况可能会出现,即认为数据很可能会被他人修改,因此在持有数据时总是先把资源或数据锁住。这样其他线程要请求这个资源时就会阻塞,直到悲观锁释放资源。
关系型数据库中应用比较广泛,如行锁、表锁、读锁、写锁等都是在操作前先上锁。
Java中的Synchronized和ReentrantLock等独占锁也属于悲观锁。
读写都加锁,导致其性能较低,对于现在互联网的高性能、高可用、高并发来说,悲观锁应用的越来越少了,但是对于多写场景还是必要使用悲观锁的。
乐观锁
总认为资源和数据不会被他人修改,所以读取时不上锁,但乐观锁在写入操作时会判断当前数据是否被修改过。 两种实现方式:版本号机制 和 CAS机制。
CAS机制体现在java.util.concurrent.atomic包下的原子变量类中,比较后替换。(修改读写的内存值V、进行比较的值A、拟写入的值B,当且仅当A==V时,将V修改为B)。
版本号机制在ZooKeeper中有体现,数据库中也有体现,每次修改都会导致版本号加1;其实也暗含了比较后设置的思想,只是比较的参照物为version(数据被修改次数)。
适用于读多写少的场景,即很少发生冲突,可以省去锁的开销,增加系统吞吐量。
乐观锁的缺点
1) ABA问题
一个变量读取值是A,CAS比较时内存值也是A,但是不能保证该值没有被修改过(A-B-A)。
该问题可以使用类似版本号机制解决,即DCAS,对于每一个V增加一个引用的表示修改次数的标记符。每修改一次标记符加1,update时同时比较变量值和计数器的值
2) 循环开销大
乐观锁在写操作时要判断是否能写入成功,如果写入不成功将触发等待――重试机制(自旋锁),适用于短期内获取不到进行等待重试的锁,不适用于长期获取不到锁的情况;自旋循环对于性能开销比较大。
注:对于竞争较少的情况,使用synchronized同步锁进行线程阻塞和还清切换以及用户态内核态间的切换操作额外浪费CPU资源;而CAS基于硬件实现,不进入内核、不切换线程,操作自选几率较小,可以获得更高的性能; 对于竞争严重的情况,CAS自旋的概率比较大,浪费更多的CPU资源,效率低于synchronized。
自旋锁和互斥锁
资源竞争时,只有获得了锁的线程能够访问,其他没有获取到锁的线程怎么办?
两种处理方式:
一直循环等待,判断该资源是否已经释放锁——自旋锁;不阻塞线程
线程阻塞,等待重新调度请求——互斥锁;
如果持有锁的线程能在短时间内释放所资源,那么等待竞争锁的线程不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取锁,避免了用户进程和内核切换的消耗。操作系统的内核经常使用自旋锁。
但是如果长时间上锁,自旋锁会非常耗费性能,它阻止了其他线程的运行和调度。线程持有锁的时间越长,则持有该锁的线程被操作系统调度程序中断的风险越大,如果发生中断,其他线程将保持旋转状态,而持有该锁的线程并不打算释放锁……
因此,需要设置一个自旋超时时间,超时立即释放CPU。自旋锁的目的是占着CPU不释放,直到获取到锁后立即处理。
自旋锁的优缺点:尽可能减少线程阻塞,锁竞争不激烈且占用锁的时间较短时,自旋的消耗会小于线程阻塞挂起再唤醒的消耗(会导致线程发生两次上下文切换);而空占CPU的做法在锁竞争激烈或锁被长时间占用时,会造成CPU更大的浪费,消耗大于线程切换的消耗。
公平锁和非公平锁
公平锁——每个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁;
非公平锁——每次锁被释放,所有线程无优先级抢占锁
TicketLock
类似票据队列管理系统,使用ticket控制线程执行顺序。基于FIFO原则。属于自旋锁。
TicketLock虽然解决了公平性问题,但是多线程频繁读写同一个变量callNum,降低了系统性能。MCSLock和CLHLock解决该问题。
public class TicketLockTest {
public static void main(String[] args) {
TicketLock lock = new TicketLock();
LockTask task = new LockTask(lock);
ArrayList<Thread> threads = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
threads.add(new Thread(task, "ThreadTest " + i));
}
threads.forEach(Thread::start);
}
}
class TicketLock {
private AtomicInteger callNum = new AtomicInteger(1);
private AtomicInteger provideNums = new AtomicInteger();//从0开始
//每个线程持有自己的号码
private ThreadLocal<Integer> currentNum = new ThreadLocal<>();
public void lock() {
int num = provideNums.incrementAndGet();
currentNum.set(num);
System.out.println(Thread.currentThread().getName() + " num is:" + num);
while (num != callNum.get()) {//如果没有叫到自己的号码,则一直等待(自旋)
}
}
public void unlock() {
Integer calledNum = currentNum.get();
callNum.compareAndSet(calledNum, calledNum + 1);
}
}
class LockTask implements Runnable{
private TicketLock lock;
public LockTask(TicketLock lock) {
this.lock = lock;
}
@Override
public void run() {
lock.lock();
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " done.");
lock.unlock();
}
}
}
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
结果如下,需要注意线程获取票号的顺序与线程的生成顺序并无关系,但线程获取锁,并且执行的顺序与其获取到的票号是一致的。
ThreadTest 6 num is:3
ThreadTest 2 num is:2
ThreadTest 1 num is:1
ThreadTest 4 num is:4
ThreadTest 3 num is:6
ThreadTest 5 num is:5
ThreadTest 9 num is:7
ThreadTest 7 num is:8
ThreadTest 10 num is:9
ThreadTest 1 done.
ThreadTest 8 num is:10
ThreadTest 2 done.
ThreadTest 6 done.
ThreadTest 4 done.
ThreadTest 5 done.
ThreadTest 3 done.
ThreadTest 9 done.
ThreadTest 7 done.
ThreadTest 10 done.
ThreadTest 8 done.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MCSLock和CLHLock
TicketLock可以理解为基于队列的,MCS基于显式链表(单向)的设计,CLH基于隐式链表的设计。
每个线程拥有独立的Node,并且通过Node或preNode的状态判断自旋能否结束,占有锁。
public class ClhLock {
public static class CLHNode{
private volatile boolean isLocked = true;
}
private volatile CLHNode tail; //必须是volatile的,用于下文的"tail"
private static final ThreadLocal<CLHNode> LOCALNODE = new ThreadLocal<>();
private static final AtomicReferenceFieldUpdater<ClhLock, CLHNode> UPDATER =
AtomicReferenceFieldUpdater.newUpdater(ClhLock.class, CLHNode.class, "tail");
public void lock() {
CLHNode node = new CLHNode();
LOCALNODE.set(node);
CLHNode preNode = UPDATER.getAndSet(this, node);//将node设置在队尾,并且取出前一个
if (preNode != null) {
while (preNode.isLocked) { //前一个还没有获取到资源并释放
}
}
}
public void unlock() {
CLHNode node = LOCALNODE.get();
//tail节点等于当前节点node,表明没有后驱了,则将其设置为null
if (!UPDATER.compareAndSet(this, node, null)) {
node.isLocked = false;
}
}
}
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
类似的测试用例执行结果如下:
ThreadTest 1 done.
ThreadTest 4 done.
ThreadTest 2 done.
ThreadTest 3 done.
ThreadTest 5 done.
ThreadTest 6 done.
ThreadTest 7 done.
ThreadTest 8 done.
ThreadTest 9 done.
ThreadTest 10 done.
————————————————
版权声明:本文为CSDN博主「ShaoZG_金刚钻」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/shaozhugui91/article/details/113874687