synchronized使用及原理解析

修饰静态方法、实例方法、代码块

Synchronized修饰静态方法,对类对象进行加锁,是类锁。java

Synchronized修饰实例方法,对方法所属对象进行加锁,是对象锁。数组

Synchronized修饰代码块时,对一段代码块进行加锁,是对象锁。数据结构

/**
 * synchronized示例
 * 一、修饰静态方法
 * 二、修饰实例方法
 * 三、修饰代码块
 */
public class SyncDemo2 {
    private static int num = 0;

    /**
     * 修饰静态方法
     */
    public static synchronized void count1() {
        for (int i = 0; i < 100000000; i++) {
            num++;
        }
    }

    /**
     * 修饰实例方法
     */
    public synchronized void count2() {
        for (int i = 0; i < 100000000; i++) {
            num++;
        }
    }

    /**
     * 修饰代码块
     * 效果与修饰静态方法相同
     */
    public void count3() {
        synchronized(SyncDemo2.class) {
            for (int i = 0; i < 100000000; i++) {
                num++;
            }
        }
    }

    /**
     * 修饰代码块
     * 效果与修饰实例方法相同
     */
    public void count4() {
        synchronized(this) {
            for (int i = 0; i < 100000000; i++) {
                num++;
            }
        }
    }

    public static void main(String[] args) {
        //两个线程运行一个类的两个对象,运行类的静态方法count1,
        //产生同步,num=200000000

        //两个线程运行一个类的两个对象,运行类的实例方法count2
        //由于调用的是不一样的对象,并未产生同步,num<=200000000
        SyncDemo2 syncDemo1 = new SyncDemo2();
        SyncDemo2 syncDemo2 = new SyncDemo2();

        //两个线程运行一个对象,运行类的实例方法count2
        //由于调用的是同一个对象,产生同步,num=200000000
        //SyncDemo2 syncDemo3 = new SyncDemo2();
        //syncDemo1 = syncDemo3;
        //syncDemo2 = syncDemo3;

        //启动两个线程进行运算
        Thread thread1 = new Thread(new ThreadDemo(syncDemo1));
        Thread thread2 = new Thread(new ThreadDemo(syncDemo2));
        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(SyncDemo2.num);
    }
}

class ThreadDemo implements Runnable {
    SyncDemo2 syncDemo2;
    public ThreadDemo(SyncDemo2 syncDemo2){
        this.syncDemo2 = syncDemo2;
    }

    @Override
    public void run() {
        //syncDemo2.count1();
        //syncDemo2.count2();
        syncDemo2.count3();
        //syncDemo2.count4();
    }
}

Synchronized底层实现原理

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现,不管是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)仍是隐式同步都是如此。在 Java 语言中,同步用的最多的地方多是被 synchronized 修饰的同步方法。同步方法并非由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。多线程

锁是加在对象上的,不管是类对象仍是实例对象。每一个对象主要由一个对象头、实例变量、填充数据三部分组成,结构如图:并发

synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(若是对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明以下:jvm

其中Mark Word在默认状况下存储着对象的HashCode、分代年龄、锁标记位等如下是32位JVM的Mark Word默认存储结构:socket

因为对象头的信息是与对象自身定义的数据没有关系的额外存储成本,所以考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象自己的状态复用本身的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有以下可能变化的结构:ide

Synchronized属于结构中的重量级锁,锁标识位为10,其中指针指向的是monitor对象的起始地址。每一个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor能够与对象一块儿建立销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构以下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)。高并发

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
            _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

结构中几个重要的字段要关注,_count、_owner、_EntryList、_WaitSet。性能

count用来记录线程进入加锁代码的次数。

owner记录当前持有锁的线程,即持有ObjectMonitor对象的线程。

EntryList是想要持有锁的线程的集合。

WaitSet 是加锁对象调用wait()方法后,等待被唤醒的线程的集合。

每一个等待锁的线程都会被封装成ObjectWaiter对象,当多个线程同时访问一段同步代码(临界区)时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程,_owner指向持有ObjectMonitor对象的线程。同时monitor中的计数器count加1。

若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。

若当前线程执行完毕也将释放monitor并复位变量的值,以便其余线程进入获取monitor(锁)。

(图摘自:https://blog.csdn.net/javazejian/article/details/72828483)

Synchronized在jvm字节码上的体现

咱们以以前的例子为例,使用javac编译代码,而后使用javap进行反编译。

反编译后部分片断以下图:

对于使用synchronized修饰的方法,反编译后字节码中会有ACC_SYNCHRONIZED关键字。

而synchronized修饰的代码块中,在代码块的先后会有monitorenter、monitorexit关键字,此处的字节码中有两个monitorexit是由于咱们有try-catch语句块,有两个出口。

 

Synchronized与等待唤醒

等待唤醒是指调用对象的wait、notify、notifyAll方法。调用这三个方法时,对象必须被synchronized修饰,由于这三个方法在执行时,必须得到当前对象的监视器monitor对象。

另外,与sleep方法不一样的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行。而sleep方法只让线程休眠并不释放锁。notify/notifyAll方法调用后,并不会立刻释放监视器锁,而是在相应的synchronized代码块或synchronized方法执行结束后才自动释放锁。

Synchronized的可重入与中断

       重入

当多个线程请求同一个临界资源,执行到同一个临界区时会产生互斥,未得到资源的线程会阻塞。而当一个已得到临界资源的线程再次请求此资源时并不会发生阻塞,仍能获取到资源、进入临界区,这就是重入。Synchronized是可重入的。

       中断

       在Thread类中与线程中断相关的方法有三个:

/**
 * Interrupt设置一个线程为中断状态
 * Interrupt操做的线程处于sleep,wait,join 阻塞等状态的时候,清除“中断”状态,抛出一个InterruptedException
 * Interrupt操做的线程在可中断通道上因调用某个阻塞的 I/O 操做(serverSocketChannel. accept()、socketChannel.connect、socketChannel.open、 
 * socketChannel.read、socketChannel.write、fileChannel.read、fileChannel.write),会抛出一个ClosedByInterruptException
 **/
public void interrupt();
/**
 * 判断线程是否处于“中断”状态,而后将“中断”状态清除
 **/
public static boolean interrupted();
/**
 * 判断线程是否处于“中断”状态
 **/
public boolean isInterrupted();

在实际使用中,当线程正处于调用sleep、wait、join方法后,调用interrupt会清除线程中断状态,并抛出异常。而当线程已进入临界区、正在执行,则须要isInterrupted()或interrupted()与interrupt()配合使用中断执行中的线程。

       Sychronized修饰的方法、代码块被多个线程请求时,调用中断。正在执行的线程响应中断。正在阻塞的线程、执行中的线程都会标记中断状态,但阻塞的线程不会马上处理中断,而是在进入临界区后再响应。

示例:中断对执行synchronized方法线程的影响

import java.util.concurrent.TimeUnit;

/**
 * 示例:中断对执行synchronized方法线程的影响
 * 正在执行的线程响应中断
 * 正在阻塞的线程、执行中的线程都会标记中断状态,
 * 但阻塞的线程不会马上处理中断,而是在进入临界区后再响应。
 */
public class SyncDemo3 {
    public static boolean flag = true;

    public static synchronized void m1() {
        System.out.println(Thread.currentThread().getName() + " hold resource!");
        while (flag) {
            if (!Thread.currentThread().isInterrupted()) {
                //不用sleep,由于sleep会对中断抛出异常
                Thread.yield();
            } else {
                System.out.println(Thread.currentThread().getName() + " interrupted and release !");
                return;
            }
        }
    }

    public static void main(String[] args) {
        SyncDemo3 syncDemo1 = new SyncDemo3();
        SyncDemo3 syncDemo2 = new SyncDemo3();
        //启动两个线程
        Thread thread1 = new Thread(new ThreadDemo3(syncDemo1), "thread1");
        Thread thread2 = new Thread(new ThreadDemo3(syncDemo2), "thread2");
        thread1.start();
        //休眠1秒,让thread1获取资源
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread2.start();
        //休眠1秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //thread1中断
        thread1.interrupt();
        //thread2中断
        thread2.interrupt();

        if (thread1.isInterrupted()) {
            System.out.println("thread1 interrupt!");
        }
        if (thread2.isInterrupted()) {
            System.out.println("thread2 interrupt!");
        }

        //休眠1秒,让thread2获取资源
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }
}

class ThreadDemo3 implements Runnable {
    SyncDemo3 syncDemo3;

    public ThreadDemo3(SyncDemo3 syncDemo3) {
        this.syncDemo3 = syncDemo3;
    }

    @Override
    public void run() {
        syncDemo3.m1();
    }
}

JDK6对Synchronized的优化

在JDK6之前synchronized的性能并不高,但在以后进行了优化,咱们在以前的Mark Word的结构中能够看到,锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁能够从偏向锁升级到轻量级锁,再升级的重量级锁,可是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

偏向锁

偏向锁是Java 6以后加入的新锁,它是一种针对加锁操做的优化手段。通过研究发现,在大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到,所以为了减小同一线程获取锁(会涉及到一些CAS操做,耗时)的代价而引入偏向锁。

偏向锁的核心思想是,若是一个线程得到了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再作任何同步操做,即获取锁的过程,这样就省去了大量有关锁申请的操做,从而也就提供程序的性能。

因此,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续屡次是同一个线程申请相同的锁。可是对于锁竞争比较激烈的场合,偏向锁就失效了,由于这样场合极有可能每次申请锁的线程都是不相同的,所以这种场合下不该该使用偏向锁,不然会得不偿失。但偏向锁失败后,并不会当即膨胀为重量级锁,而是先升级为轻量级锁。

轻量级锁

若偏向锁失败,虚拟机并不会当即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁可以提高程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。须要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,若是存在同一时间访问同一锁的场合,就会致使轻量级锁膨胀为重量级锁。

自旋锁

轻量级锁失败后,虚拟机为了不线程真实地在操做系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数状况下,线程持有锁的时间都不会太长,若是直接挂起操做系统层面的线程可能会得不偿失,毕竟操做系统实现线程之间的切换时须要从用户态转换到核心态,这个状态之间的转换须要相对比较长的时间,时间成本相对较高,所以自旋锁会假设在不久未来,当前的线程能够得到锁,所以虚拟机会让当前想要获取锁的线程作几个空循环(这也是称为自旋的缘由),通常不会过久,多是50个循环或100循环,在通过若干次循环后,若是获得锁,就顺利进入临界区。若是还不能得到锁,那就会将线程在操做系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是能够提高效率的。最后没办法也就只能升级为重量级锁了。

锁消除

消除锁是虚拟机另一种锁的优化,这种优化更完全,Java虚拟机在JIT编译时(能够简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),经过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,经过这种方式消除没有必要的锁,能够节省毫无心义的请求锁时间。

锁粗化

若是虚拟机探测到有这样一串零碎的操做都对同一个对象加锁,将会把加锁同步的范围扩展到整个操做序列的外部,这样就只须要加锁一次就够了。

 

 

 

参考:

《实战Java高并发程序设计》 葛一鸣,郭超 著

https://blog.csdn.net/javazejian/article/details/72828483

相关文章
相关标签/搜索