一.乐观锁/悲观锁
1.乐观锁
①基本定义:乐观主义者,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,是一种无锁的原子算法。适合锁竞争不激烈的场景。
②实现原理:CAS(Compare And Set or Compare And Swap),三元组CompareAndSet(V,A,B)
CAS是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)、新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操 作。
注:CAS造成的ABA问题
ABA问题描述:
*进程P1在共享变量中读到值为A
*P1被抢占了,进程P2执行
*P2把共享变量里的值从A改成了B,再改回到A,此时被P1抢占。
*P1回来看到共享变量里的值没有被改变,于是继续执行。
③C#中实现举例:System.Threading.Interlocked中的原子自增、原子递减等
④Java中的实现举例:java.util.concurrent.atomic.AtomicInteger
JVM级别的实现:
2.悲观锁(阻塞式)
①基本定义:悲观主义者,总是认为有其他线程会随时和自己争夺资源,因此每次在操作数据前会先锁定该资源资源使得其他线程不能获取该资源,具有排它性。适合锁竞争比较激烈的场景
②实现原理
③C#中实现举例:Lock关键字(底层还是使用的Monitor,是对是Monitor.Enter和Monitor.Exit的封装)、Monitor类等
反编译(汇编)
源码注释:
④Java中的实现举例:synchronized关键字、Lock类
lock方法源码注释:
synchronized反编译代码(javap -v):
private static int i=10;
private static Object o=new Object();
public static void main(String[] args) {
synchronized (o)
{
i++;
}
}
注:还有MySQL关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等。
二.可重入锁
1.定义:简单的说就是同一个线程可以重复获取到锁
加锁场景代码:
public class Count{
Lock lock = new Lock();
public void print(){
lock.lock();
doAdd();
lock.unlock();
}
public void doAdd(){
lock.lock();
//do something
lock.unlock();
}
}
2.自实现可重入锁
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock()
throws InterruptedException{
Thread thread = Thread.currentThread();
while(isLocked && lockedBy != thread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = thread;
}
public synchronized void unlock(){
if(Thread.currentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
自实现不可重入锁:
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
3.Java中的可重入锁的实现:ReentrantLock属于可重入锁
三.公平锁/非公平锁
1.公平锁
就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
2.非公平锁
上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。 非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
Java语言中应用实例:ReentrantLock可以通过构造函数的参数设置来定义公平锁和非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁:
非公平锁:
结论:相对来说非公平锁效率高于公平锁,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。但是如果线程占用锁的实践短的话可能造成先来的线程饥饿。
四.分段锁
①特点:对数据存储空间进行分段,段内加锁,段间并行
②Java中的实现:
java.util.concurrent.ConcurrentHashMap
put源码:
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;//计算segment索引
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);//获取到key取hash后所在的段
return s.put(key, hash, value, false);//存入对应的段中
}
s.put()源码:
注:之前我断章截意的认为C#中的ConcurrentDictionary和Java中的ConcurrentHashMap一样会使用分段锁的机制来实现线程安全,直到我看了源码我才惊奇的发现它没有使用分段锁,他和jdk1.8之后 ConcurrentHashMap的加锁机制是相似的,即之锁住链表的头部。
五.自旋锁
①由于线程从阻塞状态切换到运行态是一个耗费性能的过程,因此线程持有锁的实践短的话,如果当前线程获取锁失败,则不会立刻阻塞自己而是先自旋等待一会,一般采用空循环的方式(空跑)进行自旋。
②基本实现原理:
*当前线程竞争锁失败时,打算阻塞自己
*不直接阻塞自己,而是自旋(空等待,比如一个空的有限for循环)一会
*在自旋的同时重新竞争锁
*如果自旋结束前获得了锁,那么锁获取成功;否则,自旋结束后阻塞自己
③C#中的实现举例
ConcurrentQueue中的入队列操作中的空跑自旋
SpinOnce()方法源码:
public bool NextSpinWillYield
{
//PlatformHelper.IsSingleProcessor检查当前机器是否为单处理器机器,如果是则不进行自旋,直接
//执行让步处理
//以及如果自旋次数超过YIELD_THRESHOLD,也不进行自旋,同样进入让步处理
get { return m_count > YIELD_THRESHOLD || PlatformHelper.IsSingleProcessor; }
}
public void SpinOnce()
{
//检查是进入自旋状态还是进入不同程度的让步策略
if (NextSpinWillYield)
{
//We prefer to call Thread.Yield first, triggering a SwitchToThread. This
// unfortunately doesn't consider all runnable threads on all OS SKUs. In
// some cases, it may only consult the runnable threads whose ideal processor
// is the one currently executing code. Thus we oc----ionally issue a call to
// Sleep(0), which considers all runnable threads at equal priority. Even this
// is insufficient since we may be spin waiting for lower priority threads to
// execute; we therefore must call Sleep(1) once in a while too, which considers
// all runnable threads, regardless of ideal processor and priority, but may
// remove the thread from the scheduler's queue for 10+ms, if the system is
// configured to use the (default) coarse-grained system timer.
CdsSyncEtwBCLProvider.Log.SpinWait_NextSpinWillYield();
int yieldsSoFar = (m_count >= YIELD_THRESHOLD ? m_count - YIELD_THRESHOLD : m_count);
if ((yieldsSoFar % SLEEP_1_EVERY_HOW_MANY_TIMES) == (SLEEP_1_EVERY_HOW_MANY_TIMES - 1))
{
//小睡1毫秒,让所有的线程都有机会获得锁
Thread.Sleep(1);
}
else if ((yieldsSoFar % SLEEP_0_EVERY_HOW_MANY_TIMES) == (SLEEP_0_EVERY_HOW_MANY_TIMES - 1))
{
//放弃自己的剩余时间片,只允许>=自己优先级的线程占用资源,如果没有则自己继续执行
Thread.Sleep(0);
}
else
{
#if PFX_LEGACY_3_5
Platform.Yield();
#else
//将当前线程放入就绪队列,如果队列中没有其他线程,则继续执行
Thread.Yield();
#endif
}
}
else
{
//线程进入自旋等待状态
Thread.SpinWait(4 << m_count);
}
// Finally, increment our spin counter.
m_count = (m_count == int.MaxValue ? YIELD_THRESHOLD : m_count + 1);
}
注:单处理器机器不许进入自旋状态,原因很简单,“你一个人占着仅有的资源在那自嗨半天结果啥事也没干”
六.偏向锁/轻量级锁/重量级锁
①重量级锁:传统重量级锁指使用操作系统互斥量来实现加锁(PV操作),使用这种锁的话,即使没有所竞争的时候还是会调用操作系统级别的互斥量实现加锁,造成性能消耗。
②轻量级锁:通过简单的数据操作实现加锁,不需要调用OS级别的加锁机制。
*在当前线程自己的栈区创建Lock Record用于存储Object的Mark Word的拷贝
*将Mark Word副本存入lock record
*基于CAS的机制将Object的Mark Word跟新为执行Lock record的指针
*如果CAS跟新成功,则当前线程获取到锁,否则检查Mark Word出的指针是否指向自己的栈针
*如果指向自己的栈针,则说明已经获取到锁,则重入代码快执行,否则说明锁已经被占用,则膨胀为重量级锁
CAS操作前:
CAS操作后:
总结:在没有线程竞争时,JVM使用轻量级锁进行加锁处理,一旦存在多线程线程的竞争的话,开始膨胀为重量级锁。
③偏向锁:偏向锁可以说是在轻量级锁上的进一步优化,轻量级锁在没有锁竞争的条件下还需要执行CAS操作,偏向锁连CAS都不操作。所谓“偏”就是说偏袒于第一次获得锁的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将不需要执行同步操作,直接获得运行资源。直到另外一个线程到来,偏向锁开始膨胀。
七.独享锁/共享锁
①独享锁:每次只能被一个线程占有的锁
共享锁:每次可以被多个线程占有的锁
②Java中的实现举例:
Lock接口的实现类ReentrantLock,其是独享锁。ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,其中读写,写读 ,写写的过程是互斥的。
③C#中的实现举例
ReaderWriterLockSlim类
八.互斥锁/读写锁
读写锁使用场景:如果你的数据有读有些,并且读多写少,并且希望在写这个数据的时候不允许读,那么就可以使用读写锁了,这将支持你并发的读。
读写锁中的锁降级:
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
也就是说一个线程目前持有写锁,在释放写锁之前获取到读锁(注意同一线程具有可重入性),当释放写锁后直接过渡到读锁,这期间不存在任何阻塞间隔。
使用场景:如果你写完数据后需要读取数据,为了不出现幻读,则可以使用锁降级。