多线程中的锁
WangScaler: 一个用心创作的作者。声明:才疏学浅,如有错误,恳请指正。
- 公平锁
- 非公平锁
- 可重入锁
- 自旋锁
- 读写锁(独占锁/共享锁)
- 互斥锁
非公平锁
非公平锁就像去饭店吃饭,虽然你先到,但是你后面的和你前面的如果点的一样,可能老板同时将他们的一块做出来提高效率,这样你在你后边的之后执行。非公平锁有可能造成优先级反转(上述情况)或者饥饿现象(上述情况的极端情况,一直没给你做)。当线程进来会先尝试占有锁,如果成功则占有锁,如果失败则采用公平锁的方式。像传统的Synchronized就是非公平锁,ReentrantLock默认的也是非公平锁。
package com.wangscaler.lock;
/**
* @author WangScaler
* @date 2021/8/7 10:07
*/
public class Programmer {
public synchronized void sayHello() throws Exception {
System.out.println(Thread.currentThread().getName() + ": Hello");
sayWorld();
}
public synchronized void sayWorld() throws Exception {
System.out.println(Thread.currentThread().getName() + " World");
}
public static void main(String[] args) {
int num = 3;
Programmer programmer = new Programmer();
for (int i = 0; i < num; i++) {
new Thread(() -> {
try {
programmer.sayHello();
} catch (Exception e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
上述示例的执行结果
0: Hello
0 World
2: Hello
2 World
1: Hello
1 World
从执行结果来看,印证了Synchronized是非公平锁,我们可以看到线程是1是在线程2之后执行的。同样的我们将synchronized换成Lock lock = new ReentrantLock();
的方式经过测试也是非公平的。为了更直观的看到效果,你可以多增加几个线程。
接下来我们将上述的Synchronized换成公平锁的形式看看结果。
公平锁
公平锁:就像排队买饭一样,先来后到。当线程进来,先查看锁的维护队列,如果不为空则加入等待,为空则占有锁。ReentrantLock(true)为公平锁。
将sayHello方法修改为以下的代码:
Lock lock = new ReentrantLock(true);
public void sayHello() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ": Hello");
sayWorld();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
再次来看打印结果
0: Hello
0 World
1: Hello
1 World
2: Hello
2 World
变成了按线程的启动顺序执行的公平方式,不会出现线程抢占的情况。
可重入锁
可重入锁:即线程在外层函数获得锁后,递归时也可继续自动获取锁。Synchronized、ReentrantLock就是可重入锁。
在公平锁的示例的基础上将sayHello修改成下述代码
Lock lock = new ReentrantLock();
public void sayHello(int i) {
if (i < 2) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ": Hello");
sayWorld();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
sayHello(++i);
} else {
return;
}
}
同时将main方法中线程的调用programmer.sayHello();
修改为programmer.sayHello(0);
再次执行
0: Hello
0 World
0: Hello
0 World
1: Hello
1 World
1: Hello
1 World
2: Hello
2 World
2: Hello
2 World
我们发现ReentrantLock可重入锁得到了验证,线程0递归时继续获得锁,继续执行,直到线程0释放锁,其他线程才可以得到锁去执行。那么此时开启公平锁会怎么样呢?我们将Lock lock = new ReentrantLock();
修改为Lock lock = new ReentrantLock(true);
发现公平锁优先,上述的代码变成了0-1-2,0-1-2交替执行,也就是线程0执行完第一遍递归时,将进入排队的队列,等待下一次获得锁,而不是直接获得锁。
自旋锁
自旋锁:尝试获取锁的线程循环尝试获取锁,不会阻塞而是,从而减少了上下文切换的消耗,但是会消耗CPU。CAS就是自旋锁的经典案例。
package com.wangscaler.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author WangScaler
* @date 2021/8/9 9:20
*/
public class SpinLock {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void getLock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "开始获取锁");
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void releaseLock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "释放锁");
}
public static void main(String[] args) throws InterruptedException {
SpinLock spinLock = new SpinLock();
new Thread(() -> {
spinLock.getLock();
try {
TimeUnit.SECONDS.sleep(10);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinLock.releaseLock();
}
}, "MySpinLock").start();
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
spinLock.getLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinLock.releaseLock();
}
}, "MySpinLock2").start();
}
}
上述的结果是
MySpinLock开始获取锁
MySpinLock2开始获取锁
MySpinLock释放锁
MySpinLock2释放锁
即线程MySpinLock启动之后,将获得锁,进入10s的等待,而2s之后(为了线程MySpinLock2启动时,线程MySpinLock已经获得锁,当然没必要等待那么长时间)线程MySpinLock2启动,此时线程MySpinLock2将无法获得锁进入循环,直到线程MySpinLock等待10s后释放锁,线程MySpinLock2才能获取到锁,等待1s后释放锁。原理如下:
- 首先线程MySpinLock通过getLock获取锁,while循环首次比较时,期望值A是null真实值V也是null,期望值与真实值相同,则将新值B的值(新线程MySpinLock)与之前的真实值进行交换。
- 此时真实值则变为新线程的值。
- 当第二个线程MySpinLock2进入的时候,因为真实值是上个线程MySpinLock的值,则比较值为false,加上!取反之后为true,所以会进入循环等待。
- 直到上一个线程MySpinLock通过releaseLock方法释放锁,将真实值改为null。
- 第二个线程MySpinLock2,此时比较期望值和真实值都是null,将成功获得锁。
如果是耗时较长的情况,建议不要使用自旋锁,因为当某个线程获得锁,在执行的过程中,其他线程陷入循环,会大量的消耗CPU。
读写锁
读写锁(独占锁/共享锁):独占锁只能被一个线程所持有,一般是写操作,Synchronized、ReentrantLock就是独占锁,其次还有ReentrantReadWriteLock的写锁。共享锁可以被多个线程同时持有,一般是读操作,ReentrantReadWriteLock的读锁
读共享锁,写独占锁。即多个读操作可以共同进行,但是读写不可共存,写写更不可共存即为互斥锁。
package com.wangscaler.lock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author WangScaler
* @date 2021/8/9 10:38
*/
public class ReadWrite {
Map<String, String> map = new HashMap<>();
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void setName() {
lock.writeLock().lock();
try {
String keyAndValue = Thread.currentThread().getName();
String name = "线程" + keyAndValue;
System.out.println(name + "准备写入");
map.put(keyAndValue, keyAndValue);
System.out.println(name + "写入完成,值为:"+map.get(keyAndValue));
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
public void getName(String key) {
lock.readLock().lock();
try {
String name = "线程" + Thread.currentThread().getName();
System.out.println(name + "正在读取key:" + key);
String value = map.get(key);
System.out.println(name + "的读取结果是" + value);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
public static void main(String[] args) {
int num = 3;
ReadWrite readWrite = new ReadWrite();
for (int i = 0; i < num; i++) {
new Thread(() -> {
readWrite.setName();
}, String.valueOf(i)).start();
}
for (int i = num; i < num + 3; i++) {
int finalI = i - 3;
String key = String.valueOf(finalI);
new Thread(() -> {
readWrite.getName(key);
}, String.valueOf(i)).start();
}
}
}
结果如下:
线程1准备写入
线程1写入完成,值为:1
线程0准备写入
线程0写入完成,值为:0
线程2准备写入
线程2写入完成,值为:2
线程3正在读取key:0
线程3的读取结果是0
线程5正在读取key:2
线程4正在读取key:1
线程5的读取结果是2
线程4的读取结果是1
从结果可以看出,
- 读写锁在写的时候是独占锁,只能被一个线程所占有,所以当线程1先拿到锁,线程0、2都得等待线程1执行完释放锁之后才能执行。
- 在读的时候,出现了线程5、4同时读取的情况,从而可以看出读允许多个线程同时操作。
- 虽然多个线程都启动了,但是没有出现写操作和读操作同时存在的情况,可看出存在互斥锁。