了解Lock锁之前,先来回顾一下Synchronized
卖票案例:
生活中最典型的一个多线程的例子就是卖票,我们在买票的使用一般都是有多个窗口在同时卖票,对于每个售票窗口来讲,票的资源是共享的,因此,我们可以把每个售票窗口看作一个线程,而票就可以看成资源。
public class Test {
public static void main(String[] args){
//并发:多个线程操作同一个资源类
//资源类
SaleTickets ticket = new SaleTickets();
//创建三个卖票线程,卖票只要把资源类丢给线程即可
//1号线程,模拟1号卖票窗口
new Thread(()->{
for (int i = 0; i < 10; i++) {
ticket.sale();
}
},"1号窗口").start();
//2号线程,模拟2号卖票窗口
new Thread(()->{
for (int i = 0; i < 10; i++) {
ticket.sale();
}
},"2号窗口").start();
//3号线程,模拟3号卖票窗口
new Thread(()->{
for (int i = 0; i < 10; i++) {
ticket.sale();
}
},"3号窗口").start();
}
}
/** 资源类
* 在真正的多线程开发中,线程就是一个单独的资源类,没有任何附属的属性,从而降低耦合性
* 因此不需要我们去继承Thread类或者实现Runnable和Callable接口
*/
class SaleTickets{
//票量(资源数)
private int tickets = 10;
//卖票的方式
public void sale(){
if (tickets>0){
try {
Thread.sleep(200); //模拟网络延迟,提高线程问题发生的可能性
System.out.println(Thread.currentThread().getName()+"卖出了第"+(tickets--)+"张票,剩余:"+tickets+"张票");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
我们可以看到,这个卖票的例子明显出现了问题,出现了两个窗口卖出了同一张票,票的数量为0之后还能继续卖票,这在我们现实生活中是一定不被允许的。
- 我们来分析一下产生问题的原因:由于这里模拟了三个售票窗口,在程序执行时,三个窗口会同时抢夺票资源,由于我们没有设置同步操作,就会导致三个售票窗口可能会同时抢到同一张票进行售出,如果这张票是最后一张,就会导致最后一个资源同时被3个线程抢夺,由于卖票后我们又进行了- 1操作,对票数进行记录,三个线程这时同时抢夺最后一张票就会引发三次- 1操作,导致票数变为负数。
因此,我们对卖票操作设置同步,三个售票窗口在同时抢夺一张票时,如果其中一个窗口抢到票,另外两个窗口就进行等待:
//卖票的方式,设置同步操作
public synchronized void sale(){
if (tickets>0){
try {
Thread.sleep(200); //模拟网络延迟,提高线程问题发生的可能性
System.out.println(Thread.currentThread().getName()+"卖出了第"+(tickets--)+"张票,剩余:"+tickets+"张票");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果
可以看到,这次卖票就没有任何问题了。
synchronized本质就是一个 队列+锁 的操作;而锁主要锁两个东西:对象、Class
Lock锁
回顾完
synchronized
,我们就开始进行锁的学习。
在JavaAPI文档中,我们找到JUC包:
可以看到,官方文档已经对Lock锁的使用做出了详细的描述
Lock是一个接口,它有三个实现类:
- ReentrantLock (普通锁 / 可重入锁)最常用
(可以多次获取同一个锁,但也要多次释放)- ReentrantReadWriteLock.ReadLock (读锁)
- ReentrantReadWriteLock.WriteLock (写锁)
我们进入ReentrantLock源码查看:
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
* 创建一个{@code ReentrantLock}实例。
* 这相当于使用{@code ReentrantLock(false)}。
*/
public ReentrantLock() {
//默认是非公平锁
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* 属性创建一个{@code ReentrantLock}的实例
* given fairness policy.
* 考虑到公平政策。
* @param fair {@code true} if this lock should use a fair ordering policy
* @param fair {@code true} 如果这个锁应该使用公平排序策略
*/
public ReentrantLock(boolean fair) {
//根据调用来确定是公平锁还是非公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
- 公平锁:十分公平,先来后到
- 非公平锁:十分不公平,可以插队(默认)
而Java默认使用非公平锁就是为了公平(这句话很难理解,举个例子吧:假设现在有2个进行,一个执行需要3s,一个执行需要3h,如果3h在3s之前,3s的进程就需要等待3h的进程执行完毕才能执行,而使用了非公平锁,3s的进程即使后到,也可以插队在3h的进程之前执行,这样有利于提高效率。)
现在,我们就使用Lock锁来实现卖票操作,首先我们查看api文档
在这里,文档说明了我们应该如何使用一个锁:
- 1.创建锁:Lock l = new …;
- 2.加锁:l.lock();
- 3.业务代码(需要写在处理异常中,为了防止业务代码出现异常无法释放锁)
- 4.释放锁:l.unlock();(这一步很重要,一定要写在finally中!)
class SaleTickets{
//票量(资源数)
private int tickets = 10;
Lock lock = new ReentrantLock();
//卖票的方式
public void sale(){
lock.lock();
try {
if (tickets>0){
System.out.println(Thread.currentThread().getName()+"卖出了第"+(tickets--)+"张票,剩余:"+tickets+"张票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
运行结果:
使用Lock锁,一样也能保证同步,使卖票的操作正常执行。
synchronized和Lock的区别
- synchronized是Java的一个内置关键字,而Lock是一个接口
- synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁
- synchronized会自动释放锁,Lock必须要手动解锁,如果不释放就会造成死锁
- synchronized在对两个线程进行同步时,如果一个线程1获得锁,另外一个线程2就会一直等待,如果线程1阻塞了,线程2就一直保持等待。
Lock锁就不一定会一直等待下去,在Lock中有一个tryLock方法(尝试获取锁)- synchronized是可重入锁(普通锁),不可以中断,是非公平锁。
Lock是可重入锁,可以去判断锁,可以自己设置公平性;- synchronized适合锁少量的代码同步问题
Lock适合锁大量的同步代码- synchronized有代码块锁和方法锁,而Lock只有代码块锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(有多个子类)