并发编程的三个问题
可见性
可见性 是指一个线程对共享变量进行修改,其他线程立即得到修改后的最新值。
可见性演示:
1、创建一个共享变量
2、创建一条线程不断读取共享变量
3、创建另一条线程修改共享变量
public class DemoVisibility {
// 1、创建一个共享变量
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
// 2、创建一条线程不断读取共享变量
new Thread(() -> {
while(flag){
}
}).start();
Thread.sleep(2000);
// 3、创建另一条线程修改共享变量
new Thread(() -> {
flag = false;
System.out.println("线程修改了变量,变量变为 false。");
}).start();
}
/**运行结果:
* 线程修改了变量,变量变为 false。
*
* 线程1持续运行,程序并没有结束
*/
}
小结:
并发编程时,会出现可见性问题,当一个线程对共享变量进行修改后,另外的线程并没有立即看到修改后的最新值。
原子性
原子性 是指再一次或多次操作中,要么所有的操作都执行并且不会受到其他因素干扰而中断,要么所有的操作都不执行。
原子性演示:
1、定义一个共享变量 number
2、对 number 进行 1000 次 ++ 操作
3、使用 5 个线程来进行
public class DemoAtomicity {
// 1、定义一个共享变量 number
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
// 2、对 number 进行 1000 次 ++ 操作
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
number++;
}
};
List<Thread> list = new ArrayList<>();
// 3、使用 5 个线程来进行
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread t: list) {
t.join();
}
System.out.println("number = " + number);
}
/**
* output1:number = 5000
* output2:number = 2729
*/
}
小结:
并发编程时,会出现原子性问题,当一个线程对共享变量操作进行到一半时,另外的线程也有可能来操作共享变量,从而干扰了前一个线程的操作。
有序性
有序性 是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码的顺序。
Java 内存模型 (JMM)
计算机结构
CPU
中央处理器,是计算机的控制和运算的核心,我们的程序最终都会编程指令让CPU去执行,处理程序中的数据。
内存
程序都是在内存中运行的,内存会保存程序运行时的数据,供CPU处理。
缓存
CPU运算速度和内存的访问速度差异比较大,这导致了CPU每次操作内存都需要耗费很多时间等待,内存的读写速度成为计算机运行的瓶颈。于是就有了在CPU和主内存之间增加缓存的设计。最靠近CPU的缓存成为L1,然后依次是L2、L3和主内存。
CPU Cache 分为三个级别: L1, L2, L3。级别越小越接近 CPU,速度越快,容量越小。
1、L1 是最接近 CPU 的,容量最小(如:32k),速度最快,每个核上都有一个 一级缓存。
2、L2 更大一些(如:256k),速度要慢一些,一般情况下,每个核上都有一个独立的 二级缓存。
3、L3 是三级缓存中容量最大的一级(如:12Mb),同时也是速度最慢的一级,在同一个 CPU 插槽之间的核共享一个 三级缓存。
Java内存模型
Java内存模型,是JVM规范中所定义的一种内存模型,JMM是标准化的,屏蔽掉了底层不同计算机的区别。
Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量的底层细节,具体如下:
- 主内存:所有线程共享,所有共享变量都存储于主内存中
- 工作内存:每一个线程都有自己的工作内存,工作内存只存储该线程对共享变量的副本,线程对变量的所有操作都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量
JMM 作用
JMM是一套在多线程读写共享数据时,对共享数据可见性、原子性和有序性的规则和保障。
- synchronized
- volatile
主内存与工作内存之间的交互
8个原子操作保证主内存与工作内存的交互不出错:
- lock、read、load、use、assign、store、write、unlock
· 对一个变量执行lock操作,将会清空工作内存中此变量的值
· 对一个变量执行unlock操作,必须先把此变量同步到主内存中
Synchronized 如何保证并发编程三大特性
synchronized能够保证在同一时刻只有一个线程执行该段代码,以达到保证并发安全的额效果。
synchronized (锁对象) {
// 受保护的资源
}
synchronized 保证原子性
synchronized保证同一时间只有一个线程拿到锁,能够进入同步代码块。
public class DemoAtomicity {
// 1、定义一个共享变量 number
private static int number = 0;
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 2、对 number 进行 1000 次 ++ 操作
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (obj){
number++;
}
}
};
List<Thread> list = new ArrayList<>();
// 3、使用 5 个线程来进行
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread t: list) {
t.join();
}
System.out.println("number = " + number);
}
/**
* output:number = 5000
*/
}
Synchronized 保证可见性
synchronized对应lock和unlock这两个原子操作,会刷新工作内存中共享变量的值。
· 对一个变量执行lock操作,将会清空工作内存中此变量的值
· 对一个变量执行unlock操作,必须先把此变量同步到主内存中
public class DemoVisibility {
// 1、创建一个共享变量
private static boolean flag = true;
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 2、创建一条线程不断读取共享变量
new Thread(() -> {
while(flag){
synchronized (obj) {
}
}
}).start();
Thread.sleep(2000);
// 3、创建另一条线程修改共享变量
new Thread(() -> {
flag = false;
System.out.println("线程修改了变量,变量变为 false。");
}).start();
}
/**运行结果:
* 线程修改了变量,变量变为 false。
*
* 程序结束
*/
}
synchronized 保证有序性
为什么要重排序
为了提高程序的执行效率,编译器和CPU会对程序中的代码进行重排序。
as-if-serial语义
不管编译器和CPU如何重排序,必须保证在单线程情况下程序的结果是正确的。
- 当操作之间不存在依赖关系时,可以进行重排序
synchronized 保证有序性的原理
对不存在依赖关系的操作集合加 synchronized 代码块,虽然重排序还是会发生,但 synchronized 保证同一时间只有一个线程拿到锁,能够进入同步代码块,因此重排序不会影响别的线程。
synchronized 特性
可重入
synchronized 是可重入锁,内部锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,在执行完同步代码块时,计数器数量 -1,直到数量为0,就释放这个锁。
- 可重入可以避免死锁
- 可重入可以让我们更好的来封装代码
public class DemoReIn {
public static void main(String[] args){
// 3、使用两个线程来执行
new MyThreadDemo().start();
new MyThreadDemo().start();
}
}
// 1、定义一个线程类
class MyThreadDemo extends Thread {
// 2、在线程类中的 run 方法中使用嵌套的同步代码块
@Override
public void run() {
synchronized (MyThread.class) {
System.out.println(getName() + " 进入了同步代码块 1");
synchronized (MyThread.class) {
System.out.println(getName() + " 进入了同步代码块 2");
}
}
}
/**
* Thread-0 进入了同步代码块 1
* Thread-0 进入了同步代码块 2
* Thread-1 进入了同步代码块 1
* Thread-1 进入了同步代码块 2
*/
}
不可中断
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。
· synchronized 不可被中断
· Lock 的 lock() 方法 不可被中断
· Lock 的 tryLock() 方法 可被中断
public class DemoUnIntr {
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 1、定义一个 Runnable
Runnable run = () -> {
// 2、在 Runnable 定义同步代码块
synchronized (obj) {
String name = Thread.currentThread().getName();
System.out.println(name + " 进入 同步代码块");
// 保证不退出同步代码块
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 3、先开启一个线程来执行同步代码块
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
// 4、后开启一个线程来执行同步代码块(阻塞状态)
Thread t2 = new Thread(run);
t2.start();
//
Thread.sleep(500);
System.out.println("t1 状态: " + t1.getState());
System.out.println("t2 状态: " + t2.getState());
// 5、停止第二个线程
System.out.println("执行 t2.interrupt(); 停止线程2前");
t2.interrupt();
System.out.println("执行 t2.interrupt(); 停止线程2后");
Thread.sleep(500);
System.out.println("t1 状态: " + t1.getState());
System.out.println("t2 状态: " + t2.getState());
}
/**
* Thread-0 进入 同步代码块
* t1 状态: TIMED_WAITING
* t2 状态: BLOCKED
* 执行 t2.interrupt(); 停止线程2前
* 执行 t2.interrupt(); 停止线程2后
* t1 状态: TIMED_WAITING
* t2 状态: BLOCKED
* Thread-1 进入 同步代码块
*/
}
Lock 不可中断 及 可中断
不可中断 lock.lock();
public class DemoUnIntr {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
test01();
}
public static void test01() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
try {
lock.lock();
System.out.println(name + " 获得锁,进入锁执行");
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(name + "释放锁");
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
Thread.sleep(500);
System.out.println("t1 状态: " + t1.getState());
System.out.println("t2 状态: " + t2.getState());
// 5、停止第二个线程
System.out.println("执行 t2.interrupt(); 停止线程2前");
t2.interrupt();
System.out.println("执行 t2.interrupt(); 停止线程2后");
Thread.sleep(500);
System.out.println("t1 状态: " + t1.getState());
System.out.println("t2 状态: " + t2.getState());
}
/**
* Thread-0 获得锁,进入锁执行
* t1 状态: TIMED_WAITING
* t2 状态: WAITING
* 执行 t2.interrupt(); 停止线程2前
* 执行 t2.interrupt(); 停止线程2后
* t1 状态: TIMED_WAITING
* t2 状态: WAITING
*/
}
可中断 lock.tryLock();
public class DemoUnIntr {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
test01();
}
public static void test01() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
boolean sign = false;
try {
sign = lock.tryLock(3, TimeUnit.SECONDS);
if(sign){
System.out.println(name + " 获得锁,进入锁执行");
Thread.sleep(8000);
}else{
System.out.println(name + " 在指定时间内没有得到锁,去做其他操作");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(sign){
lock.unlock();
System.out.println(name + "释放锁");
}
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
}
/**
* Thread-0 获得锁,进入锁执行
* Thread-1 在指定时间内没有得到锁,去做其他操作
*/
}
Synchronized 原理
反汇编
反汇编指令
javap -p -v **.class
实例
public class DemoSyn {
private static Object obj = new Object();
public static void main(String[] args) {
synchronized (obj) {
System.out.println("1");
}
}
public synchronized void test() {
System.out.println("2");
}
}
- monitorenter
每个锁对象都与一个 monitor 监视器相关联。监视器只有在有 owner 所有者时才被锁定。执行 monitorenter 的线程尝试获取与 monitor 监控器的所有权,如下所示:
* 如果 monitor 监视器的条目计数 recursions 为 0,则线程进入 monitor 监视器并将其条目计数设置为 1。线程就是监视器的 owner 所有者。
* 如果线程已经拥有 monitor 监视器的所有权,允许它重入 monitor 监视器,其条目计数 recursions + 1。
* 如果另一个线程已经占有 monitor 监视器的所有权,则当前尝试获取 monitor 监视器所有权的线程将被阻塞,直到 monitor 监视器的条目计数 recursions 为 0,然后才能重新尝试获取 monitor 所有权。 - monitorexit
* 能执行 monitorexit 指令的线程一定拥有当前对象的 monitor 所有权。
* 执行 monitorexit 指令时会将 monitor 的进入数 recursions - 1,当 recursions 为 0 时,当前线程退出 monitor,不再拥有 monitor 所有权,此时其他被这个 monitor 阻塞的线程可以尝试获取个 monitor 监视器的所有权。
monitorexit 插入在方法结束处和异常处,JVM保证每个 monitorenter 必须有对应的 monitorexit。
所以,出现异常会释放锁。
小结
同步方法反汇编后,可以看出会增加 ACC_SYNCHRONIZED 修饰。会在执行前后分别隐式调用 monitorenter 和 monitorexit。
每个锁对象都与一个 monitor 监视器相关联,监视器才是真正的锁对象,它内部有两个重要的成员变量:
* owner: 保存获得锁的线程
* recursions: 保存线程获得锁的次数
synchronized 和 Lock 的区别
1、synchronized 是关键字,Lock 是接口
2、synchronized 会自动释放锁,Lock 必须手动释放
3、synchronized 是不可中断的,Lock 可以是可中断的,也可以是不可中断的
4、通过 Lock 可以知道线程是否有拿到锁,synchronized 不行
5、synchronized 能锁住方法和代码块,Lock 只能锁住代码块
6、Lock 可以使用读写锁提高多线程读效率,读多个线程可以同时读,写只能一个线程写
7、synchronized 是非公平锁,ReentrantLock 可以控制是否是公平锁,无参-非公平
深入 JVM 源码
- _owner:线程拥有者
- _recursions:线程进入锁次数
- _cxq:第一次竞争锁失败的线程存放位置
- _EntryList:第二次及之后竞争锁失败的线程存放位置
- _WaitSet:处于 wait 状态的线程存放位置
monitor 竞争
1、通过CAS尝试把monitor的owner字段设置为当前线程
2、如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions++,记录重入次数
3、如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回
4、如果竞争失败,则等待锁的释放
monitor 等待
1、当前线程被封装成ObjectWaiter对象node,状态设置为ObjectWaiter::TS_CXQ
2、通过CAS+循环操作把node节点push到_cxq列表中,同一时刻可能有多个线程push自己的节点
3、node节点push到_cxq列表后,通过自旋尝试获取锁,如果仍获取失败,则通过park将当前线程挂起,等待被唤醒
4、当前线程被唤醒时,会从挂起的点继续执行,通过ObjectMonitor::TryLock尝试获取锁
monitor 释放
1、推出同步代码块时会让_recursions–,当_recursions值为0时,说明线程释放了锁
2、根据QMode指定的不同策略,从_cxq或_EntryList中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒该节点封装的线程,唤醒操作由unpark完成
monitor 是重量级锁
ObjectMonitor的函数设计内核函数,如Atomic::inc_ptr、park()、unpark()等,执行这些内核函数就会存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是重量级锁。
JDK 6 对 Synchronized 的优化
CAS(比较再交换)
CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。
CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V、旧的预估值X、要修改的新值B。如果旧的预估值X 等于 要修改的新值B,就将B保存到内存中。
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据时都会上锁,别人想拿就会阻塞。
synchronized 悲观锁; ReentrantLock 悲观锁
性能较差
乐观锁
每次拿数据都认为别人不会修改,就算改了也没关系,再重试即可。所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去修改这个数据,如果没有就更新,如果有人修改就重试。
CAS 获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile可是实现无锁并发,适用于竞争不激烈,多核CPU的场景下。
1、因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
2、但如果竞争激烈,重试频繁发生,反而会影响性能
小结
作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。
原理:CAS操作依赖3个值:内存中的值V、旧的预估值X、要修改的新值B。如果旧的预估值X 等于 要修改的新值B,就将B保存到内存中。
1.6锁升级过程
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
- 偏向锁:一个线程反复执行同步块时,适合使用偏向锁,当多个线程执行时,转换为轻量级锁
- 轻量级锁:适用于多个线程交替执行同步块情况,当多个线程同一时刻进入临界区,轻量级锁升级为重量级锁
Java对象布局
对象布局 | 备注 |
---|---|
Mark World | 对象头(8字节) |
Klass pointer | 对象头(默认压缩,4字节,不压缩,8字节) |
实例数据 | |
对齐数据 |
锁状态 | 25bit | 31bit | 1bit(CMS_FREE) | 4bit(分代年龄) | 1bit(偏向锁) | 2bit(锁标志位) |
---|---|---|---|---|---|---|
无锁 | unused | hashCode | 0 | 01 |
锁状态 | 62bit | 2bit(锁标志位) |
---|---|---|
重量级锁 | 指向互斥量(重量级锁)的指针 | 01 |
偏向锁
锁状态 | 54bit | 2bit | 1bit(CMS_FREE) | 4bit(分代年龄) | 1bit(偏向锁) | 2bit(锁标志位) |
---|---|---|---|---|---|---|
偏向锁 | ThreadID | Epoch | 1 | 01 |
偏向锁必须无竞争情况下使用。当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:
1、JVM将会把对象头中的标志位设为01,即偏向模式
2、同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark World中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高
偏向锁的撤销
1、偏向锁的撤销必须等待全局安全点
2、暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
3、撤销偏向锁(0),恢复到无锁(01)或轻量级锁(00)的状态
偏向锁的优点
- 适用于只有一个线程反复执行同步块的情况。JDK6之后偏向锁默认开启,但应用程序启动几秒钟之后才激活。
轻量级锁
锁状态 | 62bit | 2bit(锁标志位) |
---|---|---|
轻量级锁 | 指向栈中锁记录的指针 | 01 |
轻量级锁的目的
- 适用于多线程交替执行同步块的情况,尽量避免重量级锁引起的性能消耗
轻量级锁的原理
当关闭偏向锁或者多个线程竞争偏向锁,导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,步骤如下:
1、判断当前对象是否处于无锁状态(hashCode,0,01),如果是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象当前的Mark World的拷贝,将对象的Mark World复制到栈帧中的Lock Record中,将Lock Record中的owner指向当前对象
2、JVM利用CAS操作尝试将对象的Mark World更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁的标志位变成00,执行同步操作
3、如果失败,则判断当前对象的Mark World是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,直接执行同步代码块;否则只能说明该锁对象被其他线程抢占了,这是轻量级锁需要膨胀为重量级锁,锁标志位变为10,后面等待的线程将会进入阻塞状态
轻量级锁的释放
轻量级锁的释放也是通过CAS操作来进行的:
1、取出在获取轻量级锁保存在Displaced Mark World中的数据
2、用CAS操作将取出的数据替换为当前对象的Mark World中,如果成功,则说明释放锁成功
3、如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁升级为重量级锁
- 对于绝大部分的锁而言,在整个生命周期内是不会存在竞争的。这是轻量级锁提升性能的依据
自旋锁
前面我们讨论monitor实现锁的时候,知道monitor会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,这些操作给系统的并发性能带来了很大的压力。
同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间阻塞和唤醒线程并不值得。
如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程‘稍等一下”。 但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋) ,这项技术就是所谓的自旋锁。
自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用XX:+UseSpinning参数来开启,在JDK 6中就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源。而不会做任何有用的工作,反而会带来性上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次用户可以使用参数-XX: PreBlockSpln来更改。
适应性自旋锁
在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次循环。
另外。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。
锁消除
锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?实际上有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。
锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。
大部分情况下,上面的原则都是正确的, 但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。
平时写代码如何对 synchronized 优化
1、减少 synchronized 的范围
同步代码块尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。可能偏向锁、轻量级锁就搞得定,避免使用重量级锁。
2、降低 synchronized 锁的粒度
将一个锁拆分为多个锁提高并发度
- Hashtable 锁定整张表
- ConcurrentHashMap 锁定桶
- LinkedBlockingQueue 入队和出队使用不同的锁,读写分离
4、读写分离
读取时不加锁,写入和删除时加锁
- ConcurrentHashMap
- CopyOnWriteArrayList
- ConyOnWriteSet