美文网首页
Java并发(02)-Synchronized原理与应用

Java并发(02)-Synchronized原理与应用

作者: 小亮__ | 来源:发表于2019-06-26 06:34 被阅读0次

    使用方法

    Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。它总共有三种用法:

    • 修饰普通方法
    • 修饰静态方法
    • 修饰代码块

    因为修饰的地方不同,synchronized使用的锁也不同,通俗的称分为:方法锁,对象锁 和 类锁 三种.

    1. 无论是修饰方法还是修饰代码块都是 对象锁,当一个线程访问一个带synchronized方法时,由于对象锁的存在,所有加synchronized的方法都不能被访问(前提是在多个线程调用的是同一个对象实例中的方法)

    2. 无论是修饰静态方法还是锁定某个对象,都是 类锁.一个class其中的静态方法和静态变量在内存中只会加载和初始化一份,所以,一旦一个静态的方法被申明为synchronized,此类的所有的实例化对象在调用该方法时,共用同一把锁,称之为类锁。

    Synchronized作用与原理

    它可以有效的解决一下问题,保证并发编程中的特性

    • 1)确保线程互斥的访问同步代码
    • 2)保证共享变量的修改能够及时可见
    • 3)有效解决重排序问题。

    下面编写一个例子,通过反编译来一起看一下Synchronized的原理

    public class SynchronizedDemo {
         public void method() {
             synchronized (this) {
                 System.out.println("Method 1 start");
             }
         }
     }
    

    从下面反编译的结果来看,起到关键性作用的主要是monitorenter和montorexit两个命令。


    JVM规范中描述:每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

    monitorenter:

    • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
    • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
    • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

    monitorexit:

    • 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
    • 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者
    • 其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

    通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

    JDK1.6之后的锁优化:

    JDK1.6对锁的实现引入了大量的优化,如自旋锁,适应性自旋锁,锁消除,锁粗化,偏向锁,轻量级锁等锁的技术来减少锁操作的开销。

    锁主要存在四种状态:无锁状态、偏向锁状态,轻量级锁状态,重量级锁状态。他们会随着竞争激烈而不断升级;

    注意锁可以升级不可以降级,这种策略是为了提高获得锁和释放锁的效率。

    自旋锁:

    • 线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的,所以引入自旋锁。

    • 自旋锁就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。如何实现等待呢?执行一段无意义的循环即可,自旋默认为10次。

    自适应自旋锁:

    • JDK1.6之后引入了更聪明的自旋锁,自适应自旋锁。所谓自适应就意味着自旋的次数不是固定的它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定的。

    • 实现原理:线程如果自旋成功,那么下次自旋的次数会更多;反正,如果对于某个锁,很少有自旋能够成功的,那么以后这个锁自旋的次数会减少甚至省略掉自旋的过程,以免浪费处理器资源。

    锁消除:

    • 为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。

    • 有时候我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。JVM检测到变量没有逃逸方法之外的,就可以大胆的将内部加锁操作消除。

    锁粗化:

    • 锁粗化就是将多个连续的加锁、解锁操作连到一起,扩展成更多范围的锁。JVM检测到同一个对象有连续的加锁、解锁操作,会合并成为一个更大范围的加锁、解锁操作,例如将加锁解锁操作移到for循环外面。

    轻量级锁:

    • 引入轻量级锁的作用主要是在没有多线程竞争的前提下,减少传统的重量级锁使用在操作系统中互斥产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁。

    • 对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢;

    偏向锁:

    • 引入偏向锁的主要目的是为了在无多线程竞争的情况下,尽量减少不要的轻量级锁执行路径。
      偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动释放偏向锁,需要等待其他线程来竞争。

    重量级锁:

    • 重量级锁通过对象内部监听器来实现,操作系统线程之间切换需要从用户态到内核态的切换,切换成本非常高。

    相关文章

      网友评论

          本文标题:Java并发(02)-Synchronized原理与应用

          本文链接:https://www.haomeiwen.com/subject/csjzqctx.html