【笔记】Java并发编程之美-Java并发包中锁原理剖析

it2025-03-05  33

LockSupport工具类

JDK中的rt.jar包里面的LockSupport是个工具类,它的主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。

LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。

park()方法

如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用Locksupport.park()时会马上返回,否则调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。

unpark()方法

当一个线程调用unpark时,如果参数thread线程没有持有thread与LockSupport类关联的许可证,则让thread线程持有。 如果thread之前因调用park()而被挂起,则调用unpark后,该线程会被唤醒。 如果thread之前没有调用park,则调用unpark方法后,再调用park方法,其会立刻返回。

parkNanos(long nanos)方法

和park方法类似,如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.parkNanos(long nanos)方法后会马上返回。该方法的不同在于,如果没有拿到许可证,则调用线程会被挂起nanos时间后修改为自动返回。

park(Object blocker)方法

当线程在没有持有许可证的情况下调用park方法而被阻塞挂起时,这个blocker对象会被记录到该线程内部。使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用getBlocker(Thread)方法来获取blocker对象的。

void parkNanos(Object blocker,long nanos)方法
void parkUntil(Object blocker,long deadline)方法

这个方法和parkNanos(Object blocker,long nanos)方法的区别是,后者是从当前算等待nanos秒时间,而前者是指定一个时间点(换算为时间点到1970年这个时间点的总毫秒数)。

抽象同步队列AQS概述

AQS——锁的底层支持

AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。 AQS是一个FIFO的双向队列。其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。

在AQS中维持了一个单一的状态信息state。对于ReentrantLock的实现来说,state可以用来表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWriteLock来说,state的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数;对于semaphore来说,state用来表示当前可用信号的个数:对于CountDownlatch来说,state用来表示计数器当前的值。

AQS有个内部类ConditionObject,用来结合锁实现线程同步。

对于AQS来说,线程同步的关键是对状态值state进行操作。根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。

使用独占方式获取的资源是与具体线程绑定的,就是说如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时会发现当前该资源不是自己持有的,就会在获取失败后被阻塞。

对应共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用CAS方式进行获取即可。

独占方式下,获取资源acquire(int arg)会使用tryAcquire方法,释放资源release(int arg)会使用tryRelease方法。 AQS类并没有提供可用的tryAcquire和tryRelease方法,正如AQS是锁阻塞和同步器的基础框架一样,tryAcquire和tryRelease需要由具体的子类来实现。子类在实现tryAcquire和tryRelease时要根据具体场景使用CAS算法尝试修改state状态值,成功则返回true,否则返回false。子类还需要定义,在调用acquire和release方法时state状态值的增减代表什么含义。

共享方式下,获取资源acquireShared(int arg)会使用tryAcquireShared方法,释放资源 releaseShared(int arg) 会使用tryReleaseShared方法。 同样需要注意的是,AQS类并没有提供可用的tryAcquireShared和tryReleaseShared方法,正如AQS是锁阻塞和同步器的基础框架一样,tryAcquireShared和tryReleaseShared需要由具体的子类来实现。子类在实现t叩AcquireShared和tryReleaseShared时要根据具体场景使用CAS算法尝试修改state状态值,成功则返回true,否则返回false。

AQS——条件变量的支持

notify和wait,是配合synchronized内置锁实现线程间同步的基础设施,条件变量的signal和await方法则是用来配合锁(使用AQS实现的锁)实现线程间同步的基础设施。 它们的不同在于,synchronized同时只能与一个共享变量的notify或wait方法实现同步,而AQS的一个锁可以对应多个条件变量。

条件变量:

ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition();

一个Lock对象可以创建多个条件变量。

其实这里的Lock对象等价于synchronized加上共享变量,调用lock.lock()方法就相当于进入了synchronized块(获取了共享变量的内置锁),调用lock.unLock()方法就相当于退出synchronized块。调用条件变量的await()方法就相当于调用共享变量的wait()方法,调用条件变量的signal方法就相当于调用共享变量的notify()方法。调用条件变量的signalAll()方法就相当于调用共享变量的notifyAll()方法。

当线程调用条件变量的await()方法时(必须先调用锁的lock()方法获取锁),在内部会构造一个类型为Node.CONDITION的node节点,然后将该节点插入条件队列末尾,之后当前线程会释放获取的锁(也就是会操作锁对应的state变量的值),并被阻塞挂起。这时候如果有其他线程调用lock.lock()尝试获取锁,就会有一个线程获取到锁。

当另外一个线程调用条件变量的signal方法时(必须先调用锁的lock()方法获取锁),在内部会把条件队列里面队头的一个线程节点从条件队列里面移除并放入AQS的阻塞队列里面,然后激活这个线程。

总结:一个锁对应一个AQS阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。

独占锁ReentrantLock的原理

类图结构

使用AQS实现,参数决定是公平锁还是非公平锁,默认为非公平。

AQS的state状态值表示线程获取该锁的可重入次数,在默认情况下,state的值为0表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时会尝试使用CAS设置state的值为1,如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置为2,这就是可重入次数。在该线程释放该锁时,会尝试使用CAS让状态值减1,如果减1后状态值为0,则当前线程释放该锁。

获取锁

1.void lock()方法 如果锁当前没有被其他线程占用并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置AQS的状态值为1,然后直接返回。 如果当前线程之前己经获取过该锁,则这次只是简单地把AQS的状态值加1后返回。 如果该锁己经被其他线程持有,则调用该方法的线程会被放入AQS队列后阻塞挂起。

2.void lockInterruptibly()方法 该方法与lock()方法类似,它的不同在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的interrupt()方法,则当前线程会抛出InterruptedException异常,然后返回。

3.boolean tryLock()方法 尝试获取锁,如果当前该锁没有被其他线程持有,则当前线程获取该锁井返回true,否则返回false。注意,该方法不会引起当前线程阻塞。

4.boolean tryLock(long timeout,TimeUnit unit)方法 增加了超时返回false。

释放锁

1.void unlock()方法 尝试释放锁,如果当前线程持有该锁,则调用该方法会让该线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而己。如果当前线程没有持有该锁而调用了该方法则会抛出IllegalMonitorStateException异常。

总结

读写锁ReentrantReadWriteLock原理

解决线程安全问题使用ReentrantLock就可以,但是ReentrantLock是独占锁,某时只有一个线程可以获取该锁,而实际中会有写少读多的场景,显然ReentrantLock满足不了这个需求,所以ReentrantReadWriteLock应运而生。ReentrantReadWriteLock采用读写分离的策略,允许多个线程可以同时获取读锁。

类图结构

ReentrantReadWriteLock巧妙地使用state的高16位表示读状态,也就是获取到读锁的次数;使用低16位表示获取到写锁的线程的可重入次数。

firstReader用来记录第一个获取到读锁的线程,firstReaderHoldCount则记录第一个获取到读锁的线程获取读锁的可重入次数。cachedHoldCounter用来记录最后一个获取读锁的线程获取读锁的可重入次数。

readHolds是ThreadLocal变量,用来存放除去第一个获取读锁线程外的其他线程获取读锁的可重入次数。ThreadLocalHoIdCounter继承了ThreadLocal,因而initialValue方法返回一个HoldCounter对象。

写锁的获取与释放

1.void lock() 写锁是独占锁、可重入锁。 2.void lockInterruptibly() 与lock()不同是对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出异常InterruptedException异常。 3.boolean tryLock() 尝试获取写锁,如果当前没有其他线程持有写锁或者读锁,则当前线程获取写锁会成功,然后返回true。 如果当前己经有其他线程持有写锁或者读锁则该方法直接返回false,且当前线程并不会被阻塞。 如果当前线程已经持有了该写锁则简单增加AQS的状态值后直接返回true。 4.boolean tryLock(long timeout,TimeUnit unit) 增加了超时返回false。 5.void unlock() 若当前线程持有该锁,AQS减1,若减1后等于0则释放该锁,否则仅仅减1。 若当前线程没有持有该锁,抛出IllegalMonitorStateException异常。

读锁的获取与释放

1.void lock() 获取读锁,若没有其他线程持有写锁,则获取读锁,AQS状态值state高16位增加1,然后方法返回。若其他线程持有写锁,当前线程被阻塞。 2.void lockInterruptibly() 不同在于当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出InterruptedException异常。 3.boolean tryLock() 尝试获取读锁,若没有其他线程持有写锁,则返回true。若有其他线程持有写锁,返回false,但当前线程不会被阻塞。若当前线程持有该读锁,则增加AQS状态值state高16位后返回true。 4.boolean tryLock(long timeout,TimeUnit unit) 多了超时时间参数。 5.void unlock()

总结

JDK8中新增的StampedLock锁探究

该锁提供了三种模式的读写控制,当调用获取锁的系列函数时,会返回一个long型的变量,我们称之为戳记(stamp),这个戳记代表了锁的状态。其中try系列获取锁的函数,当获取锁失败后会返回为0的stamp值。当调用释放锁和转换锁的方法时需要传入获取锁时返回的stamp值。

三种读写控制模式: 写锁writeLock:独占锁,请求成功返回stamp变量用来表示该锁版本。提供了非阻塞的tryWriteLock方法。

悲观读锁readLock:共享锁。悲观指具体操作数据前悲观认为其他线程可能对自己操作数据进行修改,所以先加锁。请求成功后返回stamp变量用来表示该锁版本。提供了非阻塞的tryReadLock方法。适合读少写多。

乐观读锁tryOptimisticRead:相对悲观锁来说,操作数据前没通过CAS设置锁的状态,仅通过位运算测试。若没有线程持有写锁,返回非0的stamp变量。获取该stamp后具体操作数据前还需要调用validate方法验证该stamp是否可用。由于tryOptimisticRead并没有使用CAS设置锁状态,所以不需要显式地释放该锁。适合读多写少。 由于没有使用真正的锁,在保证数据一致性上需要复制一份要操作的变量到方法栈,并且在操作数据时可能其他写线程己经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。

总结

StampedLock提供的读写锁与ReentrantReadWriteLock类似,只是前者提供的是不可重入锁。但是前者通过提供乐观读锁在多线程多读的情况下提供了更好的性能,这是因为获取乐观读锁时不需要进行CAS操作设置锁的状态,而只是简单地测试状态。

最新回复(0)