锁是用来控制多个线程访问共享资源的方式
,一般来说,一个锁能够防止多个线程同时访问共享资源
(但是有些锁可以允许多个线程并发的访问共享资源
,比如读写锁)。
只是在使用时需要显式地获取和释放锁
。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁
等多种synchronized关键字所不具备的同步特性。它将锁的获取和释放固化了,也就是先获取再释放
。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class locks {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(()->{
lock.lock();
try{
System.out.println("t1.....");
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
},"t1").start();
new Thread(()->{
lock.lock();
try{
System.out.println("t2.....");
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
},"t2").start();
}
}
输出结果
使用说明
保证在获取到锁之后,最终能够被释放
。异常抛出的同时,也会导致锁无故释放
。方法名称 | 描述 |
---|---|
void lock() | 获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回 |
void lockInterruptibly() throws InterruptedException | 可中断地获取锁,和 lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程 |
boolean tryLock() | 尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false |
boolean tryLock(1ong time,TimeUnit unit) throws InterruptedException | 超时的获取锁,当前线程在以下3种情况下会返回:①当前线程在超时时间内获得了锁 ②当前线程在超时时间内被中断 ③超时时间结束,返回false |
void unlock() | 释放锁 |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的 wait()方法,而调用后,当前线程将释放锁 |
用来构建锁或者其他同步组件的基础框架
,它使用了一个int成员变量表示同步状态
,通过内置的FIFO队列来完成资源获取线程的排队工作。继承
,子类通过继承同步器并实现它的抽象方法来管理同步状态getState()
:获取当前同步状态。setState(int newState)
:设置当前同步状态。compareAndSetState(int expect,int update)
:使用CAS设置当前状态,该方法能够保证状态设置的原子性
。独占锁就是在同一时刻只能有一个线程获取到锁
,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁
。
1. 简要说明
依赖内部的同步队列(一个FIFO双向队列)
来完成同步状态的管理。 当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列
,同时会阻塞当前线程。 当同步状态释放时,会把首节点中的线程唤醒
,使其再次尝试获取同步状态。获取同步状态失败的线程引用、等待状态以及前驱和后继节点
2. 同步器的基本结构
3. 节点加入到同步队列的过程
过程说明
4. 首节点
获取同步状态成功的节点
,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点
。它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可
。1. 独占式同步状态获取与释放的总过程
调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态
同步状态获取失败,则构造同步节点
(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态
。2. 头节点的说明
当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态
。通过调用同步器的release(int arg)
方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。
3. 节点自旋获取同步状态的过程
过程说明
4. 只有前驱节点是头节点才能够尝试获取同步状态,这是为什么?
后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点
。维护同步队列的FIFO原则
。5. 总结
tryRelease(int arg)方法
释放同步状态,然后唤醒头节点的后继节点。1. 共享式锁与独占锁的区别
同一时刻能否有多个线程同时获取到同步状态
。tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的
,因为释放同步状态的操作会同时来自多个线程。图示说明
左半部分,共享式访问资源时,其他共享式的访问均被允许,而独占式访问被阻塞,右半部分是独占式访问资源时,同一时刻其他访问均被阻塞。
2. 成功获取到同步状态并退出自旋的条件
如果当前节点的前驱为头节点
时,尝试获取同步状态,如果tryAcquireShared(int arg)方法返回值返回值大于等于0
,表示该次获取同步状态成功并从自旋过程中退出。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class test {
public static void main(String[] args) {
MyLock lock = new MyLock();
new Thread(()->{
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + " locking....");
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " unlocking....");
lock.unlock();
}
},"t1").start();
new Thread(()->{
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + " locking....");
}finally {
System.out.println(Thread.currentThread().getName() + " unlocking....");
lock.unlock();
}
},"t2").start();
}
}
// 独占锁
class MyLock implements Lock{
class MySync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0,1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
protected Condition newCondition(){
return new ConditionObject();
}
}
private MySync sync = new MySync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1,unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
同步器重写的方法
tryAcquire()
tryRelease()
isHeldExclusively()
newCondition()
重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁
。
在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞
。识别获取锁的线程是否为当前占据锁的线程
,如果是,则再次成功获取。就是获得了几次就要释放几次
)通过判断当前线程是否为获取锁的线程来决定获取操作是否成功
,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值
。该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。1. 公平锁说明
公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序
,也就是FIFO。公平的获取锁,也就是等待时间最长的线程最优先获取锁
,也可以说锁获取是顺序的。
2. 公平锁的好处
公平锁能够减少“饥饿”发生的概率
,等待越久的请求越是能够得到优先满足。
3. 非公平锁的说明
对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁
,而公平锁则不同。
4. 非公平锁的坏处
非公平性锁可能使线程“饥饿”
5. 非公平锁的好处
但极少的线程切换,保证了其更大的吞吐量
读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升
。
写操作对读操作的可见性
以及并发性的提升简化读写交互场景的编程方式
。读写锁的性能都会比排它锁好
,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。当写操作开始时,所有晚于写操作的读操作均会进入等待状态
,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读,保证写操作对读操作的可见性。
读写状态就是其同步器的同步状态。在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写
。
则读状态(S>>>16)大于0,即读锁已被获取
。1. 写锁简介
写锁是一个支持重进入的排它锁
。
2. 写锁获取过程
增加写状态
。读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态
。3. 写锁不能获取的情况
原因
读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作
。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞
。
4. 写锁的释放过程
写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放
,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。
1. 读锁简介
读锁是一个支持重进入的共享锁
2. 获取过程
它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态
。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。
3. 释放过程
读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程
。
LockSupport定义了一组以park
开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法
来唤醒一个被阻塞的线程。
wait()、wait(long timeout)、notify()以及notifyAll()
方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。类似Object的监视器方法
,与Lock配合可以实现等待/通知模式await()
方法后,当前线程会释放锁并在此等待。Condition对象的signal()方法
,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。1. 什么是等待队列?
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用
,该线程就是在Condition对象上等待的线程。
2. 等待队列的基本结构
一个Condition包含一个等待队列
首节点
(firstWaiter)和尾节点
(lastWaiter)。Condition.await()
方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。该节点更新过程是由锁来保证线程安全
的。3. 并发包中的Lock的同步队列和等待队列
而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列
Condition的await()
方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态
。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中
。调用await()方法后相关线程的工作流程
该方法会将当前线程构造成节点并加入等待队列中
,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态
。通过addConditionWaiter()方法把当前线程构造成一个新的节点并将其加入等待队列中
。1. 当前线程加入等待队列
调用该方法的前置条件是当前线程必须获取了锁
2. 节点从等待队列移动到同步队列的过程
enq(Node node)
方法,等待队列中的头节点线程安全地移动到同步队列。当前线程再使用LockSupport唤醒该节点的线程
。3. Condition的signalAll()
Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程
。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/weixin_56727438/article/details/121545401
内容来源于网络,如有侵权,请联系作者删除!