java线程安全与锁优化(三)

回顾:在前两篇文章中主要都是记录了线程安全相关概念与如何实现线程安全。在实现线程安全的过程中有一个反复提及的名词——锁,通过对一个对象或一个程序块、方法加锁让其在同一时间只能被一个线程操作从而达到线程安全。加锁虽然解决了数据共享和竞争的问题但也带来了很多其他问题:线程阻塞、死锁、程序执行效率低下等等。为了在线程之间更高效的共享数据、解决竞争从而提高程序执行效率,JVM对锁做了许多的优化。

问题:什么是锁

       一个很基本但不一定能回答的好的问题。通过阅读和学习《深入理解Java虚拟机》,对于什么是锁我有一些个人的理解(只是个人见解)。说到锁不得不提及一下java对象内存布局,在HotSpot虚拟中,对象在内存中的布局可以分为3个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

       对象头包括两部分信息:1.对象自身运行时数据(哈希码、GC分布年代、锁状态标志、线程持有的锁、偏向线程ID等) 官方称为"Mark Word";  2.类型指针

对于对象内存的详细信息在此不进行详述,但通过上面的信息可以知道,锁其实对象的一种状态,加锁(lock),释放锁(unlock)都是对这个状态的相关操作。

加锁、释放锁是java内存模型(JMM)提供的8种原子性操作里其中的两种,未直接开放给用户使用,但提供了更高层次的monitorenter和monitorexit来隐式的操作,从java代码中反映就是synchronized 关键字,因此synchronized操作也是原子性的。

问题:如何对锁优化

1.自旋锁与自适应自旋

         通过对synchronized的了解我们知道,synchronized关键字使用当前在获取锁的同时阻塞其他线程,导致其他线程挂起,在当前线程释放锁之后唤醒其他线程,在挂起与唤醒的过程中会涉及用户态与内核态的切换,也是主要影响性能的地方,特别是在同步代码执行时间比线程切换状态的时间短时。为了解决这种问题,在同步阻塞其他线程时,让其他线程执行一个忙循环(自旋),这样就能减少线程挂起与唤醒。

引申问题:如果锁一直未释放怎么办

自旋锁本身是为了解决锁占有时间少于线程挂起与唤醒状态转换时间的,如果锁一直占有,其他线程就会一直进行自旋,虽然减少了线程状态的转换但自旋本身是需要占用CPU时间的,因此自旋等待需要有一个时间或次数限制,超过了限制就使用传统的挂起方式。

引申问题:自旋的限制有没有标准

在jdk刚引入自旋锁时,默认值是10次,同时提供了相关命令和参数可以对其进行修改。但我们知道在一个程序中会有各种因素导致在实际的程序运行中同一个共享数据的锁的占用时间不固定,如IO延迟,数据库操作延时等。因此在jdk1.6中引入了自适应自旋,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定本次自旋的时间或次数。

2.锁消除

       在程序中有许多地方都可能会要求同步,但在实际的代码中,可能不存在共享的数据,例如 在一个方法中使用了StringBuffer 及相关操作,如appender(); 但StringBuffer对象并未被其他外部方法调用,因此这里不存在共享数据,但StringBuffer的appender()方法本身使用了同步,在这种情况下同步加锁就可以不用进行,可以安全消除掉。

3.锁粗化

      在使用同步加锁的时候,通常会尽量将加锁的范围缩小到只对共享数据操作的地方。示例:

public class JavaSynchronized {
	private static JavaSynchronized  jsy= null;

	private JavaSynchronized() {
		
	}
	
	public static JavaSynchronized instance(){
        //在方法体内部使用同步
		synchronized (JavaSynchronized.class) {
			if(null==jsy){
                //同一线程对锁重入
				synchronized (JavaSynchronized.class) {
					jsy = new JavaSynchronized();
				}
			}
		}
		return jsy;
	}
}

在上面的代码中,我们是获取一个单例对象,常规方式是构造方法私有化,创建一个全局类变量,暴露一个对外调用的方法,在方法使用同步保证对象只会创建一个。如果将同步放到方法上就扩大了同步的范围,使得整个方法都会被阻塞,放在方法内部只会对对象为空的情况进行阻塞。因此通常情况下,缩小同步的范围可以提升线程执行效率。凡事总有例外,让我们看下面的代码:

public class JavaSynchronized {
	private static List<Integer> list = new ArrayList<Integer>();
	
	public void add(Integer i){
		list.add(i);
	}
	
	public static void del(Integer i){
		synchronized (JavaSynchronized.class) {
			if(list.contains(i)){
				list.remove(i);
			}
		}
	}
	
	public static void main(String[] args) {
		for (int i = 0; i <10; i++) {
			del(i);
		}
	}
}

在del() 方法中有同步操作,在main方法中的一个循环体中调用了del()方法,这个时候每循环一次都会加一次锁,但在一个线程中的循环体是不存在竞争的这个时候反复加锁会导致不必要的损耗,因此我们完全可以把同步放在循环体外,减少性能损耗。在特定场景中将同步的范围扩大,这个方式就是锁粗化。

4.轻量级锁

      我们通常说synchronized 是重量级锁,那么轻量级锁就是针对重量级锁而言的,要理解重量级和轻量级锁还得从java对象的内存布局来看,前面已经简单提到了java对象的内存布局,我们现在来看一看Mark word具体的一个组成

存储内容标志位状态
对象哈希码、对象分代年龄01未锁定
指向锁定记录的指针00轻量级锁定
指向重量级锁的指针10膨胀(重量级锁定)
空,不需要记录信息11GC标志
偏向线程ID,偏向时间戳,对象分代年龄01可偏向

从表格中可以看出重量级与轻量级锁在Mark Word中其实是表现为两种不同的状态。

引申问题:轻量级锁能代替重量级锁吗?

      轻量级锁本身的出发点并不是代替重量级锁的,也无法代替重量级锁。轻量级锁主要是在没有多线程竞争的环境下减少传统生量级锁产生的性能消耗。

引申问题:轻量级锁如何减少重量级的开销(实现原理)?

       当一个程序中使用了同步块,并且程序执行进入到同步块时,没有轻量级锁的时候是直接在同步块前后增加monitorenter和monitorexit指令来获取与释放对象锁,在获取锁后阻塞其他线程。当有了轻量经锁时,首先会判断同步的对象有没有被锁定,如果是未锁定(01)状态,虚拟机会先创建一个锁记录空间用于记录一份锁对象的mark word 拷贝,然后使用CAS操作进行更新mark word指向到锁记录空间,如果更新成功则当前线程就拥有了该对象的锁。如果更新失败但表明当前线程已经拥有了锁则直接进入同步块继续执行,否则说明锁对象已经被其他线程抢占,表明有多线程竞争该锁对象,此时轻量级锁则不再有效,需要膨胀为重量级锁(10)状态,并且在获取到锁对象时后面等待的线程也要进入阻塞状态。

引申问题:轻量级锁一定能减少性能开销吗?

      在大多数情况下,一个同步周期内是不存在竞争的,这个时候通过CAS操作可以减少性能消耗,但出现锁竞争时,本身的互斥再加上额外的CAS操作反而会重量级锁慢。

5.偏向锁

       偏向锁是和轻量级锁一样都是JDK1.6中引入的锁优化机制,它的目的同样是在无竞争情况下消除同步原语。

引申问题:偏向锁与轻量级锁的区别

      首先,偏向锁与轻量级锁都不是用来代替重量级锁的,其次它们都是针对多线程中无锁竞争的情况下对重量级锁的优化。最后 :轻量级锁是通过CAS操作,在无竞争的情况下减少同步带来的性能消耗,而偏向锁则是在无竞争的情况下消除同步甚至没有CAS操作。

 偏向锁的实现原理

         偏向锁的实现也是依靠Mark Word中的状态标志。在一个同步中锁对象第一次(划重点)被线程获取时会将Mark Word中的标志设置为偏向模式(01),如果后面没有其他线程来获取该锁对象,持有偏向锁的线程在每次进入该对象锁的同步块时都不进行任何同步操作。如果有另外的线程来获取该锁时,则结束偏向模式,将状态标志恢复为01(未锁定状态)或 00(轻量级锁)状态,后续的同步操作就如上面的轻量级锁那样执行。

 

总结

       在java多线程中为了保证共享数据的线程安全,JVM提供的同步原语(重量锁)以及其他实现方式,重量锁因为线程的状态切换会给程序带来许多性能消耗,为了减少这些性能损耗,jdk通过多种锁优化的方式在特定的场景下尽量做到减少或消除同步操作来提升程序执行效。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值