多线程的这些锁知道吗?手写一个自旋锁?
多线程中的各种锁
1. 公平锁、非公平锁
1.1 概念:
公平锁就是先来后到、非公平锁就是允许加塞 Lock lock = new ReentrantLock(Boolean fair);
默认非公平
- 公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭。
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者节现象。
1.2 两者区别?
-
公平锁:
Threads acquire a fair lock in the order in which they requested it
公平锁,就是很公平,在并发环境中,每个线程在获取锁时,会先查看此锁维护的等待队列,如果为空,或者当前线程就是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己
-
非公平锁:
a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested
非公平锁比较粗鲁,上来就直接尝试占有额,如果尝试失败,就再采用类似公平锁那种方式。
1.3 如何体现公平非公平?
- 对Java ReentrantLock而言,通过构造函数指定该锁是否公平,默认是非公平锁,非公平锁的优点在于吞吐量比公平锁大
- 对Synchronized而言,是一种非公平锁
其中ReentrantLock和Synchronized默认都是非公平锁,默认都是可重入锁
2. 可重入锁(递归锁)
前文提到ReentrantLock和Synchronized默认都是非公平锁,默认都是可重入锁,那么什么是可重入锁?
2.1 概念
指的时同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块
2.2 为什么要用到可重入锁?
- 可重入锁最大的作用是避免死锁
- ReentrantLock/Synchronized 就是一个典型的可重入锁
2.3 代码验证可重入锁?
首先我们先验证ReentrantLock
package com.yuxue.juc.lockDemo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 尝试验证ReentrantLock锁的可重入性的Demo
* */
public class ReentrantLockDemo {
public static void main(String[] args) {
mobile mobile = new mobile();
new Thread(mobile,"t1").start();
new Thread(mobile,"t2").start();
}
}
/**
* 辅助类mobile,首先继承了Runnable接口,可以重写run方法
* 内部主要有两个方法
* run方法首先调用第一个方法
* */
class mobile implements Runnable {
Lock lock = new ReentrantLock();
//run方法首先调用第一个方法
@Override
public void run() {
testMethod01();
}
//第一个方法,目的是首先让线程进入方法1
public void testMethod01() {
//加锁
lock.lock();
try {
//验证线程进入方法1
System.out.println(Thread.currentThread().getName() + "\t" + "get in the method1");
//休眠
Thread.sleep(2000);
//进入方法2
testMethod02();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//第二个方法。目的是验证ReentrantLock是否是可重入锁
public void testMethod02() {
lock.lock();
try {
System.out.println("==========" + Thread.currentThread().getName() + "\t" + "leave the method1 get in the method2");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
因为同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块
之后观察输出结果为:
t1 get in the method1
==========t1 leave the method1 get in the method2
t2 get in the method1
==========t2 leave the method1 get in the method2
意味着线程t1进入方法1之后,再进入方法2,也就是说进入内层方法自动获取锁,之后释放方法2的那把锁,再释放方法1的那把锁,这之后线程t2才能获取到方法1的锁,才可以进入方法1
同样地,如果我们在方法1中再加一把锁,不给其解锁,也就是
那么结果会是怎么呢?我们运行代码可以得到
我们发现线程是停不下来的,线程t1进入方法1加了两把锁,之后进入t2,但是退出t1的方法过程中没有解锁,这就导致了t2线程无法拿到锁,也就验证了锁重入的问题
那么为了验证是同一把锁,我们在方法1对其加锁两次,方法2对其解锁两次可以吗?这锁是相同的吗?也就意味着:
我们再次运行,发现结果为:
t1 get in the method1
==========t1 leave the method1 get in the method2
t2 get in the method1
==========t2 leave the method1 get in the method2
也就侧面验证了加锁的是同一把锁,更验证了我们的锁重入问题
那么对于synchronized锁呢?代码以及结果如下所示:
package com.yuxue.juc.lockDemo;
public class SynchronizedDemo {
public static void main(String[] args) throws InterruptedException {
phone phone = new phone();
new Thread(()->{
phone.phoneTest01();
},"t1").start();
Thread.sleep(1000);
new Thread(()->{
phone.phoneTest01();
},"t2").start();
}
}
class phone{
public synchronized void phoneTest01(){
System.out.println(Thread.currentThread().getName()+"\t invoked phoneTest01()");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
phoneTest02();
}
public synchronized void phoneTest02() {
System.out.println(Thread.currentThread().getName()+"\t -----invoked phoneTest02()");
}
}
结果为:
t1 invoked phoneTest01()
t1 -----invoked phoneTest02()
t2 invoked phoneTest01()
t2 -----invoked phoneTest02()
上述两个实验可以验证我们的ReentrantLock以及synchronized都是可重入锁!
3. 自旋锁
3.1 自旋锁概念
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
就是我们CAS一文当中提到的这段代码:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
其中while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4))
这行代码更体现出了自旋锁的核心,也就是当我尝试去拿锁的时候,一直循环,直到拿到锁为止
3.2 手写一个自旋锁试试?
下面的
AtomicReference
可以去看CAS那篇有详细讲解
package com.yuxue.juc.lockDemo;
import java.util.concurrent.atomic.AtomicReference;
/**
* 实现自旋锁
* 自旋锁好处,循环比较获取直到成功为止,没有类似wait的阻塞
*
* 通过CAS操作完成自旋锁,t1线程先进来调用mylock方法自己持有锁2秒钟,
* t2随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到t1释放锁后t2随后抢到
*/
public class SpinLockDemo {
public static void main(String[] args) {
//资源类
SpinLock spinLock = new SpinLock();
//t1线程
new Thread(()->{
//加锁
spinLock.myLock();
try {
//休眠
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//解锁
spinLock.myUnlock();
},"t1").start();
//这里主线程休眠,为了让t1首先得到并加锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//t2线程
new Thread(()->{
spinLock.myLock();
try {
//这里休眠时间较长是为了让输出结果更加可视化
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLock.myUnlock();
},"t2").start();
}
}
class SpinLock {
//构造原子引用类
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//自己的加锁方法
public void myLock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t" + "come in myLock");
//自旋核心代码!!当期望值是null并且主内存值也为null,就将其设置为自己的thread,否则就死循环,也就是一直自旋
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void myUnlock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t" + "===== come in myUnlock");
//解锁,用完之后,当期望值是自己的thread主物理内存的值也是自己的,也就是被自己的线程占用
//用完之后解锁,将主物理内存中Thread的地方设置为空,供其他线程使用
atomicReference.compareAndSet(thread, null);
}
}
结果为:
//t1以及t2同时进入myLock方法,争夺锁使用权
t1 come in myLock
t2 come in myLock
//t1使用完首先释放锁
t1 ===== come in myUnlock
//t2使用完释放锁,但是在5秒之后,因为在程序中我们让其休眠了5s
t2 ===== come in myUnlock
4. 读写锁
分为:独占锁(写锁)/共享锁(读锁)/互斥锁
4.1 概念
-
独占锁:指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁
-
共享锁:只该锁可被多个线程所持有
ReentrantReadWriteLock
其读锁是共享锁,写锁是独占锁 -
互斥锁:读锁的共享锁可以保证并发读是非常高效的,读写、写读、写写的过程是互斥的
4.2 代码
首先是没有读写锁的代码,定义了一个资源类,里面底层数据结构为HashMap,之后多个线程对其写以及读操作
package com.yuxue.juc.lockDemo;
import java.util.HashMap;
import java.util.Map;
class MyCache {
//定义缓存当中的数据结构为Map型,键为String,值为Object类型
private volatile Map<String, Object> map = new HashMap<>();
//向Map中添加元素的方法
public void setMap(String key, Object value) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t" + " put value:" + value);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//底层的map直接put
map.put(key, value);
System.out.println(thread.getName() + "\t" + "put value successful");
}
//向Map中取出元素
public void getMap(String key) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t" + "get the value");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//底层的map直接get,并且返回值为Object类型
Object retValue = map.get(key);
System.out.println(thread.getName() + "\t" + "get the value successful, is " + retValue);
}
}
/**
* 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。 * 但是
* 如果有一个线程想取写共享资源来,就不应该允许其他线程可以对资源进行读或写
* 总结
* 读读能共存
* 读写不能共存
* 写写不能共存
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
//创建资源类
MyCache myCache = new MyCache();
//创建5个线程
for (int i = 0; i < 5; i++) {
//lambda表达的特殊性,需要final变量
final int tempInt = i;
new Thread(() -> {
//5个线程分别填值
myCache.setMap(tempInt + "", tempInt + "");
}, "thread-" + i).start();
}
//创建5个线程
for (int i = 0; i < 5; i++) {
final int tempInt = i;
new Thread(() -> {
//5个线程取值
myCache.getMap(tempInt + "");
}, "thread-" + i).start();
}
}
}
结果为:
thread-0 put value:0
thread-1 put value:1
thread-2 put value:2
thread-3 put value:3
thread-4 put value:4
thread-0 get the value
thread-1 get the value
thread-2 get the value
thread-3 get the value
thread-4 get the value
thread-0 put value successful
thread-1 put value successful
thread-2 put value successful
thread-3 put value successful
thread-4 put value successful
...
上述执行结果看似没有问题,但是违背了写锁最核心的本质,也就是如果有一个线程想取写共享资源来,就不应该允许其他线程可以对资源进行读或写
所以出现问题,此时就需要用到我们的读写锁,我们对我们自己的myLock()
以及myUnlock()
方法进行修改为使用读写锁的版本:
class MyCache {
//定义缓存当中的数据结构为Map型,键为String,值为Object类型
private volatile Map<String, Object> map = new HashMap<>();
//读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* 写操作:原子+独占
* 整个过程必须是一个完整的统一体,中间不许被分割,不许被打断 *
* @param key
* @param value
* */
//向Map中添加元素的方法
public void setMap(String key, Object value) {
try {
readWriteLock.writeLock().lock();
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t" + " put value:" + value);
Thread.sleep(300);
//底层的map直接put
map.put(key, value);
System.out.println(thread.getName() + "\t" + "put value successful");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
//向Map中取出元素
public void getMap(String key) {
try {
readWriteLock.readLock().lock();
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t" + "get the value");
Thread.sleep(300);
//底层的map直接get,并且返回值为Object类型
Object retValue = map.get(key);
System.out.println(thread.getName() + "\t" + "get the value successful, is " + retValue);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
之后运行结果变成:
thread-1 put value:1
thread-1 put value successful
thread-0 put value:0
thread-0 put value successful
thread-2 put value:2
thread-2 put value successful
thread-3 put value:3
thread-3 put value successful
thread-4 put value:4
thread-4 put value successful
thread-0 get the value
thread-1 get the value
thread-2 get the value
thread-4 get the value
thread-3 get the value
thread-2 get the value successful, is 2
thread-3 get the value successful, is 3
thread-0 get the value successful, is 0
thread-4 get the value successful, is 4
thread-1 get the value successful, is 1
也就对应上了写锁独占,必须当每一个写锁对应写操作完成之后,才可以进行下一次写操作,但是对于读操作,就可以多个线程共享,一起去缓存中读取数据
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步