IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 第13章 线程安全与琐优化 -> 正文阅读

[Java知识库]第13章 线程安全与琐优化

13.1 概述

高效并发:首先保证并发的正确性,在此基础上来实现高效;

13.2 线程安全

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调度方进行任何其他的协同操作,调用这个对象的行为都可以获得正确的结果,那么称这个对象是线程安全的;
特征:代码本身封装了所有必要的正确性保障手段(互斥同步等),令调用者无需关心多线程下的调用问题,更无需自己实现任何措施来保证多线程环境下的正确调用;

13.1 Java语言中的线程安全

Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

1、不可变

  • 不可变的对象一定是线程安全的。
  • 定义修饰 final
  • 除了String,java.lang.Number的部分子类,如Long、Double等数值包装类、BigInteger和BigDecimal都是不可变的;
  • 但是 AtomicInteger和AtomicLong是可变的;

2、绝对线程安全
满足“不管运行时环境如何,调用者都不需要任何额外的同步措施”;

  • java.util.Vector是线程安全的容器;因为add、get、size都被synchronized修饰;效率不高,但保证了原子性、可见性和有序性;
  • 加入vector一定要做到绝对安全,那就必须在塔内布维护一组一致性的快照访问才行;每次对其中元素进行改动都要产生新的快照,这样要付出时间和空间成本很大;

3、相对线程安全

  • 通常意义上的线程安全;
  • 需要保证对这个对象单词的操作是线程安全的;在调用的时候不需要进行额外的保障措施;
  • 但对于一些特定顺序的连续调用,可能需要在调用端使用额外的同步手段来保证调用的正确性;
  • 如: Vector、HashTable、Collections的synchronizedCollections()方法包装集合等;

4、线程兼容

  • 是指对象本身并不是线程安全的;但可以通过在调用短正确的使用同步手段来保证对象在并发环境中可以安全的使用;
  • 如 vector和hashtable对应的 ArrayList和HashMap;

5、线程对立

  • 不管调用端是采取了同步措施,都无法在多线程环境中并发使用代码;
  • 很少出现、通常有害,要尽量避免的;
  • 如: Thread的 suspend()和resume()方法;
  • 两个线程 ,一个 suspend,一个resume,对象可能死锁;

13.2 线程安全的实现方法

1、互斥同步

  • 是一种最常见的也是最主要的并发正确的保障手段;
  • 同步:多个线程并发访问共享数据时,保证共享数据同一时刻制备一个线程使用;
  • 互斥:是实现同步的一种手段,临界区、互斥量、信号量都是常见的互斥实现方式。
  • 互斥是因、同步是果;互斥是方法、同不是目的;

在java里面,最基本的互斥同步手段就是synchronized关键字,块结构同步语法;

  • synchronized经过编译后,会在同步块前后形成 monitorenter和moniterexit两个字节码指令;
  • 她两都需要一个 reference类型的参数来指明要锁定和解锁的对象;
  • 如果synchronized指明了对象参数,那么这个对象的引用可以作为reference;
  • 如果没有明确指定,来决定是取代吗所在的对象实例 还是 对应的Class对象来作为线程要持有的琐;

根据《Java虚拟机规范》,

  • monitorenter,尝试获取对象的琐, 所得计数器加一;
  • monitoreixt, 琐计数器减一; 计数器为0,琐就释放了;
  • 如果获取对象锁失败,线程就被阻塞等待,知道请求锁定的对象持有他的线程释放为止;

得出两个关于synchronized的直接推论:

  • 被synchronized修饰的同步块对于同一条线程来说是可重入的;这意味着同步线程反复进入同步块也不会出现自己把自己锁死的情况;
  • 被synchronized修饰的同步块在持有琐的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入;

持有琐是一个重量级操作 Heavy-Weight;

  • Java线程是映射到操作系统原生内核线程之上的;很耗时
  • 自JDK5起, java.util.concurrent.locks.Lock接口成了Java的另一种全新的互斥同步手段;
  • 重入锁ReentrantLock是Lock接口最常见的一种实现;它与synchronized一样是可重入的;用法相似;增加了主要三项高级特性: 等待可终端、可实现公平锁、琐可以绑定多个条件:
    • 等待可中断: 正在等待的线程可以选择 放弃等待;
    • 公平锁: 多个线程正在等待同一个锁,必须按照申请琐的时间顺序来一次获得锁;synchronized是不公平的;reentrantLock默认情况是不公平的,但可以通过布尔值使用公平锁,使用公平琐,性能急剧下降;
    • 琐绑定多个条件:一个reentrantLock对象可以同时绑定多个Condition对象;多次调用newCondition方法即可;

JDK6或以上,性能不再是选择 synchronized或 reentrantLock的决定因素;
基于一下理由,推荐优先使用synchronized:

  • synchronized是Java语法层面的同步,清晰简单;
  • lock应该确保在finally块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不会释放持有的所;synchronized会自动释放锁;
  • Java虚拟机可以在线程和对象的元素居中记录synchronized中琐的相关信息;

2、非阻塞同步
互斥同步面临的主要问题: 进行线程的阻塞和唤醒所带来的性能开销; 所以又叫阻塞同步;悲观的并发策略;

非阻塞同步,无锁(Lock-Free)编程:基于冲突检测的乐观并发策略:不管风险,先进行尝试,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享数据被争用,产生了冲突,那进行其他补偿措施,最常用的就是不断重试,直到出现没有竞争的共享数据为止;

必须要求操作和冲突检测这两个步骤具备原子性; 只能靠硬件来完成,常用的的:

  • 测试并设置(Test-andSet)
  • 获取并增加(Fetch-and-Increment);
  • 交换(Swap);
  • 比较并交换(Compare-and-Swap;CAS);
  • 加载链接/条件存储(Load-Linked/Store-Conditional,LL/SC);

前三条是 20实际就已经存在于大多数指令集之中的处理器指令;后两条是现代处理器新增的;

  • IA64、x86指令集中有 cmpxchg指令完成CAS功能;SPARC-TSO用casa指令实现;

CAS指令需要有三个操作数:内存位置(内存地址V)、旧的预期值(A)、准备设置的新值(B);
当且仅当V符合A时,处理器才会用B更新V的值;否则他就不执行更新;
不管是否更新了V的值,都会返回V的旧值; 这是一个原子操作;

ABA问题:检测时和准备赋值时都是A,但期间可能被改过并改回来了;
方案:提供了一个带有标记的原子引用类 AtomicStampedReference,可以通过控制变量值的版本来保证CAS的正确性;

3、无同步方案

  • 同步与线程安全没有必然的联系:同步只是保障存在共享数据争用时正确性的手段;
  • 如果一个方法本来就不涉及共享数据,那他自然就不需要任何同步措施来保证正确性;

天生线程安全的代码:

  • 可重入代码(Reentrant Code): 纯代码,可以在代码执行的任何时刻终端他,转而去之心给另外代码,控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响;
    所有可重入的代码都是线程安全的,并非所有线程安全的的代码时可重入的;
    共同特征: 不依赖全局变量、存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等;
    简单原则:返回结果时可预测的,相同的输入,有相同对应的输出;

  • 线程本地存储(Thread Local Storage):一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行;
    消费队列的架构模式:Web交互模型的“一个请求对应一个服务器线程”;这种处理方式的管饭应用使得很多Web服务端应用都可以使用线程本地存储来解决线程安全问题;

Java中:如果一个变量要被多线程访问,可以使用volatile关键字声明为“易变的”;

13.3 琐优化

适应性自旋、锁消除、锁膨胀、轻量级锁、偏向锁等;

13.3.1 自旋锁和自适应自旋

互斥同步,对性能最大的影响是阻塞的实现,挂起线程和恢复线程都要转入内核态中完成;这些操作给Java虚拟机的并发性能带来了很大的压力;
共享数据的锁定状态只会持续很短一段时间;
多处理器,会让后面请求锁的线程 稍等一会; 让线程执行一个忙循环(自旋);

如果锁被占用的时间很短,自旋等待的效果很好;反之,自旋的线程只会白白浪费处理器资源;
自旋等待的时间必须有一定的限度;
如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统方法挂起线程;默认十次,也可以调参修改;

JDK6对自旋锁优化,引入自适应的自旋: 由前一次在同一个锁上的自选时间 及锁的拥有者的状态决定的;

13.3.2 琐消除

指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除;
主要判定依据: 逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把他们当作栈上数据对待,认为他们是线程私有的,同步加锁自然就无需再进行;

例子: StringBuffer 连续append

13.3.3 琐粗化

原则: 总是i推荐将同步块的作用范围限制的尽量小–只在共享数据的实际作用域中才进行同步;使得同步的操作数量尽可能变少,即使存在锁竞争,等待锁的线程也能尽快的拿到锁;

连续append:如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部; 第一个append之前 和 最后一个append之后,这样只要加一次锁就可以了;

13.3.4 轻量级琐

JDK6 加入的新型锁机制;
轻量级,相对于使用操作系统互斥量 来实现的传统锁而言的,传统的锁机制 称为重量级锁;
设计初衷: 在没有多线程竞争的前提下,减少传统的重量级锁使用 操作系统互斥量产生的性能小号;

首先要对 hotspot虚拟机的 内存布局有一些了解:
hotspot虚拟机的 对象头(Object Header) 分为两个部分:

  • 第一部分Mark Word:存储对象自身运行时数据,如哈希码HashCode、GC分代年龄等;
    这部分数据长度在 32位和 64位 Java虚拟机中分别占 32个或64个比特;
    是实现轻量级锁和偏向锁的关键。
  • 另一部分:用于存储指向方法去对象类型数据的指针,如果是数组对象,还有一个额外的部分用于存储数组长度;

markword 被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息;
32位 hotspot虚拟机对象头markword:
在这里插入图片描述

轻量级锁工作过程:

  • 在代码即将进入同步块的时候,如果此同步块对象没有被锁定(01),虚拟机首先将在当前现成的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前的mark word的拷贝(Displaced mark word);堆栈与对象头如下:
    在这里插入图片描述
  • 然后,虚拟机将使用CAS操作尝试把对象的mark word更新为指向lock record的指针;
    如果这个更新动作成功了,就表示该线程拥有了这个对象的锁,并且对象mark word的所标志位变为 00,表示此对象处于轻量级锁顶状态;此时堆栈与对象头如下:
    在这里插入图片描述
  • 如果上面更新失败了,就意味着至少存在一条线程与当前线程竞争过去该对象的锁。
    虚拟机首先会检查对象的mark wrod是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了; 否则就说明 这个锁对象已经被其他线程抢占了。
    如果出现两条以上的线程争用同一个锁的情况,那轻量级锁将不再有效,必须要膨胀为重量级锁,所标志位变为 10,此时mark word中存储的就是指向重量级锁的指针,后面等待锁的线程必须进入组赛状态;

上面描述的是轻量级锁的加锁过程,他的解锁过程也是同样经过CAS操作来进行的;
如果对象的mark word仍然指向线程的所记录,那就用 CAS操作把对象当前的 mark word和线程中复制的 Displaced markd word替换回来;
加入能够成功替换,那整个同步过程就顺利完成了;如果替换失败,则说明有其他线程尝试过获得该锁,就要在释放锁的同时,唤醒被挂起的线程;

轻量级锁能提升程序同步性能的依据是: 对于绝大部分的锁,在整个同步周期内都是不存在竞争的;
如果没有竞争,轻量级锁 便通过CAS操作成功避免了使用互斥量的开销; 但如果确实存在锁竞争,除了互斥量本身开销外,还额外发生了 CAS操作的开销;
因此有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢;

13.3.5 偏向锁

JDK6引入的一项优化措施;
目的:消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能;
如果说轻量级锁是在无竞争的情况下使用 CAS操作区消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了;

偏:这个锁会偏向于第一个获得他的线程;如果在接下来的执行过程中,该琐一直没有内其他线程获取,则持有偏向锁的线程将永远不需要再进行同步;

启用偏向锁(-XX:+User Biased Locking)
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标记设为 01,把偏向模式设为1,表示进入偏向模式。
同时使用 CAS操作把获取到这个锁的线程ID记录在对象的mark word中;如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关同步块时,虚拟机都可以不再进行任何同步操作(如加锁,解锁,以及对markword的更新操作等);
一旦出现另一个线程区尝试获取这个锁的情况,偏向模式马上宣告结束。

根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为 0),撤销后标志位恢复到未锁定(标志位01)或轻量级锁定(00)状态,后续同步操作就按照上面介绍轻量级锁那样去执行。
偏向锁、轻量级锁的状态转化及对象mark word的关系如图:
在这里插入图片描述

当对象进入偏向状态时,mark word大部分空间都用于存储持有琐的线程id了,这部分空间占用了原有存储对象 哈希码的位置?

  • Java里一个对象如果计算过哈希码,就应该一直保持该值不变,否则很多依赖对象哈希码的api都可能存在出错风险。
  • Object::hashCode():绝大多数对象哈希码的来源,返回对象的一致性哈希码(Identity Hash Code), 这个值强制保证不变的,通过在对象头中存储计算结果来保证第一次计算之后,再次调用该方法取到的哈希码值永远不会再发生改变。
  • 因此,当一个对象已经计算过一致性哈希码后,他就再也无法进入偏向锁状态了;
  • 而当一个对象当前正处于偏向锁状态,有收到需要计算其一致性哈希码请求时,他的偏向锁状态会被立即撤销,并且锁会膨胀为重量级锁;
  • 在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类里有字段可以记录非加锁状态(标志位01)下的 mark word,其中自然可以存储原来的哈希码;

偏向锁可以提高带有同步但无竞争的程序性能,但他同样是一个带有效益平衡 性质的优化,也就是说它并非总是对程序运行有利。
如果程序中大多数锁总是被多个不同的线程访问,那偏向锁就是多余的。
有时用(-XX:-UseBiasedLocking)来禁止偏向锁优化反而可以提升性能;

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-15 15:22:28  更:2021-08-15 15:23:16 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/20 10:12:49-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码