美文网首页
线程安全与锁优化

线程安全与锁优化

作者: sizuoyi00 | 来源:发表于2019-11-29 02:26 被阅读0次

一、线程安全

定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

1. java语言中的线程安全

当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。
多个线程之间存在共享数据访问才会有线程安全问题,所以java共享数据按照“安全程度”由强至弱分为以下5类:
不可变:一定是线程安全的,不需要采取任何的线程安全保障措施,如final关键字修饰的常量
绝对线程安全:java API标注为线程安全的类,大多数都不是绝对线程安全。如Vector类,其remove与get方法都是线程安全的,但是如果两个线程访问,不在调用端做额外同步措施的话,一个线程遍历,一个线程删除,则会出现ArrayIndexOutOfBoundsException异常,线程不安全,需要加上同步手段才保证绝对线程安全。
相对线程安全:通常意义上的线程安全,即可以保证这个对象单独的操作是安全的。如上述的Vector类就属于相对线程安全。对于一些如上的特定顺序的连续调用,需要使用同步手段来保证决定线程安全。
线程兼容:对象本身不是线程安全的,可以通过调用端正确的使用同步手段来保证对象在并发环境安全的使用。
线程对立:无论调用段是否采取了同步措施,都无法再多线程环境中并发使用的代码。如Thread.suspend()和resume()方法,如果两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否同步,目标线程都是存在思索风险的,如果suspend()终端的线程就是即将执行resume()的线程,就肯定产生死锁了。同理还有System.setIn()和System.setOut()和System.runFinalizersOnExit()等。

2. 线程安全的实现方法

虚拟机提供了同步和锁机制来实现线程安全。

2.1. 互斥同步(阻塞同步)

同步是指再多个线程访问共享数据时,保证共享数据在同一个时刻只被一个线程使用。互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。

synchronized原理:synchronized关键字被编译成字节码后会被翻译成monitorenter和monitorexit两条指令,分别在同步块逻辑代码的起始位置与结束位置。这两个指令都需要一个refence对象来指明要锁定和解锁的对象。加锁方式:

同步实例方法:锁是当前实例对象
同步类(静态)方法:锁是当前类对象
同步代码块:锁是括号里的对象

锁控制原理:根据虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果当前对象没被锁或者当前线程已经拥有了当前对象的锁,把锁的计数器加1,相应的monitorexit会减一。计算器为0时,锁会被释放。如果获取对象锁失败,当前线程就会阻塞,知道对象锁被另一个线程释放为止。

synchronized特点
可重入:synchronized同步块对一条线程来说是可重入的,不会出现自己把自己锁死。
阻塞:synchronized同步块在已进入的线程执行之前,会阻塞其他线程的进入。
性能差:java线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态,因此状态转换是需要耗费很多的处理器时间。线程转换消耗的时间可能比用户代码执行的时间还要长。(加锁、用户态核心态转换、维护锁计数器和检查是否有阻塞线程需要唤醒等操作。)所以synchronized是java语言中一个重量级的操作。
优化:JVM内置锁在jdk1.5后做了重大优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等 技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。详见后边锁优化

monitor加锁过程

synchronized由于是给对象加索,如果想要跨方法加锁,可以使用unsafe给对象跨方法加montor锁,等于sync关键字。

ReentrantLock锁:除了synchronized以外,还可以使用java.util.concurrent包下的重入锁ReentrantLock来实现同步。详见JUC-ReentrantLock文章AQS-ReentrantLock

2.2. 非阻塞同步

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题。随着硬件指令集的发展,另一个选择:基于冲突检测的乐观锁并发策略。先进行操作,如果没有其他线程争用共享数据,则操作成功;如果共享数据有争用,产生了冲突,那就采取其他的补充措施(最常见的就是不断地重试,直到成功为止)。这种乐观锁的并发策略不需要把线程挂起,因此这种同步操作称之为非阻塞同步。

前提:需要操作和冲突检测这两个步骤保证原子性,也就是只通过一条指令就能完成。常见指令包括:

  • 测试并设置(Test and Set)
  • 获取并增加(Fetch and Increment)
  • 交换(Swap)
  • 比较与交换(Compare and Swap)
  • 加载链接/条件存储(Load Linked/Store Conditional)

乐观锁并发策略实现之CAS详见文章乐观锁之CAS-ABA

2.3. 无同步方案

同步只是保证共享数据争用时正确性的手段,如果一个方法本来不涉及共享数据,则不需要同步措施来保证其正确性,存在一些代码天生安全。
可重入代码:如果一个方法,他的返回结果是可以预测的,只要输入了相同的数据,就能返回相同的结果,就满足可重入的要求,也就是线程安全的。特点:不依赖堆上的数据和公用的系统资源,用到的状态都是参数传入,不调用非可重入方法等。
线程本地存储:如果一段代码所需要的数据必须与其他代码共享,查看这些共享数据的代码能否保证在同一个线程中执行,如果能保证,就可以把共享数据的可见性范围限制在同一个线程内,即无须同步也能保证线程之间不出现数据争用的问题。例如:消费队列架构模式(生产者-消费者)都会将产品的消费尽量在一个线程中消费完,最重要的一个应用实例就是经典web交互模型中的”一个请求对应一个服务器线程“的处理方式(java.lang.ThreadLocal实现线程本地存储功能)

3. 锁优化

3.1 锁的膨胀升级过程
无锁
-> 偏向锁(单一线程反复执行,偏向锁不会自动释放,只有其他线程竞争才会升级)
-> 轻量级锁(线程交替运行,竞争不是很激烈,占用线程时间短,多线程,CAS抢占共享数据,其余线程占cpu采取自旋(默认10次,可修改-XX:PreBlockSpin)/自适应自旋(根据上一次在同一个锁自旋时间及锁的拥有者状态决定自旋时间)抢占共享数据,如果其余线程自旋一直没有抢占到锁,也就是某线程一直没有结束
-> 重量级锁(互斥量monitor)

问题:每个synchronized加锁在对象上,对象是如何记录的锁状态呢?
锁状态是记录在每个对象的对象头上
对象的内存结构:

对象头Mark Word:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等
对象实际数据:即创建对象时,对象中成员变量,方法等
对齐填充:对象的大小必须是8字节的整数倍

对象内存结构

Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。
如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

mark word

偏向锁
      偏向锁是Java 6之后加入的新锁,它的目的是消除数据在无竞争情况下的同步锁,进一步提升性能的运行程序。
      经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提高程序的性能。
      对于没有锁竞争的场合,偏向锁有很好的优化效果。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,偏向锁失败后,升级为轻量级锁。

轻量级锁
      轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁

3.2 锁消除
Java虚拟机在JIT编译时,对一些代码要求同步,实际检测不可能存在共享数据竞争的锁进行消除。
1)如StringBuffer的append是一个同步方法,但如果某方法的StringBuffer属于一个局部变量, 并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
2)或如果在一段代码中,堆上所有数据都不会逃逸出去从而被其他程序访问到,就可以被当做栈上数据对待,认为是线程私有的,同步加锁也无须进行。

逃逸分析
使用逃逸分析,编译器可以对代码做如下优化:
(1).同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
(2).将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远 不会逃逸,对象可能是栈分配的候选,而不是堆分配。
(3).分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问 到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

是不是所有的对象和数组都会在堆内存分配空间?
不一定,如果实例对象存在线程逃逸行为,Object实例对象可能存在堆上也可能存在栈上。
在Java代码运行时,通过JVM参数可指定是否开启逃逸分析 。XX:+DoEscapeAnalysis : 表示开启逃逸分析 ­XX:­-DoEscapeAnalysis : 表示关
闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定­XX:­ DoEscapeAnalysis

3.3 锁粗化
一般同步块的作用范围会限制的尽量小,只有在共享数据的实际作用域变小,这是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,等待的锁也可以尽快拿到锁。
大部分情况下,以上原则都是正确的,但是存在一种一系列操作都是对同一个对象反复加锁和解锁,甚至在同一个循环体中这样处理,即这种情况平安的同步操作也会导致不必要的性能损耗。
如存在共享资源锁竞争的情况,一串零碎的操作都会对同一个对象加锁(StringBuffer.append),虚拟机会把加锁同步的范围粗化到整个才做序列外,即粗化到第一个append操作前和最后一个append操作后,加锁一次就可以了。

JVM锁的膨胀升级图

相关文章

  • 第13章 线程安全与锁优化

    第13章线程安全与锁优化 13.2线程安全 13.2.2线程安全的实现方法 1.互斥同步 互斥同步(Mutual ...

  • 11.线程安全与锁优化

    线程安全与锁优化 1. 线程安全 按照线程安全的安全程度由强到弱排序,Java中各种操作共享数据分为以下5类:不可...

  • Java虚拟机总结给面试的你(下)

    本篇博客主要针对Java虚拟机的晚期编译优化,Java内存模型与线程,线程安全与锁优化进行总结,其余部分总结请点击...

  • 线程安全与锁优化

    线程安全 笔者认为《JavaConcurrency In Practice》的作者 Brian Goetz 对 “...

  • 线程安全与锁优化

    1 线程安全 当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,...

  • 线程安全与锁优化

    线程安全 Java语言中的线程安全 线程安全的“安全程度”由强至弱来排序,将Java语言中的各种操作共享的数据分5...

  • 线程安全与锁优化

    synchronized的原理(非公平的) synchronized会在代码前后形成了mointorenter和m...

  • 线程安全与锁优化

    一、线程安全的实现方法 (一)互斥同步 互斥是实现同步的一种手段,临界区(Critical Section)、互斥...

  • 线程安全与锁优化

    一、线程安全 定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外...

  • 线程安全与锁优化

    什么是线程安全 过往在使用synchronized关键字的时候,通常都会和线程安全问题相挂钩。那么这个线程安全的定...

网友评论

      本文标题:线程安全与锁优化

      本文链接:https://www.haomeiwen.com/subject/gxnrwctx.html