目录
·前言
一、synchronized 的特性
1.互斥性
2.可重入
二、synchronized 使用示例
1.修饰代码块
2.直接修饰普通方法
3.修饰静态方法
三、synchronized 原理
1.基本特点
2.加锁过程
(1)偏向锁阶段
(2)轻量级锁阶段
(3)重量级锁阶段
3.其他优化操作
(1)锁消除
(2)锁粗化
·结尾
·前言
前面文章中对 synchronized 的讲解主要有两个部分,一部分讲 synchronized 的基础及使用,另一部分讲了 synchronized 的工作原理,由于在我们使用 Java 进行多线程编程时一定涉及到线程安全这样的问题,所以我们避免不了加锁这样的操作,synchronized 作为我们最常用的加锁操作,在本篇文章中我会对 synchronized 做一个全面的总结,下面就开始本篇文章的内容介绍吧。
一、synchronized 的特性
使用 synchronized 关键字可以解决线程安全问题,那么 synchronized 是通过什么来解决线程安全问题的呢?这就要谈到出现线程安全问题的原因了,其中使用 synchronized 可以做到让 synchronized 代码块内或所修饰的方法中所进行的所有操作在对同一个锁对象产生竞争的多个线程里看作一个原子操作,并且使用 synchronized 可以做到在多个线程修改同一变量时保证其不受线程随机调度的影响,synchronized 可以解决由于这两点原因产生的线程安全问题与它的特性有关,下面就来看看 synchronized 的特性是什么吧。
1.互斥性
使用 synchronized 对同一个锁对象进行加锁的多个线程在执行 synchronized 内的代码时会产生互斥的效果,如果一个线程已经针对这个对象加锁后,其他线程再想对这个对象进行加锁就会产生阻塞,这也是锁竞争或叫做锁冲突,此时被阻塞线程的线程状态就是 BLOCKED 它会一直阻塞到获取到锁之后。如果多个线程是针对多个不同的锁分别使用 synchronized 进行加锁操作,那它们彼此之间不会出现锁竞争,就不会产生阻塞。
- 进入 synchronized 修饰的代码块或方法,相当于加锁;
- 退出 synchronized 修饰的代码块或方法,相当于解锁。
2.可重入
使用 synchronized 进行加锁操作时,不会出现一个线程对同一把锁连续加锁两次而出现“死锁”的情况,这就是因为 synchronized 属于可重入锁,那么对于可重入锁,它的内部会持有两个信息,如下所示:
- 当前这个锁是哪个线程持有的;
- 记录加锁次数的计数器。
下面我来用一段代码来对可重入锁在执行一个线程连续对同一把锁连续加锁两次的过程进行一个详细介绍,如下图所示:
在上述对同一把锁进行第二次加锁操作时,就会先判断当前加锁的线程是否是持有锁的线程,如果不是同一个线程,就会进入阻塞,如果是同一个线程,就只会进行计数器 +1 的操作。
二、synchronized 使用示例
在使用 synchronized 进行加锁操作时,首先要准备好“锁对象”,加锁解锁的操作都是针对这里的“锁对象”来进行操作的,在 Java 中,任何一个对象都可以作为“锁对象”,为方便理解这里的加锁解锁操作,我们可以认为 Java 中每个对象在内存中存储时,都会有一块内存来记录当前的这个对象的“锁定”状态,如果当前对象被 synchronized 进行了加锁操作,那么状态就变为“加锁”,一旦这个对象的状态是加锁状态,此时其他的线程在想对这个对象进行加锁操作就会产生阻塞。
1.修饰代码块
使用 synchronized 对代码块进行修饰就是对这一块代码进行加锁操作,如下所示:
class Test{private int count = 0;// add 方法对 count 进行自增操作public void add() {count++;}
}public class ThreadDemo13 {// 创建锁对象private static Object locker = new Object();public static void main(String[] args) {// 创建 Test 对象,为了调用其中的 add 方法Test t = new Test();Thread t1 = new Thread(()->{// 修饰代码块,进行加锁synchronized (locker) {t.add();}});}
}
上述这种 synchronized 的使用方法就是修饰代码块,此时的 synchronized 进行加锁的对象可以是任意的对象,可以说这种加锁方式可以涵盖以下两种。
2.直接修饰普通方法
使用 synchronized 对类中普通方法进行修饰相当于对当前调用这个方法的实例化对象进行加锁操作,如下图所示:
如上图两种加锁的代码表示形式,它们的本质都是在对 this 进行加锁,也就是当前调用这个方法的实例化对象,两个线程在通过对象调用 add 方法时,如果使用的是同一个实例化对象来调用就会产生锁竞争,如果使用的是两个不同的实例化对象来调用这里的 add 方法,就不会产生锁竞争。
Hashtable 在对其内部关键方法进行加锁操作使用的就是这种方式,因此,多线程操作 Hashtable 时可以保证线程安全,如下图所示: 但同时,由于 Hashtable 是直接对普通方法进行加锁操作,就导致多线程操作 Hashtable 不同链上数据时也会产生阻塞等待(锁竞争),就出现了多余的锁冲突,造成代码在多线程中效率低下。
3.修饰静态方法
使用 synchronized 对类中的静态方法进行修饰,就相当于把类对象作为锁对象,如下图所示:
Test.class 就是获取类对象,一个类中,类对象只能有一个,所以当多线程同时调用 func 方法时就会产生锁竞争,直接修饰静态方法的效果也是一样,这是因为静态方法可以直接使用类名来调用,所以本质也是对类对象进行加锁。
三、synchronized 锁机制
1.基本特点
这里我来对关于 synchronized 是一种什么样的锁进行一个总结,如下所示:
- synchronized 是悲观锁/乐观锁自适应的;
- synchronized 是轻量级锁/重量级锁自适应的;
- synchronized 是自旋锁/挂起等待锁自适应的;
- synchronized 不是读写锁;
- synchronized 是一种非公平锁;
- synchronized 是一种可重入锁。
以上的各种锁的介绍在锁策略的文章中有详细的介绍,文章链接:多线程——锁策略及 synchronized 原理_多线程 锁-CSDN博客 ,下面我来介绍一下 synchronized 是如何做到自适应的,它加锁的过程是怎么样的。
2.加锁过程
(1)偏向锁阶段
synchronized 在偏向锁阶段的核心思想和我在前面文章介绍的单例模式中的“懒汉模式”思想相似,偏向锁阶段就是能不加锁就不加锁,能晚加锁就晚加锁,所谓的偏向锁就是指并非真正的加锁了,而只是做了一个非常轻量的标记,此时一旦有其他线程来竞争这把锁,就会在另一个线程之前先把锁获取到,此时就会从偏向锁升级到轻量级锁了,就是真的加锁了,就会产生互斥,但是这个过程中,如果没有线程来竞争这把锁,那整个过程的加锁操作就会完全省略。
偏向锁的工作过程比较简单,在每个锁对象都有自己的一个偏向锁标记的属性,当这个锁对象首次被加锁的时候就会先进入偏向锁,在这个过程中如果没有涉及到锁竞争,下次对这个锁对象进行加锁就还是偏向锁,但是这个过程中一旦涉及到了锁竞争,偏向锁就会升级成轻量级锁,后续再对这个锁对象进行加锁就都是轻量级锁了。
偏向锁就是非必要不加锁,在遇到竞争的情况下,偏向锁不会提高效率,但是如果在没有竞争的情况下偏向锁就会大幅度提高效率了。
(2)轻量级锁阶段
synchronized 在轻量级锁阶段就是假设此时有锁竞争,但是竞争不激烈,此时轻量级锁的实现方式就是以自旋锁的方式来实现的,此时的优点就是当持有锁的线程释放锁后,就会第一时间获取到锁,缺点就是比较消耗 CPU 资源。
在轻量级锁阶段时,synchronized 内部会统计当前锁对象上有多少线程参与竞争,当发现参与竞争的线程比较多,就会进一步升级到重量级锁,这是 synchronized 应对锁竞争激烈环境下作出的一个优化,由于自旋锁在锁竞争激烈的环境下会增加 CPU 的资源消耗,所以需要进一步升级。
(3)重量级锁阶段
synchronized 在重量级锁阶段,拿不到锁的线程就不会继续自旋等待了,而是进入“阻塞等待”,此时就会让出 CPU 了,不会使 CPU 的占用率太高,如果当前持有锁的线程释放锁,那么此时就会由系统随机唤醒一个线程来获取锁。
以上的几个阶段,就是 synchronized 加锁的过程,这个过程可以看出 synchronized 自适应的特性,只不过目前 synchronized 的自适应只是自适应升级,不能做到自适应的降级。
3.其他优化操作
(1)锁消除
锁消除这种优化操作是 synchronized 中内置的优化策略,也属于编译器的一种优化方式,这个操作的执行逻辑是指在编译器编译代码的过程中,如果编译器发现代码中有不需要加锁的地方,就会自动把这里的加锁操作给省略,当然,编译器的优化操作是比较保守的,如果当前代码中只有一个线程在运行,不涉及多线程还进行了加锁操作,那么编译器就会省略这里的加锁操作,但是如果代码中的加锁操作是“模棱两可”的,编译器不知道这里的加锁操作能不能省略,那么就都不会进行省略。
(2)锁粗化
锁粗化这种操作就是把多个细粒度的锁合并成一个粗粒度的锁,如何判断此处锁的粒度是粗还是细呢?其中的评判标准可以以 synchronized 代码中的代码数量为准,我们可以粗略的认为,synchronized 代码块中的代码越多,锁的粒度越粗,代码块中代码越少,锁的粒度越细。
通常情况下,我们更偏好让锁的粒度更细一些,这样有利于多个线程并发执行,但是有的情况下,我们也希望锁的粒度粗点,比如下图这种场景:
上图这种频繁加锁解锁,每次加锁解锁中执行的代码逻辑都很短,就是细粒度的锁,我们需要明确一点,每次加锁都是可能产生阻塞的并且加锁解释也都是有开销的,此时,像上面这种代的执行效率就会很低,锁粗化就是把这些细粒度的锁合并成粗粒度的锁,这样可以省去很多加锁解锁的开销,经过锁粗化后,情况如下图所示: 此时,就把三个细粒度的锁合并成了一个粗粒度的锁了,锁粗化也是为了提高效率。
·结尾
文章到此就要结束了,本篇文章是对 synchronized 做的一个总结,介绍了 synchronized 的特性、使用方式、原理,只有好好的理解了使用 synchronized 的加锁方式,才能更好的在遇见线程安全问题的时候使用 synchronized 加锁来去解决,如果本篇文章对你有所帮助,还希望能够三连支持一下,如果对文章的内容有所疑惑,也欢迎在评论区进行留言,我们下一篇文章再见吧~~~