浅谈synchronized

1. synchronized

我们大多都知道synchronized是一个重量级锁,相对于Lock,它会显得那么笨重,以至于我们认为它不是那么的高效而慢慢摒弃它。但是,随着Javs SE 1.6synchronized进行的各种优化后,synchronized并不会显得那么重了。

1.1 synchronized基本操作

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。

Java中每个对象都扮演一个用于同步的锁的角色,这些内置锁被称为内部锁(intrinsic locks)或者监视器锁(monitor locks)。执行线程进入synchronized块之前自动获取锁,而无论是正常退出synchronized还是抛出异常,线程都会放弃对synchronized块的控制时,自动释放锁。获取内部锁的唯一途径:进入内部锁保护的同步块或者方法。

从语法上讲,Synchronized总共有三种用法:

1)修饰普通方法

2)修饰静态方法

3)修饰代码块

接下来我就通过几个例子程序来说明一下这三种使用方式(为了便于比较,三段代码除了Synchronized的使用方式不同以外,其他基本保持一致)。

没有同步的情况:

/**   
* @Title: NoSyncTest.java   
* @Package sync.nosync   
* @Description: 没有同步的代码   
* @author 落叶飞翔的蜗牛   
* @date 2017年12月3日   
* @version V1.0   
*/    
package sync.nosync;  
  
/** 
 * @author 落叶飞翔的蜗牛 
 * @date 2017年12月3日 上午10:20:31 
 */  
public class NoSyncTest {  
  
    public void method1(){  
        System.out.println("Method 1 start");  
        try {  
            System.out.println("Method 1 execute");  
            Thread.sleep(3000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("Method 1 end");  
    }  
  
    public void method2(){  
        System.out.println("Method 2 start");  
        try {  
            System.out.println("Method 2 execute");  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("Method 2 end");  
    }  
  
    public static void main(String[] args) {  
        final NoSyncTest test = new NoSyncTest();  
  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                test.method1();  
            }  
        }).start();  
  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                test.method2();  
            }  
        }).start();  
    }  
  
}

执行结果如下,线程1和线程2同时进入执行状态,线程2执行速度比线程1快,所以线程2先执行完成,这个过程中线程1和线程2是同时执行的。

Method 1 start  
Method 1 execute  
Method 2 start  
Method 2 execute  
Method 2 end  
Method 1 end 

②对普通方法同步:

/**   
* @Title: MethodSyncTest.java   
* @Package sync.methodsync   
* @Description: 方法级别同步 
* @author 落叶飞翔的蜗牛   
* @date 2017年12月3日 上午10:24:40 
* @version V1.0   
*/    
package sync.methodsync;  
  
/** 
 * @author 落叶飞翔的蜗牛 
 * @date 2017年12月3日 上午10:24:40 
 */  
public class MethodSyncTest {  
  
    public synchronized void method1(){  
        System.out.println("Method 1 start");  
        try {  
            System.out.println("Method 1 execute");  
            Thread.sleep(3000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("Method 1 end");  
    }  
  
    public synchronized void method2(){  
        System.out.println("Method 2 start");  
        try {  
            System.out.println("Method 2 execute");  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("Method 2 end");  
    }  
  
    public static void main(String[] args) {  
        final MethodSyncTest test = new MethodSyncTest();  
  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                test.method1();  
            }  
        }).start();  
  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                test.method2();  
            }  
        }).start();  
    }  
  }

执行结果如下,可以很明显的看出,线程2需要等待线程1method1执行完成才能开始执行method2方法。

Method 1 start  
Method 1 execute  
Method 1 end  
Method 2 start  
Method 2 execute  
Method 2 end

③静态方法(类)同步

/**   
* @Title: StaticMethodSyncTest.java   
* @Package sync.staticmethod   
* @Description: TODO   
* @author 落叶飞翔的蜗牛   
* @date 2017年12月3日 上午10:28:43 
* @version V1.0   
*/    
package sync.staticmethod;  
  
/** 
 * @author 落叶飞翔的蜗牛 
 * @date 2017年12月3日 上午10:28:43 
 */  
public class StaticMethodSyncTest {  
  
    public static synchronized void method1(){  
        System.out.println("Method 1 start");  
        try {  
            System.out.println("Method 1 execute");  
            Thread.sleep(3000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("Method 1 end");  
    }  
  
    public static synchronized void method2(){  
        System.out.println("Method 2 start");  
        try {  
            System.out.println("Method 2 execute");  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("Method 2 end");  
    }  
  
    public static void main(String[] args) {  
        final StaticMethodSyncTest test = new StaticMethodSyncTest();  
        final StaticMethodSyncTest test2 = new StaticMethodSyncTest();  
  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                test.method1();  
            }  
        }).start();  
  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                test2.method2();  
            }  
        }).start();  
    }  
  
} 

执行结果如下,对静态方法的同步本质上是对类的同步(静态方法本质上是属于类的方法,而不是对象上的方法),所以即使testtest2属于不同的对象,但是它们都属于SynchronizedTest类的实例,所以也只能顺序的执行method1method2,不能并发执行。

Method 1 start  
Method 1 execute  
Method 1 end  
Method 2 start  
Method 2 execute  
Method 2 end

④代码块同步

/**   
* @Title: CodeBlockSyncTest.java   
* @Package sync.codeblocksync   
* @Description: TODO   
* @author 落叶飞翔的蜗牛   
* @date 2017年12月3日 上午10:31:46 
* @version V1.0   
*/    
package sync.codeblocksync;  
  
/** 
 * @author 落叶飞翔的蜗牛 
 * @date 2017年12月3日 上午10:31:46 
 */  
public class CodeBlockSyncTest {  
  
    public void method1(){  
        System.out.println("Method 1 start");  
        try {  
            synchronized (this) {  
                System.out.println("Method 1 execute");  
                Thread.sleep(3000);  
            }  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("Method 1 end");  
    }  
  
    public void method2(){  
        System.out.println("Method 2 start");  
        try {  
            synchronized (this) {  
                System.out.println("Method 2 execute");  
                Thread.sleep(1000);  
            }  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("Method 2 end");  
    }  
  
    public static void main(String[] args) {  
        final CodeBlockSyncTest test = new CodeBlockSyncTest();  
  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                test.method1();  
            }  
        }).start();  
  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                test.method2();  
            }  
        }).start();  
    }  
 } 

执行结果如下,虽然线程1和线程2都进入了对应的方法开始执行,但是线程2在进入同步块之前,需要等待线程1中同步块执行完成。

Method 1 start  
Method 1 execute  
Method 2 start  
Method 1 end  
Method 2 execute  
Method 2 end

1.1 synchronized基本原理

synchronized原理需要理解monitorentermonitorexit概念:

monitorenter

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

大概意思是:

每个对象都有一个与其关联的监视器。当监视器被占有的时候,监视器就处于锁定状态。线程执行moniterenter尝试获得监视器的所有权。

·如果监视器的进入次数为0,线程进入监视器,并将监视器的进入次数置为1次。接下来此线程就是这个监视器的所有者。

·如果线程已经拥有了监视器的所有权,当线程重现进入监视器,监视器的进入次数加1

·如果其他线程已经拥有了监视器的所有权,那么线程将会阻塞,直到监视器的进入次数减到0时,线程才会再次常识获取监视器的所有权。

 

monitorexit

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.

The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

大概意思是:

执行monitorexit的线程必定是与对象实例相关联的监视器的所有者。

线程退出监视器的时候,减少一次监视器的进入次数。如果监视器的进入次数减少到0,表示监视器目前没有所有者,这时候其他阻塞的线程就可以尝试获取监视器的所有权。

下面先来看看CodeBlockSyncTest反编译的结果:


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

我们再来看一下同步方法的反编译结果:


从反编译的结果来看,方法的同步并没有通过指令monitorentermonitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

1.1 Monitor

什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。

所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,每一个Java对象可以叫做内部锁或者Monitor锁。

Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下:

Owner

EntryQ

RcThis

Nest

HashCode

Candidate


EntryQ: 关联一个系统互斥锁( semaphore ),阻塞所有试图锁住 monitor record 失败的线程。Owner :初始时为 NULL 表示当前没有任何线程拥有该 monitor record ,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为 NULL

RcThis:表示blockedwaiting在该monitor record上的所有线程的个数。

Nest:用来实现重入锁的计数。

HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。

Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

1.4 Java对象头

synchronized用的锁是存在Java对象头里的。Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键,下面将重点阐述Mark Word


1.5 锁的类型

锁一共有四种状态,无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。状态转化:

无锁 --> 偏向锁 --> 轻量级 --> 重量级

1.5.1偏向锁

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。

加偏向锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程(此时会引发竞争,偏向锁会升级为轻量级锁)。

偏向锁获取过程:

1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。

3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

5)执行同步代码。

 

偏向锁向轻量级锁膨胀

当前线程执行CAS获取偏向锁失败(这一步是偏向锁的关键),表示在该锁对象上存在竞争并且这个时候另外一个线程获得偏向锁所有权。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,并从偏向锁所有者的私有Monitor Record列表中获取一个空闲的记录,并将对象设置轻量锁状态并且Mark Word中的LockRecord指向刚才持有偏向锁线程的Monitor record,最后被阻塞在安全点的线程被释放,进入到轻量级锁的执行路径中,同时被撤销偏向锁的线程继续往下执行同步代码。


1.5.2轻量级锁

这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorentermonitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒。

加锁

1)当对象处于无锁状态时(RecordWord值为HashCode,状态位为001),线程首先从自己的可用moniter record列表中取得一个空闲的moniter record,初始NestOwner值分别被预先设置为1和该线程自己的标识,一旦monitor record准备好,然后我们通过CAS原子指令将该monitor record的起始地址指到对象头的LockWord字段,如果存在其他线程竞争锁的情况而调用CAS失败,则只需要简单的回到monitorenter重新开始获取锁的过程即可。

2)对象已经被膨胀同时Owner中保存的线程标识为获取锁的线程自己,这就是重入(reentrant)锁的情况,只需要简单的将Nest1即可。不需要任何原子操作,效率非常高。

3)对象已膨胀但Owner的值为NULL,当一个锁上存在阻塞或等待的线程同时锁的前一个拥有者刚释放锁时会出现这种状态,此时多个线程通过CAS原子指令在多线程竞争状态下试图将Owner设置为自己的标识来获得锁,竞争失败的线程在则会进入到步骤情况(4)的执行路径。

4)对象处于膨胀状态同时Owner不为NULL(被锁住),在调用操作系统的重量级的互斥锁之前先自旋一定的次数,当达到一定的次数时如果仍然没有成功获得锁,则开始准备进入阻塞状态,首先将rfThis的值原子性的加1,由于在加1的过程中可能会被其他线程破坏Objectmonitor record之间的关联,所以在原子性加1后需要再进行一次比较以确保LockWord的值没有被改变,当发现被改变后则要重新monitorenter过程。同时再一次观察Owner是否为NULL,如果是则调用CAS参与竞争锁,锁竞争失败则进入到阻塞状态。


1.5.3自旋锁

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

所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。

自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。

自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;

如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

1.5.4 自适应自旋锁

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

1.5.5重量级锁

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

1.5.6锁消除

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

如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些JDK的内置API时,如StringBufferVectorHashTable等,这个时候会存在隐形的加锁操作。比如StringBufferappend()方法,Vectoradd()方法:

public void vectorTest(){
     Vector<String> vector = new Vector<String>();
     for(int i = 0 ; i < 10 ; i++){
         vector.add(i + "");
     }
 
     System.out.println(vector);
 }

在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。

1.5.7锁粗化

我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。

锁粗化概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。

参考资料

1. 周志明:《深入理解Java虚拟机》

2. http://www.importnew.com/23511.html

3. http://blog.csdn.net/u012465296/article/details/53022317

4. http://blog.csdn.net/wangtaomtk/article/details/52264043

5. http://ifeve.com/java-synchronized/#header



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值