当前位置:   article > 正文

Java并发工具类_java并发工具类和同步工具类

java并发工具类和同步工具类

并发编程领域,有两大核心问题:

  • 互斥:即同一时刻只允许一个线程访问共享资源;
  • 同步:即线程之间如何通信、协作。

这两大问题,管程都是能够解决的。
Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。

Lock

Java 语言本身提供的 synchronized 是管程的一种实现,既然 Java 从语言层面已经实现了管程了,那为什么还要在 SDK 里提供另外一种实现呢?

  • 解决死锁时,提出了一个破坏不可抢占条件方案,但是这个方案 synchronized 没有办法解决。原因是 synchronized
    申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。但我们希望的是:对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
    • 重新设计一把互斥锁去解决这个问题的三种方案

      1、能够响应中断。

      synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。

      2、支持超时。

      如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

      3、非阻塞地获取锁。

      如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

    • 三个方案就是“重复造轮子”的主要原因,体现在 API 上,就是 Lock 接口的三个方法 :

      支持中断的 API

      void lockInterruptibly()
      throws InterruptedException;

      支持超时的 API

      boolean tryLock(long time, TimeUnit unit)
      throws InterruptedException;

      支持非阻塞获取锁的 API

      boolean tryLock();

Lock 靠什么保证可见性?

它是利用了 volatile 相关的 Happens-Before 规则。Java SDK 里面的 ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值。

ReentrantLock 可重入锁,线程可以重复获取同一把锁。有两个构造函数,一个是无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的公平策略,如果传入 true 就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。

  • 用锁的最佳实践

    1、永远只在更新对象的成员变量时加锁
    2、永远只在访问可变的成员变量时加锁
    3、永远不在调用其他对象的方法时加锁

Condition

synchronized 管程里只有一个条件变量,而 Lock&Condition 实现的管程是支持多个条件变量的,在很多并发场景下,支持多个条件变量能够让我们的并发程序可读性更好,实现起来也更容易。

Lock 和 Condition 实现的管程,线程等待和通知需要调用 await()、signal()、signalAll();

synchronized 实现的管程才能使用 wait()、notify()、notifyAll() ;

Semaphore(信号量)

Semaphore 可以允许多个线程访问一个临界区,当多个线程进入临界区时,如果需要访问共享变量就会存在并发问题,所以必须加锁,Semaphore 需要锁中锁。

信号量模型

信号量模型:一个计数器,一个等待队列,三个方法。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down() 和 up(),三个方法都是原子性的,并且这个原子性是由信号量模型的实现方保证的。

在这里插入图片描述

1、init():设置计数器的初始值。
2、down():计数器的值减 1;如果此时计数器的值小于或者等于 0,则当前线程将被阻塞,否则当前线程可以继续执行。
3、up():计数器的值加 1;如果此时计数器的值大于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。

  • 在 Java SDK 并发包里,down() 和 up() 对应的则是 acquire() 和 release()。
  • 应用场景

    各种池化资源,例如连接池、对象池、线程池等等。
    init() 初始化指定数量的凭证,当多个线程同时访问,每个线程调用 acquire() 方法获取一个凭证,当凭证数量归零,获取不到凭证的线程将阻塞在等待队列中,持有凭证的线程调用 release() 归还凭证,同时唤醒一个阻塞队列中的线程,并将其移除阻塞队列。acquire() 与 release()成对使用

ReadWriteLock(读写锁接口)

读写锁类似于 ReentrantLock,也支持公平模式和非公平模式。读锁和写锁都实现了 java.util.concurrent.locks.Lock 接口,所以支持 lock() 方法、tryLock()
、lockInterruptibly() 等方法也都是支持的。但是有一点需要注意,那就是只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出
UnsupportedOperationException 异常。

所有的读写锁都遵守以下三条基本原则:

1、允许多个线程同时读共享变量;
2、只允许一个线程写共享变量;
3、如果一个写线程正在执行写操作,此时禁止读线程读共享变量。

ReentrantReadWriteLock

  • 用 ReadWriteLock 快速实现一个通用的缓存工具类
class Cache<K, V> {
   
    final Map<K, V> m = new HashMap<>();
    final ReadWriteLock rwl = new ReentrantReadWriteLock();
    // 读锁
    final Lock r = rwl.readLock();
    // 写锁
    final Lock w = rwl.writeLock();


    V get(K key) {
   
        V v = null;
        // 读缓存
        r.lock();
        try {
   
            return m.get(key);
        } finally {
   
            r.unlock();
        }
        // 缓存中存在,返回
        if (v != null) {
   
            return v;
        }
        // 缓存中不存在,查询数据库
        w.lock();
        try {
   
            // 再次验证
            // 其他线程可能已经查询过数据库
            v = m.get(key);
            if 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号