synchronized 深入理解

synchronize是jdk提供的进行辅助同步的关键字。

它可以修饰一个 方法体,可以同步代码块修饰一个对象。

  1. 修饰一个成员函数
    会对当前对象实例进行加锁, 每一个对象实例只有一把锁,也就是说在同一时刻只会有一个方法被执行 。
  2. 修饰一个静态函数

    修饰静态函数时 ,会对当前对象的class对象进行枷锁,不管生成多少个对象实例,它的calss对象是唯一的

当使用synchronize在函数内修饰一个代码块时 ,会在底层字节码生成一个 monitorenter 以及一个或者多个 monitorexit指令与之相对应,保障锁被释放

当使用synchronize修饰一个函数时候,会在方法添加一个 acc_synchronize 。jvm来使用acc_synchronize来来表示这个同步方法 。 当方法被调用时,jvm会先持有当前对象的monitor方法,然后在去执行方法体,在方法执行期间,其他线程均无法获取次monitor ,当线程执行完该方法,就会释放monitor

 flags: ACC_SYNCHRONIZED

jvm中是基于进入和退出监视器对象( Monitor) 来实现的,每个对象实例都会有一个Monitor对象会和java对象一起创建(c++)和销毁 。

当多个线程同时访问一个同步代码时候,代码会被放到一个EetryList集合中,处于阻塞的线程都会放到该列表当中,接下里,当线程获取到Monitor时候依赖底层操作系统的 mutex lock来实现互斥 ,线程获取mutex成功,会持有它,其他线程就无法获取到该mutex 。

如果该线程中调用了 wait方法,那么就会释放掉它持有的mutex ,并且该线程进入waitset集合当中,等待被其他线程唤醒,如果方法执行完毕也会释放mutex

同步锁的这种实现方式,因为Monitor依赖底层操作系统的实现 , 所以存在用户态和内核态之间的切换, 会增加内存开销。

自旋锁:
因为内核态和用户态之前的切换会增加内存开销,所以jvm引入了自旋的方式。 其原理是当发生对monitor的争夺时,如果owner会很快执行结束,那么就让参与争夺的线程稍微等待一下(所谓的自旋) ,不进入内存态 。 自旋会增加cpu消耗

在 owner释放monitor后,争夺的线程可能会立刻获取到锁去执行代码 ,从而避免阻塞。 不过,当owner执行时间超过临界以后, 会停止自旋进入阻塞。

总体思想是 : 先自旋(spin),不成功再阻塞,尽量降低阻塞的可能性 。对于执行时间短的代码有很大的提升,在多处理器的上会有意义。

互斥锁的属性:

  1. pthread_mutext_timed_np : 普通锁,当一个线程获取到锁以后,其余线程会进入阻塞队列 ,当锁被释放以后,会按照优先级获取到锁 。
  2. pthread_mutext_rersursive_np : 嵌套锁,允许一个线程对通一把锁获取多次,并通过unlock进行解锁, 如果是不同线程请求,那么则在加锁线程执行结束以后再重新进行竞争 。
  3. pthread_mutext_errorcheck_np : 检错锁, 如果一个线程请求同一把锁,那么返回edeadlk, 否则和 pthread_mutext_timed_np 相同, 保证了不允许多次加锁会后的死锁问题。
  4. pthread_mutext_adaptive_np : 适应锁: 仅仅等待解锁后进行竞争。

在jdk1.5之前,只能通过synchronize关键字来实现线程同步 。在底层java也是通过synchronize关键字来实现原子操作的。它是一种内置锁, 是jvm进行隐式实现的。

从jdk1.5开始引入了java并发包 lock相关的锁 。lock本身是基于java来实现的,锁的获取和释放都是通过java来实现的,然而synchronize是通过系统底层的mutex_lock实现的,每次对锁的获取和释放都会带来用户态和内核态的切换,增加系统开销。在并发较高或者锁竞争很激烈的时候,synchronize的性能会很差。

从jdk1.6开始,synchronize的实现发生很大的优化。 引入了【偏向锁,轻量级锁,重量级锁】的概念 , 从而减少在用户态和内核态之间的切换 ,使得尽量发生在用户态。 这种锁的优化,实际上是通过java对象头中的一些标志位来实现的 ,对于锁的访问和改变都与对象头息息相关。

从jdk1.6开始,对象实例划分为三部分,对象头,实例数据, 对齐填充 。

对象头的组成:

  1. mark word
  2. 指向类的指针
  3. 数组长度

其中mark word 里面记录了对象的锁和垃圾回收相关的信息,在64位jvm中长度是64bit,包括如下信息:

  1. 无锁标记
  2. 偏向锁标记
  3. 轻量锁标记
  4. 重量锁标记
  5. GC标记

对于synchronize来说,锁的升级主要是通过mark word中的锁标志位和是否偏向锁标志位来实现的。 synchronize中的锁都是从偏向锁开始逐步升级,演化成轻量级锁,最后变成了重量级锁。

对于锁的演化有如下阶段:

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


我们也可以通过jvm参数来决定是否要是哪种锁。

偏向锁 (偏向锁标记,对应的线程id ):

针对于一个线程来说的 , 优化同一个线程多次获取一个锁的情况,如果一个synchronize方法被一个线程访问,那么这个方法所在的对象,会在其mark word中将偏向锁进行标记,同时还会存储该线程的id ,当这个线程访问同一个synchronize方法,会检查mark word 的偏向锁标记和线程id ,如果是同一个,那么不会进入管程(monitor),而直接执行方法体 。

如果另外一个线程访问了同步方法,如果获取锁失败,偏向锁会被取消,升级成轻量级锁。如果是大量的并发,那偏向锁性能开销反而会更大,可以通过jvm参数关闭偏向锁。

轻量级锁 :

如果当前锁已经被第一个线程获取,那么这时候还有其他线程尝试去争夺锁,由于锁已经被第一个线程获取,第二个线程去争夺时候,会发现mark word中已经是偏向锁,里面存储的线程id并不是自己,那么会进入CAS(compare and swap) ,从而获取到锁,这里存在2中情况:

  1. 获取锁成功,将mak word中的线程id修改为自己 ,保持偏向锁状态。
  2. 获取失败,表示有多个线程进行争夺,那么升级成轻量级锁。

比较理想的情况是适合 2个线程互相的轮流进行访问

自旋锁(轻量的一种实现方式): 当自旋依然失败, 转为重量级锁,无法获取到锁的线程都会进入到monitor 内核态。

自旋最大的特点是避免进入内核态,但是会增加cpu开销 。

重量级锁 :

线程最终从用户态进入内核态。

锁消除 :

使用逃逸分析的技术检测是否锁对象只会被一个线程使用,不会散布到其他线程。 那么不会生成synchronize关键字对应的申请与释放机器码, 从而消除锁的使用流程。

1
2
3
4
5
6
public void tet() {
Object object = new Object();
synchronized (object) {
throw new RuntimeException();
}
}
锁粗化 :

jit若发现相邻的同步代码块锁的是同一个对象,就会合并成一个大的同步代码 , 从而达到无需频繁的申请和释放锁 ,从而提高性能。

1
2
3
4
5
6
7
8
9
public void tet() {
synchronized (object) {
}

synchronized (object) {
}
synchronized (object) {
}
}
Lock 锁:

lock 和 synchronize的区别:

  1. 前者通过开发者调用代码手工获取,后者通过jvm自动获取
  2. 前者务必通过unlock在finally中手工释放,后者jvm自动释放
  3. 前者通过java代码实现,后者jvm底层实现
  4. 前者提供了多种 ,入公平锁,非公平锁。前后两者都提供了可重入锁