mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-25 02:27:10 +08:00
update 多线程部分内容
This commit is contained in:
parent
f54664f27f
commit
ce2cfb21ba
@ -1,5 +1,63 @@
|
|||||||
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
|
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
|
||||||
|
|
||||||
|
|
||||||
|
<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->
|
||||||
|
|
||||||
|
<!-- code_chunk_output -->
|
||||||
|
|
||||||
|
- [Java 并发进阶常见面试题总结](#java-并发进阶常见面试题总结)
|
||||||
|
- [1.synchronized 关键字](#1synchronized-关键字)
|
||||||
|
- [1.1.说一说自己对于 synchronized 关键字的了解](#11说一说自己对于-synchronized-关键字的了解)
|
||||||
|
- [1.2. 说说自己是怎么使用 synchronized 关键字](#12-说说自己是怎么使用-synchronized-关键字)
|
||||||
|
- [1.3. 构造方法可以使用 synchronized 关键字修饰么?](#13-构造方法可以使用-synchronized-关键字修饰么)
|
||||||
|
- [1.3. 讲一下 synchronized 关键字的底层原理](#13-讲一下-synchronized-关键字的底层原理)
|
||||||
|
- [1.3.1. synchronized 同步语句块的情况](#131-synchronized-同步语句块的情况)
|
||||||
|
- [1.3.2. `synchronized` 修饰方法的的情况](#132-synchronized-修饰方法的的情况)
|
||||||
|
- [1.3.3.总结](#133总结)
|
||||||
|
- [1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗](#14-说说-jdk16-之后的-synchronized-关键字底层做了哪些优化可以详细介绍一下这些优化吗)
|
||||||
|
- [1.5. 谈谈 synchronized 和 ReentrantLock 的区别](#15-谈谈-synchronized-和-reentrantlock-的区别)
|
||||||
|
- [1.5.1. 两者都是可重入锁](#151-两者都是可重入锁)
|
||||||
|
- [1.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API](#152synchronized-依赖于-jvm-而-reentrantlock-依赖于-api)
|
||||||
|
- [1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能](#153reentrantlock-比-synchronized-增加了一些高级功能)
|
||||||
|
- [2. volatile 关键字](#2-volatile-关键字)
|
||||||
|
- [2.1. CPU 缓存模型](#21-cpu-缓存模型)
|
||||||
|
- [2.2. 讲一下 JMM(Java 内存模型)](#22-讲一下-jmmjava-内存模型)
|
||||||
|
- [2.3. 并发编程的三个重要特性](#23-并发编程的三个重要特性)
|
||||||
|
- [2.4. 说说 synchronized 关键字和 volatile 关键字的区别](#24-说说-synchronized-关键字和-volatile-关键字的区别)
|
||||||
|
- [3. ThreadLocal](#3-threadlocal)
|
||||||
|
- [3.1. ThreadLocal 简介](#31-threadlocal-简介)
|
||||||
|
- [3.2. ThreadLocal 示例](#32-threadlocal-示例)
|
||||||
|
- [3.3. ThreadLocal 原理](#33-threadlocal-原理)
|
||||||
|
- [3.4. ThreadLocal 内存泄露问题](#34-threadlocal-内存泄露问题)
|
||||||
|
- [4. 线程池](#4-线程池)
|
||||||
|
- [4.1. 为什么要用线程池?](#41-为什么要用线程池)
|
||||||
|
- [4.2. 实现 Runnable 接口和 Callable 接口的区别](#42-实现-runnable-接口和-callable-接口的区别)
|
||||||
|
- [4.3. 执行 execute()方法和 submit()方法的区别是什么呢?](#43-执行-execute方法和-submit方法的区别是什么呢)
|
||||||
|
- [4.4. 如何创建线程池](#44-如何创建线程池)
|
||||||
|
- [4.5 ThreadPoolExecutor 类分析](#45-threadpoolexecutor-类分析)
|
||||||
|
- [4.5.1 `ThreadPoolExecutor`构造函数重要参数分析](#451-threadpoolexecutor构造函数重要参数分析)
|
||||||
|
- [4.5.2 `ThreadPoolExecutor` 饱和策略](#452-threadpoolexecutor-饱和策略)
|
||||||
|
- [4.6 一个简单的线程池 Demo](#46-一个简单的线程池-demo)
|
||||||
|
- [4.7 线程池原理分析](#47-线程池原理分析)
|
||||||
|
- [5. Atomic 原子类](#5-atomic-原子类)
|
||||||
|
- [5.1. 介绍一下 Atomic 原子类](#51-介绍一下-atomic-原子类)
|
||||||
|
- [5.2. JUC 包中的原子类是哪 4 类?](#52-juc-包中的原子类是哪-4-类)
|
||||||
|
- [5.3. 讲讲 AtomicInteger 的使用](#53-讲讲-atomicinteger-的使用)
|
||||||
|
- [5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理](#54-能不能给我简单介绍一下-atomicinteger-类的原理)
|
||||||
|
- [6. AQS](#6-aqs)
|
||||||
|
- [6.1. AQS 介绍](#61-aqs-介绍)
|
||||||
|
- [6.2. AQS 原理分析](#62-aqs-原理分析)
|
||||||
|
- [6.2.1. AQS 原理概览](#621-aqs-原理概览)
|
||||||
|
- [6.2.2. AQS 对资源的共享方式](#622-aqs-对资源的共享方式)
|
||||||
|
- [6.2.3. AQS 底层使用了模板方法模式](#623-aqs-底层使用了模板方法模式)
|
||||||
|
- [6.3. AQS 组件总结](#63-aqs-组件总结)
|
||||||
|
- [6.4. 用过 CountDownLatch 么?什么场景下用的?](#64-用过-countdownlatch-么什么场景下用的)
|
||||||
|
- [7 Reference](#7-reference)
|
||||||
|
- [公众号](#公众号)
|
||||||
|
|
||||||
|
<!-- /code_chunk_output -->
|
||||||
|
|
||||||
|
|
||||||
# Java 并发进阶常见面试题总结
|
# Java 并发进阶常见面试题总结
|
||||||
|
|
||||||
## 1.synchronized 关键字
|
## 1.synchronized 关键字
|
||||||
@ -20,7 +78,6 @@
|
|||||||
|
|
||||||
所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。
|
所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。
|
||||||
|
|
||||||
|
|
||||||
### 1.2. 说说自己是怎么使用 synchronized 关键字
|
### 1.2. 说说自己是怎么使用 synchronized 关键字
|
||||||
|
|
||||||
**synchronized 关键字最主要的三种使用方式:**
|
**synchronized 关键字最主要的三种使用方式:**
|
||||||
@ -33,7 +90,7 @@ synchronized void method() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**2.修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 **当前 class 的锁**。因为静态成员不属于任何一个实例对象,是类成员( *static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份*)。所以,如果一个线程 A 调用一个实例对象的非静态 `synchronized` 方法,而线程 B 需要调用这个实例对象所属类的静态 `synchronized` 方法,是允许的,不会发生互斥现象,**因为访问静态 `synchronized` 方法占用的锁是当前类的锁,而访问非静态 `synchronized` 方法占用的锁是当前实例对象锁**。
|
**2.修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 **当前 class 的锁**。因为静态成员不属于任何一个实例对象,是类成员( _static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份_)。所以,如果一个线程 A 调用一个实例对象的非静态 `synchronized` 方法,而线程 B 需要调用这个实例对象所属类的静态 `synchronized` 方法,是允许的,不会发生互斥现象,**因为访问静态 `synchronized` 方法占用的锁是当前类的锁,而访问非静态 `synchronized` 方法占用的锁是当前实例对象锁**。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
synchronized void staic method() {
|
synchronized void staic method() {
|
||||||
@ -83,6 +140,7 @@ public class Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
另外,需要注意 `uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要。
|
另外,需要注意 `uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要。
|
||||||
|
|
||||||
`uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要的, `uniqueInstance = new Singleton();` 这段代码其实是分为三步执行:
|
`uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要的, `uniqueInstance = new Singleton();` 这段代码其实是分为三步执行:
|
||||||
@ -105,7 +163,7 @@ public class Singleton {
|
|||||||
|
|
||||||
**synchronized 关键字底层原理属于 JVM 层面。**
|
**synchronized 关键字底层原理属于 JVM 层面。**
|
||||||
|
|
||||||
**① synchronized 同步语句块的情况**
|
#### 1.3.1. synchronized 同步语句块的情况
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public class SynchronizedDemo {
|
public class SynchronizedDemo {
|
||||||
@ -126,9 +184,17 @@ public class SynchronizedDemo {
|
|||||||
|
|
||||||
**`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。**
|
**`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。**
|
||||||
|
|
||||||
当执行 `monitorenter` 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
|
当执行 `monitorenter` 指令时,线程试图获取锁也就是获取 **对象监视器 `monitor`** 的持有权。
|
||||||
|
|
||||||
**② synchronized 修饰方法的的情况**
|
> 在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由[ObjectMonitor](https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/runtime/objectMonitor.cpp)实现的。每个对象中都内置了一个 `ObjectMonitor`对象。
|
||||||
|
>
|
||||||
|
> 另外,**`wait/notify`等方法也依赖于`monitor`对象,这就是为什么只有在同步的块或者方法中才能调用`wait/notify`等方法,否则会抛出`java.lang.IllegalMonitorStateException`的异常的原因。**
|
||||||
|
|
||||||
|
在执行`monitorenter`时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
|
||||||
|
|
||||||
|
在执行 `monitorexit` 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
|
||||||
|
|
||||||
|
#### 1.3.2. `synchronized` 修饰方法的的情况
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public class SynchronizedDemo2 {
|
public class SynchronizedDemo2 {
|
||||||
@ -141,8 +207,15 @@ public class SynchronizedDemo2 {
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
|
`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
|
||||||
|
|
||||||
|
#### 1.3.3.总结
|
||||||
|
|
||||||
|
`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。
|
||||||
|
|
||||||
|
`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。
|
||||||
|
|
||||||
|
**不过两者的本质都是对对象监视器 monitor 的获取。**
|
||||||
|
|
||||||
### 1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
|
### 1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
|
||||||
|
|
||||||
@ -150,30 +223,32 @@ JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、
|
|||||||
|
|
||||||
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
|
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
|
||||||
|
|
||||||
关于这几种优化的详细信息可以查看笔主的这篇文章:<https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/Multithread/synchronized.md>
|
关于这几种优化的详细信息可以查看下面这几篇文章:
|
||||||
|
|
||||||
|
- [Java 性能 -- synchronized 锁升级优化](https://blog.csdn.net/qq_34337272/article/details/108498442)
|
||||||
|
- [Java6 及以上版本对 synchronized 的优化](https://www.cnblogs.com/wuqinglong/p/9945618.html)
|
||||||
|
|
||||||
### 1.5. 谈谈 synchronized 和 ReentrantLock 的区别
|
### 1.5. 谈谈 synchronized 和 ReentrantLock 的区别
|
||||||
|
|
||||||
|
#### 1.5.1. 两者都是可重入锁
|
||||||
|
|
||||||
**① 两者都是可重入锁**
|
**“可重入锁”** 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
|
||||||
|
|
||||||
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
|
#### 1.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
|
||||||
|
|
||||||
**② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API**
|
`synchronized` 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 `synchronized` 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。`ReentrantLock` 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
|
||||||
|
|
||||||
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
|
#### 1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能
|
||||||
|
|
||||||
**③ ReentrantLock 比 synchronized 增加了一些高级功能**
|
相比`synchronized`,`ReentrantLock`增加了一些高级功能。主要来说主要有三点:
|
||||||
|
|
||||||
相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)**
|
- **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
|
||||||
|
- **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。
|
||||||
|
- **可实现选择性通知(锁可以绑定多个条件)**: `synchronized`关键字与`wait()`和`notify()`/`notifyAll()`方法相结合可以实现等待/通知机制。`ReentrantLock`类当然也可以实现,但是需要借助于`Condition`接口与`newCondition()`方法。
|
||||||
|
|
||||||
- **ReentrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
|
> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 Condition 接口默认提供的。而`synchronized`关键字就相当于整个 Lock 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而`Condition`实例的`signalAll()`方法 只会唤醒注册在该`Condition`实例中的所有等待线程。
|
||||||
- **ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。
|
|
||||||
- synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),**线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”** ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
|
|
||||||
|
|
||||||
如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。
|
**如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。性能已不是选择标准**
|
||||||
|
|
||||||
**④ 性能已不是选择标准**
|
|
||||||
|
|
||||||
## 2. volatile 关键字
|
## 2. volatile 关键字
|
||||||
|
|
||||||
@ -205,9 +280,9 @@ synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
要解决这个问题,就需要把变量声明为**volatile**,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
|
要解决这个问题,就需要把变量声明为**`volatile`**,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
|
||||||
|
|
||||||
所以,**volatile 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。**
|
所以,**`volatile` 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -743,7 +818,6 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是
|
|||||||
|
|
||||||
所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
|
所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
|
||||||
|
|
||||||
|
|
||||||
并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。
|
并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。
|
||||||
|
|
||||||

|

|
||||||
@ -762,7 +836,6 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是
|
|||||||
|
|
||||||
使用原子的方式更新数组里的某个元素
|
使用原子的方式更新数组里的某个元素
|
||||||
|
|
||||||
|
|
||||||
- AtomicIntegerArray:整形数组原子类
|
- AtomicIntegerArray:整形数组原子类
|
||||||
- AtomicLongArray:长整形数组原子类
|
- AtomicLongArray:长整形数组原子类
|
||||||
- AtomicReferenceArray:引用类型数组原子类
|
- AtomicReferenceArray:引用类型数组原子类
|
||||||
@ -779,7 +852,6 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是
|
|||||||
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
|
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
|
||||||
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
|
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
|
||||||
|
|
||||||
|
|
||||||
### 5.3. 讲讲 AtomicInteger 的使用
|
### 5.3. 讲讲 AtomicInteger 的使用
|
||||||
|
|
||||||
**AtomicInteger 类常用方法**
|
**AtomicInteger 类常用方法**
|
||||||
@ -797,6 +869,7 @@ public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet
|
|||||||
**AtomicInteger 类的使用示例**
|
**AtomicInteger 类的使用示例**
|
||||||
|
|
||||||
使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。
|
使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
class AtomicIntegerTest {
|
class AtomicIntegerTest {
|
||||||
private AtomicInteger count = new AtomicInteger();
|
private AtomicInteger count = new AtomicInteger();
|
||||||
@ -865,7 +938,6 @@ AQS 原理这部分参考了部分博客,在5.2节末尾放了链接。
|
|||||||
|
|
||||||
看个 AQS(AbstractQueuedSynchronizer)原理图:
|
看个 AQS(AbstractQueuedSynchronizer)原理图:
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
|
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
|
||||||
@ -946,9 +1018,7 @@ tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true
|
|||||||
|
|
||||||
### 6.4. 用过 CountDownLatch 么?什么场景下用的?
|
### 6.4. 用过 CountDownLatch 么?什么场景下用的?
|
||||||
|
|
||||||
👨💻**面试官** :用过 CountDownLatch 么?什么场景下用的?
|
`CountDownLatch` 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 `CountDownLatch` 。具体场景是下面这样的:
|
||||||
|
|
||||||
🙋 **我** : `CountDownLatch` 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 `CountDownLatch ` 。具体场景是下面这样的:
|
|
||||||
|
|
||||||
我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。
|
我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。
|
||||||
|
|
||||||
@ -988,13 +1058,9 @@ public class CountDownLatchExample1 {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
👨💻**面试官** :有没有可以改进的地方呢?
|
**有没有可以改进的地方呢?**
|
||||||
|
|
||||||
🙋 **我** :可以提示一下具体的改进方向不?
|
可以使用 `CompletableFuture` 类来改进!Java8 的 `CompletableFuture` 提供了很多对多线程友好的方法,使用它可以很方便地为我们编写多线程程序,什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便。
|
||||||
|
|
||||||
👨💻**面试官** :Java 8 的新增加的一个多线程处理的类。
|
|
||||||
|
|
||||||
🙋 **我** :是 `CompletableFuture` 吧!这个确实可以通过这个类来改进。Java8 的 `CompletableFuture` 提供了很多对多线程友好的方法,使用它可以很方便地为我们编写多线程程序,什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便。
|
|
||||||
|
|
||||||
```java
|
```java
|
||||||
CompletableFuture<Void> task1 =
|
CompletableFuture<Void> task1 =
|
||||||
@ -1017,7 +1083,7 @@ CompletableFuture<Void> task6 =
|
|||||||
System.out.println("all done. ");
|
System.out.println("all done. ");
|
||||||
```
|
```
|
||||||
|
|
||||||
👨💻**面试官** :嗯嗯!大概意思说清楚了,不过代码还可以接续优化,当任务过多的时候,把每一个 task 都列出来不太现实,可以考虑通过循环来添加任务。
|
上面的代码还可以接续优化,当任务过多的时候,把每一个 task 都列出来不太现实,可以考虑通过循环来添加任务。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
//文件夹位置
|
//文件夹位置
|
||||||
@ -1033,8 +1099,6 @@ CompletableFuture<Void> allFutures = CompletableFuture.allOf(
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 7 Reference
|
## 7 Reference
|
||||||
|
|
||||||
- 《深入理解 Java 虚拟机》
|
- 《深入理解 Java 虚拟机》
|
||||||
|
422
docs/java/Multithread/Untitled.md
Normal file
422
docs/java/Multithread/Untitled.md
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
## synchronized / Lock
|
||||||
|
|
||||||
|
1. JDK 1.5之前
|
||||||
|
|
||||||
|
,Java通过
|
||||||
|
|
||||||
|
synchronized
|
||||||
|
|
||||||
|
关键字来实现
|
||||||
|
|
||||||
|
锁
|
||||||
|
|
||||||
|
功能
|
||||||
|
|
||||||
|
- synchronized是JVM实现的**内置锁**,锁的获取和释放都是由JVM**隐式**实现的
|
||||||
|
|
||||||
|
2. JDK 1.5
|
||||||
|
|
||||||
|
,并发包中新增了
|
||||||
|
|
||||||
|
Lock接口
|
||||||
|
|
||||||
|
来实现锁功能
|
||||||
|
|
||||||
|
- 提供了与synchronized类似的同步功能,但需要**显式**获取和释放锁
|
||||||
|
|
||||||
|
3. Lock同步锁是基于
|
||||||
|
|
||||||
|
Java
|
||||||
|
|
||||||
|
实现的,而synchronized是基于底层操作系统的
|
||||||
|
|
||||||
|
Mutex Lock
|
||||||
|
|
||||||
|
实现的
|
||||||
|
|
||||||
|
- 每次获取和释放锁都会带来**用户态和内核态的切换**,从而增加系统的**性能开销**
|
||||||
|
- 在锁竞争激烈的情况下,synchronized同步锁的性能很糟糕
|
||||||
|
- 在**JDK 1.5**,在**单线程重复申请锁**的情况下,synchronized锁性能要比Lock的性能**差很多**
|
||||||
|
|
||||||
|
4. **JDK 1.6**,Java对synchronized同步锁做了**充分的优化**,甚至在某些场景下,它的性能已经超越了Lock同步锁
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 实现原理
|
||||||
|
|
||||||
|
复制
|
||||||
|
|
||||||
|
```
|
||||||
|
public class SyncTest {
|
||||||
|
public synchronized void method1() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void method2() {
|
||||||
|
Object o = new Object();
|
||||||
|
synchronized (o) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
复制
|
||||||
|
|
||||||
|
```
|
||||||
|
$ javac -encoding UTF-8 SyncTest.java
|
||||||
|
$ javap -v SyncTest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修饰方法
|
||||||
|
|
||||||
|
复制
|
||||||
|
|
||||||
|
```
|
||||||
|
public synchronized void method1();
|
||||||
|
descriptor: ()V
|
||||||
|
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
|
||||||
|
Code:
|
||||||
|
stack=0, locals=1, args_size=1
|
||||||
|
0: return
|
||||||
|
```
|
||||||
|
|
||||||
|
1. JVM使用**ACC_SYNCHRONIZED**访问标识来区分一个方法是否为**同步方法**
|
||||||
|
|
||||||
|
2. 在方法调用时,会检查方法是否被设置了
|
||||||
|
|
||||||
|
ACC_SYNCHRONIZED
|
||||||
|
|
||||||
|
访问标识
|
||||||
|
|
||||||
|
- 如果是,执行线程会将先尝试**持有Monitor对象**,再执行方法,方法执行完成后,最后**释放Monitor对象**
|
||||||
|
|
||||||
|
### 修饰代码块
|
||||||
|
|
||||||
|
复制
|
||||||
|
|
||||||
|
```
|
||||||
|
public void method2();
|
||||||
|
descriptor: ()V
|
||||||
|
flags: ACC_PUBLIC
|
||||||
|
Code:
|
||||||
|
stack=2, locals=4, args_size=1
|
||||||
|
0: new #2 // class java/lang/Object
|
||||||
|
3: dup
|
||||||
|
4: invokespecial #1 // Method java/lang/Object."<init>":()V
|
||||||
|
7: astore_1
|
||||||
|
8: aload_1
|
||||||
|
9: dup
|
||||||
|
10: astore_2
|
||||||
|
11: monitorenter
|
||||||
|
12: aload_2
|
||||||
|
13: monitorexit
|
||||||
|
14: goto 22
|
||||||
|
17: astore_3
|
||||||
|
18: aload_2
|
||||||
|
19: monitorexit
|
||||||
|
20: aload_3
|
||||||
|
21: athrow
|
||||||
|
22: return
|
||||||
|
```
|
||||||
|
|
||||||
|
1. synchronized修饰同步代码块时,由**monitorenter**和**monitorexit**指令来实现同步
|
||||||
|
2. 进入**monitorenter**指令后,线程将**持有**该**Monitor对象**,进入**monitorexit**指令,线程将**释放**该**Monitor对象**
|
||||||
|
|
||||||
|
### 管程模型
|
||||||
|
|
||||||
|
1. JVM中的**同步**是基于进入和退出**管程**(**Monitor**)对象实现的
|
||||||
|
|
||||||
|
2. **每个Java对象实例都会有一个Monitor**,Monitor可以和Java对象实例一起被创建和销毁
|
||||||
|
|
||||||
|
3. Monitor是由**ObjectMonitor**实现的,对应[ObjectMonitor.hpp](https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/objectMonitor.hpp)
|
||||||
|
|
||||||
|
4. 当多个线程同时访问一段同步代码时,会先被放在**EntryList**中
|
||||||
|
|
||||||
|
5. 当线程获取到Java对象的Monitor时(Monitor是依靠
|
||||||
|
|
||||||
|
底层操作系统
|
||||||
|
|
||||||
|
的
|
||||||
|
|
||||||
|
Mutex Lock
|
||||||
|
|
||||||
|
来实现
|
||||||
|
|
||||||
|
互斥
|
||||||
|
|
||||||
|
的)
|
||||||
|
|
||||||
|
- 线程申请Mutex成功,则持有该Mutex,其它线程将无法获取到该Mutex
|
||||||
|
|
||||||
|
6. 进入
|
||||||
|
|
||||||
|
WaitSet
|
||||||
|
|
||||||
|
- 竞争锁**失败**的线程会进入**WaitSet**
|
||||||
|
- 竞争锁**成功**的线程如果调用**wait**方法,就会**释放当前持有的Mutex**,并且该线程会进入**WaitSet**
|
||||||
|
- 进入**WaitSet**的进程会等待下一次唤醒,然后进入EntryList**重新排队**
|
||||||
|
|
||||||
|
7. 如果当前线程顺利执行完方法,也会释放Mutex
|
||||||
|
|
||||||
|
8. Monitor依赖于**底层操作系统**的实现,存在**用户态**和**内核态之间**的**切换**,所以增加了**性能开销**
|
||||||
|
|
||||||
|
[](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-monitor.png)
|
||||||
|
|
||||||
|
复制
|
||||||
|
|
||||||
|
```
|
||||||
|
ObjectMonitor() {
|
||||||
|
_header = NULL;
|
||||||
|
_count = 0; // 记录个数
|
||||||
|
_waiters = 0,
|
||||||
|
_recursions = 0;
|
||||||
|
_object = NULL;
|
||||||
|
_owner = NULL; // 持有该Monitor的线程
|
||||||
|
_WaitSet = NULL; // 处于wait状态的线程,会被加入 _WaitSet
|
||||||
|
_WaitSetLock = 0 ;
|
||||||
|
_Responsible = NULL ;
|
||||||
|
_succ = NULL ;
|
||||||
|
_cxq = NULL ;
|
||||||
|
FreeNext = NULL ;
|
||||||
|
_EntryList = NULL ; // 多个线程访问同步块或同步方法,会首先被加入 _EntryList
|
||||||
|
_SpinFreq = 0 ;
|
||||||
|
_SpinClock = 0 ;
|
||||||
|
OwnerIsThread = 0 ;
|
||||||
|
_previous_owner_tid = 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 锁升级优化
|
||||||
|
|
||||||
|
1. 为了提升性能,在**JDK 1.6**引入**偏向锁、轻量级锁、重量级锁**,用来**减少锁竞争带来的上下文切换**
|
||||||
|
2. 借助JDK 1.6新增的**Java对象头**,实现了**锁升级**功能
|
||||||
|
|
||||||
|
### Java对象头
|
||||||
|
|
||||||
|
1. 在**JDK 1.6**的JVM中,对象实例在**堆内存**中被分为三部分:**对象头**、**实例数据**、**对齐填充**
|
||||||
|
2. 对象头的组成部分:**Mark Word**、**指向类的指针**、**数组长度**(可选,数组类型时才有)
|
||||||
|
3. Mark Word记录了**对象**和**锁**有关的信息,在64位的JVM中,Mark Word为**64 bit**
|
||||||
|
4. 锁升级功能主要依赖于Mark Word中**锁标志位**和**是否偏向锁标志位**
|
||||||
|
5. synchronized同步锁的升级优化路径:***偏向锁** -> **轻量级锁** -> **重量级锁***
|
||||||
|
|
||||||
|
[](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-mark-word.jpg)
|
||||||
|
|
||||||
|
### 偏向锁
|
||||||
|
|
||||||
|
1. 偏向锁主要用来优化**同一线程多次申请同一个锁**的竞争,在某些情况下,大部分时间都是同一个线程竞争锁资源
|
||||||
|
|
||||||
|
2. 偏向锁的作用
|
||||||
|
|
||||||
|
- 当一个线程再次访问同一个同步代码时,该线程只需对该对象头的**Mark Word**中去判断是否有偏向锁指向它
|
||||||
|
- **无需再进入Monitor去竞争对象**(避免用户态和内核态的**切换**)
|
||||||
|
|
||||||
|
3. 当对象被当做同步锁,并有一个线程抢到锁时
|
||||||
|
|
||||||
|
- 锁标志位还是**01**,是否偏向锁标志位设置为**1**,并且记录抢到锁的**线程ID**,进入***偏向锁状态***
|
||||||
|
|
||||||
|
4. 偏向锁
|
||||||
|
|
||||||
|
**不会主动释放锁**
|
||||||
|
|
||||||
|
- 当线程1再次获取锁时,会比较**当前线程的ID**与**锁对象头部的线程ID**是否一致,如果一致,无需CAS来抢占锁
|
||||||
|
|
||||||
|
- 如果不一致,需要查看
|
||||||
|
|
||||||
|
锁对象头部记录的线程
|
||||||
|
|
||||||
|
是否存活
|
||||||
|
|
||||||
|
- 如果**没有存活**,那么锁对象被重置为**无锁**状态(也是一种撤销),然后重新偏向线程2
|
||||||
|
|
||||||
|
- 如果
|
||||||
|
|
||||||
|
存活
|
||||||
|
|
||||||
|
,查找线程1的栈帧信息
|
||||||
|
|
||||||
|
- 如果线程1还是需要继续持有该锁对象,那么暂停线程1(**STW**),**撤销偏向锁**,**升级为轻量级锁**
|
||||||
|
- 如果线程1不再使用该锁对象,那么将该锁对象设为**无锁**状态(也是一种撤销),然后重新偏向线程2
|
||||||
|
|
||||||
|
5. 一旦出现其他线程竞争锁资源时,偏向锁就会被
|
||||||
|
|
||||||
|
撤销
|
||||||
|
|
||||||
|
- 偏向锁的撤销**可能需要**等待**全局安全点**,暂停持有该锁的线程,同时检查该线程**是否还在执行该方法**
|
||||||
|
- 如果还没有执行完,说明此刻有**多个线程**竞争,升级为**轻量级锁**;如果已经执行完毕,唤醒其他线程继续**CAS**抢占
|
||||||
|
|
||||||
|
6. 在
|
||||||
|
|
||||||
|
高并发
|
||||||
|
|
||||||
|
场景下,当
|
||||||
|
|
||||||
|
大量线程
|
||||||
|
|
||||||
|
同时竞争同一个锁资源时,偏向锁会被
|
||||||
|
|
||||||
|
撤销
|
||||||
|
|
||||||
|
,发生
|
||||||
|
|
||||||
|
STW
|
||||||
|
|
||||||
|
,加大了
|
||||||
|
|
||||||
|
性能开销
|
||||||
|
|
||||||
|
- 默认配置
|
||||||
|
|
||||||
|
- `-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000`
|
||||||
|
- 默认开启偏向锁,并且**延迟生效**,因为JVM刚启动时竞争非常激烈
|
||||||
|
|
||||||
|
- 关闭偏向锁
|
||||||
|
|
||||||
|
- `-XX:-UseBiasedLocking`
|
||||||
|
|
||||||
|
- 直接
|
||||||
|
|
||||||
|
设置为重量级锁
|
||||||
|
|
||||||
|
- `-XX:+UseHeavyMonitors`
|
||||||
|
|
||||||
|
红线流程部分:偏向锁的**获取**和**撤销**
|
||||||
|
[](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-1.png)
|
||||||
|
|
||||||
|
### 轻量级锁
|
||||||
|
|
||||||
|
1. 当有另外一个线程竞争锁时,由于该锁处于**偏向锁**状态
|
||||||
|
|
||||||
|
2. 发现对象头Mark Word中的线程ID不是自己的线程ID,该线程就会执行
|
||||||
|
|
||||||
|
CAS
|
||||||
|
|
||||||
|
操作获取锁
|
||||||
|
|
||||||
|
- 如果获取**成功**,直接替换Mark Word中的线程ID为自己的线程ID,该锁会***保持偏向锁状态***
|
||||||
|
- 如果获取**失败**,说明当前锁有一定的竞争,将偏向锁**升级**为轻量级锁
|
||||||
|
|
||||||
|
3. 线程获取轻量级锁时会有两步
|
||||||
|
|
||||||
|
- 先把**锁对象的Mark Word**复制一份到线程的**栈帧**中(**DisplacedMarkWord**),主要为了**保留现场**!!
|
||||||
|
- 然后使用**CAS**,把对象头中的内容替换为**线程栈帧中DisplacedMarkWord的地址**
|
||||||
|
|
||||||
|
4. 场景
|
||||||
|
|
||||||
|
- 在线程1复制对象头Mark Word的同时(CAS之前),线程2也准备获取锁,也复制了对象头Mark Word
|
||||||
|
- 在线程2进行CAS时,发现线程1已经把对象头换了,线程2的CAS失败,线程2会尝试使用**自旋锁**来等待线程1释放锁
|
||||||
|
|
||||||
|
5. 轻量级锁的适用场景:线程**交替执行**同步块,***绝大部分的锁在整个同步周期内都不存在长时间的竞争***
|
||||||
|
|
||||||
|
红线流程部分:升级轻量级锁
|
||||||
|
[](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-2.png)
|
||||||
|
|
||||||
|
### 自旋锁 / 重量级锁
|
||||||
|
|
||||||
|
1. 轻量级锁
|
||||||
|
|
||||||
|
CAS
|
||||||
|
|
||||||
|
抢占失败,线程将会被挂起进入
|
||||||
|
|
||||||
|
阻塞
|
||||||
|
|
||||||
|
状态
|
||||||
|
|
||||||
|
- 如果正在持有锁的线程在**很短的时间**内释放锁资源,那么进入**阻塞**状态的线程被**唤醒**后又要**重新抢占**锁资源
|
||||||
|
|
||||||
|
2. JVM提供了**自旋锁**,可以通过**自旋**的方式**不断尝试获取锁**,从而***避免线程被挂起阻塞***
|
||||||
|
|
||||||
|
3. 从
|
||||||
|
|
||||||
|
JDK 1.7
|
||||||
|
|
||||||
|
开始,
|
||||||
|
|
||||||
|
自旋锁默认启用
|
||||||
|
|
||||||
|
,自旋次数
|
||||||
|
|
||||||
|
不建议设置过大
|
||||||
|
|
||||||
|
(意味着
|
||||||
|
|
||||||
|
长时间占用CPU
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
- `-XX:+UseSpinning -XX:PreBlockSpin=10`
|
||||||
|
|
||||||
|
4. 自旋锁重试之后如果依然抢锁失败,同步锁会升级至
|
||||||
|
|
||||||
|
重量级锁
|
||||||
|
|
||||||
|
,锁标志位为
|
||||||
|
|
||||||
|
10
|
||||||
|
|
||||||
|
- 在这个状态下,未抢到锁的线程都会**进入Monitor**,之后会被阻塞在**WaitSet**中
|
||||||
|
|
||||||
|
5. 在
|
||||||
|
|
||||||
|
锁竞争不激烈
|
||||||
|
|
||||||
|
且
|
||||||
|
|
||||||
|
锁占用时间非常短
|
||||||
|
|
||||||
|
的场景下,自旋锁可以提高系统性能
|
||||||
|
|
||||||
|
- 一旦锁竞争激烈或者锁占用的时间过长,自旋锁将会导致大量的线程一直处于**CAS重试状态**,**占用CPU资源**
|
||||||
|
|
||||||
|
6. 在
|
||||||
|
|
||||||
|
高并发
|
||||||
|
|
||||||
|
的场景下,可以通过
|
||||||
|
|
||||||
|
关闭自旋锁
|
||||||
|
|
||||||
|
来优化系统性能
|
||||||
|
|
||||||
|
- ```
|
||||||
|
-XX:-UseSpinning
|
||||||
|
```
|
||||||
|
|
||||||
|
- 关闭自旋锁优化
|
||||||
|
|
||||||
|
- ```
|
||||||
|
-XX:PreBlockSpin
|
||||||
|
```
|
||||||
|
|
||||||
|
- 默认的自旋次数,在**JDK 1.7**后,**由JVM控制**
|
||||||
|
|
||||||
|
[](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-3.png)
|
||||||
|
|
||||||
|
## 小结
|
||||||
|
|
||||||
|
1. JVM在**JDK 1.6**中引入了**分级锁**机制来优化synchronized
|
||||||
|
|
||||||
|
2. 当一个线程获取锁时,首先对象锁成为一个
|
||||||
|
|
||||||
|
偏向锁
|
||||||
|
|
||||||
|
- 这是为了避免在**同一线程重复获取同一把锁**时,**用户态和内核态频繁切换**
|
||||||
|
|
||||||
|
3. 如果有多个线程竞争锁资源,锁将会升级为
|
||||||
|
|
||||||
|
轻量级锁
|
||||||
|
|
||||||
|
- 这适用于在**短时间**内持有锁,且分锁**交替切换**的场景
|
||||||
|
- 轻量级锁还结合了**自旋锁**来**避免线程用户态与内核态的频繁切换**
|
||||||
|
|
||||||
|
4. 如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁
|
||||||
|
|
||||||
|
5. 优化synchronized同步锁的关键:
|
||||||
|
|
||||||
|
减少锁竞争
|
||||||
|
|
||||||
|
- 应该尽量使synchronized同步锁处于**轻量级锁**或**偏向锁**,这样才能提高synchronized同步锁的性能
|
||||||
|
- 常用手段
|
||||||
|
- **减少锁粒度**:降低锁竞争
|
||||||
|
- **减少锁的持有时间**,提高synchronized同步锁在自旋时获取锁资源的成功率,**避免升级为重量级锁**
|
||||||
|
|
||||||
|
6. 在**锁竞争激烈**时,可以考虑**禁用偏向锁**和**禁用自旋锁**
|
@ -1,171 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### synchronized关键字最主要的三种使用方式的总结
|
|
||||||
|
|
||||||
- **修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁**
|
|
||||||
- **修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁** 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。
|
|
||||||
- **修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。** 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能!
|
|
||||||
|
|
||||||
下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。
|
|
||||||
|
|
||||||
面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**双重校验锁实现对象单例(线程安全)**
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class Singleton {
|
|
||||||
|
|
||||||
private volatile static Singleton uniqueInstance;
|
|
||||||
|
|
||||||
private Singleton() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Singleton getUniqueInstance() {
|
|
||||||
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
|
|
||||||
if (uniqueInstance == null) {
|
|
||||||
//类对象加锁
|
|
||||||
synchronized (Singleton.class) {
|
|
||||||
if (uniqueInstance == null) {
|
|
||||||
uniqueInstance = new Singleton();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return uniqueInstance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。
|
|
||||||
|
|
||||||
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
|
|
||||||
|
|
||||||
1. 为 uniqueInstance 分配内存空间
|
|
||||||
2. 初始化 uniqueInstance
|
|
||||||
3. 将 uniqueInstance 指向分配的内存地址
|
|
||||||
|
|
||||||
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
|
|
||||||
|
|
||||||
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
|
|
||||||
|
|
||||||
|
|
||||||
###synchronized 关键字底层原理总结
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**synchronized 关键字底层原理属于 JVM 层面。**
|
|
||||||
|
|
||||||
**① synchronized 同步语句块的情况**
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class SynchronizedDemo {
|
|
||||||
public void method() {
|
|
||||||
synchronized (this) {
|
|
||||||
System.out.println("synchronized 代码块");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
从上面我们可以看出:
|
|
||||||
|
|
||||||
**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
|
|
||||||
|
|
||||||
**② synchronized 修饰方法的的情况**
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class SynchronizedDemo2 {
|
|
||||||
public synchronized void method() {
|
|
||||||
System.out.println("synchronized 方法");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
|
|
||||||
|
|
||||||
|
|
||||||
在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
|
|
||||||
|
|
||||||
|
|
||||||
### JDK1.6 之后的底层优化
|
|
||||||
|
|
||||||
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
|
|
||||||
|
|
||||||
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
|
|
||||||
|
|
||||||
**①偏向锁**
|
|
||||||
|
|
||||||
**引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。
|
|
||||||
|
|
||||||
偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。
|
|
||||||
|
|
||||||
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
|
|
||||||
|
|
||||||
**② 轻量级锁**
|
|
||||||
|
|
||||||
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。** 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。
|
|
||||||
|
|
||||||
**轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!**
|
|
||||||
|
|
||||||
**③ 自旋锁和自适应自旋**
|
|
||||||
|
|
||||||
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
|
|
||||||
|
|
||||||
互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
|
|
||||||
|
|
||||||
**一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。
|
|
||||||
|
|
||||||
百度百科对自旋锁的解释:
|
|
||||||
|
|
||||||
> 何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
|
|
||||||
|
|
||||||
自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过`--XX:+UseSpinning`参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。**自旋次数的默认值是10次,用户可以修改`--XX:PreBlockSpin`来更改**。
|
|
||||||
|
|
||||||
另外,**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。
|
|
||||||
|
|
||||||
**④ 锁消除**
|
|
||||||
|
|
||||||
锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
|
|
||||||
|
|
||||||
**⑤ 锁粗化**
|
|
||||||
|
|
||||||
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
|
|
||||||
|
|
||||||
大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。
|
|
||||||
|
|
||||||
### Synchronized 和 ReenTrantLock 的对比
|
|
||||||
|
|
||||||
|
|
||||||
**① 两者都是可重入锁**
|
|
||||||
|
|
||||||
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
|
|
||||||
|
|
||||||
**② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API**
|
|
||||||
|
|
||||||
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
|
|
||||||
|
|
||||||
**③ ReenTrantLock 比 synchronized 增加了一些高级功能**
|
|
||||||
|
|
||||||
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)**
|
|
||||||
|
|
||||||
- **ReenTrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
|
|
||||||
- **ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。
|
|
||||||
- synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),**线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”** ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
|
|
||||||
|
|
||||||
如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。
|
|
||||||
|
|
||||||
**synchronized 异常就会释放锁,而 ReenTrantLock 异常需要在 finally 里 unlock**
|
|
||||||
|
|
||||||
**④ 性能已不是选择标准**
|
|
||||||
|
|
||||||
在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。**JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作**。
|
|
62
docs/java/Multithread/synchronized在JDK1.6之后的底层优化.md
Normal file
62
docs/java/Multithread/synchronized在JDK1.6之后的底层优化.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
JDK1.6 对锁的实现引入了大量的优化来减少锁操作的开销,如: **偏向锁**、**轻量级锁**、**自旋锁**、**适应性自旋锁**、**锁消除**、**锁粗化** 等等技术。
|
||||||
|
|
||||||
|
锁主要存在四中状态,依次是:
|
||||||
|
|
||||||
|
1. 无锁状态
|
||||||
|
2. 偏向锁状态
|
||||||
|
3. 轻量级锁状态
|
||||||
|
4. 重量级锁状态
|
||||||
|
|
||||||
|
锁🔐会随着竞争的激烈而逐渐升级。
|
||||||
|
|
||||||
|
另外,需要注意:**锁可以升级不可降级,即 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的。** 这种策略是为了提高获得锁和释放锁的效率。
|
||||||
|
|
||||||
|
### 偏向锁
|
||||||
|
|
||||||
|
**引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。
|
||||||
|
|
||||||
|
偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!(关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。)
|
||||||
|
|
||||||
|
#### 偏向锁的加锁
|
||||||
|
|
||||||
|
当一个线程访问同步块并获取锁时, 会在锁对象的对象头和栈帧中的锁记录里存储锁偏向的线程ID, 以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁, 只需要简单的测试一下锁对象的对象头的MarkWord里是否存储着指向当前线程的偏向锁(线程ID是当前线程), 如果测试成功, 表示线程已经获得了锁; 如果测试失败, 则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁), 如果没有设置, 则使用CAS竞争锁, 如果设置了, 则尝试使用CAS将锁对象的对象头的偏向锁指向当前线程.
|
||||||
|
|
||||||
|
#### 偏向锁的撤销
|
||||||
|
|
||||||
|
偏向锁使用了一种等到竞争出现才释放锁的机制, 所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁. 偏向锁的撤销需要等到全局安全点(在这个时间点上没有正在执行的字节码). 首先会暂停持有偏向锁的线程, 然后检查持有偏向锁的线程是否存活, 如果线程不处于活动状态, 则将锁对象的对象头设置为无锁状态; 如果线程仍然活着, 则锁对象的对象头中的MarkWord和栈中的锁记录要么重新偏向于其它线程要么恢复到无锁状态, 最后唤醒暂停的线程(释放偏向锁的线程).
|
||||||
|
|
||||||
|
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
|
||||||
|
|
||||||
|
### 轻量级锁
|
||||||
|
|
||||||
|
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。** 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。
|
||||||
|
|
||||||
|
**轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!**
|
||||||
|
|
||||||
|
### 自旋锁和自适应自旋
|
||||||
|
|
||||||
|
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
|
||||||
|
|
||||||
|
互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
|
||||||
|
|
||||||
|
**一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。
|
||||||
|
|
||||||
|
百度百科对自旋锁的解释:
|
||||||
|
|
||||||
|
> 何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
|
||||||
|
|
||||||
|
自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过`--XX:+UseSpinning`参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。**自旋次数的默认值是10次,用户可以修改`--XX:PreBlockSpin`来更改**。
|
||||||
|
|
||||||
|
另外,**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。
|
||||||
|
|
||||||
|
### 锁消除
|
||||||
|
|
||||||
|
锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
|
||||||
|
|
||||||
|
### 锁粗化
|
||||||
|
|
||||||
|
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
|
||||||
|
|
||||||
|
大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。
|
||||||
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user