synchronized关键字原理解析

引言

了解synchronized关键字原理之前首先简单了解下JVM的工作内存和主内存的一个概念,这也是导致线程安全性问题的原因所在;线程在操作变量的时候首先会从主内存读取到自己的工作内存中,然后进行各种操作,操作完成后再load到主内存中完成数据共享,此时在这个工程中如果出现另外一个线程也在操作同样的数据,就会出现线程安全性的问题了,如下图所示:

基于上面的问题,JVM提供了大量的非常好的解决方案,像synchronized关键字,volatile关键字等等,以及并发包下面的各种并发工具类,像线程安全的集合工具类,atomic,CountDownLatch等等,以及个人认为更高级的AQS,了解AQS有助于了解JVM的锁机制。

Java并发博大精深,抛砖引玉,今天我们主要来谈下synchronized关键字的基本原理。

synchronized锁

synchronized就是保证线程安全的,线程安全主要是体现在两个方面,一个是可见性,一个是原子性。
(1)内存可见性:synchronized对同步代码块加锁的时候,一个线程进入,就是锁住所操作的变量,对变量加锁并且将此变量的值在工作内存中清空,这样使得必须从主内存中读取数据保证数据安全性,同时对这个变量进行操作,操作完成,释放锁,unlock之前工作内存有了新的值并且会把此变量的值同步到主内存完成数据可见性的工作;如果只能保证数据可见性,此时另外一个线程进来也会有线程安全问题,那就是第二点原子性保障的。
(2)原子性:对一个代码块加锁,同步代码块中此时只能有一个线程进入,其他线程需要等待,具体如何等待见后文。

synchronized锁是通过底层操作系统的mutex lock实现的,一种互斥锁,(mutex lock的机制是尝试获取锁,若得到就占有;若不能就进入睡眠等待;什么时机使用?当线程进入睡眠没有伤害,或者需要等待一段足够长的时间才能获取锁;有什么缺点?会引起context switch和scheduling开销。)

因为Java线程就是基于操作系统的原生线程来实现的,如果出现线程的锁定和释放,此时都是需要跟底层操作系统去交互的,这个时候就会出现线程上下文切换出现的非常耗时的操作。在JDK的低版本中synchronized是一个名副其实的重量级锁,很不推荐使用,因为每次线程的加锁和释放锁都会涉及到跟底层操作系统的一个切换,大大增加了CPU的处理时间,但是从JDK1.6版本开始,就开始做了大量的优化,出现了自旋锁(避免频繁的上下文切换),轻量级锁等等,就像是计算机并发领域的一次次革命一样,很厉害。

Java对象头

synchronized锁是存放在Java对象头里面的,而对象头是由标记字段(MarkWord)和类型指针(klass Pointer)组成,类型指针的作用是对象指向它的类元数据的指针,JVM虚拟机会通过这个指针判断这个对象是哪个类的实例,而标记字段用于存储对象自身运行时的数据,像hashcode,分代年龄,锁状态标识,偏向锁线程ID等等信息,Mark Word是实现synchronized锁优化的关键所在,像自旋锁,偏向锁等等,在运行时期间,Mark Word里面存储的数据会随着锁标志位的变化而变化,如下表格所示:

锁优化

像前面说的JDK1.6对锁进行了大量的优化,像自旋锁,适应性自旋锁,偏向锁,轻量级锁,锁有四种状态:无锁,偏向锁,轻量级锁,重量级锁,他们会随着锁粒度竞争的增加而增加,注意不会降级,这样可以提高效率。
偏向锁:
    引入偏向锁的目的是为了在没有多线程环境竞争的情况下提高性能,因为这样减少了不必要的轻量级锁的执行,轻量级锁需要多次CAS操作,而偏向锁减少不必要的CAS操作;为什么会减少不必要的CAS操作呢?原因是这样的,当出现一个线程访问同步代码块并且获取锁的时候,会在对象头的里面以及栈帧中的锁记录存储偏向锁的线程ID,以后该线程在进入和退出同步代码块时不需要再进行CAS操作来加锁和释放锁,只需要简单的测试下对象头的Mark Word里面是否存储着当前线程的偏向锁,而轻量级锁在获取锁和释放锁的时候要进行多次的CAS操作,而且CAS指令操作是需要跟底层操作系统进行交互的,影响性能,所以偏向锁相对轻量级锁性能更优。
    只要一出现竞争,偏向锁就会立即撤销。另外偏向锁在JDK1.6和1.7中默然是开启的,不过可以通过JVM参数关闭掉:XX:-UseBiasedLocking=false,关闭之后,程序就会进入轻量级锁状态。

轻量级锁:
    竞争的线程不会堵塞,提高了程序的响应速度,轻量级锁是为了在线程近似于交替的执行同步代码的时候提高性能;如果始终得不到锁竞争,就会使用接下来的自旋锁,比较消耗CPU。

自旋锁:
    线程的堵塞和唤醒因为要跟底层的操作系统进行交互,上下文切换,反反复复的这种操作会加重CPU的负担,间接的给系统的性能和稳定性也带来了很大的隐患,Java团队里面的大神自然不会听之任之,于是就出现了自旋锁,所谓自旋锁就是让线程等待一段时间,不会直接挂起,如果持有锁的线程能很快释放锁,那就避免了跟底层操作系统的交互,带来了很好的性能,当然也不会无限的等待下去,是有限制次数的。
    自旋锁从JDK1.4.2开始引入,默认关闭,JDK1.6中开始默认开启了,同时默认次数是10次,可以通过JVM参数:-XX:PreBlockSpin,进行调整。

适应性自旋锁:
    相比较自旋锁,适应性自旋锁更加聪明智能锁消除和锁粗化:
    锁消除:
        为了保证数据安全性,必要的时候需要使用同步操作,但是JVM变的很聪明,有些场景下不可能存在共享数据竞争,这个时候聪明的JVM就进行锁消除操作,避免加锁从而提高性能,例如下面的代码:
        

public class LockEliminationTest {
    public static void main(String[] args) {
        LockEliminationTest letest = new LockEliminationTest();

            for (int i = 0; i < 10000; i++) {
                letest.append("a", "111");
            }
    }

    public void append(String v1, String v2) {
        StringBuffer sbuff = new StringBuffer();
        sbuff.append(v1).append(v2);
    }
}


    锁粗化:
        正常来说在使用同步锁的时候,需要让同步代码块的作用域范围尽可能的小,这样的目的是为了使得需要同步的操作数量尽可能的小,如果存在锁竞争,那么等待锁的线程也会尽快的拿到锁;如果一系列的连续操作都对同一个对象反复加锁和解锁,那即使没有出现线程竞争,频繁的进行加锁解锁操作也会出现不必要的性能损耗,因此JVM又变的聪明了起来,检测到一系列的操作都是对同一对象加锁,将会把加锁同步的返回粗化到整个操作序列的外部,这就是所谓的锁粗化,可以提高性能。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值