线程安全与锁优化

本文深入探讨了线程安全的定义与实现方法,包括互斥同步、非阻塞同步和无同步方案,以及锁优化策略如自旋锁、锁消除、锁粗化和轻量级锁的详细解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程安全与锁优化-------------------线程安全
线程安全的定义:当多个线程访问同一个对象时,如果不需要考虑这些线程是如何进行调用和执行的;也不要添加同步的操作;也不需要调用任何其他的方法来进行协调操作时,可以使得调用这个对象的行为都能得到正确的结果,那么我们就说这个对象是线程安全的。
线程安全与锁优化-------------------java语言中的线程安全
1 造成线程不安全的原因:共享数据的存在,使得多线程对该变量进行操作时,会出现混乱的情况。
2 java中共享数据按强弱关系分为一下五类:不可变;绝对安全;相对安全;线程兼容;线程对立
2.1 不可变
让变量拥有不可变性质:使用final关键字修饰
让对象拥有不可变性质:保证对象的行为不会对其产生影响。如:String类型
2.2 绝对安全
代码本身封装了所有必要的正确性保障手段
2.3 相对安全
保证对象单独操作的时候是线程安全的
2.4 线程兼容
对象本身不是线程安全的,但是可以通过添加同步手段来保证对象在多线程的情况下是线程安全的
2.5 线程对立
不管是否采取来同步手段,在多并发的情况下都不是线程安全的。
线程安全与锁优化-------------------线程安全的实现方法
1 互斥同步—悲观锁
a 互斥同步是用来在多并发情况下,保证正确性的一种手段
b 同步: 在多个线程并发访问某一个共享数据时,在同一个时间点内,保证共享数据只被一条线程使用
c 互斥: 是用来实现同步的一种手段。其内包括临界区、信号量、互斥量这些方法
d synchronized关键字是java中最常见用于实现线程互斥同步的手段。
经由synchronized关键字修饰的同步代码块,
在编译之后,在同步代码块的前后会添加monitorenter和monitorexit这两个字节码。
monitorenter和monitorexit字节码拥有reference属性,需要指向所要锁定或者解锁的对象。(即:需要获得对象锁。synchronized修饰实例方法,reference为实例对象; synchronized修饰类方法,reference为类对象;)
上述已知,进入同步代码块,需要获得对象锁。当线程进入了同步代码块时,对象锁+1,同一个线程再次进入了同步代码块,对象锁+1,当退出时-1。当对象锁被其他线程占用时,该线程需要处于阻塞状态。
由于java线程的实现采用的是线程的混合实现模式,如下:
内核------内核线程---------轻量级线程---------用户线程
因此,当涉及到线程的阻塞和唤醒时,需要内核的帮忙,使线程从用户态转换到核心态,这个转换过程需要耗费很多的处理器时间。所以我们称synchronized这个过程是一个重量级锁。
e 还可以使用java.ugil.concurrent包中的重入锁(ReenterantLock)来实现同步。重入锁(ReenterantLock)比synchronized新增了一些高级功能:
等待可中断:正在等待对象锁的线程,即处于阻塞状态的线程可以选择自动放弃等待这个状态,处理其他事情
可实现公平锁:公平锁的概念是,先到的线程先获得锁。但是默认重入锁(ReenterantLock)是非公平锁
锁可以绑定多个条件:重入锁(ReenterantLock)对象可以绑定多个Condition对象
f 重入锁(ReenterantLock)是API层面上的互斥锁(lock()和unlock()方法配合try catch块来实现)
synchronized是原生语法层面上的互斥锁。
2 非阻塞同步----乐观锁
由于悲观锁存在的问题,在唤醒和阻塞线程时,需要内核的嵌入,将线程从用户态转换成内核态,会产生很大程度的耗费。于是,在硬件指令集发展的情况下,引入了非阻塞同步的概念
非阻塞同步:是一种基于冲突检测的乐观并发测略。具体过程就是,先执行操作,如果没有遇到其他线程共享数据,那么该操作就成功了。倘若,遇到共享数据争用的情况,即发生了冲突,那么采用其他的补偿措施(最常见的补偿措施:不断的重试,直到试到成功为止),这种乐观策略不要把线程挂起,因此这种操作称为非阻塞同步。
非阻塞同步的中非常重要的一点是:需要保证操作过程和冲突检测过程具备原子性。而这一点得以实现主要归功于硬件指令集的发展。可以通过靠硬件来完成原子性的保证。常用的指令集如下:
测试并设置(Test-and-Set)
获取并增加(Fetch-and-Increment)
交换(Swap)
比较并交换(CAS)
加载链接/条件存储(LL/SC)
CAS :假设有一个内存地址V, 在V 中存储的旧值为A,想要在V中存储的新值为B。当CAS指令执行时,只有当处理器检测到V中的值确实为A时,才可以将其更新为B。否则,就不知性更新操作。同时无论真的执行来更新操作,返回的值都是旧值A。这就是一个原子操作的过程。可以很好的解决同步问题。
CAS 存在的漏洞:‘ABA问题’。当读取到V中的值为A,在准备赋值的时候检测到也是A,但是这个过程中并不能保证这个时候的A,没有经过其他线程的改变,倘若其他线程先将这个A改成了B,又将B改成了A。所以这个是存在问题的
3 无同步方案
当不存在数据共享时,自然不需要同步。同时存在一些天生就线程安全的代码。
1 可重入代码
可以在代码执行的任何时刻中断它,转去执行另一段代码,而控制权返回以后,原来的线程不会出现错误。
2 线程本地存储
当共享数据部分的范围能够保证在同一个线程中执行完毕。如:生产者-消费者模式。
线程安全与锁优化-------------------锁优化
1 需要对锁进行优化的原因
在JDK1.6之前,在并发过程中的互斥同步通常使用的都是有synchronized(重量级锁)。但是在重量级锁的使用过程中,存在线程阻塞、线程的切换和线程的恢复都是通过内核来进行操作的。由内核对锁所进行的操作,需要消耗很大的代价。因此,需要对锁进行优化
自旋锁
a 首先,在知道在阻塞状态时,由内核对线程进行挂起和恢复操作时需要很大消耗的基上。在实际开发中发现,对共享数据的锁定状态,其实只会持续很短的一段时间。因此,JVM开发人员就想出,让后面请求对象锁的线程“稍微等一会,执行一个忙循环(自旋),该过程不放弃处理器的执行时间”,这个概念就是自旋锁。
b 自旋锁的优点:可以避免线程频繁切换时的开销。当数据锁定状态很短时,自旋锁效果很好
c 自旋锁的缺点:在进行自旋的过程中,线程仍占有着处理器的执行时间,如果数据锁定时间很长,那么只会白白消耗处理器的执行时间。因此,自旋锁的自旋是有一定的限度的。当自旋结束还没有获得锁,那么按传统的方法将线程挂起
自适应自旋锁
a 自旋锁的自旋通过自适应得到
b 自适应的方式:通过前一次在同一个锁上的自选时间及锁的拥有者状态来决定。对同一个锁,上一次成功过那么下一次继续自旋。对同一个锁自旋没有成功过,那么下次就直接不再自旋
锁消除
a 在JVM即时编译器的运行期间,对一些代码要求同步,的那是检测到不可能存在共享数据的锁进行消除。
锁粗化
a 如果一系列的连续操作都对同一个对象进行反复加锁和解锁,甚至加锁操作是出现在循环体中,将会把加锁同步的范围扩展到这个操作序列的外部。
轻量级锁
a 对象:在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗
b 详解
b.1 首先,需要了解一个概念:HotSpot虚拟机中的对象头的内存布局。一共分为两部分,分别是用于存储对象自身的运行时数据(MarkWord)、存储执行方法区对象类型的数据的数据指针。
b.2 MarkWord中记录了与锁相关的标志位,如下图所示
在这里插入图片描述
b.3 轻量级锁的执行过程

轻量级锁的加锁过程:

  1. 当前线程在代码要进入同步代码块时,如果这个同步对象没有被锁定,此时标志位=01
  2. JVM在当前线程的栈帧中建立一个名为锁记录(Lock record)的空间,
  3. 然后将当前对象目前的Mark Word中的内容拷贝到,栈帧中的Lock Record中
  4. JVM使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针
  5. 如果CAS更新成功,当前线程获得了这对象的锁,并且此时对象的Mark Word的锁标志位需要转变为 = 00.用于表示这个对象处在轻量级锁的状态
  6. 如果CAS更新失败,JVM首先检查Mark Word是不是已经指向当前栈帧的Lock Record,是则表示当前线程已经或者这个对象的锁。可以进入同步代码块进行操作。不是则表示当前对象正在被其他线程使用
    轻量级锁的解锁过程:

当有两条以上的线程争用同一个对象锁时,此时的轻量级锁,就会膨胀为重量级锁。

  1. 如果当前对象的Mark Word仍然指向当前线程的Lock Record成立
  2. 使用CAS操作把对象的Mark Word和当前线程的Lock Record替换回来
  3. 替换成功,则解锁完成,同步成功
  4. 替换失败,说明有其他线程尝试过获取这个锁,那么就要在释放锁的同时,唤起被挂起的线程
    偏向锁
    a 对象:消除数据在无竞争的情况下的同步原语。即:在无竞争的情况下,吧整个同步都消除掉,连CAS操作都不在执行。
    所谓偏向是指:这个锁会偏向于第一个来获得锁的线程,如果在之后的运行过程中,没有其他线程来对象锁进行获取,那么第一个获得锁的线程就不再需要进行同步
    b 详解
  5. 当锁对象第一次被线程获取的时候,JVM会把对象头总的标志位=01.
  6. 使用CAS操作把获取到这个锁的线程的ID记录在对象Mark Word中。
  7. CAS成功,持有偏向锁的线程以后每次进入这个锁的相关同步块时,JVM都可以不在进行任何同步操作
  8. 当另一个线程尝试获取这个对象锁时,偏向模式宣告结束。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值