java并发-synchronized

317 阅读6分钟

synchronized简介

1.当我们使用 synchronized 关键字来修饰代码段时,字节码层次是通过monitorenter,monitorexit 指令来实现锁的获取与释放
2.当线程进入到monitorenter指令后,线程将会持有monitor对象,执行monitorexit之后 线程将会释放monitor 对象
3.对于 synchroized 修饰方法来说,并没有出现monitorrenter,monitorexit指令,而是出现出现了 ACC_SYNCHRONIZED 标志jvm
使用了 ACC_SYNCHRONIZED 访问标志来区分一个方法是否是同步方法,当方法被调用时,调用指令会检查是否存在.
假如存在,那么访问线程将会先持有方法所在对象的 monitor对象,然后再去执行方法,
在该方法执行期间,其他的线程均无法再获取到monitor对象,当线程执行完会释放monitor对象

代码例子以及反编译的字节码

public void method() {
    synchronized (obj) {
        System.out.println("hello world");
    }
}

/**
	 * com.kiss.concurrency3.MyTest1.method()  反编译的字节码
	 * public void method();
    Code:
       0: aload_0
       1: getfield      #3                  // Field obj:Ljava/lang/Object;
       4: dup
       5: astore_1
       6: monitorenter //获取锁
       7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: ldc           #5                  // String hello world
      12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      15: aload_1
      16: monitorexit  //释放锁
      17: goto          25
      20: astore_2
      21: aload_1
      22: monitorexit  //异常结束释放锁
      23: aload_2
      24: athrow
      25: return
	 */
     
     
     
public synchronized void method() {

  System.out.println("hello");

}
	
	
	
	/**
	
	public synchronized void method();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED  (标志位多出 ACC_SYNCHRONIZED)
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 7: 0
        line 9: 8
	
	
	 */

synchronized总结

jvm中的同步是基于进入与推出监视器(monitor)(管程)来实现的,每个对象实例都会有一个monitor对象,monitor对象会和java对象一起创建和销毁,monitor对象是c++实现的
当多个线程,同时访问一段同步代码,这些线程会被放到一个叫entryList 中,处于阻塞的线程都会放到这个列表中,接下来,当线程获取到对象的monitor时,monitor是依赖底层操作系统的
mutex lock 来实现互斥的,线程获取mutex成功,则会持有mutex,这时其他线程则无法获取到 mutex
如果线程调用了wait方法,那么该线程就会释放持有的mutex,并进入到waitSet(等待集合),等待下次被其他线程调用,notify或者notifyAll唤醒
如果线程顺利执行完毕,也会释放mutex

同步锁在这种实现方式当中,因为monitor是依赖底层的操作系统实现,这也就存在用户态与内核态的切换,所以会增加性能消耗.
通过对象互斥的概念来保证共享数据的完整性,每个对象都存在一个可称为互斥锁的标记,这个标记用于保证在任何时刻,只能有一个线程返回该对象.
那些处于entrylist和waitset中的线程均处于阻塞状态,阻塞操作是由操作系统来完成的,再linux是通过pthread mutex lock函数实现的
线程在被阻塞 后会进入内核调度状态,这会导致系统再用户态和内核态直接切换,严重影响锁的性能
解决上述问题的办法是自旋.其原理是:当发生monitor再争用时,若owner线程能在很短的时间内释放锁,则那些正在争用的线程就可以稍微等待一下(自旋),owner线程释放锁时,争用线程可能会立即获取到锁,从而避免了系统阻塞
不过owner运行时间过长,超过了临界值,争用线程自旋了一段时间还是没有获取到锁 , 这时争用线程就会停止自旋进入阻塞状态    即  先自旋 , 不成功再进行阻塞, 尽量减低阻塞的可能性,这对那些执行时间很短的代码来说极大的性能提升
显然自旋在多处理器上才有意义

synchronized锁升级

mark work

1. mark work 存在于对象头中,它包含:
  <1.无锁标记
  <2.偏向锁标记
  <3.轻量级锁标记
  <4.重量级锁标记
  <5.GC标记

升级流程

对于synchronized来说,锁的升级主要都是通过mark work中的锁标志位与是否是偏向锁标志位来达成的;
synchronized关键字所有的锁都是从偏向锁开始,随着锁竞争的不断升级,逐步演化为轻量级锁,重量解锁.
对于锁的演化来说,他会经历如下阶段:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

偏向锁

针对于一个线程来说,他的主要作用就是优化同一个线程多次获取一个锁的情况;
如果一个synchronized方法被一个线程访问,那么这个方法加锁的对象的mark work 中就会有偏向锁标记,
同时还会有一个字段来记录该线程的id;
当这个线程再次访问同一个synchronized方法,他会检查这个锁对象的mark work的偏向锁标记及其是否指向了这个线程,
如果是的话,直接进入,不用进入管程(monitor)
假如是另一个线程访问,则会取消偏向锁标志

轻量级锁

若第一个线程已经获取到当前对象的锁,这时第二个线程又开始尝试抢占该对象的锁,由于该对象的锁已经被第一个线程
获取到,因此它是偏向锁,而第二个线程在争抢时,会发现对象头中mark work 已经是偏向锁,
但里面储存的线程id并不是自己.那么他会进行CAS操作;
1.获取成功:那么它会直接将mark work 中的线程id替换成自己,偏向锁标记位保持不变,这样这个锁还是偏向锁
2.获取失败:表示当前有多个线程争抢这个锁,那么这时偏向锁就会升级为轻量级锁

自旋锁

若自旋依然失败的话,那么锁就会进入到重量级锁,这种情况下,无法获取到锁的线程就会进入到monitor(内核态)
自旋的最大特点就是避免线程从用户态进入内核态
自旋锁其实不能成为锁,是线程的行为

重量级锁

从用户态进入内核态(jdk1.6之前synchronized都是重量级锁)

编译器对锁的优化措施

锁消除技术

JIT编译器可以动态编译同步代码,使用一种叫做逃逸分析的技术,
来通过该项技术来判别程序中所使用的锁对象是否只被一个线程使用,
如果是这种情况,那么JIT编译器在编译这段同步代码的时候就不会生成synchronized关键字所标识的锁的申请和释放机器码,
从而消除锁的使用流程

实例代码

public void method() {

      Object obj = new Object();

      synchronized(obj) {
          System.out.println("hello");
      }

}

锁粗化

JIT编译器在执行动态编译时,若发现前后相邻的synchronized块使用的是同一个锁对象,
那么它就会把这几个synchronized块给合并为一个较大的同步块,这样做的好处在于线程在执行这些代码时,
就无需频繁申请与释放锁了,从而达到申请与释放锁一次,就可以执行完全部的同步代码块,从而提升了性能

实例代码


public class MyTest {
	
	
	Object obj = new Object();

	public void method() {
		
		synchronized(obj) {
			System.out.println("hello");
		}
		
		synchronized(obj) {
			System.out.println("hello");
		}
		
		synchronized(obj) {
			System.out.println("hello");
		}
		
	}
	

}