一、公平锁、非公平锁
公平锁:获取不到锁的时候,会自动加入队列,等待线程释放后,队列的第一个线程获取锁
非公平锁:获取不到锁的时候,会自动加入队列,等待线程释放锁后所有等待的线程同时去竞争
Synchronized和ReentrantLock(默认构造)都是非公平的可重入锁:
Synchronized
常用的重量级锁Synchronized,它就是一个非公平的锁,因为在多线程的环境中,抢夺cpu的资源的过程中假如有7个线程,第一次抢到的可能是3号线程抢到,3号线程释放锁之后,第二次这7个线程继续抢,有可能还是3号线程抢到,所以是非公平的锁,无顺序纯靠运气。
ReentrantLock
//默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//也可以传参获取公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
通过构造函数指定该锁是否是公平的,默认是非公平的。非公平锁的有点在于吞吐量比公平锁大。
二、可重入锁
可重入锁又名递归锁,最大的作用就是避免死锁,什么是可重入?
Synchronized和ReentrantLock(默认构造)都是非公平的可重入锁:
Synchronized
public class ReenterLock_Demo01 {
public static void main(String[] args) throws InterruptedException {
Home home = new Home();
new Thread(() -> {
home.enterGate();
}, "A").start();
new Thread(() -> {
home.enterGate();
}, "B").start();
}
}
class Home {
//家中大门锁
public synchronized void enterGate() {
System.out.println(Thread.currentThread().getName() + "===> enterGate");
enterBedroom();
}
//卧室门锁
public synchronized void enterBedroom() {
System.out.println(Thread.currentThread().getName() + "===> enterBedroom");
}
}
解释一下:
当前两个方法的锁对象都是当前类的示例对象,两个线程共用同一个示例,也就是共同竞争同一把锁home ,以线程A为例,A执行enterGate方法获得了锁home,再执行enterBedroom方法时再次获取锁home,这就说明Synchronized支持可重入锁。
ReentrantLock
public class ReenterLock_Demo02 {
public static void main(String[] args) throws InterruptedException {
Home2 home = new Home2();
new Thread(() -> {
home.enterGate();
}, "A").start();
new Thread(() -> {
home.enterGate();
}, "B").start();
}
}
class Home2 {
Lock lock = new ReentrantLock();
//家中大门锁
public void enterGate() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "===> enterGate");
enterBedroom();
} catch (Exception ignored) {
} finally {
lock.unlock();
}
}
//卧室门锁
public void enterBedroom() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "===> enterBedroom");
} catch (Exception ignored) {
} finally {
lock.unlock();
}
}
}
解释:和上一个例子差不多意思,以线程A为例,先是 lock了一次,未释放,又再lock了一次,这就是可重入了。
注意:lock和unlock是配对出现的,lock了多少次,就需要unlock多少次。
三、自旋锁
在初步认识CAS的过程中我们见识到了自旋锁:
简言之:自旋锁就是让线程暂时死循环一阵子。
自定义自旋锁:
package com.atguigu.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class TestSpinLock {
public static void main(String[] args) throws InterruptedException {
MySpinlock lock = new MySpinlock();
new Thread(() -> {
lock.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (Exception ignored) {
} finally {
lock.myUnLock();
}
}, "T1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
lock.myLock();
try {
} catch (Exception ignored) {
} finally {
lock.myUnLock();
}
}, "T2").start();
}
}
class MySpinlock {
Integer a = 0;
AtomicReference<Integer> atomicReference = new AtomicReference<>(a);
// 加锁
public void myLock() {
System.out.println(Thread.currentThread().getName() + "==> mylock");
// 自旋锁
while (!atomicReference.compareAndSet(a, 1)) {
}
}
// 解锁
public void myUnLock() {
System.out.println(Thread.currentThread().getName() + "==> myUnlock");
atomicReference.compareAndSet(1, a);
}
}
解释一下:t1优先修改了atomicReference的值,t2后入因条件满足进入死循环(自旋);等t1解锁将atomicReference值改回来后,t2因条件不满足了走出死循环,再执行自己的相关操作。这就是自旋锁。
注意,自旋锁进入死循环的while循环条件必须具备线程安全,因此这里采用了atomicReference.compareAndSet(),CAS锁本身就是线程安全的
了解CAS可以看这篇文章:https://blog.csdn.net/weixin_41979002/article/details/123023634
看下面一个例子:
class MySpinlock {
volatile String flag = "unlock";
public boolean setToLock(){
if(flag.equals("unlock")){
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
flag = "lock";
return true;
}
return false;
}
public void setToUnlock(){
flag = "unlock";
}
// 加锁
public void myLock() {
System.out.println(Thread.currentThread().getName() + "==> mylock");
// 自旋锁
while (!setToLock()) {
}
}
// 解锁
public void myUnLock() {
System.out.println(Thread.currentThread().getName() + "==> myUnlock");
setToUnlock();
}
}
这种情况是不可行的,因为while循环条件本身就线程不安全了。自然这个自旋锁是不能实现的。
四、死锁
死锁,死锁的四个必要条件以及处理策略
互斥条件:一个资源每次只能被一个进程使用; 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺; 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
写个死锁,并看看如何排查死锁:
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new MyThread(lockA, lockB), "T1").start();
new Thread(new MyThread(lockB, lockA), "T2").start();
}
}
class MyThread implements Runnable {
private final String lockA;
private final String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
//锁住lockA对象
synchronized (lockA) {
//并在输出中尝试获取lockB对象
System.out.println(Thread.currentThread().getName() + "lock:" + lockA + "=>get" + lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//锁住lockB对象
synchronized (lockB) {
//并在输出中尝试获取lockA对象
System.out.println(Thread.currentThread().getName() + "lock:" + lockB + "=>get" + lockA);
}
}
}
}
此时我们可以看到程序已经锁死了:
但此时无任何报错,那我们应该如何排查呢?
问题解决:
使用 jps -l 定位进程号:
使用 jstack 进程号 找到死锁的问题