栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

java虚拟机-线程安全与锁优化

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

java虚拟机-线程安全与锁优化

这里写目录标题

线程安全

java中的线程安全线程安全的实现 锁优化

自旋锁和自适应锁锁消除锁粗化

轻量级锁偏向锁

线程安全

多线程访问对象时,如果不用考虑线程的调度和交替执行,不需要额外的同步(不需要调用方做额外的操作),调用这个对象的行为都能得到正确的结果,那么这个对象就是线程安全的。总而言之,代码本身封装了所有必要的正确性保障手段,无需调用者担心。

java中的线程安全

java语言把各种操作共享的数据分为五类:不可变,绝对线程安全,相对线程安全,线程兼容,线程对立。

不可变:不可变的对象在多线程不存在不一致情况。如果共享数据是基本数据类型,则用final修饰即可。如果共享数据是对象,则需要保证对象行为不会对其状态产生影响,最简单的方法就是把带有状态的变量都声明为final,例如String类,它的subString(),replace()方法都不会改变原来的对象,只会返回一个修改以后的对象。除此之外,Integer包装类,也是不可变的.绝对线程安全:java API中标注自己是线程安全的类,大多数都不是绝对线程安全,因为完全满足线程安全的定义,通常需要付出很大的代价,一般都需要调用者做一些操作,即使是java.util.Vector这样,每个方法都被 synchronized修饰的的类,也需要调用者进行一些操作,才能保证绝对线程安全。相对线程安全:这就是我们通常意义上所说的线程安全,它保证这个对象单独操作时是安全的,对于一些特定顺序的的连续调用,就需要我们在调用端做一些保障操作,例如一些有类似于读写(remove和get)操作的类,我们需要外部保证它不能同时执行 put 和get 操作(线程中给对象引用加 synchronized修饰),防止出现一个线程移除第1个元素后,另一个线程get 第一个元素,导致空指针异常。线程兼容:指对象本身不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中安全的使用。我们平时说一个类不是线程安全的,就是这种情况。例如与Vector和HashTable对应的集合类ArraList,HashMap。线程对立:指不管调用端是否采用同步措施,都无法在多线程环境中并发使用的代码。在java中,这种排斥多线程的代码是很少出现的,通常都是有害的。 线程安全的实现

(一)互斥同步(悲观锁)
最常见的一种并发正确性保障手段。同步是指共享数据同一时刻只能被一条线程使用。互斥是实现同步的一种手段,临界区,互斥量,信号量都是实现互斥的主要方式。
(1)synchronize关键字
java里面,最基本的互斥同步手段就是 synchronize关键字,synchronized关键字在编译后,会在同步块前后分别形成 monitorenter 和 monnitorexit 两行字节码指令,这两个指令都需要一个reference类型的参数指明锁定和解锁的对象。java程序中 如果明确指定了对象参数,那这个就是对象的reference(例如 synchronied (a){}),如果没有指明,就根据 synchroniezd修饰的是实例方法还是类方法,去取对应的实例对象和class对象作为锁对象。
执行monitorenter指令时。会尝试获取对象的锁,如果对象没有被锁定,或者当前线程已经拥有锁了,则锁计数器加一,执行monitorexit指令时,锁计数器减一,计数器为0,则锁被释放。对象获取锁失败,则阻塞等待,直至另外一个线程释放。
注意:
synchronized 同步块,对于同一个线程来说,是可重入的,不会把自己锁死。
同步块执行完之前,不允许其他线程进入。

synchronize修饰方法时,表示某个线程执行到该方法时会锁定当前对象,其他线程不可以调用该对象中含有synchronized关键字的方法,因为这些线程的这些方法要执行前提是要获得该对象的锁。

(2)ReenttrantLock
除了synchronized关键字之外,还有 java.util.concurrent(JUC)包中的重入锁(ReenttrantLock) 实现同步。

ReentrantLock与synhronized很相似,只是它表现为API层面,使用lock()和unlock方法配合try 和finally语句完成。

比synchronized多了三个功能:

等待可中断:当持有锁的线程长时间没有得到锁,正在等待的线程可以选择放弃,改为处理其他事情可实现公平锁:按申请顺序获得锁锁可以绑定多个条件:一个reenttrantLock对象可以同时绑定多个Condition对象。

关于lock和condition

https://blog.csdn.net/qqq3117004957/article/details/104566938?spm=1001.2101.3001.6650.3&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-3.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-3.pc_relevant_default&utm_relevant_index=6

在性能上,lock和synchronized基本持平。但是也有差别,这个可以自行查阅。

(二)非阻塞同步(乐观锁)
互斥同步最大的问题,就是进行线程阻塞与唤醒带来的线程问题。这是悲观锁策略,认为不进行同步,就会出问题,所以悲观锁,无论共享数据是否真正出现竞争,都会进行加锁(虽然虚拟机会优化一部分没必要的加锁),用户态核心态转换,维护锁计数器,检查是否有线程需要唤醒等等。

随着指令集的发展,出现了基于冲突检测的乐观并发策略,先进行操作,没有其他线程争用数据,则操作成功,否则进行补偿措施(一般就是不断重试)。

之所以需要随着指令集发展,是因为操作和冲突检测需要进行原子操作,这个只能靠硬件来完成,毕竟不能用互斥同步来实现吧。硬件保证“多次操作,只用一条指令”这样的语义。这类指令常用的有:

测试并设置交换比较并交换(Compare and Swap 就是我们熟知的 CAS )加载链接/条件储存(load link、store condtianal)

我们熟知的乐观锁就是通过 CAS来实现。CAS指令(不是方法)需要三个操作数,内存地址 V,旧的预期值 A,新值 B。当 V符合 A时,才会用B更新 V,否则不执行更新。但是不管是否执行更新,该操作返回的是V的旧值。上面的过程是原子操作。

JDK1.5后,java程序中才可以使用 CAS操作,由sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等方法提供包装。但是Unsafe类不提供给用户程序调用,只有启动类加载器加载的class才能访问它。不采用反射的话,只能用其他javaApi间接使用它,例如J.U.C包里的整数原子类,例如AtomicInteger,可以用来替换int。

当然,乐观锁,有著名的ABA问题。这就看具体使用场景,是用乐观锁还是悲观锁。

(三)无同步方案

可重入代码线程本地储存:把共享数据代码控制在一个线程之内,就不存在同步问题了。这里有我们熟悉的 threadLocal(使得变量被线程独享) 锁优化 自旋锁和自适应锁

互斥同步对性能最大的影响就是阻塞的实现,挂起线程和恢复线程都需要转入内核态,这给系统的并发性能带来了很大压力。

自旋锁就是系统有一个以上处理器,让两条或者两条以上处理器并行执行,让后面请求锁的线程等一会,但不放弃处理器的执行时间,处于一个忙循环(自璇)。

自璇等待虽然避免了线程切换的开销,但是也占用处理器时间,锁占用时间段的话,非常高效,反之则造成处理器资源的浪费。

JDK1.4.2引入自璇锁,默认关闭。JDK1.6,默认开启,但是没有取代阻塞等待,自璇时间超过一定时间,则采用传统方式挂起线程,有参数可以设置,默认自璇十次。JDK1.6中还引入了自适应自旋锁,自璇时间不再固定,而是由前一次自璇时间和锁的拥有者决定这次自璇时间。

锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁消除。主要判断来源与逃逸分析的数据支持(就是堆上所有数据不会逃逸出去被其他线程引用到)

锁粗化

原则上,我们编写代码,会把锁的范围划得非常小,这样有利于等待线程尽快的拿到锁。
大部分情况下,这样的原则是正确的。但是,如果一系列的操作都对同一个对象反复加锁解锁,甚至加锁操作出现在循环体中,这样即使没有线程竞争,进行互斥同步操作也会导致不必要的性能损耗。
虚拟机检测到这样一串零碎的操作对同一个对象加锁,就会把加锁的同步范围扩展到整个序列外部(用循环体中加锁举例,就是把锁粗化到循环体外),这样一系列对同一个对象加锁,就会被优化成只加一次锁。

轻量级锁

偏向锁是JDK 1.6中加入的新机制。
重量级锁使用操作系统的互斥量来实现,会有性能消耗。而轻量级锁,就运用于每月多线程竞争的前提下,减少传统锁带来的性能消耗。

要理解轻量级锁,首先要理解HotSpot虚拟机的对象内存布局。HotSpot虚拟机的对象头分为两部分,第一部分储存对象运行时的数据(哈希码,GC年龄分代等),运行在32位和64位虚拟机中分别占32个和64个bits,官方称为Mark Word,是轻量级锁和偏向锁实现的关键。另一部分就是指向方法区的对象类型数据指针,如果是数组对象,则还有一个部分储存数组长度。

对象头信息是与对象自身定义的数据无关的储存成本,所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间储存极多的信息。例如32位虚拟机的未锁定对象(根据锁定状态不同,Mark Word储存信息也不同)Mark Word,25Bits储存哈希码,4Bits储存年龄,2Bits储存锁标志位,1Bits固定为0.

轻量级锁执行过程:代码进入同步块的时候,如果同步对象没有被锁定(01),则虚拟机在当前栈帧中建立一个名为 锁记录(lock record) 的空间,用于储存对象目前 Mark Word 的拷贝(displace Mark Word)。然后虚拟机使用CAS操作(说明轻量级锁是非阻塞同步)尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。
操作成功,则线程拥有锁,把 Mark Word锁标志位改为 00。
操作失败,则先查看对象的 Mark Word是否指向本线程的栈帧,是则说明该线程已经拥有锁,可以直接进入同步块。否则说明该锁对象被其他线程抢占了。如果两条以上线程抢占锁,那轻量级锁就失效,膨胀为重量级锁,状态变为 10。只有 Mark Word中指向重量级锁,那等待锁的进程就要进入阻塞状态了。

轻量级锁解锁过程:通过CAS操作进行,如果对象Mark Word仍然指向线程的lock record ,就利用CAS操作把对象当前的 Mark Word和复制的 displace Mark Word 替换回来,替换成功,则同步过程完成。否则,说明其他线程尝试过获取该锁(就是锁状态位改变了),释放锁的同时,唤醒被挂起的线程。

轻量级锁的依据是:绝大部分锁,整个同步周期内是不存在竞争的。如果不存在竞争,则省去了同步量的开销。如果存在,则额外增加了CAS操作,比悲观锁更慢。

偏向锁

偏向锁是JDK 1.6中引入的新的锁优化。
如果虚拟机启动用了偏向锁(有相关参数可以设置),那么锁对象第一次被线程获取的时候,虚拟机会将对象头的标志位设为 01 ,偏向模式。同时使用CAS操作把获取到这个锁的线程 ID记录在对象的Mark Word中,如果CAS成功,则持有偏向锁的线程,之后可以直接进入这个同步锁相关的同步块时(就是同一个锁的所有同步块),不在进行任何同步操作。
当有另外一个线程尝试获取锁时,偏向模式宣告结束。根据锁对象是否处于锁定状态,撤销偏向(恢复到未锁定 01)或者偏向锁(00)状态。

偏向锁可以提高有同步,但无竞争的程序性能。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/782285.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号