在学习锁之前,我们先一起来了解几个关于锁的概念。了解之后,再进一步去学习锁的知识。
公平锁和非公平锁
公平锁:意思就是各个线程之间是公平的,不存在相互竞争的情况,多个线程之间按照申请锁的顺序来获取锁。
非公平锁:是指多个线程之间获取锁的顺序并不是按照申请的所的顺序,而是通过相互之间的竞争来获取锁,谁抢到了谁就拥有这把锁。
可重入锁
允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
乐观锁和悲观锁
乐观锁:顾名思义,代表他这把锁十分乐观。他总是觉得线程中的资源和数据不会被修改。所以他在读取的时候是不会加锁的,但是在乐观锁再进行写入操作的时候会判断数据是否被修改过。
悲观锁:与乐观锁截然不同,他具有严重的受害心理,他具有强烈的独占和排他性质。他总是认为资源和数据会被别人修改,所以他在持有数据的时候会把资源和数据都锁上,这样其他线程想要请求这个资源就会阻塞,直到悲观锁释放资源为止。
了解完这几个概念之后,我们一起来看看Java中的锁:
Synchronized
Synchronized,就是一个:非公平,悲观,可重入的锁。
Synchronized关键字的用法
1.修饰实例方法:默认为this当前方法调用对象;
2.修饰静态方法:默认为Class对象;
3.修饰代码块:是某个指定的Java对象;
Synchronized修饰实例方法
使用this当前对象作锁,锁定该方法,只有获取到this锁,才可以访问当前方法。
在并发过程中,可以有多个线程同时请求该方法,但是只有一个方法可以获取到锁,执行该方法。
对于不同的线程,他们所持有的对象,必须是相同的。
有以下两种方法使用Synchronized修饰实例方法,其作用和意义都是相同的。
public class DoSth {
// 实例方法
public synchronized void doSth1() {
// 获取this锁,执行该方法
}
// 实例方法
public void doSth2() {
synchronized(this) {
// 获取this锁,执行该代码块
}
}
}
public static void main(String[] args) {
// 实例化一个对象
DoSth do = new DoSth();
// 创建不同的线程1
Thread t1 = new Thread() {
public void run() {
// 使用相同的对象访问synchronized方法
do.doSth1();
}
};
// 创建不同的线程2
Thread t2 = new Thread() {
public void run() {
// 使用相同的对象访问synchronized方法
do.doSth1();
}
};
// 启动线程
t1.start();
t2.start();
}
Synchronized修饰静态方法
使用当前对象的class对象作为锁,锁定该方法,只有获取到了class锁,才能执行该方法;
多个线程所持有的对象可以不同,但其class类型必须相同;
public class DoSth {
// 静态方法
public synchronized static void doSth1() {
// 获取当前对象的Class对象锁,执行该方法
}
// 实例方法
public static void doSth2() {
synchronized(this.getClass()) {
// 获取当前对象的Class对象锁,执行该代码块
}
}
}
public static void main(String[] args) {
// 创建不同的对象(相同类型)
DoSth do1 = new DoSth();
DoSth do2 = new DoSth();
// 创建不同线程1
Thread thread01 = new Thread() {
public void run() {
// 使用不同的对象访问synchronized方法
do1.doSth2();
}
};
// 创建不同线程2
Thread thread02 = new Thread() {
public void run() {
// 使用不同的对象访问synchronized方法
do2.doSth2();
}
};
// 启动线程
thread01.start();
thread02.start();
}
Synchronized修饰代码块
使用自定义对象作为锁;
synchronized(自定义对象) {
}
Synchronized的底层实现
Synchronized的底层是由Monitor监视器所实现的,Synchronized代码块是通过一对 monitorenter / monitorexit 指令实现的。线程通过monitorenter指令来尝试获取monitor的所有权,当monitor被占用时就会处于锁定状态。
线程获取monitor的过程
1.如果monitor的进入数为0,则当前线程可以进入monitor,然后设置进入数为1,代表当前线程为monitor的持有者,即持有锁;
2.如果当前线程已经持有了monitor,只是重新进入,则monitor的进入数会+1;
3.如果在当前线程之前,已经有其他线程持有了monitor,则当前线程就会进入阻塞状态;直到monitor的进入数为0的时候,会再次尝试获取monitor;
Synchronized的锁升级机制
锁升级的针对Synchronized的一种机制,是对于Synchronized的一种优化,JVM会检测不同的情况,自动切换到适应的锁实现,即为锁的升级与降级。
在JVM中实现了三种类型的锁:偏向锁、轻量级锁、重量级锁
偏向锁
偏向锁适用于单线程的情况,如果发现有多个线程执行就会自动升级为轻量级锁;
轻量级锁
轻量级锁适用于多线程串行执行的情况,即多个线程按顺序执行,如果发现多个线程并行执行了就会自动升级为重量级锁;
重量级锁
重量级锁适用于多线程并行执行。
下图为Synchronized锁升级的原理:
ReentartLock
ReentartLock,就是默认非公平但可实现公平的,悲观,可重入的锁。
ReentartLock与Synchronized不同的是:在Synchronized中,线程只要获取不到锁,就会进入阻塞状态,一直等待,没有额外的尝试机制;而ReentartLock可以在获取失败后,可以尝试获取锁。
ReentartLock内部结构
ReentartLock内部共有三个类:Sync、NotfairSync、FairSync
NotfairSync类继承了Sync类:表示不公平策略获取锁,在获取锁的时候,不会按照公平的原则进行,不会让等待时间最久的线程获得锁,按照抢占机制来争夺锁;
FairSync类继承Sync类:表示公平策略获取锁,在获取锁的时候,按照公平的原则进行,当资源处于空闲状态时,他总会判断是否有等待时间更长的线程,如果有则将当前线程加入到等待队列的尾部,实现了公平获取原则。
ReentartLock构造方法:默认采用非公平锁。
public ReentrantLock() {
// 默认非公平策略
sync = new NonfairSync();
}
也可以通过传递参数确定采用公平策略或者是非公平策略,参数为 true表示公平策略,否则,采用非公平策略。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
Synchronized和ReentartLock的对比
Synchronized | ReentartLock | |
是一个关键字,依赖于JVM的底层实现 | 是一个类,依赖于JDK API | |
实现机制 | Monitor监视器 | AQS(AbstractQueueSynchronizer) |
锁的获取 | 抢占机制 | 可以通过tryLock()尝试获取锁,更加灵活 |
锁的释放 | 自动释放 | 必须手动释放锁 |
锁的类型 | 非公平锁 | 公平锁和非公平锁 |
可重入性 | 可重入 | 可重入 |
补充:死锁
死锁是指多个线程之间,都需要获取对方所持有的锁或者资源,所导致的无线等待状态。
例如:
package com.zwd.demo;
public class DeadLock {
private static Object lockA = new Object();
private static Object lockB = new Object();
public void add() throws InterruptedException {
synchronized (lockA) { // 获得lockA的锁
Thread.sleep(100); // 线程休眠
synchronized (lockB) { // 获得lockB的锁
System.out.println("执行add()");
} // 释放lockB的锁
} // 释放lockA的锁
}
public void dec() throws InterruptedException {
synchronized (lockB) { // 获得lockB的锁
Thread.sleep(100); // 线程休眠
synchronized (lockA) { // 获得lockA的锁
System.out.println("执行dec()");
} // 释放lockA的锁
} // 释放lockB的锁
}
}
对于上述代码而言:
当线程1和线程2分别执行add()和dec()时:
线程1:进入add(),获得lockA; 线程2:进入dec(),获得lockB;
线程1:休眠100毫秒,准备获取lockB,失败,等待中...
线程2:休眠100毫秒,准备获取lockA,失败,等待中...
现在,两个线程各持有一把锁,而双方都想要获取对方手里的锁,造成了无限等待状态,即为死锁。
当死锁放生之后,没有任何办法可以解决死锁,只能强制结束JVM进程。
产生死锁的四个条件:
1.资源互斥:在同一时刻,一把锁只能被一个线程所持有;
2.不可剥夺:当一个线程以获得了资源,在没有使用完之前,不能被剥夺,只能等待所持有资源的线程自行释放锁;
3.请求等待:当一个线程想要请求其他资源发生阻塞时,对于自己所持有的资源也不释放;
4.循环等待:多个线程之间的循环等待;
如何让避免死锁?
1.每次只占用不超过1个锁;
2.按照相同的顺序申请锁;
3.使用信号量;
使用信号量解决死锁:
public class DeadLock {
private Semaphore semaphoreA = new Semaphore(1);
private Semaphore semaphoreB = new Semaphore(1);
public void add() throws InterruptedException {
semaphoreA.acquire();
Thread.sleep(1000);
semaphoreB.acquire();
try {
System.out.println("执行add()");
} finally {
semaphoreB.release();
semaphoreA.release();
}
}
public void dec() throws InterruptedException {
while (semaphoreB.tryAcquire()) {
Thread.sleep(100);
if (semaphoreA.tryAcquire()) {
try {
System.out.println("执行dec()");
return;
} finally {
semaphoreA.release();
semaphoreB.release();
}
} else {
semaphoreA.release();
semaphoreB.release();
}
}
}
}