u10-线程锁型

114 阅读10分钟

1. CAS操作

概念: CAS(Compare And Swap)底层操作是一条汇编指令 lock cmpxchg,是一种轻量级锁,也叫自旋锁:

  • cas(num,3,4):我希望将3改成4:
    • 将num获取到
    • 判断是否为期望值3
    • 如果3,将其改为4(这个过程是加锁的)。
    • 如果不是3,说明有人动了这个值,重新获取num,或者放弃获取。
    • 判断是否为期望值3
    • ...
    • 这个过程就是自旋
  • ABA问题:
    • 线程A将num获取到,判断为期望值3,但其实并不能一定说明中间没有其他线程动过这个num变量,因为有可能某个线程将其改为了5,又改为了93,又改回了3,如果num是基本类型变量,ABA问题影响不大,无论中间过程中num经历了什么,只要最后是3就行,但如果num是引用数据类型,存储的是0x9527,那么有可能0x9527中的某些属性值已经被偷偷改过了。
    • 解决方案:添加版本号字段,只要被修改过,版本号自增,每个线程在判断期望值的同时,连带着版本号一同检查。

CAS底层都是基于UnSafe类的某些调用,UnSafe类是可以直接操作JVM内存的一个类,由JVM设计者使用,我们几乎用不到它。

2. synchronized锁升级

概念: 即使在多线程环境下,某些共享变量也有可能极少发生线程争抢,即大多数的情况下都是只有一个线程在访问这个共享资源,如果对这个共享资源添加OS锁,那么每次访问的时候都需要向OS申请,也会很浪费性能,早期的synchronized会直接对锁实例obj添加OS锁,后来随着JDK的升级而做了优化,变成了先添加偏向锁,然后视情况而定,是否需要向重量锁方向进行进化。

  • 锁实例obj的mark-word的后两bit记录了锁类型的信息,其中:
    • 刚new出来的实例,没有加锁,mark-work后两位为 01,再辅助一个倒数第三位的 0 同时描述。
    • 获得了偏向锁后,mark-work后两位为 01,再辅助一个倒数第三位的 1 同时描述。
    • 获得了轻量级锁-自旋锁后,mark-work后两位为 00,不需要辅助位。
    • 获得了重量级锁-OS锁后,mark-work后两位为 10,不需要辅助位。
    • 如果锁实例obj将要被GC回收,mark-work后两位为 11,不需要辅助位。

锁升级流程图

image.png

2.1 偏向锁

概念:

  • 偏向锁会偏向第一个获取锁的线程(假设为threadA),将获得了偏向锁的信息 101 记录在琐实例obj的mark-word中的后三位,再将threadA的线程ID记录mark-word其他位,然后每次有线程想要获取锁的时候,只需要一个简单的判断:
    • 如果新线程还是threadA,直接放行进入同步代码中。
    • 如果其他线程如threadB,则表示发生了资源争抢,需要将偏向锁撤销并升级为轻量级锁。
    • 如果同步代码发生了严重耗时的情况,如调用了 wait(),则直接升级为重量级锁。
  • 偏向锁在升级之前需要先撤销revoke,这个过程也会消耗一定的CPU资源,所以如果你明确知道会发生多线程争抢事件,就不要使用偏向锁,而直接使用自旋锁,以免不必要的性能开销。
  • 偏向锁是需要启动的:因为JVM在启动的会执行很多sync代码,这些代码明确会有多线程竞争,所以偏向锁都是延迟4秒才启动的,以避免频繁的锁升级和锁撤销,浪费性能。
    • 可以通过 -XX:biasdLockingStartupDelay=0 参数来调整匿名偏向锁的启动延迟时间。
    • 偏向锁未启动时new出来的实例没有任何锁的信息,添加sync锁后,直接升级成为自旋锁。
    • 偏向锁启动之后再new出来的实例直接会获得一个匿名偏向锁,指向0(null),锁信息为 101,添加sync锁后仍为偏向锁,但mark-down存入了偏向的线程ID。

源码: /javase-advanced/

  • src: c.y.thread.lock.LockUpgradeTest.biasLock()
/**
 * @author yap
 */
public class LockUpgradeTest {

    @Test
    public void biasedLock() throws InterruptedException {

        Object obj = new Object();
        // 00000001 => 001: no lock
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        TimeUnit.SECONDS.sleep(5L);

        obj = new Object();
        // 000001(01): anonymous biased lock, and thread-recorded is null
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        synchronized (obj) {
            // 00000(101): non-anonymous biased lock, and thread-recorded is main thread
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
    }
}

2.2 轻量级锁-自旋锁

概念: 偏向锁撤销后,争抢资源的两个线程threadA和threadB,会各自在自己的线程栈中生成LR(Lock Record),并通过自旋的方式开始争抢资源,假设threadB争抢到了资源,则将threadB的LR记录在锁实例obj中,此时threadA开始在用户内存空间CAS自旋,此时锁实例obj中的偏向锁便升级为了自旋锁。

  • 自旋可以理解为在同步代码块的"门口",建立一个死循环不停地尝试获取锁的过程。
  • 自旋锁会消耗CPU资源,所以不适用于同步代码的执行时间长,或并发访问量高的情况。

自旋锁也叫无锁,但为了避免误导,尽量不要使用这个叫法。

源码: /javase-advanced/

  • src: c.y.thread.lock.LockUpgradeTest.selfRotatingLock()
/**
 * @author yap
 */
public class LockUpgradeTest {

    @Test
    public void selfRotatingLock() {

        Object obj = new Object();
        // 00000001 => 001: no lock
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        synchronized (obj) {
            // 11001000(00): self-rotating lock, and thread-recorded is the LR of main thread
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
        }
    }
}

2.3 重量级锁-OS锁

概念: 如果某个线程自旋次数超过10次,或所有自旋线程个数总和超过CPU核数的一半,那么自旋锁将升级为重量级锁,即OS锁。

  • JDK6之前可以使用 -XX:PreBlockSpin 调整自旋最大次数,JDK6之后JVM引入了自适应自旋的概念,即JVM自动管理自旋最大次数,无需我们操心。
  • OS锁需要向内核空间申请并得到返回,效率比自旋锁低,但它底层使用等待队列来存放和调度那些没能获取锁的线程,不消耗CPU资源,所以更适用于同步代码的执行时间长,或并发访问量高的情况。

源码: /javase-advanced/

  • src: c.y.thread.lock.LockUpgradeTest.osLock()
/**
 * @author yap
 */
public class LockUpgradeTest {

    @Test
    public void osLock() throws InterruptedException {

        Object obj = new Object();
        // 00000001 => 001: no lock
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());

        Thread threadA = new Thread(()->{
            synchronized (obj) {
                try {
                    obj.wait();
                    System.out.println("notified...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        threadA.start();

        Thread threadB = new Thread(()->{
            synchronized (obj) {
                try {
                    TimeUnit.SECONDS.sleep(1L);
                    obj.notify();
                    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        threadB.start();

        threadA.join();
        threadB.join();
    }
}

3. AQS类

概念: AbstractQueuedSynchronizer(AQS)是大部分锁的底层原理类,底层是CAS加volatile实现的。

  • AQS和核心是必须保持可见性的 volatile int state 属性,state值所表示的意义是随子类而变的,如:
    • ReentrantLock拿state来记录线程重入次数。
    • CountDownLatch拿state来记录线程倒数计数。
    • Semaphore拿state来记录信号量个数。
  • AQS中的state属性关联着一个双链表队列,每个节点都装着一个线程实例,由AQS类自己在类的内部维护。
  • 双链表队列的入队和出队过程都是CAS操作的,比锁定整条链表效率高。

4. ReentrantLock

概念: ReentrantLock表示一种可重入锁,是可以替代synchronized锁的,且比synchronized更灵活。

  • 构造:
    • new ReentrantLock():构建一个可重入锁,
    • new ReentrantLock(boolean fair):指定构建一个非公平(默认false)/公平锁。
      • 非公平锁:新来的线程不进等待队列,直接进入就绪状态,直接有机会争抢CPU资源。
      • 公平锁:新来的线程先入等待队列进行排队,等待线程调度器的调度。
  • 方法:
    • void lock():添加可重入锁。
    • void unlock():释放可重入锁,这个必须写在finally块中。
    • boolean tryLock():尝试获取锁,无论是否成功都不阻塞,方法继续执行。
      • p1:指定多长时间内尝试获取锁。
      • p2:时间单位。
  • vs synchronized:
    • ReentrantLock具有tryLock方法,可以自行指定获取不到锁时的处理方案,synchronized在获取不到锁的时候只能阻塞。
    • ReentrantLock可以指定为公平锁或非公平锁,synchronized只有非公平锁。

源码: /javase-advanced/

  • src: c.y.thread.lock.ReentrantLockTest
/**
 * @author yap
 */
public class ReentrantLockTest {

    private static class LockDemo implements Runnable {
        private Lock lock = new ReentrantLock();

        private void method() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread() + ": over");
            } finally {
                lock.unlock();
            }
        }

        @Override
        public void run() {
            lock.lock();
            try {
                for (int i = 0, j = 5; i < j; i++) {
                    TimeUnit.SECONDS.sleep(1L);
                    System.out.println(Thread.currentThread() + ": " + i);
                }
                method();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    private static class TryLockDemo {
        private Lock lock = new ReentrantLock();

        private void methodA() {
            try {
                lock.lock();
                for (int i = 0, j = 5; i < j; i++) {
                    TimeUnit.SECONDS.sleep(1L);
                    System.out.println(Thread.currentThread() + ": " + i);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }

        private void methodB() {
            boolean locked = false;
            try {
                // Try to acquire the lock within 3 seconds
                locked = lock.tryLock(3, TimeUnit.SECONDS);
                System.out.println(Thread.currentThread().getName() + ": " + locked);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (locked) {
                    lock.unlock();
                }
            }
        }
    }

    @Test
    public void lock() {
        LockDemo lockDemo = new LockDemo();
        new Thread(lockDemo).start();
        new Thread(lockDemo).start();
    }

    @Test
    public void tryLock() throws Exception {
        TryLockDemo trylockDemo = new TryLockDemo();
        new Thread(trylockDemo::methodA, "threadA").start();
        TimeUnit.SECONDS.sleep(1L);
        new Thread(trylockDemo::methodB, "threadB").start();
    }

    @SneakyThrows
    @After
    public void after() {
        System.out.println(System.in.read());
    }
}

4.1 lock()源码

流程: 以非公平锁为例:

  1. lock() 底层流程:

    • 调用 Sync 类(AQS子类)的 lock()
    • 调用 NonfairSync 类(Sync子类)的 lock()
    • 通过CAS将 state 的值由 0 改为 1
    • 如果成功,则令当前线程独占这把锁(使用 AbstractOwnableSynchronizer 中的 exclusiveOwnerThread 变量记录当前线程)。
    • 如果失败:调用 acquire(1)
  2. acquire(1) 底层流程:

    • 调用 tryAcquire() 尝试获取锁,如果成功,直接获取锁。
    • 如果失败:
      • 调用 addWaiter() 将当前线程加入等待队列。
      • 调用 acquireQueued() 在队列中不断轮询申请获取锁。
    • 如果加入等待队列也失败,调用 selfInterrupt() 打断当前线程。
  3. tryAcquire() 底层流程:

    • 调用 NonfairSync 类中的 tryAcquire()
    • 调用 ReentrantLock 类中的 nonfairTryAcquire()
    • 如果当前 state0
      • 通过CAS将 state 的值由 0 改为 1
      • 令当前线程独占这把锁。
      • 返回true。
    • 如果当前线程已经独占了锁。
      • state 自增1,表示重入一次。
      • 返回true。
    • 如果条件均不满足,返回false。
  4. addWaiter() 底层流程:

    • 创建新节点,内容是当前线程。
    • 将新节点通过CAS的方式追加到链表尾并返回。
  5. acquireQueued() 底层流程:

    • 判断当前节点的前置节点是不是头节点(判断自己是不是脖子节点)。
      • 头节点就是正在使用CPU资源的节点。
    • 如果自己是脖子节点,尝试获取锁(和头节点竞争)。
    • 如果竞争成功,获得锁,并将自己设置为新头节点。
    • 如果条件均不满足,无限轮询,直至满足。

4.2 unlock()源码

流程: 以非公平锁为例:

  1. unlock() 底层流程:

    • 调用 Sync 类(AQS子类)的 release(1)
    • 调用AQS类的 release()
    • 调用 Sync 类的 tryRelease() 尝试释放锁。
    • 如果释放成功,对当前头节点调用 unparkSuccessor() 并返回true。
    • 如果释放失败,返回false。
  2. tryRelease() 底层流程:

    • 如果当前线程没有获得锁,直接抛出异常。
    • state 的值自减1,表示释放一把锁。
    • 如果 state 的值减为0,表示所有锁释放完毕。
    • 如果 state 为0,则将 exclusiveOwnerThread 变量置空,并返回true。
    • 否则返回false,表示尝试释放锁失败。
  3. unparkSuccessor() 底层流程:

    • 对指定线程调用 unpark()

5. ReentrantReadWriteLock

概念: ReentrantReadWriteLock可以通过 Lock readLock() 获取读锁分支,或者通过 Lock writeLock() 获取写锁分支。

  • 读读共享:我读的时候允许别人也来读。
  • 读写互斥:我写的时候不允许别人读,我读的时候不允许别人写。
  • 写写互斥:我写的时候不允许别人写。

源码: /javase-advanced/

  • src: c.y.thread.lock.ReentrantReadWriteLockTest
/**
 * @author yap
 */
public class ReentrantReadWriteLockTest {
    private static class ReadWriteLockDemo {
        private ReadWriteLock lock = new ReentrantReadWriteLock();
        private Lock readLock = lock.readLock();
        private Lock writeLock = lock.writeLock();

        void read() {
            readLock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + ":reading...");
                TimeUnit.SECONDS.sleep(5L);
                System.out.println(Thread.currentThread().getName() + ":read over...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                readLock.unlock();
            }
        }

        void write() {
            writeLock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + ":writing...");
                TimeUnit.SECONDS.sleep(2L);
                System.out.println(Thread.currentThread().getName() + ":write over...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                writeLock.unlock();
            }
        }
    }

    @Test
    public void readWriteLock() {
        ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
        new Thread(readWriteLockDemo::read, "reader-A").start();
        new Thread(readWriteLockDemo::read, "reader-B").start();
        new Thread(readWriteLockDemo::write, "writer-A").start();
        new Thread(readWriteLockDemo::write, "writer-B").start();
    }

    @SneakyThrows
    @After
    public void after() {
        System.out.println(System.in.read());
    }
}