JVM:线程安全与锁优化(十三)
Posted 漫步君
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM:线程安全与锁优化(十三)相关的知识,希望对你有一定的参考价值。
程序编写都是以算法为核心,程序员把数据和过程分别作为独立的过程来考虑,数据代表问题空间中的客体。程序代码则用于处理这些数据,这种思维方式直接站在计算机的角度去抽象问题和解决问题,称为面向过程的编程思想。
与之相对的是,面向对象的编程思想是站在现实角度去抽象和解决问题,它把数据和行为都看作是对象的一部分,这样可以让程序员能以符合现实世界的思维方式来编写和组织程序。
线程安全:当多个线程访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
我们可以将Java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立。
1.不可变:不可变的对象一定是线程安全的(使用final修饰),不可变带来的安全性是最简单和最纯粹的。
2.绝对线程安全:付出的代价很大、甚至不切实际。
3.相对线程安全:也就是我们通常意义上所讲的线程安全,他需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保护措施,但是对于一些特定顺序的连续调用,皆可能需要在调用端使用额外的同步手段来保证调用的正确性。例如Vevtor、HashTable、Collections的synchronizedCollection方法包装的集合等。
4.线程兼容:对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全的使用,我们平常说的一个类不是线程安全的,绝大多数指的都是这种情况,如HashMap、ArrayList等
5.线程对立:无论调用端是否才采取了同步措施,都无法在多线程环境中并发使用的低吗。由于Java语言天生具备多线程特性,所以这种代码很少见,而且都是有害的,应尽量避免。如Thread的suspend和resume。
2.线程安全的实现方法
主要分析虚拟机如何实现同步锁,了解了虚拟机线程安全手段的运作过程,再去思考如何编写代码来实现线程安全也并不是什么坏事。
2.1互斥同步:
互斥同步是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用。互斥是实现同步的一种手段,在Java中最基本的互斥同步手段就是使用synchronized关键字。
synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,虚拟机在执行monitorenter指令时首先尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,则把锁的计数器加1;相应的,在执行monitorexit指令时会将锁的计数器减1;当计数器为0时锁就会被释放。如果获取对象锁失败,那么当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
Java内置锁是一个互斥锁,每个Java对象都可以用作一个实现同步的锁,这些锁被称为内置锁。线程进入同步代码块的方法时会自动获得该锁,在退出同步代码块时会释放该锁。获得内置锁的唯一途径是进入这个锁所保护的同步代码块或方法。
synchronized同步块对同一个线程时可以重入的,不会出现把自己锁死的问题;同步块在已进入的线程执行完之前,会阻塞后面其他的线程进入。
除了synchronized之外,还可以使用JUC包中的重入锁(ReentrantLock)来实现同步。他与synchronized很相似,都具备线程重入特性;区别是重入锁是API层面的互斥锁(lock和unlock配合finally完成),synchronized则是原生语法层面的互斥锁。
重入锁还有如下三个高级特性:等待可中断、可实现公平锁、锁可以绑定多个条件。
>等待可中断是指当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事。可中断特性对处理执行时间非常长的同步块很有帮助。
>公平锁是指多个线程在等待同一个锁时,必须按照申请所的时间顺序来一次获得锁;而非公平锁不能保证这一点。synchronized以及默认ReentrantLock都是非公平锁,但后者可以通过带布尔值的构造函数实现公平锁。
>锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait/notify/notifyAll可实现一个隐含条件。如果要和多于一个条件关联时,必须额外添加锁。而ReentrantLock只需多次调用new Condition即可。
这里的Condition是一种条件对象,实现了线程等待和通知机制。Condition在jdk1.5之后才出现,用来替代传统Object的wait和notify实现线程操作。Condition通过使用await和signal这种方式实现线程间协作更加高效,阻塞队列实际上是使用了Condition来模拟线程间协作的。在jdk1.6以上两种锁性能差不多,未来虚拟机改进会逐渐优化原生synchronized所以提倡在synchronized能实现需求时尽量优先选择使用。
2.2非阻塞同步
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,又称阻塞同步,属于悲观并发。另一种是基于冲突检测的乐观并发策略:即先进行操作,如果没有其他线程争用共享数据,那么操作就成功了;如果共享数据有争用、产生冲突,就采取其他补救措施(最常见的是不断重试,直到成功为止)。这种乐观并发策略的许多实现都不需要把线程挂起,所以称为非阻塞同步(基于硬件的CAS指令,jdk1.5之后才可以使用)。
2.3无同步方案
要保证线程安全,并不是一定就要进行同步,两者并不存在因果关系。同步只是保证共享数据争用时的正确性手段,如果硬方法不涉及共享数据,那么自然无需任何同步操作。
3.锁优化
高效并发是jdk1.5到jdk1.6的一个重要改进,实现了各种锁优化技术,如:适应性自旋、锁消除、锁粗化、轻量级锁、偏向锁等。
3.1自旋锁与适应性自旋:
互斥同步对性能最大的影响是阻塞的实现,挂起、恢复线程给系统并发带来很大压力。若共享数据锁定状态只持续很短时间,挂起、恢复就很不值得。因此如果物理机有一个以上的处理器能让两个或两个以上线程同时并行执行,我们还可以让后面请求锁的线程“稍等一下”但不放弃处理器时间。为了实现这一点只需让线程执行一个忙循环(自旋),这就是自旋锁(在jdk1.6中默认开启)。自旋等待不能代替阻塞,锁便面了线程切换开销,但仍需占用处理器时间,自旋次数默认是10次。jdk1.6中引入了适应性自旋锁,由前一次在同一个锁上的自旋时间即拥有者状态来决定:自旋成功次数多,则允许自旋等待的持续相对更长的时间,反之缩短甚至圣罗自选过程直接阻塞。
3.2锁消除:
锁消除是指虚拟机即时编译器在运行时对一些代码上要求同步、但被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据来源是逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他的线程访问到,那么就认为他们是线程私有的,同步加锁自然无需进行(虽然也代码层面看不到锁,但底层代码中应用了大量的锁如StringBuffer.append())。
3.3锁粗化:
原则上我们总是将同步块的作用范围限制的尽量小(值在共享数据实际作用域才进行同步),大部分时间这个原则都正确,但特殊嗜好如果一系列连续操作都对同一个对象反复加锁、解锁,甚至都是出现在循环体中,那即使没有现成竞争,频繁的家解锁、进行互斥同步操作也会导致不必要的性能损耗。因此可以把锁粗花道整个操作序列外部只需加锁一次,如StringBuffer中第一个append之前到最后一个append之后。
3.4轻量级锁:
轻量级锁是jdk1.6之后加入的新型锁机制。传统的使用操作系统互斥来实现的锁成为重量级锁。轻量级锁不是来代替重量级锁的,本意是在没有多线程竞争的前提下减少传统重量级锁的使用操作系统互斥量产生的性能消耗。轻量级锁能提升程序同步性能的一句是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”这以经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销;但如果存在锁竞争,除了互斥量开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统重量级锁更慢。
3.5偏向锁:
偏向锁也是jdk1.6中引入的锁优化。目的是消除数据在无竞争情况下的同步原语,进一步提高性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除,包括CAS。这个锁会偏向于第一个获得他的线程,如果再接下来的执行过程中,该锁没有被其他线程获取,则蚩尤偏向锁的线程不虚在进行同步操作;当有另外一个线程去尝试获取这个锁时,偏向模式结束。
随笔,是记忆的一种延伸
以上是关于JVM:线程安全与锁优化(十三)的主要内容,如果未能解决你的问题,请参考以下文章