参考文章
1. Java 中的 锁
可中断锁:
在等待获取锁过程中可中断。synchronized就不是可中断锁,而Lock是可中断锁。
读写锁:
对资源读取和写入的时候拆分为2部分处理,一个读锁和一个写锁。读的时候可以多线程一起读,写的时候必须同步地写。ReadWriteLock
就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。
乐观锁: (冲突检测和数据更新)
很乐观,每次去拿数据的时候都认为别人不会修改
,所以不会上锁,但是在更新
的时候会去判断
在此期间有没有人去更新这个数据(可以使用版本号等机制)。如果因为冲突失败就重试
。乐观锁适用于写比较少
的情况下,即冲突比较少发生,这样可以省去了锁的开销,加大了系统的整个吞吐量。像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
悲观锁:
总是假设最坏的情况,每次去拿数据
的时候都认为别人会修改
,因此每次拿数据
的时候都会上锁
,这样别人想拿
这个数据就会阻塞
直到它拿到锁,效率比较低
。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
公平锁
是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
非公平锁
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。在高并发的情况下,有可能造成优先级反转或者饥饿现象。
ReentrantLock
默认是非公平锁,但是可以设置为true成为公平锁,Synchonized
也是非公平锁。
可重入锁(递归锁)
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
线程可以进入任何一个它已经拥有的锁所同步着的代码块。
最大作用:避免死锁
ReentrantLock
和Synchonized
都是可重入锁
。
public class SynchronziedDemo {
private synchronized void print() {
doAdd();
}
private synchronized void doAdd() {
System.out.println("doAdd...");
}
public static void main(String[] args) {
SynchronziedDemo synchronziedDemo = new SynchronziedDemo();
synchronziedDemo.print(); // doAdd...
}
}
/**
注意:Lock.lock();和Lock.unlock();个数匹配,多少个锁都可以正常运行,若是不匹配,则程序运行卡死。
*/
public class ReentrantLockDemo {
private Lock lock = new ReentrantLock();
private void print() {
lock.lock();
doAdd();
lock.unlock();
}
private void doAdd() {
lock.lock();
lock.lock();
System.out.println("doAdd...");
lock.unlock();
lock.unlock();
}
public static void main(String[] args) {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
reentrantLockDemo.print();
}
}
自旋锁
指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
- 手动实行自旋锁
public class SpinLock {
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
private void lock () {
System.out.println(Thread.currentThread() + " coming...");
while (!atomicReference.compareAndSet(null, Thread.currentThread())) {
// loop
}
}
private void unlock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(thread + " unlock...");
}
public static void main(String[] args) throws InterruptedException {
SpinLock spinLock = new SpinLock();
new Thread(() -> {
spinLock.lock();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hahaha");
spinLock.unlock();
}).start();
Thread.sleep(1);
new Thread(() -> {
spinLock.lock();
System.out.println("hehehe");
spinLock.unlock();
}).start();
}
}
1、乐观锁 Compare and Swap ( CAS )
乐观锁的实现方式(CAS)
乐观锁的实现主要就两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是 Compare and Swap ( CAS )。
CAS:CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。 如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。
无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 ”
这其实和乐观锁的冲突检查+数据更新
的原理是一样的。
乐观锁是一种思想,CAS是这种思想的一种实现方式。
CAS的缺点
1. ABA问题
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。ava并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
循环时间长开销很大
2. 自旋CAS(不成功,就一直循环执行,直到成功)
如果长时间不成功,会给CPU带来非常大的执行开销。
3. 只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
2. volatile
3. synchronize
死锁 及 如何解决
什么是死锁:两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是两个进程都陷入了无限的等待中。
产生死锁的四个必要条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
- 考虑如下情形:
- 线程A当前持有互斥锁lock1,线程B当前持有互斥锁lock2。
- 线程A试图获取lock2,因为线程B正持有lock2,因此线程A会阻塞等待线程B对lock2释放。
- 如果此时线程B也在试图获取lock1,同理线程也会阻塞。
- 两者都在等待对方所持有但是双方都不释放的锁,这时便会一直阻塞形成死锁。
- 死锁的解决方法:
- 撤消陷于死锁的全部进程;
- 逐个撤消陷于死锁的进程,直到死锁不存在;
- 从陷于死锁的进程中逐个强迫放弃所占用的资源,直至死锁消失。
- 从另外一些进程那里强行剥夺足够数量的资源分配给死锁进程,以解除死锁状态
如何确保 N 个线程可以访问 N 个资源同时又不导致死锁?
使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁
。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了