乐观锁
乐观锁总是认为不会发生并发冲突,所以并不会上锁,只有在更新内存中的数据的时候,才会判断数据是否被修改过(底层使用版本号和CAS实现)。
悲观锁
悲观锁总是认为会发生并发冲突,内存中的数据会被修改,所以每次拿数据的时候都会上锁。
一般来说乐观锁做的事比较少,悲观锁做的事比较多。
就拿疫情举例:
小明认为接下来的疫情没那么严重,小红认为接下来的疫情会比较严重。
小明直接躺平:
小红去药店买了许多药,去超时买了很多吃的。
轻量级锁,加锁、解锁的过程比较快,比较高效。
重量级锁,加锁、解锁的过程比较慢,比较低效。
轻量级锁比较不依赖于操作系统内核,重量级锁比较依赖操作系统内核,所以会比较慢。
自旋锁是轻量级锁的典型实现,挂起等待锁是重量级锁的典型实现。
自旋锁
自旋锁被一个线程占有的时候,其他的线程会不断地继续尝试拿锁,不会涉及到内核态操作,纯属用户态操作,一旦锁被释放,可以立马拿到锁。
自旋锁加锁和解锁的过程比较快。
挂起等待锁
挂起等待锁被一个线程占有的时候,其他的线程不会继续尝试拿锁,而是直接挂起等待,等到锁被释放了,其他线程被唤醒了,才继续竞争锁。
因为这个挂起等待操作涉及到操作系统内核,所以会比较慢。
挂起等待锁加锁和解锁的过程比较慢。
互斥锁
单纯的加锁和解锁操作,即进入代码块加锁,出了代码块解锁,synchronized
就是互斥锁。
读写锁
读写锁分为读锁和写锁。分为为读加锁和读解锁、写加锁和写解锁。
java
标准库提供了ReentrantReadWriteLock
类,实现了读写锁。
ReentrantReadWriteLock.ReadLock
是一个读锁类,提供了lock()
和unlock()
方法
ReentrantReadWriteLock.WriteLock
是一个写锁类,也提供了lock()
和unlock()
方法
读锁和读锁之间不互斥测试:
public class ReadWriteLockTest1 {
public static void main(String[] args) {
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock1 = rwLock.readLock();
ReentrantReadWriteLock.ReadLock readLock2 = rwLock.readLock();
Thread t1 = new Thread(()->{
readLock1.lock();
for (int i = 0; i < 100; i++) {
System.out.println("读加锁 1");
}
readLock1.unlock();
});
Thread t2 = new Thread(()->{
readLock2.lock();
for (int i = 0; i < 100; i++) {
System.out.println("读加锁 2");
}
readLock2.unlock();
});
t1.start();
t2.start();
}
}
读锁和写锁之间互斥测试:
public class ReadWriteLockTest2 {
public static void main(String[] args) {
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
Thread t1 = new Thread(()->{
readLock.lock();
for (int i = 0; i < 100; i++) {
System.out.println("读加锁");
}
readLock.unlock();
});
Thread t2 = new Thread(()->{
writeLock.lock();
for (int i = 0; i < 100; i++) {
System.out.println("写加锁");
}
writeLock.unlock();
});
t1.start();
t2.start();
}
}
写锁和写锁之间互斥测试:
public class ReadWriteLockTest3 {
public static void main(String[] args) {
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.WriteLock writeLock1 = rwLock.writeLock();
ReentrantReadWriteLock.WriteLock writeLock2 = rwLock.writeLock();
Thread t1 = new Thread(()->{
writeLock1.lock();
for (int i = 0; i < 100; i++) {
System.out.println("读加锁 1");
}
writeLock2.unlock();
});
Thread t2 = new Thread(()->{
writeLock2.lock();
for (int i = 0; i < 100; i++) {
System.out.println("写加锁 2");
}
writeLock2.unlock();
});
t1.start();
t2.start();
}
}
公平锁
线程A和B竞争一把锁,线程A抢到了锁,线程B等待。
线程A还没释放锁,此时又多了一个线程C和线程B竞争。
等线程A释放锁后,按照先来后到原则,这把锁给线程B。
这就是公平锁。
非公平锁
线程A和B竞争一把锁,线程A抢到了锁,线程B等待。
线程A还没释放锁,此时又多了一个线程C和线程B竞争。
等线程A释放锁后,这把锁随机分配给线程B或者线程C。
这就是非公平锁。
且看下面这段代码:
public class ReeantrantAndUnReeantrant {
public static Object locker = new Object();
public static void main(String[] args) {
synchronized (locker) {
synchronized (locker) {
System.out.println("ABC");
}
}
}
}
会不会打印ABC
?
执行结果:
结果打印了ABC
,为什么呢?
在java中synchronized
是可重入锁。那到底什么是可重入锁?
可重入锁
可重入锁是当一个线程获取一个锁多次,这个线程仍然可以拿到这个锁。
不可重入锁
不可重入锁跟可重入锁相反,一个线程拿到一个锁后,后面再尝试拿锁是拿不了的。
CAS
全称是Compare And Swap
,中文译过来就是比较并交换
。
CAS
操作通常涉及到两个寄存器和内存中的一个变量。
寄存器A里的值和变量V里的值进行比较,
如果相等,把寄存器B里的值给V。
如果不等,则操作失败。
如下图:
伪代码:
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) {
&address = swapValue;
return true;
}
return false;
}
这里的伪代码只是为了更好地理解CAS
,真正的CAS
操作是靠一个原子的硬件指令完成的。
1.原子类
java.util.concurrent.atomic
包下,都是基于CAS
实现的。
比如:AtomicInteger
类,提供了getAndIncrement()
和incrementAndGet()
对象方法。
getAndIncrement()
相当于i++
incrementAndGet()
相当于++i
不过这两个操作都是原子的。
案例:
public class AtomicTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger();
//创建线程t1,并执行incrementAndGet()方法10000次
Thread t1 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
int val = atomicInteger.incrementAndGet();
System.out.println(val);
}
});
//创建线程t2,并执行incrementAndGet()方法10000次
Thread t2 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
int val = atomicInteger.incrementAndGet();
System.out.println(val);
}
});
t1.start();
t2.start();
}
}
实现原子类的伪代码:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}
2.实现自旋锁
伪代码:
public class SpinLock {
private Thread owner = null;
public void lock(){
// 通过 CAS 看当前锁是否被某个线程持有.
// 如果这个锁已经被别的线程持有, 那么就自旋等待.
// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}
现有线程A,线程A想把变量val的值改为3,需要做的操作如下:
伪代码如下:
oldVal = val;
while (CAS(val, oldVal, 3) != true) {
oldVal = val;
}
假设线程A中的val本来为7,此时oldVal=7,期望是把val改为3。
但是线程A在执行CAS操作之前
线程B,有可能把val改为5。
线程C,有可能把val再改为7。
这时候到底要不要把val改为3呢?
这就是ABA
问题:
虽然这是小概率事件,但是有可能发生,这种情况下不能修改val的值,因为现实世界种有一些特殊情况,如果改了会出问题。
解决方案:
1.设置一个版本号
2.读取旧值的时候,也要读取版本号
jvm
会根据线程之间竞争锁的激烈程度,对锁进行升级,如下图:
偏向锁
偏向锁不是锁,只是一个标志,标志着这个锁属于哪个线程。
如果后面没有锁竞争,就不进行其他同步操作了。
如果后面有其他线程来竞争这个锁,那么就将变成轻量级锁(自旋锁实现)。
轻量级锁
这里的轻量级锁基于自旋锁实现,即线程一直判断锁是否被释放,尝试获取这把锁。
重量级锁
随着锁的竞争愈发激烈,轻量级锁变成了重量级锁。没抢到锁的线程被操作系统内核等待挂起,直到下次唤醒,继续竞争锁。
jvm+编译器
会判断锁是否可以消除,如果可以就消除。
就比如下面这段代码:
public class LockEliminate {
public static Object lock = new Object();
public static void main(String[] args) {
int count = 0;
synchronized (lock) {
for (int i = 0; i < 100000; i++) {
count++;
}
}
System.out.println(count);
}
}
只有主线程一个线程,但是却加了锁,这个锁完全没有必要存在,这时候就可以把锁消除掉。
锁的粒度
锁的粒度是指被锁住代码的多少。
比如下面这段代码:
synchronized (this) {
System.out.println("Hello, World");
}
因为被锁住的代码很少,锁的粒度很细。
如果被锁住的代码很多,锁的粒度就很粗。
看下面这段代码:
public class LockLightAndStrong {
public static Object o = new Object();
public static void main(String[] args) {
Thread t = new Thread(()->{
synchronized (o) {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
synchronized (o) {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
});
}
}
在线程t中,完全没有必要加两次锁,解两次锁。
但是如果没有其他线程来竞争这个锁,编译器和JVM
就会进行锁的粗化。
从而更好地节省资源,避免频繁地加锁解锁。