Synchronzied关键字解析

摘要

   在Java程序涉及多线程的同步问题的时候,往往需要给线程同步安全的功能加锁,可能是需要同步一个方法,也可能只是一段代码。


synchronzied关键字介绍:

  其中synchronized关键字就是Java内部实现的一个锁机制,通过给需要添加锁机制的方法或代码块加上synchronzied关键字,这样就能保证同一时间内只允许一个线程执行这段代码。

  synchronized块是Java提供的一种原子内置锁,Java程序中的每个对象都可以把它当作一个同步锁来使用(也就是锁,我们实例的所有对象,或着是class对象,都可以当作一把锁),这些Java内置的,我们看不到的锁一般称为内部锁,也叫监视器锁。


同步过程:

  线程执行到synchronized代码块时,会先自动获取内部锁,这个时候,如果其他线程也想访问这部分代码块的时候就会获取锁失败,进入阻塞状态。拿到内部锁的线程会在 :正常退出同步代码块、抛出异常后、在同步块内调用了该内置锁的wait等方法以上三种情况下释放内置锁。因为这把内置锁是属于排他锁,所以其他线程必须等到内置锁被释放后才能获取到内置锁。

这样就实现了原子性操作。


synchronized的内存语义:

  进入synchronized块的内存语义:把synchronized块内使用到的变量从工作内存中清除,从主内存中直接获取。

  退出synchronized块的内存语义:把synchronized块内对共享变量的修改刷新到主内存中。

这样就保证了共享变量的可见性。


synchronized的使用:

1.把synchronized关键字用在非静态方法内部,这时候获取的锁是:实例对象的锁

2.把sychronized关键字用作静态方法内部,这时候获取的锁是:该Class对象的锁

3.sychronized关键字包住一部分代码块,获取的锁是括号里面的锁

简单代码演示:

/**
	 * 获取实例对象的锁
	 */
	public synchronized void testSynchronized(){
		//需要同步的内容.......
		//我是要同步的内容.....
	}

	/**
	 * 获取的是该类的Class对象的锁
	 */
	public static synchronized void testStaticSynchronized(){
		//需要同步的内容.......
		//我是要同步的内容.....
	}

	/**
	 * 获取的是括号内部对象的锁,一般会传入this,
	 * 因为一直new obj有很多个,不是唯一的,就达不到锁的功能了
	 * 
	 */
	public void testInSynchronized(){
		Object obj = new Object();
		//传入obj对象的锁,一般为this,也就是该方法所在对象的实例对象
		synchronized (obj){
			//需要同步的内容.....
			//我是要同步的内容......
		}
	}

性能的开销:

因为Java程序的线程是与我们操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,是一种很耗时的操作,synchronized的使用就会导致线程阻塞,导致上下文的一个切换。因此我们在使用多线程的时候往往要注意合理的使用锁,避免系统性能大幅度下降。


实现原理

synchronized是JVM内置锁,通过内部对象Monitor(监视器锁)实现,基于进入Monitor对象实现方法,基于进入与退出Monitor对象实现方法与代码块同步,但是两者实现细节不一样。监视器锁的实现依赖底层操作系统的Mutex Lock(互斥锁)实现。

同步代码块实现过程:准备执行同步代码块(尝试获取对象对应的monitor所有权)--monitorenter(获取锁成功,进入同步代码块) - 》 同步代码块逻辑  -》 monitorexit(程序退出同步代码块,释放锁)

JVM所有锁机制都是围绕Monitor(监视器锁)去实现的,每个同步对象都有一个自己的Monitor

synchronized用的锁是存在Java对象头里的,Mark Word里存储的数据会随着锁标志位变化而变化。

Mark Word
锁状态25bit4bit1bit2bit
23bit2bit是否是偏向锁锁标志位
轻量级标记指向栈中锁记录的指针00
重量级标记指向互斥量(重量级锁)的指针10
GC标记11
偏向锁线程IDEpoch对象分代年龄101

JDK1.5后对synchronized的优化:

锁状态:无锁、偏向锁、轻量级锁、重量级锁。

synchronized同步过程:

1、当线程进入同步代码块的时候,先会判断对象标记(Mark Word)中的锁状态,假如是程序刚运行处于无锁状态(锁标记为01,偏向锁标记为0),这时里面的ThreadId为0,说明这个时候没有线程获取到该对象的锁,ThreadId就会写入当前当前线程的ID,修改偏向锁标志位,线程获取到偏向锁,再下一次该线程重新进入这里的时候,就会先判断对象头里面的ThreadId是否跟当前线程的ID相等,是的话就会直接进入代码块运行,无须再次获取锁; (锁偏向,减少无并发时获取锁的次数,减少性能开销(只依赖一次CAS原子指令操作,更换ThreadId))

2、如果当别的线程尝试获取,发现ThreadId不一致(开始出现了竞争,这时候获取到偏向锁的线程准备撤销偏向锁),这个外来线程会陷入自旋(这时候锁膨胀为轻量级锁,Mark Word里面的锁改为轻量级锁),使用CAS进行不断尝试,一个空循环(实现方式); (这类锁称为自旋锁,多线程竞争不激烈,同步块执行快,自旋可以减少阻塞线程带来的线程上下文切换带来的性能开销(依赖多次CAS原子指令操作,尝试把对象头的Mark Word指向自身线程,如果成功,则锁竞争成功))

3、如果在一定自旋次数获取不到锁,这个时候说明锁竞争比较激烈,这个时候取消自旋锁,锁膨胀为重量级锁,等待锁的线程就会进入阻塞状态,在阻塞队列中等待锁释放(因为这时候自旋是无效的,一直自旋最后还是拿不到锁)

 

锁的优缺点比较:

优点缺点适用场景
偏向锁

加锁和解锁不需要额外的消耗,

和执行非同方法基本一样

如果线程间存在锁竞争,

会带来额外的锁撤销的消耗

只有一个线程访问的同步块场景
轻量级锁

竞争的线程不会被阻塞,

提高了程序的响应速度

如果始终得不到锁的竞争线程,

使用自旋会消耗CPU

追求响应时间

同步块执行速度非常快

重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应缓慢

追求吞吐量

同步块执行时间稍长

 

锁粗化(扩大锁的范围)

如果JVM检测到在同一个地方经常加锁和解锁,它就会把这些加锁和解锁合并成一次,以减少频繁获取锁释放锁带来的性能开销。

 

锁消除(消除没有用的锁):

如果JVM发现同步代码块执行的地方并不需要锁也能正确运行,就会删除这些没有必要的锁。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。


参考书籍:《Java并发编程的艺术》,这里也推荐大家读一读,是本好书。

关于Java的内存模型的介绍可以点这里见我的另外一篇博客

  

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值