java学习笔记-synchronized

杨晓峰老师《java核心技术36讲》15,16课&郑雨迪老师《深入拆解java虚拟机》14课学习总结

参考文章:https://www.cnblogs.com/charlesblc/p/5994162.html

synchronized的实现原理

在java程序中,我们利用synchronized关键字来对程序进行加锁,它可以申明一个synchronized代码块,也可以标记静态方法或者实例方法。

1.声明synchronized 代码块

是由一对 monitorenter/monitorexit指令实现的,monitor 对象是同步的基本实现单元。

例如:

1
2
3
4
5
public void foo(Object lock) {
synchronized (lock) {
lock.hashCode();
}
}

这段代码编译的字节码会包括一个monitorenter指令和多个monitorexit,jvm需要确保所获得的锁在正常/异常执行路径都能够被解锁。

执行monitorenter时

  • 如果目标锁的计数器为0,说明他没有被其他线程所持有,jvm会将锁对象的持有线程设置为当前线程,并将计数器加1.
  • 如果目标锁的计数器不为0,如果锁对象的持有线程是当前线程,jvm可以将计数器加1,否则等待,直到持有线程释放该锁。

执行monitorexit时,jvm将锁对象的计数器减1,如果计数器为0,代表锁已经被释放。

2.用synchronized标记方法

例如:

1
2
3
public synchronized void foo(Object lock) {
lock.hashCode();
}

这段代码编译的字节码中方法的访问有ACC_SYNCHRONIZED。但是没有monitorenter或者monitorexit。

因为方法级别的同步是隐式的,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构(method_info structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否是同步方法。

当调用方法时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。

锁的升级降级

  • jvm 提供三种不同的Monitor实现,即偏斜锁,轻量锁,重量锁。
  • 当jvm检测到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级降级。
  • 当没有竞争出现时,默认使用偏斜锁。

image
其中,00代表轻量锁,01代表无锁(或者偏向锁),10代表重量锁,11则根垃圾回收算法的标记有关。

在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,如果以上两种都失败,则启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;

重量锁

重量锁会阻碍加锁失败的线程,并在目标锁被释放时唤醒这些线程。
为了避免昂贵的线程阻塞,唤醒的过程,jvm会在线程进入阻塞状态之前,以及被唤醒后竞争不到锁的情况下,进入自旋状态,在处理器上空跑并轮询锁是否释放,如果此时锁恰好被释放了,那线程便无须进入阻塞状态,直接获得这把锁。

轻量锁

轻量级锁是相对于重量级锁而言在获取锁和释放锁时更加高效,但轻量级锁并不能代替重量级锁。轻量级锁适用的场景是在线程交替获取某个锁执行同步代码块的场景,如果出现多个进程同时竞争同一个锁时,轻量级锁会膨胀成重量级锁。

轻量锁采用CAS操作,把锁对象的标记字段替换成一个指针,指向当前线程栈上的一块空间,存储这锁对象原本的标记字段。

偏向锁

偏向锁只会在第一次请求是采用CAS操作,在锁对象的标记字段中记录下当前线程的地址,在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。

适用场景 优点 缺点
重量锁 多个线程同时进入临界区 线程竞争不使用自旋不消耗cpu,吞吐量高 线程阻塞,响应缓慢
轻量锁 多个线程交替进入临界区 竞争的线程不会阻塞,响应速度快 如果始终得不到锁竞争的线程使用自旋会消耗cpu
偏向锁 仅有一个线程进入临界区 加锁和解锁不需要额外的消耗 如果线程间存在锁竞争,会带来额外的锁撤销的消耗

ReentrantLock和synchronized的区别

1.用法比较

  • Lock使用起来比较灵活,但是必须有释放锁的配合动作
  • Lock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁
  • Lock只适用于代码块锁,而synchronized可用于修饰方法、代码块等

2.特性比较

ReentrantLock的优势体现在:

  • 具备尝试非阻塞地获取锁的特性:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
  • 能被中断地获取锁的特性:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
  • 超时获取锁的特性:在指定的时间范围内获取锁;如果截止时间到了仍然无法获取锁,则返回

3.注意事项

在使用ReentrantLock类的时,一定要注意三点:

  • 在finally中释放锁,目的是保证在获取锁之后,最终能够被释放
  • 不要将获取锁的过程写在try块内,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故被释放。
  • ReentrantLock提供了一个newCondition的方法,以便用户在同一锁的情况下可以根据不同的情况执行等待或唤醒的动作。