线程锁,并发锁有哪些(9)

x33g5p2x  于2021-08-23 转载在 Java  
字(8.3k)|赞(0)|评价(0)|浏览(250)

一 前言

java蕴涵丰富锁的种类,使用不同的锁会带来不同的性能开销,理解各种主流锁在不同的场景中使用有助于提高我们系统的性能。总体的分类我帮读者们做了个总结请看下面的类目;

二 乐观锁VS悲观锁

2.1 悲观锁

悲观锁是指对数据修改保持悲观的态度,在处理和访问数据之前就已经上锁;我们常见的悲观锁有synchronized和Lock;其工作方式也很简单,就是在同一时期只有一个线程能获取到锁,只有等该线程释放锁,其他线程才有机会获取到锁;

2.2 乐观锁

乐观锁是指认为一般情况数据不会造成冲突,在数据进行提交时才对数据进行检查更新,其是直接通过操作同步资源实现;
乐观锁的实现方式是CAS(compare and swap); 在 java.util.concurrent包下的原子类就是经过CAS实现;更底层CAS实现是功能强大的Unsafe类;

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

上面是Unsafe类中的CAS方法源码之一,var1 代表对象内存位置,var2代表对象中变量的偏移,var4代表期望值,var6代表新值;操作含义是如果var1对象在内存中的偏移量为var2的变量值是var4,那么就会用var6更新原始值;

public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

看上面的源码, this.getLongVolatile(var1, var2); 是获取变量的值,循环体中是指 var1对象的偏移是var2,如果期望值是var6,那么就会用var6+var4代替原来的值(即this.getLongVolatile(var1, var2);的值);如果期望值不是var6那么会继续循环直到设置成功为止;

CAS问题概述

  1. ABA问题:线程1使用CAS将A变量改为B,变量值不一定就是B;可能线程1在获取变量后执行CAS之前,线程2使用CAS将变量A改为B,又将B改为A;经过线程2修改后的变量A就有可能不是原来线程1获取变量时的A(对象的地址偏移等可能已经发生改变);ABA问题其实就是变量的状态值经过形态转换造成的,那么通常的解决方式就是加版本号(1A2B3A),或者加上时间戳;java中也提供了AtomicStampedReference来解决ABA问题;
  2. 时间问题:我们分析源码看见是返回false那么会一值循环直到设置原始值是期望值为止,会造cpu成长等待,性能开销大;
  3. 多共享变量问题:对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性,javase1.5也提供了AtomicReference类来解决这个问题;

三 公平锁VS非公平锁

3.1 公平锁

公平锁是指按照队列中的顺序取锁也就是时间顺序取锁,越早请求获取锁的线程将越快获得锁;常见的公平锁就是synchronized和 new ReentrantLock(true);

下面的源码可以看见当传入的值是true就是公平锁,传入的值是false就是非公平锁;

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

FairSync()与 NonfairSync()最大的区别其实FairSync()有进行hasQueuedPredecessors()判断,然后在将当前线程设置进setExclusiveOwnerThread里面;

		//   FairSync()
           if (!hasQueuedPredecessors() &&
               compareAndSetState(0, acquires)) {
               // 设置当前线程占用锁
               setExclusiveOwnerThread(current);
               return true;
           }
       }
       //  NonfairSync()
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
   

hasQueuedPredecessors源码如下其就是判断当前线程是否是队列中的第一个线程;

public final boolean hasQueuedPredecessors() {

        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        // 当head不等于tail,并且head下一个为空或者当前线程不是下一个线程
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

3.2 非公平锁

非公平锁就是不按照队列中的线程顺序获取锁,常见就是 new ReentrantLock(false); 源码已经在3.1分析过;

四 可重入锁VS不可重入锁

4.1可重入锁

可重入锁是指线程在外层方法中已经获得过一个锁,后面此线程还能再次获取锁而不会陷入阻塞状态;常见的可重入锁就是synchronizedReentranLock

synchronized 锁内部拥有一个线程标识标明持有锁的线程,线程标识关联维护着一个计数器,当同一线程获得锁,会将计数器加1;当计数器的值大于1的时候其他线程是无法获得该锁的拥有权,直到拥有锁的线程释放相应次数的锁使计数器为0,其他线程才有机会获得锁;

ReentranLock 支持被同一个线程2147483647 次数递归拥有(可重入锁又称递归锁);当前线程第一次进入nonfairTryAcquire方法时会先判断state(默认值为0)的状态,获得锁会将state状态设置为1,后面如果还是该线程再次获取锁,就会将state的状态再次加1;如果是其他线程(非占用锁的线程)进入该方法判断state的状态大于1,会直接返回false,表示尝试获取失败;tryRelease释放锁也是等state为0的时候才释放;详细的源码分析看如下:

	final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // state初始值为0 表示没有其他线程获得锁
            int c = getState();
            if (c == 0) {
            // acquires的默认传入是1
                if (compareAndSetState(0, acquires)) {
                // 设置当前线程拥有锁
                    setExclusiveOwnerThread(current);
                    // 获取成功
                    return true;
                }
            }
          // 当前线程就是占用锁的线程,可重入
            else if (current == getExclusiveOwnerThread()) {
            // 每次进入都会将 nextc 的值加1
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                    // 将 nextc 计数后的值设置为新的state
                setState(nextc);
                // 获取成功
                return true;
            }
              // state的初始值不为0表示该锁已经被其他线程持有,获取失败
            return false;
        }

	protected final boolean tryRelease(int releases) {
	// 状态值减去释放的次数
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
                // 设置 free的标志位
            boolean free = false;
            // 当state为0时才真正释放,并且设置占有锁的线程为null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 每次都会设置计算后的c 赋值给state
            setState(c);
            // 返回 free的值
            return free;
        }

4.2 非可重入锁

非可重入锁是指当前线程获得锁,执行完同步后释放锁,二次进入需要重新获取锁,设置当前线占锁;在ThreadPoolExecutor的内部类Worker中有一段代码就是获得非可重入锁;

	protected boolean tryAcquire(int unused) {
		// 如果期望值是0,直接将state设置为1
            if (compareAndSetState(0, 1)) {
            // 设置当前线程占有锁
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
    protected boolean tryRelease(int unused) {
    // 直接设置占有线程为空,表示释放锁资源
            setExclusiveOwnerThread(null);
            // 直接设置state为0
            setState(0);
            return true;
        }       

五 独占锁VS共享锁

5.1 独占锁

独占锁又称排它锁,即一个锁只能同时被一个线程所拥有;常见的排它锁有synchronized,和ReentrantLock ,ReentrantReadWriteLock中的WriteLock

以ReentrantReadWriteLock中写锁的获取锁和释放锁都是独占并且可重入,其具体的源码分析如下

	 // 获取锁
	protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            // 获得state
            int c = getState();
            // 表示占有锁的次数,内部是将c 与上 65535(2的16次方减1即0xffff)
            int w = exclusiveCount(c);
            // state状态非0,表示已经有线程占有锁
            if (c != 0) {
               // 如果当前线程不是占有锁的线程,或者锁被当前线程占有0次,获取写锁失败
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                    // 占有的次数超过锁允许的最大次数抛出异常
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // 重新获取锁,设置state
                setState(c + acquires);
                return true;
            }
            //  state为0,写锁处于阻塞状态或者,CAS操作state的期望值不是0,获取写锁失败
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
                // 获取锁成功
            setExclusiveOwnerThread(current);
            return true;
        }
        // 释放锁
        protected final boolean tryRelease(int releases) {
        // 当前线程非占有锁的线程会抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
                // 表示剩余的占有锁的次数
            int nextc = getState() - releases;
            // 占有锁的次数是否为0
            boolean free = exclusiveCount(nextc) == 0;
            // 占有锁的次数为0就释放锁
            if (free)
                setExclusiveOwnerThread(null);
                // 设置state为计算过后的nextc
            setState(nextc);
            return free;
        }

5.2 共享锁

共享锁表示锁可以被多个线程同时拥有;常见的共享锁就是ReentrantReadWriteLock中的readLock;共享锁的线程和次数是由LocalThread所维护,state的每次设置都是以(1>>>16)为单位,从而实现共享安全,具体的源码分析如下;

	// 获得共享锁
	protected final int tryAcquireShared(int unused) {
			// 当前线程
            Thread current = Thread.currentThread();
            // 获得state
            int c = getState();
            // 占有锁的次数非0,并且当前线程不是锁的占有者,返回-1表示获取锁失败
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
                // 表示共享锁的次数(其内部是无符号右移16位)
            int r = sharedCount(c);
            // 读锁处于非阻塞状态,共享锁次数小于锁的最大次数,CAS操作的期望值是c,会设置state为 c + 65536(2的16次方)
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                // 共享锁的次数为0,设置当前线程是第一个获取读锁的线程,持有数为1,返回1表示获取共享锁成功
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                    // 当前线程就是第一个获取读锁的线程,持有数累加1,返回1
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                    // 当前线程非第一个读取锁的线程
                } else {
                // HoldCounter内部维护着线程的id和线程的持有读锁count;
                    HoldCounter rh = cachedHoldCounter;
                    // HoldCounter为null或者线程id不是当前线程id
                    if (rh == null || rh.tid != getThreadId(current))
                    // 设置当前线程持有的读锁的次数。readHolds由ThreadLocal维护,保证每个线程是独立
                        cachedHoldCounter = rh = readHolds.get();
                        // 如果线程持有读锁次数为0,设置HoldCounter,并且将 count累加1
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                // 获取成功
                return 1;
            }
            // 处理CAS失效的情况和读锁是否获取问题
            return fullTryAcquireShared(current);
        }
        
        // 释放共享锁
	protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            // 当前线程是第一个获取读锁的线程,
            if (firstReader == current) {
               if (firstReaderHoldCount == 1)
                              // 设置 第一个读锁的线程为null
                    firstReader = null;
                else
                // 读锁次数持有减1
                    firstReaderHoldCount--;
                    // LocalThread维护的非第一个持有读锁的线程次数减1
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            // CAS操作,当state正好减少65536表示释放一次锁,返回true
            for (;;) {
                int c = getState();
                // 以 65536 为单位减少
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

六 自旋锁和自适应锁

6.1 自旋锁

自旋锁指当前线程尝试去获取锁的时候发现,锁已被其他线程占用,当前线程不会马上陷入阻塞状态,而是会在锁外自旋尝试获取锁(默认是自旋10次,可以使用-XX:PreBlockSpin来设置),其实现原理也是CAS操作,Unsafe类中的CAS设置long值如下,会在while中循环尝试获取;

public final long getAndSetLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var4));

        return var6;
    }

6.2 自适应锁

自适应锁表示自旋的时间不再固定,如果上次该线程成功自旋获取到锁,那么jvm会认为这次该线程也能自旋获取相同的锁成功,从而加长自旋时间;如果该线程上次自旋获取锁失败,那么jvm会认为这次该线程自旋获取相同的锁会失败,从而减少自旋时间;

七 锁粗化VS锁消除

7.1 锁消除

锁消除是指在运行时期,同步代码块中没有共享数据安全问题,就会消除锁;

示例:

public class EliminateLock {
   
    private void   lock()  {
        synchronized(this){
            // do nothing
        }
    }
}

7.2 锁粗化

锁粗化是指在一串连续的同步锁中,同步锁直接扩展至最外部;

粗化前:

public class ExtendLock {
    
    private int v1 = 0;
    
    private int v2 = 1;
    
    public void  lock(){
        synchronized (this){
            v1++;
        }
        synchronized (this){
            v2++;
        }
    } 
}

粗化后:

public class ExtendLock {

    private int v1 = 0;

    private int v2 = 1;

    public void  lock(){
        synchronized (this){
            v1++;
            v2++;
        }
    }
}

八 无锁 vs 偏向锁 vs 轻量级锁 vs 重量级锁

8.1 mark word

mark word 是 java头对象中的一部分,其存储对象的HashCode,分代年龄和锁标志位信息和运行时数据;

8.2 无锁

无锁即对同步资源没进行加锁,所有线程都可以修改数据,当同一时期只有一个线程能修改数据成功,上面提到的CAS操作就是无锁的实现;

8.3 偏向锁

偏向锁是指偏向于第一个获取到锁的线程;在后续的同步中,如果没有其他线程竞争锁,偏向锁则会消除同步;当使用:
-XX:-UseBiasedLocking=true 开启(默认开启)偏向锁,第一个获取到锁的线程会在对象头的标志位设置为01;

8.4 轻量级锁

轻量级锁是指当前锁是偏向锁,被其他线程锁获取,偏向锁就会升级为轻量级锁;加锁过程中,虚拟机会在帧栈中建立锁记录(Lock Record),会将 mark word拷贝至 Lock Record ;然后jvm会使用CAS将mark world 的指针指向 Lock Record 中的指针,并将对象头中的标志位就会修改为00;

8.5 重量级锁

当超过2个线程同时竞争一个锁时,轻量级锁失效会导致轻量级锁膨胀为重量级锁,锁的标志位会设置为10,其他等待的线程将不会自旋而是陷入阻塞状态;

九参考文档

《java并发编程的艺术》
《java并发编程之美》
《深入理解java虚拟机 jvm高级特性与最佳实践》

相关文章