java多线程之Synchronized关键字

引言

上一篇我们复习了线程的基本状态及三种实现方式,本篇我们继续来复习下synchronized关键字,synchronized是解决java并发问题最简单、最常用的方法之一,主要的作用是保证线程互斥的访问同步代码。

1、synchronized 原理

synchronized底层是通过一个monitor(监视器)的对象来完成的,java中每个对象都有一个monitor,monitor的初始值为0,当线程进入时monitor加1,线程退出时monitor减1,此时monitor重新回到初始值0,其它线程才能再次进入。

从上面synchronized底层加锁过程,我们可以知道:在java中,每一个对象有且仅有一个同步锁。这也意味着,synchronized同步锁是依赖于对象而存在。

这里再补充一个知识点:由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存,这种转换非常消耗性能。

下面我们通过反编译一段代码来看看底层的实现,java源代码:

public class SynchronizedTest {
    public void test(){
        synchronized (this){}
    }
}

反编译的字节码:

//上面省略部分字节码……
public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_1
         5: monitorexit
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return
//下面省略部分字节码……

从上面的反编译字节码中,我们关注到标记3处有一个monitorenter表示获取了monitor的所有权,标记5处有一个monitorexit表示释放了monitor的所有权,可能有的同学注意到标记11处也有一个monitorexit,会不会导致monitor减2次导致为-1了?
答案肯定是不会的,我们再看看标记6处的goto,跳转到了标记14的return执行完毕返回了,不会执行到标记11,标记11处的monitorexit只有程序执行异常的时候才会执行,所以不管程序怎么执行,都会最后释放monitor的所有权。

2、深入源码

实际上说到这里,已经算是讲完了,但是为了说得更加清楚,再深入到源码来说,synchronized实际上有两个队列waitSet和entryList,这里就不贴具体的代码了,只说一下简单的流程:

  1. 当多个线程进入同步代码块时,首先进入entryList。
  2. 有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1。
  3. 如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁。
  4. 如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null

3、锁的优化机制

从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是一个很重量级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。

锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。

自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。

自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。

锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。

锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。

偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。

轻量级锁:JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。

整个锁升级的过程非常复杂,我尽力去除一些无用的环节,简单来描述整个升级的机制。

简单点说,偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。

4、对象锁和类锁

对象锁和类锁可能是开发中最容易混淆的,下面通过一个伪代码来说明下

pulbic class ObjectTest {
	//方法1
    public synchronized void syncA(){}
    //方法2
    public synchronized void syncB(){}
    //方法3
    public static synchronized void syncC(){}
    //方法4
    public static synchronized void syncD(){}
}

方法1和方法2都是对象锁,方法3和方法4都是static修饰的类锁,如果有两个实例x,y,那么下面的4个方法能否同时访问

  1. x.syncA和x.syncB
  2. x.syncA和y.syncA
  3. x.syncC和y.syncD
  4. x.syncA和ObjectTest.syncC

验证的代码限于篇幅就不贴出来了,下面是结论:

  1. 不能被同时访问。因为syncA()和syncB()都是访问同一个对象(对象x)的同步锁。
  2. 可以同时被访问。因为访问的不是同一个对象的同步锁,x.syncA()访问的是x的同步锁,而y.syncA()访问的是y的同步锁。
  3. 不能被同时访问。因为syncC()和syncD()都是static类型,x.syncC()相当于ObjectTest.syncC(),y.syncD()相当于ObjectTest.syncD(),因此它们共用一个同步锁,不能被同时反问。
  4. 可以被同时访问。因为syncA()是实例方法,x.syncA()使用的是对象x的锁;而syncC()是静态方法,ObjectTest.syncC()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的。

5、总结

synchronized的优化是在jdk底层进行的,每个版本都会有差异,我们只要记住,在java中,每一个对象有且仅有一个同步锁,当我们调用某对象的synchronized方法时,就获取了该对象的同步锁,不同线程对同步锁的访问是互斥的。

如果你觉得本篇文章对你有帮助的话,请帮忙点个赞,再加一个关注。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值