3.Lock锁(重点)

了解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将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(有多个子类)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值