多线程——各种锁和相关名词含义总结

8 篇文章 0 订阅
5 篇文章 0 订阅

接上篇,《多线程——锁机制(线程同步)》
本篇来说一下多线程下的各种锁和相关名词。

乐观锁

在读写数据的时候,认为读多写少,在读的时候不加锁,默认别人没有改动过。在将读来的数据进行更新的时候,才去判断数据是否被改动过,没有改动,则直接更新;改动过,则再获取最新的数据,重复前面的操作。
就是乐观的觉得读取的数据不会这么巧刚好在其他地方被改动过。直到最后要更新数据了,才保险一点,进行一下判断。
简单总结就是
1、数据读取时不加锁;
2、对读取来的数据进行更新的时候,判断当前版本是否与上次一致;
3、加锁,如果版本号一致,更新;
4、如果版本号不一致,重复上述操作。
例子:原子类、CAS的具体实现……


悲观锁

与前面的乐观锁相反,认为时刻都在写。所以读取数据的时候需要先加上锁,防止数据当时就被改写了。
简单总结就是
每次读写都上锁。最保险的做法。
例如synchronizedReentrantLock、……


自旋锁

如果持有锁的线程能很快地释放锁,为了减少时间资源消耗,等待锁的线程会先不进入阻塞队列(因为阻塞挂起等状态切换是比较耗资源的),而是执行自我循环一直在尝试获取锁(即自旋),等锁被释放,就能立即获取到锁。节省了线程在用户态内核态之间切换的消耗。
但是自旋锁适用的是那种持有锁的线程能够快速释放锁的情况。因为如果持有锁的时间过长,那么自旋对系统的消耗会比线程状态切换消耗更大,这样就达不到提升效率的作用,而且还会影响性能。
同时,如果等待锁的线程过多,也不适合使用自旋锁。因为大量等待锁的线程一直在自旋,会造成资源浪费,其他不需要等待锁、但又需要执行的线程就不容易得到机会。
简单总结就是
自旋锁是线程等待锁的时候,不进入阻塞,而是自我循环,保持活跃态,等锁释放了就能马上获取到锁。
适用于等待锁的线程不多,且持有锁的线程能够快速释放锁的情况。

简单解释一下两种状态
用户态:用户使用的空间,用户程序都是运行在用户态上。
内核态:操作系统使用的空间,涉及系统底层的操作。


互斥锁

这是锁的基本概念,即加了锁,能获取到锁的线程就执行,获取不到锁的线程就进入阻塞队列,等待锁的释放。
简单总结就是
日常使用的除自旋锁外的各种锁。


公平锁

线程等待锁的时候,进入阻塞队列。锁释放的时候,按照阻塞队列先进先出(FIFO)的顺序,给线程分配锁。
例子ReentrantLock lock = new ReentrantLock(true)


非公平锁

锁释放的时候,不按照阻塞队列的顺序,随机给线程分配锁。
例子
1、ReentrantLock lock = new ReentrantLock(false)
2、synchronized


可重入锁

正持有锁的线程,在其他需要用到同一个锁才能进入的地方,不需要重新获取锁,可以直接进入。
例子ReentrantLocksynchronized


共享锁

运行多个线程同时获取锁,共享资源。
例子ReentrantReadWriteLock读写锁中的读锁:ReentrantReadWriteLock.readLock()


独占锁

每次只能有一个线程持有锁,其他需要锁的线程只能等待。
例子ReentrantReadWriteLock读写锁中的写锁:ReentrantReadWriteLock.writeLock()


偏向锁、 轻量级锁、重量级锁

这三种锁,都是根据锁的底层实现来区分的。主要表现在各自对系统资源的消耗程度和对锁的调配方式的不同。这里简单介绍一下:
偏向锁
一个锁一直被某个线程访问,那么这个锁就会偏向这个线程。下次这个线程访问的时候,自动获取该锁。降低了获取锁的消耗。
轻量级锁
锁的竞争是在用户态之上。譬如当前有两条线程在交替着使用锁,即没有线程需要进入到阻塞队列。又或者,使用了自旋,同样还是不进入阻塞队列的前提下等待锁。
这种情况下,相较于进入阻塞队列等待(进入到内核态),资源消耗少。
重量级锁
可以见到理解为,需要进入到阻塞队列。就是需要再用户态和内核态之间进行切换的情况。这种情况下,消耗相比之上,是最大的。


分段锁

就是ConcurrentHashMap的实现原理。ConcurrentHashMap在内部进行分段,每段各自加锁,而不是在整个ConcurrentHashMap上进行加锁。
这每个分段的锁就是分段锁,在put数据的时候,通过hashcode计算出具体分段,然后对应分段进行锁操作。
使用分段锁的好处就是,对这个ConcurrentHashMap进行操作的时候,多线程是根据hashcode计算各个线程对应ConcurrentHashMap的段,在各自的段上加锁进行读写操作。这样就能细化锁粒度,减少锁竞争,使得对于整个ConcurrentHashMap层面上,多线程的操作是同步的。


死锁

上一篇有介绍,简单来说就是,线程陷入了一直等待锁释放,而锁又一直无法释放的情况。


同步锁

就是为了同步互斥而使用的锁,使得并发执行的多线程可以安全地访问同一资源。类似synchronized等,就是同步锁。


锁优化准则

  1. 减少锁持有时间:提高锁的流通率,减少等待中(或者自旋中)的线程。
  2. 减小锁粒度:增加了多线程并发度,减少了锁竞争。

CAS

CAS(compare and swap/set)比较并交换,是用于降低锁消耗的一种机制。
CAS操作有三个参数VEN
V:要更新的变量,即从内存取出时值。
E:预期值,即旧值,更新保存前用于判断当前值是否不变的依据。
N:新值,即要更新保存的新的值。
逻辑是:

  1. 当V == E,即当V值等于旧值E时,就可以更新V值为N;
  2. 当V != E,即当V值与旧值E不相同时,说明已有其他线程对V值做了修改并保存了,当前线程不进行更新(可以再次尝试,也可以什么都不做)。CAS返回最新的V值。

从上面的逻辑可以发现:

  1. CAS采用了一种类似乐观锁的方式。默认操作是可以完成的,直到最后才进行检查。
    且CAS甚至可以不加锁就实现这种方式。这对于多线程的锁竞争,起到了很好的降低消耗的作用。
  2. 除了这种类似乐观锁的方式,CAS也可以采用自旋锁的方式,用非阻塞的方式来降低消耗。
    就如上面逻辑2,当判断不能进行更新后,可以再次尝试,这时就类似于自旋,直到满足了更新条件(类似于自旋直到锁释放),再进行更新操作。

CAS的使用:

  • Unsafe类是CAS的核心类,CAS操作依赖于Unsafe类的方法。

ABA问题

CAS操作时,可能会遇上ABA问题。
ABA问题就是,在CAS操作时,从取出V值更新操作完成的这段时间内,V值可能会出现的变动。
举个例子
线程A、B几乎同时取出V值,然后,线程B修改完V值后,又改回来了,内存中V值还是原来的值。然后,线程A也修改V值,并更新。因为这时V值在内存中还是原来的值,所以线程A的更新操作可以完成。
例子分析
在这样的操作过程中,结果是没问题,可以完成更新的。但是中途,线程B曾经修改过V值并以此做了相应的操作,所以这个CAS操作过程,是可能存在问题的。

解决方法
可以使用版本号。在每次对V值修改的时候,就对比版本号,版本号一致,就修改完成,并且版本号+1。
这样,就上面例子来说,线程B中途修改了V值,然后又改回去了,最终V值没变,但是版本号已经增加了两次,+2了。所以线程A再执行CAS操作时,对比版本号不一致,就更新不了,也就不存在ABA问题了。


AQS

AbstractQueuedSynchronizer,抽象的队列同步器。
上一篇(多线程——锁机制(线程同步))有介绍过,Condition就是其内部类,而ReentrantLock的内部类Sync则是其子类。

  • AQS只是一个框架,它定义了一套多线程访问共享资源的同步器方案,对于资源的获取和释放,只有接口,需要具体的自定义同步器去实现。如上面说的sync
  • 在这个框架里,主要维护了一个共享资源volatile int state)和一个共享资源的线程等待队列(FIFO)。
  • 对于AQS同步器的实现,只需要实现这个state的获取和释放方法即可。其余的,如线程的等待队列等,AQS已经在顶层实现好了。
  • 具体state的获取和释放方法,需要实现如下:
    • isHeldExclusive():返回线程是否正在独占资源。
    • tryAcquire(int):获取锁(独占方式)。
    • tryRelease(int):释放锁(独占方式)。
    • tryAcquireShare(int):获取锁(共享方式)。
    • tryReleaseShare(int):释放锁(共享方式)。
  • AQS定义了两种资源的共享方式:
    • Exclusive:独占,同一时间只能有一个线程占用一个资源,如写锁。
    • Share:共享,同一时间可以有多个线程占用一个资源,如读锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值