java synchronized 原理_java-synchronized原理

介绍

synchronized是一种独占式的重量级锁,在运行到同步方法或者同步代码块的时候,让程序的运行级别由用户态切换到内核态,把所有的线程挂起,通过操作系统的指令,去调度线程。这样会频繁出现程序运行状态的切换,线程的挂起和唤醒,会消耗系统资源,为了提高效率,引入了偏向锁、轻量级锁、尽量让多线程访问公共资源的时候,不进行程序运行状态的切换。

synchronized实现原理

synchronized是在jvm中实现,是基于进入和退出Monitor对象来实现方法和代码块的同步

同步代码块:

monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;

同步方法

synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,有一个ACC_SYNCHRONIZED标志,JVM就是通过该标志来判断是否需要实现同步的,具体过程为:当线程执行该方法时,会先检查该方法是否标志了ACC_SYNCHRONIZED,如果标志了,线程需要先获取monitor,获取成功后才能调用方法,方法执行完后再释放monitor,在该线程调用方法期间,其他线程无法获取同一个monitor对象。其实本质上和synchronized块相同,只是同步方法是用一种隐式的方式来实现,而不是显式地通过字节码指令。

synchronized 作用

(1)确保线程互斥的访问同步代码

(2)保证共享变量的修改能够及时可见

(3)有效解决重排序问题

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

普通同步方法,锁是当前实例对象

静态同步方法,锁是当前类的class对象

同步方法块,锁是括号里面的对象

自旋概念

互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能 带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如 果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有 锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

--->自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK 1.6中就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的, 所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作, 反而会带来性能的浪费。因此自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次 数的默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改。

--->在JDK 1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象 上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间, 比如100个循环。另一方面,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自 旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。

锁削除

锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。锁削除的主要判定依据来源于逃逸分析的数 据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待, 认为它们是线程私有的,同步加锁自然就无须进行。

变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的 情况下要求同步呢?答案是有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。比如:(只是说明概念,但实际情况并不一定如例子)在线程安全的环境中使用stringBuffer进行字符串拼加。则会在java文件编译的时候,进行锁销除。

锁粗化

我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快地拿到锁。

大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(锁粗化)到整个操作序列的外部。

锁的状态

锁一共有四种状态(由低到高的次序):无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态

锁的等级只可以升级,不可以降级。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得所得代价更低而引入了偏向锁,当一个线程访问同步代码块并获取锁时,会在线程的栈帧里创建lockRecord,在lockRecord里和锁对象的MarkWord里存储线程a的线程id.以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,说明是其他线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。

轻量级锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果,完成自旋策略还是发现线程没有释放锁,或者让别的线程占用,则线程试图将轻量级锁升级为重量级锁。

轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

重量级锁

就是让争抢锁的线程从用户态转换成内核态。让cpu借助操作系统进行线程协调。

具体流程

每一个线程在准备获取共享资源时:

第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁”.跳过轻量级锁直接执行同步体。

第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。

第三步,两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord.

第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋.

第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败

第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己.

0af23aa2db72d29e6f9cbc4a939342d3.png

偏向锁,轻量级锁,重量级锁对比

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距

如果线程间存在锁竞争,会带来额外的锁撤销的消耗

适用于只有一个线程访问同步块场景(只有一个线程进入临界区)

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度

如果始终得不到索竞争的线程,使用自旋会消耗CPU

追求响应速度,同步块执行速度非常快(多个线程交替进入临界区)

重量级锁

线程竞争不使用自旋,不会消耗CPU

线程阻塞,响应时间缓慢

追求吞吐量,同步块执行速度较慢(多个线程同时进入临界区)

如果上面的没看懂,可以先看看下面形象的例子:

打个比喻,假设你要回家上厕所,你要关上家里的大门,再关上自己房间的门,再关上房间厕所的门。如果你是一个人在家的话,那么其实只要关上家里的大门就好了,没人会来跟你抢上厕所(或者偷窥你~),就不用关上房间门和厕所门了,这就是偏向锁,只是你一个人的情况时才有用,即一个线程在获取锁的时候,重入的时候就不用做任何操作了,这不是很省事嘛。

那,如果家里有人,你就不能这么做了,他可能会跟你抢厕所呢,这就是偏向锁膨胀成轻量级锁。这就是多个线程交替获取锁的情况。那这个时候又要怎么做呢?比如说你哥上着厕所了,你也想上厕所,你猜你哥上厕所的时间不会太久,于是你就在厕所门口等一会(自旋),以前synchronized的方案就是让你回房间躺着等(阻塞),可能回房间的时间都比你拉尿的时间长(挂起线程的时间比执行同步方法中的时间还要长的情况)。这就是轻量级锁。

接着上面的故事,当你等的有点久了,你会觉得你哥可能这次要上很久了,所以你就回房间等了(阻塞),这时候就从轻量级锁升级到重量级锁了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java中的synchronized关键字是用于实现线程同步的机制。当一个Java线程进入synchronized代码块时,它会尝试获取锁(也称为监视器锁)来保护同步代码块。如果锁已被其他线程持有,则该线程将阻塞,直到锁可用为止。 在Java中,每个对象都有一个关联的监视器,也称为内部锁。synchronized关键字可以用于方法和代码块,以便只有一个线程可以持有该对象的锁。这确保了同一时间只有一个线程可以访问共享资源,从而避免了多个线程同时修改共享资源导致的数据不一致问题。 当一个线程进入一个被synchronized关键字保护的代码块时,它会尝试获取锁,如果锁已被其他线程持有,则该线程会被阻塞。只有当锁被释放时,其他线程才能获得锁并继续执行同步代码块中的代码。这样可以确保同步代码块中的代码只有一个线程执行,避免了数据竞争和其他并发问题。 ### 回答2: Java是一种面向对象编程语言,具有良好的可移植性,广泛应用于互联网、移动设备和嵌入式设备等领域。在Java中,synchronized关键字是实现多线程同步的重要机制之一。 Java中的synchronized关键字是用来控制线程的访问权限的,它可以将语句块或方法声明为同步的,从而避免多个线程同时执行这些代码。synchronized代码块的基本语法是: synchronized(object){ //需要同步的代码块 } 其中,object表示需要锁定的对象。在synchronized代码块中,每次只有一个线程能够获得对象的锁定,其他线程则需要等待该锁释放后再进行访问。 synchronized关键字的原理是基于Java中的“互斥锁”(Mutex)机制实现的。Mutex是一种特殊的信号量,用于协调多个线程的访问。当一个线程需要访问某个对象时,它会试图获取该对象的互斥锁。如果该锁已经被其他线程占用,则该线程会被阻塞,直到该锁被释放后才能继续执行。 在Java中,对于每个对象都有一个相应的互斥锁,称为“内置锁”(Intrinsic Lock)或“监视器锁”(Monitor Lock)。当一个线程需要进入synchronized代码块时,它需要先获得该对象的内置锁,在执行完代码块后再释放该锁。 具体来说,内置锁由两部分组成:锁定状态和处于等待状态的线程队列。当一个线程需要获得某个对象的锁时,它会先判断该锁是否为空闲的。如果是,则该线程就会获得该锁;否则,该线程就会被加入到对象的线程队列中,并进入等待状态。当锁释放后,会通知等待队列中的线程,让它们重新竞争锁的所有权。 需要注意的是,synchronized关键字不仅可以用于同步代码块,还可以用于同步方法。在Java中,每个对象都有一个隐藏的锁定对象,当调用某个对象的同步方法时,该对象的锁定对象就会被锁定。这样,其他线程就无法访问该对象的其他同步方法和同步代码块,直到该线程执行完该方法并释放锁为止。 总之,在Java中,synchronized关键字是实现多线程同步的基本机制之一,它是基于内置锁和监视器锁实现的。通过锁定对象,它可以确保同一时间只有一个线程能够访问被保护的代码块或方法,从而避免多个线程之间的不必要竞争和冲突,确保程序的正确性和可靠性。 ### 回答3: Java中的synchronized是一种用于保护共享资源的关键字。当多个线程尝试访问同一个对象的synchronized代码块时,只有一个线程能够进入代码块,其他线程必须等待直到同步操作完成并释放锁定。 synchronized的实现原理涉及到Java中的对象头和Monitor,其中对象头包含了一些元数据和指向Monitor的指针,Monitor则包含了互斥锁(mutex lock)和等待队列(wait set)这两个关键元素。 当一个线程进入synchronized代码块时,它会尝试去获取Monitor的互斥锁,如果锁定成功则该线程可以执行代码块中的代码,如果锁定不成功则该线程就会进入等待队列并陷入阻塞状态。当一个线程完成synchronized代码块时,它会释放Monitor的互斥锁,并且唤醒所有在等待队列中的线程,这些线程会重新尝试去获取锁定并继续执行。 需要注意的是,在Java中,每个对象都有一个关联的Monitor,因此使用synchronized的时候,锁定的粒度是对象级别的。所以如果多个线程同时执行的是不同对象的同步方法,则它们之间不会产生竞争;反之,如果多个线程同时执行的是同一个对象的不同同步方法,则这些方法之间还是会存在竞争关系。 除了使用synchronized关键字之外,在Java中还可以使用Lock接口和Condition接口来实现线程的同步和互斥。不过无论是使用synchronized还是Lock接口,它们的本质都是基于Monitor和互斥锁的实现原理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值