线程安全与锁优化

阅读《深入理解Java虚拟机-JVM高级特性与最佳实践》.周志明 笔记

线程安全的实现方法:

1.互斥同步

互斥同步是一种常见的并发正确性保障手段。同步是指在多个并发线程访问共享数据时,保证共享数据在同一时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。因此,在这4字里面,互斥是因,同步是果,同步是目的。

Java中,最基本的互斥同步手段就是synchronized关键字。synchronized关键字经过编译之后,会在同步块的前后分别形成monitorentor(根据虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象的锁)和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定或解锁的对象。如果Java程序中明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或者Class对象来作为锁对象。

为什么说synchronized的效率低?跟Java的多线程实现有关系,Java的线程是映射到操作系统上的原生线程之上的,如果要阻塞或者唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多处理器时间。对于简单的同步块(如被synchronized修饰的getter()或setter()方法),状态转换消耗的时间有可能比用户代码执行时间还长。所以synchronized是Java语言中一个重量级的操作。

除了synchronized之外,JDK中提供了java.until.current(下边简称J.U.C)包中的重入锁(ReentrantLock)来实现同步。在用法上,ReentrantLock与synchronized很相似,都具备一样的线程重入特性,只是代码的写法上有点区别,一个表现为API层面上的互斥锁(lock()和unlock()方法配合try/finally语句块来完成),另一个表现为原生语法层面的互斥锁。不过,相比synchronized,ReentrantLock增加了一些高级工能,主要有3项:等待可中断、实现公平锁、锁可以绑定多个条件:

  • 等待可中断是指当持有的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对于处理执行时间非常长的同步块很有帮助;
  • 公平锁(先来后到)是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但是可以通过带有参数的构造函数要求使用公平锁。
  • 锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多余一个条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无需这样做,只需要多次调用newCondition()方法即可。

多线程环境下synchronized的吞吐量下降的非常严重,而ReentrantLock则能基本保持在同一个比较稳定的水平。与其说ReentrantLock性能好,还不如说synchronized还有非常大的优化余地。后续的技术也证明了这一点,JDK1.6中加入很多针对锁的优化措施,JDK1.6发布之后,人们就发现synchronized与ReentrantLock的性能基本上完全持平了。因此,如果读者的程序是使用JDK1.6或以上部署的话,性能因素就不再是选择ReentrantLock的理由了,虚拟机能实现需求的情况下,有限考虑使用synchronized。

-----引自 《深入理解Java虚拟机-JVM高级特性与最佳实践.第二版》第13章(P392)

希望有路过的大佬,对于上边这段骚粉色的内容给予验证品论,然后留言

2.非阻塞同步

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。从处理问题的方式上,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是都真的出现竞争,都要进行加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。随着硬件指令集的发展(后边解释),有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程征用共享数据,那就操作成功了;如果共享数据有竞争,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,知道成功为止),这种乐观锁的并发策略的许多实现都不需要把线程挂起来,因此这种同步操作称为非阻塞同步。下面说说硬件发展带来的提升:

因为需要操作和冲突检测这两个步骤具备原子性,如果这里再使用互斥同步来保证就失去意义了,所以只能靠硬件来完成这件事情,硬件保证一个从语义上看起来需要多次操作的行为只通过一次处理器指令就能完成,之类指令常用的有:

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

3.无同步方案

要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据库争用时的正确的手段,如果一个方法根本就不涉及共享数据,那它自然就无需任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。书中介绍了两类:

可重入代码(Reentrant Code):这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。相对多线程安全来说,可充重入性是更基本的特性,它可以保证先后才能安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全代码都是可重入的。可冲入代码的共性有:不依赖存储在堆上的数据和共用的系统资源、用到的状态都由参数中传入、不调用非可重入的方法等。有简单的办法可以判断代码的可重入性:如果一个方法,它的结果是可预测的,只要输入相同的数据,就都能返回相同的结果,那它就能满足可重入性的要求,也就是线程安全的;

线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,就无需同步也能保证线程之间不出现数据争用的问题了。

锁优化

高效并发是从JDK1.5到JDK1.6的一个重要改进,HotSopt虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如:适应性锁(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等,这些技术都是为了在线程之间更高效的共享数据,以及解决冲突问题。

1.自旋锁与自适应自旋

互斥同步对性能最大的影响是阻塞的实现,挂起线程恢复线程的操作需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。开发团队注意到在很多应用上,共享数据的锁定状态只会持续很短一段时间,为了这段时间去挂起和回复线程并不值得。所以,如果物理机有一个以上的处理器,能让两个或以上的线程并行执行,就可以让后请求锁的线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,只需要让线程执行一个忙循环(自旋),这项技术就是自旋锁。

自旋锁在JDK1.4.2中引入,起初默认关闭,JDK1.6中默认开启了。自旋等待不等于代替阻塞,且不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋锁等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自选的线程只会白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须有一定限度,如果自旋超过了限定次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。次数可设置(-XX:PreBlockSpin)。

JDK1.6中引入了自适应的自旋锁。意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者状态来决定。

2.锁消除

锁消除是指在虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。如return str1+str2+str3;JDK1.5之前是使用StringBuffer.append()操作的(synchronized同步),但是之后就改为StringBuilder.append();

3.锁粗化

原则上,编码过程中,总是推荐将同步块的作用范围限制得尽量小——只在数据共享的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果锁竞争,那么等待的线程也能尽快拿到锁。但是,如果是一系列的操作需要对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中,那么即便是没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果遇到这种情况,就需要把加锁同步的范围扩张(粗化)到整个操作序列的外部。

4.轻量级锁

轻量级锁是JDK1.6之中加入的新型锁机制。轻量级锁是相对于传统锁(重量级锁)来说的,它并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。基于“Mark Word”(对象头信息)。

5.偏向锁

同轻量级锁一样,偏向锁也是在JDK1.6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都秀出掉,连CAS都不做了。

偏向锁的意思是,它会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁还没有被其他的线程获取,则持有偏向锁的线程将会永远不需要再进行同步(如Locking、Unlocking以及对MarkWord的Update等)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值