java monitorenter_java并发编程-synchronized

关注微信公众号:程序猿的日常分享,定期更新分享。

在java中synchronized关键字是同步锁,他可以让我们的程序运行起来线程安全,屏蔽多线程带来的问题,实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

实现原理

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的互斥锁(Mutex Lock)来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。这种依赖于操作系统互斥锁(Mutex Lock)所实现的锁我们称之为“重量级锁”。

监视器锁(monitor)

当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,线程执行monitorexit就会释放monitor所有权。

monitorenter

执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。

3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit

monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权。

获取和释放监视器锁(monitor)

锁定代码块:

public class TestSync {

public void sync() {

synchronized (TestSync.class) {

System.out.println("test");

}

}

}

通过反编译看到如下代码:

public void sync();

descriptor: ()V

flags: ACC_PUBLIC

Code:

stack=2, locals=3, args_size=1

0: ldc #2 // class com/example/demo/test/TestSync

2: dup

3: astore_1

4: monitorenter

5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;

8: ldc #4 // String test

10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

13: aload_1

14: monitorexit

15: goto 23

18: astore_2

19: aload_1

20: monitorexit

21: aload_2

22: athrow

23: return

从里面可以看出,synchronized 代码块实际上多了 monitorenter 和 monitorexit 指令,在第4、14、20行指令分别使用了 monitorenter 和 monitorexit。这里有一个 monitorenter,却有两个 monitorexit 指令的原因是,JVM 要保证每个 monitorenter 必须有与之对应的 monitorexit,monitorenter 指令被插入到同步代码块的开始位置,而 monitorexit 需要插入到方法正常结束处和异常处两个地方,这样就可以保证抛异常的情况下也能释放锁。

再来看一个添加到方法上的例子:

public class TestSync {

public static synchronized void syncStatic() {

}

}

反编译后的代码:

public static synchronized void syncStatic();

descriptor: ()V

flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED

Code:

stack=0, locals=0, args_size=0

0: return

在同步块中使用了monitorenter 和 monitorexit 指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的,无论哪种方式,本质上都是通过监视器锁(monitor)来完成的,在获取这个锁的过程中是排他的,也就是同一时间只能有一个线程获取到由synchronized所保护的对象监视器。

锁定的范围

synchronized可以被应用在方法上和代码块上,那么就存在几种情况:

1、synchronized加在普通方法上,则它取得的锁是当前对象。

2、synchronized加在一段代码块上,则它取得的锁是synchronized(Object)括号中的对象。

3、synchronized加在静态方法上,则它取得的锁是对类,该类所有的对象同一把锁。

锁优化

Java SE1.6对Synchronized进行了各种优化之后,它并不那么重了。在不同的场景中引入不同的锁优化。

1.偏向锁:适用于锁没有竞争的情况,假设共享变量只有一个线程访问。如果有其他线程竞争锁,锁则会膨胀成为轻量级锁。

2.轻量级锁:适用于锁有多个线程竞争,但是在一个同步方法块周期中锁不存在竞争,如果在同步周期内有其他线程竞争锁,锁会膨胀为重量级锁。

3.重量级锁:竞争激烈的情况下使用重量级锁。

偏向锁和轻量级锁之所以会在性能上比重量级锁是因为好,本质上是因为偏向锁和轻量级锁仅仅使用了CAS。

4.锁粗化:如果释放了锁,紧接着什么都没做,又重新获取锁,那么其实这种释放和重新获取锁是完全没有必要的,如果把同步区域扩大,也就是只在最开始加一次锁,并且在最后直接解锁。那么就可以把中间这些无意义的解锁和加锁的过程消除。如下代码:

public void lockCoarsening() {

synchronized (this) {

//do something

}

synchronized (this) {

//do something

}

synchronized (this) {

//do something

}

}

5.锁消除

在大多数情况下,方法只会在一个线程内被使用,如果编译器能确定这个方法只会在一个线程内被使用,就代表肯定是线程安全的,那么我们的编译器便会做出优化,把对应的 synchronized 给消除,省去加锁和解锁的操作,以便增加整体的效率。

6.自适应自旋锁

自旋就是通过不断的循环,不释放CPU资源,不断的尝试去获取锁,但是如果自旋的时间过长,那么性能开销就会很大,浪费了CPU的资源。

在 JDK 1.6 中引入了自适应的自旋锁来解决长时间自旋的问题。自适应意味着自旋的时间不再固定,而是会根据最近自旋尝试的成功率、失败率,以及当前锁的拥有者的状态等多种因素来共同决定。自旋的持续时间是变化的,自旋锁变“聪明”了。比如,如果最近尝试自旋获取某一把锁成功了,那么下一次可能还会继续使用自旋,并且允许自旋更长的时间;但是如果最近自旋获取某一把锁失败了,那么可能会省略掉自旋的过程,以便减少无用的自旋,提高效率。

关注微信公众号:程序猿的日常分享,定期更新分享。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值