公平锁和非公平锁
回头看一下之前的一个多线程买票案例
public class LockSaleTicket {
public static void main(String[] args) {
LockTicket lockTicket = new LockTicket();
new Thread(()->{
for (int i = 0; i < 20; i++) {
lockTicket.sale();
}
}, "AA").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
lockTicket.sale();
}
}, "BB").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
lockTicket.sale();
}
}, "CC").start();
}
}
class LockTicket {
private static int number = 20;
private final ReentrantLock lock = new ReentrantLock();
public void sale() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + " : 卖出一张票, 剩余票数: " + (--number));
}
} finally {
lock.unlock();
}
}
}
运行结果:
CC线程几乎卖出了一多半的票, 而BB线程只卖出了2张票, 这是因为在线程抢占的过程中, CC总能抢到锁, 这样就容易导致其他线程饥饿, 那么为什么会这样呢? 来看一下ReentrantLock的源码:
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
从字面意思上也能看出来, ReentrantLock在创建的时候如果不传入任何参数, 默认创建的是一个非公平锁, 当传入一个布尔值为true时, 创建的则是一个公平锁, 使用公平锁修改上面的案例:
public class LockSaleTicket {
public static void main(String[] args) {
LockTicket lockTicket = new LockTicket();
new Thread(()->{
for (int i = 0; i < 20; i++) {
lockTicket.sale();
}
}, "AA").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
lockTicket.sale();
}
}, "BB").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
lockTicket.sale();
}
}, "CC").start();
}
}
class LockTicket {
private static int number = 20;
private final ReentrantLock lock = new ReentrantLock(true);
public void sale() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + " : 卖出一张票, 剩余票数: " + (--number));
}
} finally {
lock.unlock();
}
}
}
运行结果:
可以看到, 当三个线程都创建完成后, 遵循的就是一个线程卖一张票的的规则, 很公平, 但是公平锁也有缺点, 那就是效率低下, 所以使用的时候还是要视情况而定。
可重入锁
什么是可重入锁?
- 可重入锁又称递归锁, 是指在同一个线程在外层方法获取锁的时候, 再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象), 不会因为之前已经获取过还没有释放而阻塞
- Java中synchronized和ReentrantLock都是可重入锁, 可重入锁的存在就是为了在一定程度上避免死锁
只看概念不太能理解可重入锁这个东西, 来通过代码演示一下:
//synchronized版本
/**
* 外中内三层代码块上的都是同一把锁o
*/
Object o = new Object();
new Thread(()->{
synchronized(o) {
System.out.println(Thread.currentThread().getName()+"进入外层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+"进入中层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+"进入内层");
}
}
}
},"AA").start();
运行结果:
可以看到因为三层代码块用的是同一把锁, 所以不用等到上一层得到锁的代码块释放锁就能进入, 接着看一下ReentrantLock版本
//Lock演示可重入锁
Lock lock = new ReentrantLock();
//创建线程
new Thread(()->{
//上锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"进入外层");
//上锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"进入内层");
}finally {
//释放锁
lock.unlock();
}
}finally {
//释放锁
lock.unlock();
}
},"BB").start();
运行结果:
依旧是不用等到上一层代码块的锁释放就可以得到锁进入代码块
死锁
什么是死锁?
-
死锁是指两个或两个以上的线程在执行过程中, 因争夺资源而造成的一种互相等待的现象, 若无外力干涉那它们都将无法推进下去, 如果资源充足, 进程的资源请求都能够得到满足, 死锁出现的可能性就很低, 否则就会因争夺有限的资源而陷入死锁
-
如果有两个线程各自持有一把锁, 却同时想获取对方的锁而双发都未释放锁, 这时就产生了死锁
来看一下代码演示:
public class DeadLock {
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
new Thread(()->{
synchronized (a) {
System.out.println("获取到锁a, 等待锁b释放");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println("获取到锁b");
}
}
}, "A").start();
new Thread(()->{
synchronized (b) {
System.out.println("获取到锁b, 等待锁a释放");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println("获取到锁a");
}
}
}, "B").start();
}
}
运行结果:
两个线程都在等待对方的锁释放, 然而如果它们获取不到对方的锁就不会释放自己的锁, 这就产生了死锁, 如果我们不人为去干预, 会一直卡在这里占用系统资源, 但是很多时候思索地产生是因为资源分配不当导致的, 我们很难这么清晰的观察到, 那么就需要通过命令的方式排查死锁并人为关闭死锁线程
乐观锁和悲观锁
悲观锁:
- 认为自己在使用数据的时候一定有别的线程来修改数据, 因此在获取数据的时候会先加锁, 确保数据不会被别的线程修改
- 适合写操作多的场景, 先加锁可以保证写操作时数据正确(写操作包括增删改), 显式的锁定之后再操作同步资源
- synchronized关键字和Lock的实现类都是悲观锁
乐观锁:
- 乐观锁认为自己在使用数据时不会有别的线程修改数据, 所以不会添加锁, 只是在更新数据的时候去判断之前有没有别的线程更新了这个数据, 如果这个数据没有被更新, 当前线程将自己修改的数据成功写入, 如果数据已经被其他线程更新, 则根据不同的实现方式执行不同的操作
- 适合读操作多的场景, 不加锁的特点能够使其读操作的性能大幅度提升
- 乐观锁一般有两种实现方式(采用版本号机制、CAS算法实现)
- 乐观锁在Java中通过使用无锁编程来实现, 最常采用的时CAS算法, Java原子类中的递增操作就通过CAS自旋实现的
读写锁
在多线程对同一资源进行操作的时候肯定是要加锁的, 对于写操作(增删改)来说, 肯定是要考虑并发场景下的操作原子性的, 所以写操作必须是独占锁, 但是对于读操作来说, 多线程同时读一个资源并没有任何问题, 为了满足并发情况下的效率, 读操作应为共享锁。为了满足这种需求就出现了读写锁。
读写锁特点:
- 读-读可以共享
- 读-写互斥
- 写-写互斥
用一个小案例来演示一下读写锁的使用: 多个线程对Map进行写入和读取操作
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//创建写线程
for (int i = 1; i <= 5; i++) {
final int number = i;
new Thread(() -> {
try {
myCache.put(number + "", number + "");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "" + i).start();
}
//创建读线程
for (int i = 1; i <= 5; i++) {
final int number = i;
new Thread(() -> {
try {
myCache.get("" + number);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "" + i).start();
}
}
}
//资源类
class MyCache {
//创建读写锁
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//创建map集合
private volatile static Map<String, Object> map = new HashMap<>();
//放数据
public void put(String key, Object value) throws InterruptedException {
//添加写锁, 独占锁啊
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "正在进行写操作");
TimeUnit.SECONDS.sleep(1);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写操作结束");
} finally {
readWriteLock.writeLock().unlock();
}
}
//取数据
public Object get(String key) throws InterruptedException {
//添加读锁, 共享锁
readWriteLock.readLock().lock();
try {
Object result = null;
System.out.println(Thread.currentThread().getName() + "正在进行读操作");
TimeUnit.SECONDS.sleep(1);
result = map.get(key);
System.out.println(Thread.currentThread().getName() + "读操作结束");
return result;
} finally {
readWriteLock.readLock().unlock();
}
}
}
运行结果:
可以看到, 写操作未结束时, 不会有其他的线程进行操作, 但是读操作未结束时, 其他读操作线程也可以进行操作, 这就说明写锁是独占的, 读锁是共享的。
锁降级
如果我们在做完写操作之后又想进行读操作, 但是由于写锁是独占锁, 这时候进行读操作很浪费资源, 这时候就可以将写锁降级为读锁, 这样读取的时候其他线程也能共享读取
具体步骤:
获取写锁->进行写操作->获取读锁->释放写锁->进行读操作->释放读锁
代码演示:
public class ReadWriteLockDemo2 {
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
//写锁上锁
writeLock.lock();
//进行写操作
System.out.println("进行写操作");
//锁降级, 先读锁上锁再释放写锁
readLock.lock();
writeLock.unlock();
//进行读操作
System.out.println("进行读操作");
//释放读锁, 操作完成
readLock.unlock();
}
}
运行结果:
成功从写锁降级成读锁
写锁可以降级为读锁, 那么读锁可不可以升级为写锁呢, 把上面的代码改一下试试
public class ReadWriteLockDemo2 {
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
//读锁上锁
readLock.lock();
System.out.println("进行读操作");
//锁升级, 先写锁上锁再释放读锁
writeLock.lock();
readLock.unlock();
//进行写操作
System.out.println("进行写操作");
//释放写锁
writeLock.unlock();
}
}
看一下运行结果:
卡在这里了, 说明不能将读锁升级为写锁!
我的个人主页: www.ayu.link
本文连接: ┏ (゜ω゜)=☞