这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
听到公平锁、非公平锁、可重如锁、非公平锁、共享锁、独占锁 ....这些名词时是不是很困惑?下面我们就对常见几种锁概念进行归类和总结。希望可以帮助到你
通过文章你将主要了解到java中所提及的各种锁同时了解到各种锁背后的含义和特点,本文主要介绍乐观锁、悲观锁、公平锁等内容。
锁信息概览
开始探索之际,我们先对java中的所有锁信息进行一个概览。根据锁的不同特点可以将锁划分成如图所示的不同种类。
乐观锁、悲观锁到底是个什么鬼?
乐观锁
有这样一则寓言:两个口渴的人找到半杯水。一个人说到:“啊,我终于找到水了!虽然眼下只有半杯水,但千里之行始于足下,有良好的开端,我一定还能找到更多的水”于是他变得幸起来。而另外一个人想到的人则想:“怎么就只有这半杯水?就这半杯水有什么用?”一气之下他摔掉水杯,然后坐以渴毙。
同样是半杯水,在乐观的人看到了希望,而悲观的人看到则是希望破灭。这里面所折射出来的其实就是乐观锁、悲观锁的哲学。
对于乐观锁而言,它认为在多线程操作时,并不会受到其他线程的干扰,所以在操作共享资源时,乐观锁并不会将资源锁住,其允许其他线程同时操作。
其面对多线程同时操作更新数据时大致流程一般如下:
-
当更新数据时,会判断在修改的期间有没其他线程对我们的数据进行过修改,如果没有被修改,就说明在这段时间内只有自己在操作。此时正常修改数据信息。
-
如果发现在修改时的数据和刚获取到时不一致,说明这段时间内有人对数据进行了修改。那么此时就不会再继续修改更新动作,而是会选择放弃、报错、重试等策略。
乐观锁最典型例子就是:git、原子类等。
在mysql中数据表中会增设一个隐藏字段信息来判断数据在某一时间内是否被修改,其工作是乐观锁最典型的体现。 当读取数据时,version字段信息会被独处,数据更新个字段发生一次变化。 当提交时,判断数据表中该字段信息和之前读出来的是否相同,如果一直则更新成功,如果不同则更新失败。
悲观锁
悲观锁则同乐观锁正好背道而驰,在悲观锁看来所有操作都存在潜在的线程安全性问题,所以其在处理共享资源数据采用的策略一般是:先对资源进行加锁,随后在进行操作。
悲观锁最典型的例子就是synchronized关键字,通过synchronized修饰可以避免大量多线程并发所带来的线程问题。
乐观锁,悲观锁该如何选择
乐观锁和悲观锁各自有自己适合的场景,一般而言:
悲观锁其实比较适合那些并发操作中发写多读少、或是临界区持锁时间较长的情况。 而乐观锁则比较适合并发操作中读多写少的情况。
公平?不公平?什么是公平!
通俗来讲,公平锁就相当于现实中的排队,先来后到;非公平锁就是被认为是无秩序,谁抢到是谁的;
在java的锁机制中,如果一个线程组里能保证每个线程都能拿到锁,那么这个锁就是公平锁。相反,如果保证不了每个线程都能拿到锁,也就是存在有线程饿死,那么这个锁就是非公平锁。而在非公平的情况下一般是允许线程可以插队的。
在JUC中锁的公平性和非公平锁则是通过向ReentrantLock传入的true或fasle来进行判断。
示例代码:
我们模拟信息打印的一个场景,每个线程需要执行两次打印任务,每次打印都需要获取到锁信息才能进行。
// 打印任务
class PrintQueue {
// 打印任务锁进行时所持有的锁信息
private Lock queueLock = new ReentrantLock(true);
public void printJob(Object document) {
// 进行第一次打印
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
// 进行第二次打印
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}
// 任务线程
class Job implements Runnable {
PrintQueue printQueue;
public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始打印");
printQueue.printJob(new Object());
System.out.println(Thread.currentThread().getName() + "打印完毕");
}
}
/**
* 描述:演示公平情况
*/
public class FairLock {
public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread thread[] = new Thread[10];
for (int i = 0; i < 3; i++) {
thread[i] = new Thread(new Job(printQueue));
}
for (int i = 0; i < 3; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
代码分析:在主线程中启动多个打印任务线程,而每个打印线程中需要完成打印任务。而每次打印任务
需要打印两次。
公平锁时的结果信息:
Thread-0开始打印
Thread-0正在打印,需要6
Thread-1开始打印
Thread-2开始打印
Thread-1正在打印,需要3
Thread-2正在打印,需要9
Thread-0正在打印,需要2秒
Thread-0打印完毕
Thread-1正在打印,需要8秒
Thread-1打印完毕
Thread-2正在打印,需要3秒
Thread-2打印完毕
结论:不难发现,公平锁时输出的信息完全是按顺序进行打印的,并不存在任何的插队现象。如果存在插队现象,
那么当一个线程打印任务打印完成后应该可以进入到第二次打印任务。
当类FairLock 中的private Lock queueLock = new ReentrantLock(true);改为false后输出结果信息:
非共公平锁时的输出结果信息:
Thread-0开始打印
Thread-0正在打印,需要3
Thread-1开始打印
Thread-2开始打印
Thread-0正在打印,需要6秒
Thread-0打印完毕
Thread-1正在打印,需要7
Thread-1正在打印,需要10秒
Thread-1打印完毕
Thread-2正在打印,需要6
Thread-2正在打印,需要10秒
Thread-2打印完毕
在非公平锁的情况下当一个线程的第一次打印任务完成后其并没按照线程中的排列顺序来执行打印任务,而是出现了'插队'的现象——执行当前线程的第二次打印任务。
公平锁 可以保证线程获取执行的机会相等,但其会降低吞吐量,减慢执行效率。
非公平锁可以提高系吞吐量,提高效率。但随机性插队有可能产生饥饿的发生。
小结
为了文章整体的结构,后面的共享锁、独占锁、可重入锁等内容将在下一篇进行详细介绍。本次我们主要分析了乐观锁、悲观锁、公平锁、非公平锁等信息。希望能给你带来帮助 。