牛客秋招训练营笔记整理 | Java并发(三)

4 篇文章 0 订阅

Java并发(三)

1.Synchronized

利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现 为以下3种形式。

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽 (Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽 等于4字节,即32bit,

image-20220111092719155

在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变 化为存储以下4种数据

image-20220111092801196

锁标志位“01”表示无锁或者偏向锁,具体要看前面的标志,是否为偏向锁。

无锁的时候存hashcode,偏向锁的时候存线程ID,轻量级的时候存栈帧指针,重量级的时候存互斥量的指针

2.锁的升级与对比

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在 Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状 态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏 向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高 获得锁和释放锁的效率

  • 锁升级的过程

    从竞争的角度来说,没有竞争的时候没有锁。当有一个线程的介入的时候(没有竞争)就处于偏向锁状态。当有第二个或者更多的线程和第一个线程竞争的时候,就要升级锁,此时竞争较小,升级为轻量级锁,升级轻量级锁的前提就是把偏向锁撤销。于是此时多个线程开始抢占轻量级锁,他们把对象头中的mark word信息拷贝到各自的栈帧中,然后同时再以CAS的方式去抢着往对象头中写入自己的栈帧指针,谁写成功就占有轻量级锁。然后就执行同步体,其他没抢到的线程就是自旋,自旋到一定次数,就会触发锁膨胀,升级为重量级锁,且此线程阻塞。而刚才抢到锁的线程执行问同步体,最后释放锁的时候要处理的就是重量级锁,不但要释放锁,还要唤醒其他阻塞的线程。

  • CAS原子替换【往对象头中写入指向自己栈帧的指针】

    比较替换,先比较原来的值变没变,如果变了,就修改失败,如果没变,就修改成功。

  • 补充:synchronized为啥不能修饰构造器和成员变量?1:31

    执行一段逻辑,直接锁定一段程序就可以锁定这段程序中使用的成员变量。不需要单独去对成员变量加锁。
    两个线程同时调用构造器去创建两个对象,没有矛盾,不需要加锁。

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

image-20220111093441866

线程1访问同步块,判断对象头中没有保存线程1,它就尝试去CAS原子替换Mark word中的线程ID,此时只有线程1访问,所以一定会修改成功。然后线程1执行同步体。此时线程2访问同步块,判断对象头中是否保存了自己的线程ID,发现没有,那就要去原子替换Mark word 中的线程ID,此时替换失败(因为里面被线程1占着),然后此时就触发了撤销偏向锁,然后在一个全局安全点上暂停线程,将对象头中的线程ID置空。此时线程1和线程2都处于活跃状态。进行下一步的抢锁。
一旦有竞争,不是直接将偏向锁给线程2,而是要升级为轻量级锁,升级轻量级锁的前提就是先撤销偏向锁。

当线程无法改变MarkWord中的ID时,就发出撤销锁的指令,两个线程在进行抢锁

轻量级锁

(1)轻量级锁加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

(2)轻量级锁解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。图2-2是 两个线程同时争夺锁,导致锁膨胀的流程图

如何抢占轻量级锁,就是两个线程同时把对象头中的Mark word复制到自己栈帧中(因为每个线程都有自己的栈帧,所以这一步一定会成功),然后线程1和线程2要使用CAS把对象头中的Mark word指针指向自己的栈帧。【分为两步,先把对象头中的maekword拷贝到自己的空间中,然后再去把对象头中的指针指向自己。抢锁,就是抢着往对象头中写东西,写什么呢,写的就是指向自己栈帧的指针】

线程1抢到轻量级锁,执行同步块,线程2采用自旋的方式获取锁。一直失败,线程2就会触发锁膨胀,锁升级为重量锁级

image-20220111094055892

当线程没有获得锁,通过自旋的方式来抢锁,当自旋到一定程度膨胀为重量级锁,重量级锁会发出线程阻塞的信号,其他线程试图获取锁时, 都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

3.AQS队列同步器

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,它是实现大部分同步需求的基础。 同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状 态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3 个方法**(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作**,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部 类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来 供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获 取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、 ReentrantReadWriteLock和CountDownLatch等)。

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的 方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些 模板方法将会调用使用者重写的方法。

  • getState():获取当前同步状态。
  • setState(int newState):设置当前同步状态。
  • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态 设置的原子性。

需要重写的方法:

image-20220111094727108

模板方法不需要重写:

image-20220111094804846

同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放 同步状态和查询同步队列中的等待线程情况。

lock为什么比synchronized更好用?

  • 因为可以实现共享锁。读写分离的时候,可以同时加读

同步队列

同步队列是双向链表,同步器指向同步队列的头尾节点。

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。 同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称

image-20220111095213628

节点是构成同步队列的基础,同步器拥有首节点(head) 和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部。

当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式 与之前的尾节点建立关联。

image-20220111095327057

同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态 时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点

image-20220111095351547

设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态没有竞争发生,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。

独占式同步状态获取与释放

通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出

public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

其主要逻辑是:首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点并通过addWaiter(Node node) 方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。

共享式同步状态获取与释

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状 态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操 作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享 式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况,

image-20220111111053619

那么这一时刻对于该文件的写操 作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享 式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况,

[外链图片转存中…(img-FN7eUVTH-1641886127585)]

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小七rrrrr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值