浅谈同步Synchronized关键字

最近在公司帮同事面试Java开发,多线程与锁一直也是面试中常问的点,本文就给大家讲讲同步关键字 synchronized 的使用和原理,本文纯属学习时记录所学,如有不足之处还请多多指教。

引言

什么是线程安全?

线程安全的解释是:当一个类被多个线程进行访问并且正确运行,它就是线程安全的

也可以说,当多个线程访问某各类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

正文

Synchronized简介

能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果

Synchronized的地位

同步关键字,最基本的互斥同步手段

对象锁和类锁

对象锁:包括方法锁(默认锁对象为this当前实例对象,synchronized修饰普通方法)和同步代码块锁(自己手动指定锁对象)

Alt SpringBoot

类锁:指synchronized修饰静态的方法或指定锁为Class对象,synchronized(*.class)代码块

Java类可能有很多个对象,但只有一个Class对象

Alt SpringBoot

本质:所以所谓的类锁,不过是Class对象的锁而已

用法和效果:类锁只能在同一时刻被一个对象拥有

Alt SpringBoot

多线程访问同步方法的8种情况

  1. 两个线程同时访问一个对象的同步方法:对象锁中的方法锁,对同一对象上锁,能实现同步 √
  2. 两个线程访问的是两个对象的同步方法:两个对象不能实现同步 ×
  3. 两个线程访问的是synchronized的静态方法:类锁中的static形式,两个线程访问两个对象的静态同步方法,能实现同步 √
  4. 同时访问同步方法和非同步方法:非同步方法不会受到影响,不会实现同步 ×
  5. 访问同一对象的不同的普通同步方法:访问的都是同步方法,能实现同步 √
  6. 同时访问静态的synchronized和非静态的synchronized方法:类锁中的静态锁,因为其中一个不是静态锁所以不能实现同步 ×
  7. 方法抛出异常后,会释放锁吗:会自动由JVM释放锁
  8. 在synchronized方法调用非synchronized方法,是不是线程安全的?不是

3个核心点

  1. 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1,5种情况)
  2. 每个实例都对应有自己的一把锁,不同实例之间互不影响,例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把类锁(对应第2,3,4,6情况)
  3. 无论是方法正常执行完毕或者方法抛出异常都会释放锁(对应第7种情况)

Synchronized解析

Synchronized的性质:可重入,不可中断

synchronized是可重入锁吗?为什么?

首先什么是可重入锁?

通俗的来说,就是当线程请求一个由其他线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是可重入锁,请求就会成功,否则阻塞。

synchronized 和 ReentrantLock都是可重入锁,下面简单介绍一下synchronized

synchronized 每一个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其他线程请求该锁,则必须等待,而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增,当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。

什么是可重入:指的是同一线程的外层函数获取锁之后,内层函数可以直接再次获取该锁(还有ReentrantLock)

好处:避免死锁,提升封装性

粒度:线程而非调用

  1. 证明同一个方法是可重入的
  2. 证明可重入不要求是同一个方法
  3. 证明可重入不要求是同一个类中的

不可中断:一旦这个锁已经被别的线程获得了,只能选择等待或者阻塞,直到别的线程释放了这个锁(相比Lock类,可以拥有中断能力,打断和退出)

原理:加解锁原理,可重入原理,可见性原理

加锁和释放锁的原理: 现象,时机,深入JVM看字节码

获取和释放锁的时机: 内置锁

获取内置锁的唯一途径就是进入这个锁保护的同步代码块或者方法中

等价代码

Alt SpringBoot

深入JVM看字节码: 反编译,monitor指令

Monditorenter和Monditorexit指令

Monitor计数器加1,其他线程将不再获取锁,重入锁会使计数器累加,其他线程获取不到计数器时,进入阻塞状态

Monitorexit指令是释放锁,将Monitor计数器减1,到0之后其他线程也能访问锁

可重入原理: 加锁次数计数器

JVM负责跟踪对象被加锁的次数

线程第一次给对象加锁的时候,计数变为1,每当这个相同的线程在此对象上再次获得锁时,计数会递增

每当任务离开时,计数递减,当计数为0的时候,锁被完全释放

保证可见性的原理: 内存模型

Alt SpringBoot

JVM(Java内存模型)控制线程通信

Alt SpringBoot

Synchronized的缺陷

  • 效率低,不够灵活,无法预判是否成功获取到锁
  • 锁的释放情况少,试图获得锁时不能设定超时,不能中断一个正在试图获得锁的线程
  • 不够灵活(读写锁更灵活): 加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
  • 无法知道是否成功获取到锁

最后分享几个面试可能会问的问题:

  1. Synchronized使用的时候有哪些注意点?

    锁对象不能为空,作用域不宜过大,避免死锁

  2. 2.如何选择Lock或Synchronized?

    使用锁之前优先使用atomic和countdownlanuch

  3. 如何提高性能,JVM如何决定哪个线程获取锁

  4. 多个线程等待同一个Synchronized锁的时候,JVM如何选择下一个获取锁的是哪个线程? 随机不可控制

  5. Synchronized使得同时只有一个线程可以执行,性能较差,有什么办法提升性能?

  6. 我想更加灵活的控制锁的获取和释放(现在释放锁的时机都被规定死了),怎么办?

  7. 什么是锁的升级,降级?什么是JVM里的偏斜锁,轻量级锁,重量级锁?

本人实战经验有限,关于Synchronized关键字的使用就到这里结束了,有关Synchronized其他的知识下次再总结分享出来,欢迎有疑问和想法的朋友交流 ^_^