目录
1.1.1.修饰静态方法(锁粒度太大,用得少)(不能修饰静态变量)
--->PS:上述代码中synchronized是如何保证线程安全问题的?
2.可重入锁/手动锁:Lock(Lock是一个接口,通常所说的可重入锁是指Lock的一个实现子类ReentrantLock)
①创建锁对象Lock lock = new ReentrantLock();
a.unlock()一定要放在finally里,否则可能导致锁资源永久占用问题。
b.lock()要放到try外(官方建议)或try中的首行(问题不大)。
3.(面试必问)synchronized VS Lock(ReentrantLock)
①Lock 更灵活,有更多的方法,比如tryLock()。粒度可以更小(不明显)。
②Lock(接口级别)需要开发者手动操作锁(加/释放);而 synchronized 是 JVM 层面提供的锁,自动进行加锁和释放锁操作,对于开发者是无感的。
③Lock 只能修饰代码块;而 synchronized 可以修饰普通方法、静态方法和代码块。
④锁类型不同:Lock 默认是非公平锁,但可以指定为公平锁;而 synchronized 只能是非公平锁。
⑤调用Lock和synchronized线程等待锁的状态不同:lock会变为WAITING;而synchronized会变为BLOCKED。
使用锁(最主要有以下2种)是Java中解决线程安全问题最主要的手段 。
1.内置锁:synchronized(JVM中内置了)
1.1.synchronized基本用法
1.1.1.修饰静态方法(锁粒度太大,用得少)(不能修饰静态变量)
public class ThreadSynchronized {
//变量
private static int number = 0;
static class Counter{
//循环次数
private static int MAX_COUNT = 1000000;
//++方法
public synchronized static void incr() {
for (int i = 0; i < MAX_COUNT; i++) {
number++;
}
}
//--方法
public synchronized static void decr() {
for (int i = 0; i < MAX_COUNT; i++) {
number--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
Counter.incr();
});
t1.start();
Thread t2 = new Thread(() -> {
Counter.decr();
});
t2.start();
t1.join();
t2.join();
System.out.println("最终的结果:" + number);
}
}
--->PS:上述代码中synchronized是如何保证线程安全问题的?
- 当使用synchronized给整个方法加锁时,其效率极低,甚至比单线程还慢。
- 性能的好坏取决于synchronized修饰的范围,范围越大,安全性越高,性能越低;反之,锁的粒度越小,修饰的代码越少,安全性越低,性能越高。
1.1.2.修饰普通方法(锁粒度太大,用得少)
public class ThreadSynchronized2 {
//变量
private static int number = 0;
static class Counter{
//循环次数
private static int MAX_COUNT = 1000000;
//++方法
public synchronized void incr() {
for (int i = 0; i < MAX_COUNT; i++) {
number++;
}
}
//--方法
public synchronized void decr() {
for (int i = 0; i < MAX_COUNT; i++) {
number--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
counter.incr();
});
t1.start();
Thread t2 = new Thread(() -> {
counter.decr();
});
t2.start();
t1.join();
t2.join();
System.out.println("最终的结果:" + number);
}
}
1.1.3.修饰代码块(最常用)
public class ThreadSynchronized3 {
//变量
private static int number = 0;
static class Counter{
//循环次数
private static int MAX_COUNT = 1000000;
//++方法
public void incr() {
for (int i = 0; i < MAX_COUNT; i++) {
synchronized (this) { //注意:synchronized (加锁的对象),两个方法里加锁的对象要一样
number++;
}
}
}
//--方法
public void decr() {
for (int i = 0; i < MAX_COUNT; i++) {
synchronized (this) { //注意:synchronized (加锁的对象),两个方法里加锁的对象要一样
number--;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
counter.incr();
});
t1.start();
Thread t2 = new Thread(() -> {
counter.decr();
});
t2.start();
t1.join();
t2.join();
System.out.println("最终的结果:" + number);
}
}
--->PS:3种常用场景:
- this(非静态方法)
- xxx.class
- 自定义锁对象(更灵活,最常用)
①使用synchronized时,对于同一个业务的多个线程加锁对象,一定要是同一个对象(加同一把锁),否则加锁无效。
②synchronized修饰代码块,代码块在静态方法中时,不能使用this对象,否则会直接报错。
那么改为使用类型:
③自定义锁对象(普通/静态(加static)都可以)
1.2.synchronized特性
1.2.1.互斥性/排他性
synchronized(非公平锁)会起到互斥效果,某个线程执⾏到某个对象的 synchronized 中时,其他线程如果也执⾏到同⼀个对象 synchronized 就会阻塞等待。
- 进⼊ synchronized 修饰的代码块, 相当于加锁。
- 退出 synchronized 修饰的代码块, 相当于解锁。
--->PS:公平锁 VS 非公平锁
- 公平锁:按资排辈,先到先得。需要“唤醒”这一步操作,会牺牲一定的性能。(线程发现锁占用,尝试获取锁一段时间后,进入休眠状态,进入排队队列中等待。上一个线程释放锁后,会唤醒排队等候的其他线程中最前面的线程从阻塞状态又切换至运行状态)
- 非公平锁:来得早不如来得巧,不需要“唤醒”,性能高。(线程1发现锁占用,尝试获取锁一段时间后,进入休眠状态。此时线程2来了,处于运行状态,尝试获取锁,此时刚好上一个线程释放了锁,那么线程2直接得到了锁并去运行它的任务了。排队等待的其他线程获取锁的顺序不是按照访问的顺序先来先到的,而是由线程自己竞争,随机获取到锁)Java里所有的锁默认是非公平锁。
synchronized⽤的锁是存在Java对象头(隐藏的属性)⾥的:
Java中每个对象都有一个隐藏的对象头,它用来存储拥有当前锁的线程ID;它有一个标识,标识当前锁是被占用状态还是闲置状态。
- 可粗略理解成, 每个对象在内存中存储时, 都存有⼀块内存(对象头)表示当前的 "锁定" 状态(类似于厕所的 "有⼈/⽆⼈")。
- 如果当前是 "⽆⼈" 状态, 那么就可以使⽤, 使⽤时需要设为 "有⼈" 状态。
- 如果当前是 "有⼈" 状态, 那么其他⼈⽆法使⽤, 只能排队。
理解 "阻塞等待":
针对每⼀把锁,操作系统内部都维护了⼀个等待队列。当这个锁被某个线程占有的时候,其他线程尝试进⾏加锁,就加不上了,就会阻塞等待,⼀直等到之前的线程解锁之后,由操作系统唤醒⼀个新的线程,再来获取到这个锁。
注意:
上⼀个线程解锁之后,下⼀个线程并不是⽴即就能获取到锁,⽽是要靠操作系统来"唤醒",这也就是操作系统线程调度的⼀部分⼯作。
假设有 A B C 三个线程,线程 A 先获取到锁,然后 B 尝试获取锁,然后 C 再尝试获取锁,此时B 和 C 都在阻塞队列中排队等待。但是当 A 释放锁之后,虽然 B ⽐ C 先来的,但是 B 不⼀定就能获取到锁,⽽是和 C 重新竞争,并不遵守先来后到的规则。
1.2.2.刷新内存
synchronized 的⼯作过程:
- 获得互斥锁。
- 从主内存拷贝变量的最新副本到⼯作的内存。
- 执⾏代码。
- 将更改后的共享变量的值刷新到主内存。
- 释放互斥锁。
所以 synchronized 也能保证内存可见性。
1.2.3.可重入性
synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题。
/**
* synchronized 可重入性测试
*/
public class ThreadSynchronized4 {
public static void main(String[] args) {
synchronized (ThreadSynchronized4.class) {
System.out.println("当前主线程已经得到了锁"); //当执行到此行代码时,表示已经获得锁
synchronized (ThreadSynchronized4.class) { //同一个线程获取锁两次
System.out.println("当前主线程再次得到了锁"); //若两行代码都能打印,说明具备可重入性
}
}
}
}
1.3.synchronized实现原理
(面试必问)synchronized是如何实现的?
①在Java代码层面:
synchronized加锁的对象里有一个的隐藏的对象头,这个对象头(可看作一个类)里有很多属性,其中比较关注的两个属性是:是否加锁的标识和拥有当前锁的线程id。
每次进⼊ synchronized 修饰的代码块时,会去对象头中先判断加锁的标识,再判断拥有当前锁的线程id,从而决定当前线程能否往下继续执行。
- 判断加锁标识为false->对象头未加锁,当前线程可以进入synchronized 修饰的代码块,并设置加锁标识为true,设置拥有当前锁的线程id为此线程id。
- 判断加锁标识为true->对象头已加锁,需进一步判断拥有当前锁的线程id是否为此线程id,若是,则继续往下执行;否则,不能往下执行,需要等待锁资源释放后重新竞争再获取锁。
②在JVM层面和操作系统层面:
synchronized同步锁是通过JVM内置的Monitor监视器实现的,而监视器又是依赖操作系统的互斥锁Mutex实现的。↓
1.3.1.监视器
监视器是一个概念或者说是一个机制,它用来保障在任何时候,只有一个线程能够执行指定区域的代码。
一个监视器像是一个建筑,建筑里有一个特殊的房间,这个房间同一时刻只能被一个线程占有。
一个线程从进入该房间到离开该房间,可以全程独占该房间的所有数据。
- 进入该建筑叫做进入监视器(entering the monitor);
- 进入该房间叫做获得监视器(acquiring the monitor);
- 独自占有该房间叫做拥有监视器(owing the monitor);
- 离开该房间叫做释放监视器(releasing the monitor);
- 离开该建筑叫做退出监视器(exiting the monitor)。
严格意义说,监视器和锁的概念是不同的,但很多地方将二者互相指代。
1.3.2.底层实现
查看字节码:
只能通过命令去看:
public class ThreadSynchronized5 {
public static void main(String[] args) {
synchronized (ThreadSynchronized5.class) {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
}
源代码.java运行后就会在out目录下生成字节码文件.class,用命令查看字节码:
在字节码中,main方法中多了一对monitorenter和monitorexit指令,分别表示进入监视器和退出监视器。可知synchronized是依赖Monitor监视器实现的。
1.3.3.执行流程
在Java中,synchronized是非公平锁,也是可重入锁(指一个线程获取到锁之后,可以重复得到该锁)。
在HotSpot虚拟机(JVM的一种)中,Monitor底层是由C++实现的,它的实现对象是ObjectMonitor(隐藏的对象头),ObjectMonitor结构体的实现如下:
//结构体如下
ObjectMonitor::ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; //线程的重入次数
_object = NULL;
_owner = NULL; //标识拥有该monitor的线程
_WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁进入时的单向链表
FreeNext = NULL ;
_EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
以上代码中几个关键属性:
- _count:记录该线程获取锁的次数。
- _recursions:线程重入锁的次数。
- _owner:拥有者,是持有该ObjectMonitor(监视器)对象的线程,记录的是线程id。
- _EntryList:监控集合,存放的是处于阻塞状态的线程队列,在多线程下,竞争失败的线程会进入EntryList队列。
- _WaitSet:待授权集合,存放的是处于wait状态的线程队列,当线程执行了wait()方法之后,会进入WaitSet队列。
监视器执行流程如下:
- 线程通过CAS(是乐观锁里的一个对比并替换机制)尝试获取锁,如果获取成功,就将owner字段设置为当前线程,说明当前线程已经持有锁,并将recursions重入次数的属性+1。如果获取失败,则先通过自旋CAS尝试获取锁,如果还是失败则将当前线程放入到EntryList监控队列(阻塞)。
- 当拥有锁的线程执行了wait方法之后,线程释放锁,将owner变量恢复为null状态,同时将该线程放入WaitSet待授权队列中等待被唤醒。
- 当调用notify方法时,随机唤醒WaitSet队列中的某一个线程,当调用notifyAll时,唤醒所有的WaitSet中的线程尝试获取锁。
- 线程执行完释放了锁之后,会唤醒EntryList中的所有线程尝试获取锁。
1.4.synchronized历史发展进程
- 在JDK1.6之前(多使用Lock)synchronized使用很少,那时synchronized默认使用重量级锁实现,所以性能较差。
- 在JDK1.6时,synchronized做了优化。锁升级流程如下:
- 无锁:没有线程访问时默认是无锁状态,加了synchronized也是无锁状态。有线程访问时才加锁。更大程度上减少锁带来的程序上的开销。
- 偏向锁:当有一个线程访问时会升级为偏向锁。(在对象头里存了这样一把锁,后面再来线程时会判断,如果线程id和拥有锁的线程id相同,会让它进去,只偏向某一个线程,其他线程来了之后要继续等)
- 轻量级锁:当有多个线程访问时会升级为轻量级锁。
- 重量级锁:当有更多线程访问时会升级为重量级锁。
- Oracle也有设计锁降级的功能。当线程访问变少时,会由重量级锁->降级为轻量级锁->偏向锁->无锁。但是这个机制目前还没有完善,只是有一个设计的思想和理念。
锁升级/降级就好比HashMap在JDK1.8之后底层实现:数组+链表/红黑树(存储结构的升级/降级)
- 当数组长度>8且链表长度>64,会将链表升级为红黑树。
- 当数组长度<8且链表长度<64,会将红黑树降级为链表。
2.可重入锁/手动锁:Lock(Lock是一个接口,通常所说的可重入锁是指Lock的一个实现子类ReentrantLock)
比synchronized更灵活,功能更多~
2.1.Lock实现步骤:
①创建锁对象Lock lock = new ReentrantLock();
②加锁lock.lock();
③释放锁lock.unlock();
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 手动锁(可重入锁的基本使用)
*/
public class ThreadLock {
public static void main(String[] args) {
//1.创建锁对象
Lock lock = new ReentrantLock();
//2.加锁操作
lock.lock();
try{
//业务代码(可能会非常复杂->可能会导致异常,后面的代码就不执行了,锁就无法释放,当前线程会永久占用锁资源)
//所以要用try-catch-finally(可以没有catch)
System.out.println("你好,ReentrantLock.");
} finally {
//3.释放锁(一定要放在finally中)
lock.unlock();
}
}
}
--->注意事项:
a.unlock()一定要放在finally里,否则可能导致锁资源永久占用问题。
b.lock()要放到try外(官方建议)或try中的首行(问题不大)。
oracle官方文档中的说明https://docs.oracle.com/javase/8/docs/api/
lock.lock(); try { } finally { lock.unlock() }
try { lock.lock(); int n = 1/0; } finally { lock.unlock() }
↓不行:
try { int n = 1/0; lock.lock(); } finally { lock.unlock() }
原因有两个:
- 如果放在 try ⾥⾯,因为 try 代码中的异常导致加锁失败,但还会执行 finally 释放锁的操作。 未加锁却释放锁,肯定会报错。
- 报错信息中:unlock 异常会覆盖 try ⾥⾯的业务异常,从而增加调式程序和修复程序的复杂度,增加排查错误的难度。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock2 {
private static int number = 0;
static class Counter{
//1.创建锁对象
private static Lock lock = new ReentrantLock();
//循环次数
private static int MAX_COUNT = 1000000;
//++方法
public static void incr() {
for (int i = 0; i < MAX_COUNT; i++) {
lock.lock(); //2.加锁
try{
number++;
} finally {
lock.unlock(); //3.释放锁
}
}
}
//--方法
public static void decr() {
for (int i = 0; i < MAX_COUNT; i++) {
lock.lock();
try{
number--;
} finally {
lock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
Counter.incr();
});
t1.start();
Thread t2 = new Thread(() -> {
Counter.decr();
});
t2.start();
t1.join();
t2.join();
System.out.println("最终的结果:" + number);
}
}
2.2.Lock指定锁类型——公平锁和非公平锁
查看源码:
//1.创建锁对象
Lock lock = new ReentrantLock(true); //公平锁
Lock lock = new ReentrantLock(); //非公平锁
Lock lock = new ReentrantLock(false); //非公平锁
使用公平锁比使用非公平锁要慢!
3.(面试必问)synchronized VS Lock(ReentrantLock)
①Lock 更灵活,有更多的方法,比如tryLock()。粒度可以更小(不明显)。
boolean result = lock.tryLock(); //判断拿这个锁是否成功
boolean result = lock.tryLock(5, TimeUnit.SECONDS); //设置最大等待时间和单位,最多等5s,5s后如果还是没拿到锁,就不等了,返回false
//设置动态等待时间
for (int i = 0; i < 15; i++) {
boolean result = lock.tryLock(1 + i, TimeUnit.SECONDS);
}
//第一次设置最大等待时间为1s,如果1s后没有获取到锁,继续循环,下一次最大等待时间就是2s,如果没得到,第3次就等待3s.
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock4 {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
// synchronized (lock) { //如果获取锁失败会死等,直到拿到锁,执行业务代码
//
// }
boolean result = lock.tryLock(5, TimeUnit.SECONDS);
if(result) {
//获取锁成功,执行相应的代码
} else {
//获取锁失败,执行相应的业务
//可以灵活处理,可以不等这个锁了,执行其他的业务操作;或回到最初代码,再去执行拿锁操作。可以人为控制。
}
}
}
②Lock(接口级别)需要开发者手动操作锁(加/释放);而 synchronized 是 JVM 层面提供的锁,自动进行加锁和释放锁操作,对于开发者是无感的。
③Lock 只能修饰代码块;而 synchronized 可以修饰普通方法、静态方法和代码块。
④锁类型不同:Lock 默认是非公平锁,但可以指定为公平锁;而 synchronized 只能是非公平锁。
⑤调用Lock和synchronized线程等待锁的状态不同:lock会变为WAITING;而synchronized会变为BLOCKED。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock4 {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
lock.lock();
System.out.println("线程1得到了锁");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("线程1释放锁");
lock.unlock();
}
});
t1.start();
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try{
System.out.println("线程2得到了锁");
} finally {
System.out.println("线程2释放锁");
lock.unlock();
}
});
t2.start();
t1.join();
t2.join();
}
}
Lock:
public class ThreadLock4 {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
lock.lock();
System.out.println("线程1得到了锁");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("线程1释放锁");
lock.unlock();
}
});
t1.start();
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try{
System.out.println("线程2得到了锁");
} finally {
System.out.println("线程2释放锁");
lock.unlock();
}
});
t2.start();
Thread.sleep(1500);
System.out.println("线程2的状态:" + t2.getState());
t1.join();
t2.join();
}
}
synchronized:
public class ThreadLock4 {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
synchronized (ThreadLock4.class) {
System.out.println("线程1得到了锁");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1释放锁");
}
});
t1.start();
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ThreadLock4.class) {
System.out.println("线程2得到了锁");
System.out.println("线程2释放锁");
}
});
t2.start();
Thread.sleep(1500);
System.out.println("线程2的状态:" + t2.getState());
t1.join();
t2.join();
}
}
Lock和synchronized线程2都不是runnable运行状态。因为如果一直让线程2尝试获取锁,就会造成资源的浪费。