线程系列 6 - JUC相关的显示锁

本文详细介绍了Java并发编程中锁的概念和类型,包括悲观锁与乐观锁、可重入锁与不可重入锁、公平锁与非公平锁、独占锁与共享锁,以及读写锁的实现。通过示例代码展示了公平性和非公平性的差异,并对比了Lock接口实现的ReentrantLock与synchronized的区别。
摘要由CSDN通过智能技术生成

1、显示锁的分类

 
       不同的角度来讲,一般把显式锁分为以下几类:悲观锁和乐观锁、可重入锁和不可重入锁、公平锁和非公平锁、共享锁和独占锁、可中断锁和不可中断锁。
 

1.1、悲观锁和乐观锁

 

1.1.1、悲观锁

 
悲观锁 :每次进入临界区操作数据的时候,都认为别的线程会修改,所以线程每次在读写数据时都会上锁,锁住同步资源,这样其他线程需要读写这个数据时就会阻塞,一直等到拿到锁。悲观锁可以确保无论哪个线程持有锁,都能独占式访问临界区。

  • 使用场景:

    悲观锁适用于写多读少的场景,遇到高并发写时性能高。 synchronized 重量级锁就是一种典型的悲观锁。 传统的关系型数据库用到了很多悲观锁,比如行锁、表锁、读锁、写锁等。

  • 使用悲观锁存在的问题:

    ① 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

    ② 一个线程持有锁后,会导致其他所有抢占此锁的线程挂起。

    ③ 如果一个优先级高的线程等待一个优先级低的线程释放锁,就会导致线程的优先级倒置,从而引发性能风险。

       解决以上悲观锁的这些问题的有效方式是使用乐观锁去替代悲观锁。与之类似,数据库操作中的带版本号数据更新、JUC包的原子类,都使用了乐观锁的方式提升性能。

 

1.1.2、乐观锁

 

乐观锁 :每次去拿数据的时候,都认为别的线程不会修改,所以不会上锁。但是在更新的时候会判断一下,在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(跟上一次的版本号比较,如果一样就更新),如果失败就要重复读-比较-写的操作。乐观锁是一种思想,而CAS是这种思想的一种实现。

  • 乐观锁的操作主要有两步:

    ① 冲突检测。

    ② 数据更新。

  • 使用场景:

    乐观锁适用于读多写少的场景,遇到高并发写时性能低。synchronized 轻量级锁 是一种典型的乐观锁 另外,JUC中基于抽象队列同步器(AQS)实现的显式锁(如ReentrantLock)都是乐观锁。

 

       Java中的乐观锁基本都是通过 CAS 自旋操作实现的。CAS 是一种更新原子操作,比较当前值跟传入值是否一样,是则更新,不是则失败。在争用激烈的场景下,CAS 自旋会出现大量的空自旋,会导致乐观锁性能大大降低。

       既然在争用激烈的场景下乐观锁的性能非常低,那么为什么JUC的显式锁都是乐观锁呢?因为 JUC 的显式锁都是基于 AQS 实现的,而 AQS 通过对队列的使用很大程度上减少了锁的争用,从而极大地减少了空的CAS自旋。所以,即使在争用激烈的场景下,基于 AQS 的 JUC 乐观锁也能表现出比悲观锁更佳的性能。
 

1.2、可重入锁和不可重入锁

 

1.2.1、可重入锁(递归锁)

 
可重入锁(递归锁)指的是一个线程可以多次抢占同一个锁。 JUC 的 ReentrantLock 类是可重入锁的一个标准实现类。

例如:线程 A 在进入外层函数抢占了一个 Lock 显式锁之后,当线程 A 继续进入内层函数时,如果遇到有抢占同一个 Lock 显式锁的代码,线程A依然可以抢到该 Lock 显式锁。
 

1.2.1、不可重入锁

 
不可重入锁指的是一个线程只能抢占一次同一个锁,不可重入锁与可重入锁正好相反。

例如:线程 A 在进入外层函数抢占了一个 Lock 显式锁之后,当线程 A 继续进入内层函数时,如果遇到有抢占同一个 Lock 显式锁的代码,线程A不可以抢到该 Lock 显式锁。除非线程 A 提前释放了该 Lock 显式锁,才能第二次抢占该锁。

 

1.3、公平锁和非公平锁

 

两种锁的执行流程

  • 公平锁:
    获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
     
  • 非公平锁:
    当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。

 

1.3.1、公平锁

 
公平锁 :指不同的线程抢占锁的机会是公平的、平等的。从抢占时间上来说,先对锁进行抢占的线程,一定被先满足,抢锁成功的次序体现为FIFO(先进先出)顺序。简单来说,公平锁就是保障各个线程获取锁都是按照顺序来的,先到的线程先获取锁。

 

1.3.2、非公平锁

 
非公平锁指不同的线程抢占锁的机会是非公平的、不平等的,每个线程获取锁的顺序是随机的。从抢占时间上来说,先对锁进行抢占的线程不一定被先满足,抢锁成功的次序不会体现为FIFO(先进先出)顺序。非公平锁的 优点在于吞吐量比公平锁大 ,它的 缺点是有可能会导致线程优先级反转或者线程饥饿现象

 
       synchronized 就是非公平锁,它选择要获得锁的线程是随机的,而不考虑等待时间的长短。默认情况下,ReentrantLock 实例也是非公平锁,但如果在实例构造时传入了参数true,所得到的锁就是公平锁。需要注意的是,ReentrantLock的tryLock()方法是一个特例,一旦有线程释放了锁,正在 tryLock 的线程就能优先取到锁,即使已经有其他线程在等待队列中。

 

1.3.3、代码示例

 

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 公平锁和非公平锁demo
 */
@Slf4j
public class LockDemo {

    public static void main(String[] args) throws InterruptedException {
        Sync sync = new Sync();
        // 非公平锁,线程执行顺序随机
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                sync.buyTicket();
            }, "非公平线程" + i).start();
        }
        TimeUnit.SECONDS.sleep(5);
        System.out.println("--------------");


        // 申明一个公平锁,Lock 默认是非公平锁,构造函数为true则为公平锁
        Lock lock = new ReentrantLock(true);
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                log.info("{} -> 开始抢锁", Thread.currentThread().getName());
                lock.lock();
                sync.saleTicket();
                lock.unlock();
            }, "公平线程" + i).start();
        }

    }


}

@Slf4j
class Sync {

    private Integer ticket = 0;

    public Integer buyTicket() {
        log.info("{} -> 开始抢锁", Thread.currentThread().getName());
        synchronized (this) {
            ticket++;
            log.info("{} 执行-> ticket = {}", Thread.currentThread().getName(), ticket);
            return ticket;
        }
    }

    public Integer saleTicket() {
        ticket--;
        log.info("{} 执行-> ticket = {}", Thread.currentThread().getName(), ticket);
        return ticket;
    }
}

 
注意看打印结果,抢锁顺序和执行顺序。
 
执行结果:

11:21:25.326 [非公平线程6] INFO com.demo.Sync - 非公平线程6 -> 开始抢锁
11:21:25.332 [非公平线程6] INFO com.demo.Sync - 非公平线程6 执行-> ticket = 1
11:21:25.326 [非公平线程9] INFO com.demo.Sync - 非公平线程9 -> 开始抢锁
11:21:25.326 [非公平线程3] INFO com.demo.Sync - 非公平线程3 -> 开始抢锁
11:21:25.326 [非公平线程8] INFO com.demo.Sync - 非公平线程8 -> 开始抢锁
11:21:25.327 [非公平线程7] INFO com.demo.Sync - 非公平线程7 -> 开始抢锁
11:21:25.326 [非公平线程5] INFO com.demo.Sync - 非公平线程5 -> 开始抢锁
11:21:25.326 [非公平线程1] INFO com.demo.Sync - 非公平线程1 -> 开始抢锁
11:21:25.326 [非公平线程4] INFO com.demo.Sync - 非公平线程4 -> 开始抢锁
11:21:25.326 [非公平线程2] INFO com.demo.Sync - 非公平线程2 -> 开始抢锁
11:21:25.327 [非公平线程10] INFO com.demo.Sync - 非公平线程10 -> 开始抢锁
11:21:25.333 [非公平线程9] INFO com.demo.Sync - 非公平线程9 执行-> ticket = 2
11:21:25.335 [非公平线程2] INFO com.demo.Sync - 非公平线程2 执行-> ticket = 3
11:21:25.335 [非公平线程10] INFO com.demo.Sync - 非公平线程10 执行-> ticket = 4
11:21:25.335 [非公平线程4] INFO com.demo.Sync - 非公平线程4 执行-> ticket = 5
11:21:25.335 [非公平线程1] INFO com.demo.Sync - 非公平线程1 执行-> ticket = 6
11:21:25.335 [非公平线程5] INFO com.demo.Sync - 非公平线程5 执行-> ticket = 7
11:21:25.335 [非公平线程7] INFO com.demo.Sync - 非公平线程7 执行-> ticket = 8
11:21:25.336 [非公平线程8] INFO com.demo.Sync - 非公平线程8 执行-> ticket = 9
11:21:25.336 [非公平线程3] INFO com.demo.Sync - 非公平线程3 执行-> ticket = 10
--------------
11:21:30.330 [公平线程1] INFO com.demo.LockDemo - 公平线程1 -> 开始抢锁
11:21:30.330 [公平线程2] INFO com.demo.LockDemo - 公平线程2 -> 开始抢锁
11:21:30.330 [公平线程3] INFO com.demo.LockDemo - 公平线程3 -> 开始抢锁
11:21:30.330 [公平线程5] INFO com.demo.LockDemo - 公平线程5 -> 开始抢锁
11:21:30.330 [公平线程2] INFO com.demo.Sync - 公平线程2 执行-> ticket = 9
11:21:30.330 [公平线程4] INFO com.demo.LockDemo - 公平线程4 -> 开始抢锁
11:21:30.330 [公平线程6] INFO com.demo.LockDemo - 公平线程6 -> 开始抢锁
11:21:30.331 [公平线程3] INFO com.demo.Sync - 公平线程3 执行-> ticket = 8
11:21:30.331 [公平线程7] INFO com.demo.LockDemo - 公平线程7 -> 开始抢锁
11:21:30.332 [公平线程8] INFO com.demo.LockDemo - 公平线程8 -> 开始抢锁
11:21:30.332 [公平线程9] INFO com.demo.LockDemo - 公平线程9 -> 开始抢锁
11:21:30.333 [公平线程1] INFO com.demo.Sync - 公平线程1 执行-> ticket = 7
11:21:30.333 [公平线程10] INFO com.demo.LockDemo - 公平线程10 -> 开始抢锁
11:21:30.333 [公平线程5] INFO com.demo.Sync - 公平线程5 执行-> ticket = 6
11:21:30.333 [公平线程4] INFO com.demo.Sync - 公平线程4 执行-> ticket = 5
11:21:30.334 [公平线程6] INFO com.demo.Sync - 公平线程6 执行-> ticket = 4
11:21:30.336 [公平线程7] INFO com.demo.Sync - 公平线程7 执行-> ticket = 3
11:21:30.337 [公平线程8] INFO com.demo.Sync - 公平线程8 执行-> ticket = 2
11:21:30.337 [公平线程9] INFO com.demo.Sync - 公平线程9 执行-> ticket = 1
11:21:30.338 [公平线程10] INFO com.demo.Sync - 公平线程10 执行-> ticket = 0

Process finished with exit code 0

 

1.4、独占锁和共享锁

 

1.4.1、独占锁

 
独占锁指的是每次只有一个线程能持有的锁。例如,JUC 的 ReentrantLock 类、Synchronized内置锁。

独占锁 也叫 排他锁互斥锁独享锁是一种 悲观保守的加锁策略 ,它不必要地限制了读/读竞争,如果某个只读线程获取锁,那么其他的读线程都只能等待,这种情况下就限制了读操作的并发性,因为读操作并不会影响数据的一致性。

 

1.4.2、共享锁

 
共享锁允许多个线程同时获取锁,容许线程并发进入临界区。例如,JUC的ReentrantReadWriteLock(读写锁)类、JUC中的 Semaphore(信号量)、CountDownLatch倒数闩。使用该读写锁时,读操作可以有很多线程一起读,但是写操作只能有一个线程去写,而且在写入的时候,别的线程也不能进行读的操作。

与独占锁不同,共享锁是一种乐观锁,它放宽了加锁策略,并不限制读/读竞争,允许多个执行读操作的线程同时访问共享资源

用 ReentrantLock 锁替代 ReentrantReadWriteLock 锁虽然可以保证线程安全,但是也会浪费一部分资源,因为多个读操作并没有线程安全问题,所以在读的地方使用读锁,在写的地方使用写锁,可以提高程序执行效率。

 
我们 JUC 常用的共享锁 Semaphore,具体可参考本人博客: 《Java 并发编程 - 共享锁 Semaphore》

以及常用的共享锁 CountDownLatch -> 《Java 并发编程 - CountDownLatch》

 

1.5、读写锁

 
        接下来聊聊基于共享锁和独占锁基础上的一种组合锁:读写锁。与单一的互斥锁相比,组合起来的读写锁允许对于共享数据进行更大程度的并发操作。虽然每次只能有一个写线程,但是同时可以有多个线程并发地读数据。读写锁适用于读多写少的并发情况。

       读写锁的内部包含两把锁:一把是 读(操作)锁 ,是一种 共享锁;另一把是 写(操作)锁 ,是一种 独占锁 。在没有写锁的时候,读锁可以被多个线程同时持有。写锁是具有排他性的:如果写锁被一个线程持有,其他的线程不能再持有写锁,抢占写锁会阻塞;如果写锁被一个线程持有,其他的线程不能再持有读锁,抢占读锁也会阻塞。

读写锁的读写操作之间的互斥原则如下:

  • 读操作、读操作能共存,是相容的。
  • 读操作、写操作不能共存,是互斥的。
  • 写操作、写操作不能共存,是互斥的。

 
       JUC包中的读写锁接口为ReadWriteLock,通过ReadWriteLock接口能获取其内部的两把锁:一把是ReadLock,负责读操作;另一把是WriteLock,负责写操作。JUC中ReadWriteLock接口的实现类为ReentrantReadWriteLock。
 

1.5.1、ReadWriteLock 接口的实现类 ReentrantReadWriteLock

 

  • ReentrantLock虽然可以灵活地实现线程安全,但是他是一种完全互斥锁,即某一时刻永远只允许一个线程访问共享资源,不管是读数据的线程还是写数据的线程。这导致的结果就是,效率低下。ReentrantReadWriteLock 类很好的解决了该问题。
  • ReentrantReadWriteLock 类实现了 ReadWriteLock 接口,而 ReadWriteLock 接口中维护了两个锁:读锁(共享锁)和写锁(排他锁)。
  • ReentrantReadWriteLock 允许线程同时读取共享资源;但是如果有一个线程是写数据,那么其他线程就不能去读写该资源。即会出现三种情况:读读共享,写写互斥,读写互斥。

       锁升级是指读锁升级为写锁,锁降级指的是写锁降级为读锁。在 ReentrantReadWriteLock 读写锁中,只支持写锁降级为读锁,而不支持读锁升级为写锁

 

ReentrantReadWriteLock 主要特性 公平性 支持公平锁和非公平锁,默认是非公平锁,可以根据构造方式设置公平锁。
// 构造函数默认是false 非公平锁,由于读线程之间没有锁竞争,所以读操作,没有公平性和非公平性。
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true);

在这里插入图片描述
 
ReentrantReadWriteLock 实现类相关方法说明
 
在这里插入图片描述

  • 例子
package com.pzf.lock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 独占锁(写锁)  一次只能被一个线程占有
 * 共享锁(读锁)  多个线程可以同时占有
 * <p>
 * ReadWriteLock  实现类  ReentrantReadWriteLock
 * <p>
 * 读-读  可以共存
 * 读-写  不能共存
 * 写-写  不能共存
 */
public class ReadWriteLockDemo {

    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        // 普通写
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {myCache.put(temp + "", temp + "");}, String.valueOf(i)).start();
        }

        // 普通读
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {myCache.get(temp + "");}, String.valueOf(i)).start();
        }

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println();

        MyLockCache myLockCache = new MyLockCache();
        // 读写锁 -> 写
        for (int i = 6; i <= 10; i++) {
            final int temp = i;
            new Thread(() -> {myLockCache.put(temp + "", temp + "");}, String.valueOf(i)).start();
        }

        // 读写锁 -> 读
        for (int i = 6; i <= 10; i++) {
            final int temp = i;
            new Thread(() -> {myLockCache.get(temp + "");}, String.valueOf(i)).start();
        }
    }

}

class MyCache {

    private volatile Map<String, Object> map = new HashMap<>();


    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "-> key = " + key + "开始写入");
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "-> key = " + key + "写入成功");
    }

    public Object get(String key) {
        System.out.println(Thread.currentThread().getName() + "-> key = " + key + "开始读");
        Object value = map.get(key);
        System.out.println(Thread.currentThread().getName() + "-> key = " + key + "读成功");
        return value;
    }
}


class MyLockCache {

    private volatile Map<String, Object> map = new HashMap<>();

    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "-> key = " + key + "开始写入");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "-> key = " + key + "写入成功");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    public Object get(String key) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "-> key = " + key + "开始读");
            Object value = map.get(key);
            System.out.println(Thread.currentThread().getName() + "-> key = " + key + "读成功");
            return value;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
        return null;
    }
}


  • 执行结果:
2-> key = 2开始写入
2-> key = 2写入成功
1-> key = 1开始写入
5-> key = 5开始写入
5-> key = 5写入成功
3-> key = 3开始写入
3-> key = 3写入成功
4-> key = 4开始写入
4-> key = 4写入成功
1-> key = 1开始读
1-> key = 1读成功
1-> key = 1写入成功
2-> key = 2开始读
2-> key = 2读成功
3-> key = 3开始读
4-> key = 4开始读
4-> key = 4读成功
5-> key = 5开始读
3-> key = 3读成功
5-> key = 5读成功



7-> key = 7开始写入
7-> key = 7写入成功
10-> key = 10开始写入
10-> key = 10写入成功
6-> key = 6开始写入
6-> key = 6写入成功
8-> key = 8开始写入
8-> key = 8写入成功
9-> key = 9开始写入
9-> key = 9写入成功
6-> key = 6开始读
6-> key = 6读成功
10-> key = 10开始读
8-> key = 8开始读
7-> key = 7开始读
8-> key = 8读成功
9-> key = 9开始读
9-> key = 9读成功
10-> key = 10读成功
7-> key = 7读成功

1.6、可中断锁和不可中断锁

 
       在抢锁过程中能通过某些方法终止抢占过程,这就是可中断锁,否则就是不可中断锁。Java 的 synchronized 内置锁就是一个不可中断锁,而JUC的显式锁(如 ReentrantLock)是一个可中断锁。

 

  • 可中断锁 :如果某一线程A正占有锁在执行临界区代码,另一线程B正在阻塞式抢占锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己的阻塞等待,这种就是可中断锁。

  • 不可中断锁 :一旦这个锁被其他线程占有,如果自己还想抢占,只能选择等待或者阻塞,直到别的线程释放这个锁,如果别的线程永远不释放锁,那么自己只能永远等下去,并且没有办法终止等待或阻塞。

 

2、关于 Lock 接口

 

       Lock 是 java.util.concurrent (JUC) 包中的一个接口类,JUC中的显式锁都实现了Lock接口。

       锁是一种通过多个线程控制对共享资源的访问的工具。通常,一个锁提供对共享资源的独占访问:在一个时间只有一个线程可以获得锁和所有访问共享资源,需要先获得锁。然而,有些锁可以允许并发访问共享资源,如一个ReadWriteLock读锁。

       Lock 和 Synchronized 都可以达到锁的目的,不过 Lock 有更灵活的结构,有完全不同的特性,可以支持多个相关的 Condition对象
 

3、 Lock 和 Synchronized

  • 不加锁
package com.pzf.suo;

import lombok.AllArgsConstructor;
import java.util.concurrent.TimeUnit;

public class UnSync {
    public static void main(String[] args) {
        SaleTickets saleTicket = new SaleTickets(2);
        new Thread(() -> saleTicket.sale(), "A").start();
        new Thread(() -> saleTicket.sale(), "B").start();
        new Thread(() -> saleTicket.sale(), "C").start();
    }
}

@AllArgsConstructor
class SaleTickets {
    private int num;
    public void sale() {
        if (num > 0) {
          try {
             TimeUnit.SECONDS.sleep(2);
             System.out.println(Thread.currentThread().getName()+"-> 当前票数" + (num--)+",剩余票数" + num);
          } catch (Exception e) {
              e.printStackTrace();
          }
        }
    }
}
运行结果:
C-> 当前票数0,剩余票数-1
A-> 当前票数1,剩余票数0
B-> 当前票数2,剩余票数0
  • Synchronized 锁
package com.pzf.suo;

import lombok.AllArgsConstructor;
import java.util.concurrent.TimeUnit;

public class ToSync {
    public static void main(String[] args) {
        SaleTickets1 saleTicket = new SaleTickets1(2);
        new Thread(() -> saleTicket.sale(), "A").start();
        new Thread(() -> saleTicket.sale(), "B").start();
        new Thread(() -> saleTicket.sale(), "C").start();
    }
}

@AllArgsConstructor
class SaleTickets1 {
    private int num;
    // synchronized 修饰非静态方法,是对调用该方法的对象加锁,俗称"对象锁"
    public synchronized void sale() {
        if (num > 0) {
            try {
              TimeUnit.SECONDS.sleep(2);
              System.out.println(Thread.currentThread().getName()+"-> 当前票数" + (num--)+",剩余票数"+num);
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}
运行结果:
A-> 当前票数2,剩余票数1
C-> 当前票数1,剩余票数0
  • Lock 锁
package com.pzf.suo;

import lombok.NoArgsConstructor;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ToLock {

    public static void main(String[] args) {
        SaleTickets2 saleTicket = new SaleTickets2(5);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> saleTicket.sale(), i + "").start();
        }
    }
}

@NoArgsConstructor
class SaleTickets2 {

    private int num;
    private Lock lock = new ReentrantLock();
    public SaleTickets2(int num) {
        this.num = num;
    }

    public void sale() {
        lock.lock();
        try {
            if (num > 0) {
              TimeUnit.SECONDS.sleep(1);
              System.out.println(Thread.currentThread().getName()+"-> 当前票数"+(num--)+",剩余票数" + num);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 手动释放锁
            lock.unlock();
        }


    }
}

运行结果:
0-> 当前票数5,剩余票数4
2-> 当前票数4,剩余票数3
1-> 当前票数3,剩余票数2
3-> 当前票数2,剩余票数1
4-> 当前票数1,剩余票数0

 

3.1、Lock 和 Synchronized 区别

 

  • Lock 是 java.util.concurrent (JUC) 包中的一个类,Synchronized 是一个内置的 java 关键字。
  • Lock 可以判断是否获取到了锁,Synchronized 无法判断获取锁的状态。
  • Lock 必须要手动释放锁,如果不释放锁会产生死锁。Synchronized 会自动释放锁。
  • Lock 是可重入锁,默认为非公平锁,但是可以设置为公平锁。 Synchronized 可重入锁,不可中断的非公平锁。
  • Lock 线程1(获得锁,阻塞)、线程2(不一定等待)。Synchronized 线程2 会等待下去。
  • Lock 适合锁大量的同步代码,Synchronized 适合少量的代码同步问题。

 

4、Lock 接口的实现类 ReentrantLock

 
ReentrantLock 重入锁和 synchronize 关键字一样,是互斥锁。比synchronize关键字更加灵活。
ReentrantLock 默认为非公平锁,构造函数传 true 时,可设置为公平锁。
 

在这里插入图片描述

ReentrantLock 实现类相关方法说明
 
在这里插入图片描述
 

 
 
 
线程系列博文:
 
线程系列 1 - 线程基础
线程系列 2 - 并发编程之线程池 ThreadPool 的那些事
线程系列 3 - 关于 CompletableFuture
线程系列 4 - synchronized 和线程间的通信
线程系列 5 - CAS 和 JUC原子类
线程系列 7 - JUC高并发容器类
 
 
 
 
 
.

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值