java多线程-线程安全与锁优化(二)

java多线程-线程安全与锁优化(二)

线程安全

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。

线程安全问题都是由全局变量及静态变量引起的。 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

局部变量不存在线程安全问题,因为局部变量存储在java栈内存中,java内存栈是线程私有的.
全局变量及静态变量存储在java方法去,线程共享区

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

  • 不可变:申明为final,这就是不可变。等等
  • 绝对线程安全:比如加上synchronized。
  • 相对安全:类如:vector集合,HashTable之类的。
  • 线程兼容:比如ArrayList,HashMap是线程不安全的,但是可以通过代码手段使之正确使用。
  • 线程对立:是指无论如何都无法保证同步操作,这种java里面很少有这种情况,也不应该有。

使用先行发生原则。查看如下链接
先发性原则

线程安全的实现方法

互斥同步(同步的原理)

同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用。而互斥是实现同步的一个手段,临界区(Critical Section),互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。因此,在这里互斥是因,同步是果;互斥是方法,同步是目的。

java里面最基本的互斥手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个引用(reference)类型的参数来指明要锁定和解锁的对象。如果使用时没有指定则根据synchronized修饰的是实例方法(取对象实例)还是类方法(Class实例,如静态方法)分别取对应的锁对象了。

根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

在虚拟机规范对monitorenter和monitorexit的行为描述中,有两点是需要特别注意的。
- 首先,synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
- 其次,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进人。

对象头

锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。
请参看jvm-虚拟机内存管理机制中的对象头结构。

方法可重入

一个方法被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该方法,这称为重入。
一个方法访问一个全局属性,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个方法只访问自己的局部变量或参数,则称为可重入方法

可重入方法与线程安全:

  • 一个方法对于多个线程是可重入的,则这个方法是线程安全的。
  • 一个函数是线程安全的,但并不一定是可重入的。
  • 如果一个函数中用到了全局或静态变量,那么它不是线程安全的,也不是可重入的;如果我们对它加以改进,在访问全局或静态变量时使用互斥量或信号量等方式加锁,则可以使它变成线程安全的,但此时它仍然是不可重入的,因为通常加锁方式是针对不同线程的访问,而对同一线程可能出现问题;
  • 如果一个方法当中的数据全部来自栈空间的,则这个方法即是线程安全也是可重入的。
  • 如果将对临界资源的访问加锁,则这个函数是线程安全的;但如果重入函数的话加锁还未释放,则会产生死锁,因此不能重入。
  • 线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作不影响结果,使结果是相同的。

锁优化

这里说的锁优化是说的jdk锁优化的方法理论,我们开发过程中使用时只需要调用Synchronized关键字就OK了,其他我们不需要关心,这里的锁优化效果也直接反应到Synchronized的执行效率上面。
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,所以这里说的锁优化也是Synchronized锁的优化,这里的锁优化也是jdk底层的锁优化。

Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

jdk1.7与1.6中的实现方式不一样哦!不过这下面讲的是理论,具体实现得看代码了。
偏向锁-》轻量级锁-》重量级锁。

偏向锁

比轻量锁更轻的一种锁,谁先第一个获取到锁,就偏向谁。

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

偏向锁获取过程:

  1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
  2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
  5. 尝试使用CAS将对象头的偏向锁指向当前线程

偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

关闭偏向锁:偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟-XX:BiasedLockingStartupDelay = 0。如果你确定自己应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁-XX:-UseBiasedLocking=false,那么默认会进入轻量级锁状态。

轻量锁

轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁解锁:轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

自旋锁与自适应自旋

从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。

上面说到的互斥同步对性能影响最大的是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来很大的压力。
在实际应用中,共享数据的锁定状态只会持续很短的时间,为了这个很短的时间挂起恢复线程明显不值得。

自旋锁,说白了就是,m方法添加了互斥同步synchronized锁,A线程先获取到m方法的锁,B线程执行执行m方法时被阻塞了,B线程被阻塞后线程就会被挂起等待A线程释放锁后才能恢复线程,B线程挂起恢复操作很消耗时间,虚拟机开发团队就弄了一个忙循环(自旋)B线程不用挂起,B线程在调用m方法时发现被阻塞了,就在m方法门口自我循环等待,等待A线程释放锁了,B就直接进入m方法了。B线程在等待的这个过程就为自旋。
自适应自旋,就是一个自旋锁的优化。自旋时如果A线程半天不出来释放锁,B线程又在一直自旋(自旋的过程中会占用CPU资源,而线程挂起不占用CPU资源),如果自旋时间过长了则达不到优化的效果,自旋优化的效果就是为了自旋等待时间<线程挂起时间。如果自旋时间过长则效果不如挂起,所以自适应自旋就出来了,自适应自旋会记录其他线程操作m方法的平均锁定时间,然后决定是不是要自旋还是挂起。

参看文献:

http://www.infoq.com/cn/articles/java-se-16-synchronized
http://www.cnblogs.com/paddix/p/5405678.html
《深入理解java虚拟机》

猜你喜欢

转载自blog.csdn.net/piaoslowly/article/details/81460026
今日推荐