前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JAVA并发万字长文从ReentrantLock到juc框架

JAVA并发万字长文从ReentrantLock到juc框架

作者头像
青山师
发布2023-05-05 20:12:53
1600
发布2023-05-05 20:12:53
举报

JAVA并发万字长文从ReentrantLock到juc框架

ReentrantLock 是 Java 中的可重入锁,它实现了 Lock 接口,与 synchronized 相比,ReentrantLock提供了更强大和灵活的锁机制。

ReentrantLock 的原理

ReentrantLock 是通过一个volatile 的变量和一个 FIFO 的队列来实现的。该 volatile 变量表示当前获得锁的线程,FIFO 队列用来存储等待锁的线程。

具体实现方式是:当一个线程获取锁时,将当前线程设置为 volatile 变量的值。如果其他线程试图获取该锁,则会加入 FIFO 队列的尾部,并标记为等待状态。当持有锁的线程释放锁时,它会唤醒 FIFO 队列头部的线程,这个线程继续执行并获取锁。

ReentrantLock 是可重入锁,意味着同一个线程可以多次获取这把锁。这是通过一个计数器实现的,每获取一次锁,计数器的值就加1。释放锁时计数器减1,减到0时才会真正释放锁。

ReentrantLock 的使用

使用 ReentrantLock 主要有三个步骤:

  1. 创建 ReentrantLock 对象
代码语言:javascript
复制
ReentrantLock lock = new ReentrantLock();
  1. 获取锁
代码语言:javascript
复制
lock.lock();
  1. 释放锁
代码语言:javascript
复制
lock.unlock();

一个使用示例:

代码语言:javascript
复制
ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    // do something...
} finally {
    lock.unlock();
}

相比 synchronized,ReentrantLock 提供了更丰富的功能,如可以中断正在等待锁的线程,可以设置锁的尝试获取时间等。这使得 ReentrantLock 在许多场景下可以替代 synchronized,成为并发程序的主流选择。

ReentrantLock 的框架使用实例常见于:线程池、死锁检测、队列等。日常开发中也经常会使用 ReentrantLock 来优化并发场景的性能。

ReentrantLock 的公平性选择

ReentrantLock 提供了公平锁和非公平锁两种模式,默认是非公平锁。

公平锁的锁获取顺序是按照线程请求锁的时间顺序来分配的,这保证了所有的线程获取锁的机会都是公平的。而非公平锁的锁获取顺序并不是根据请求锁的时间顺序来的,有可能刚请求的线程先获取到锁,这就是非公平的。

非公平锁的吞吐量通常高于公平锁,但可能造成饥饿,即某些线程长期等待锁。所以选择哪种模式需要根据具体应用来权衡。

使用方式:

代码语言:javascript
复制
// 公平锁:
ReentrantLock lock = new ReentrantLock(true); 

// 非公平锁:
ReentrantLock lock = new ReentrantLock(false);  

// 默认构造方法:
ReentrantLock lock = new ReentrantLock(); 

CAS操作

ReentrantLock 底层使用 CAS(Compare And Swap)操作来实现锁的获取和释放。

CAS操作包含三个操作数:

  • 要更新的内存值 V
  • 预期的内存值 A
  • 新值 B

如果V的值等于A,那么processors将内存值V更新为B。否则,processor不会执行任何操作。

ReentrantLock 通过CAS操作来更新锁的状态,实现高效且线程安全的锁获取和释放。

通过分析 ReentrantLock 的原理和使用,我们了解到它是一个高效且灵活的锁实现,相比 synchronized 具有更高的扩展性和更丰富的功能。在并发环境下,ReentrantLock 应该是我们首选的锁机制,它的公平与非公平实现和CAS操作也为我们提供了细粒度的控制选项。

参考框架中的ReentrantLock使用

ThreadPoolExecutor 中的应用

线程池ThreadPoolExecutor 使用 ReentrantLock 来控制对线程池状态的访问。主要有两个地方:

  1. 用于控制对 workerCount 和 runState 变量的访问。这两个变量分别代表线程池中的线程数和线程池状态。
  2. 用于控制对等待队列和已完成队列的访问。这是为了保证在将任务添加/删除到这些队列时的线程安全。
代码语言:javascript
复制
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

// CAS (atomic) access to ctl 
private boolean compareAndIncrementWorkerCount(int expect) {
    return ctl.compareAndSet(expect, expect + 1);
}

private boolean compareAndDecrementWorkerCount(int expect) {
    return ctl.compareAndSet(expect, expect - 1); 
}

private void decrementWorkerCount() {
    do {} while (!compareAndDecrementWorkerCount(ctl.get()));
}  

可以看到,ThreadPoolExecutor 通过 CAS 操作来原子地更新 ctl 变量,而 ctl 变量 Contains 线程池的状态和线程数,这就保证了线程池状态的修改的线程安全。

同时,ThreadPoolExecutor 也使用 ReentrantLock 来保护队列的访问,如:

代码语言:javascript
复制
private final ReentrantLock mainLock = new ReentrantLock();
private void addWorker(Runnable firstTask, boolean core) {
    mainLock.lock();
    try {
        // ...
    } finally {
        mainLock.unlock(); 
    }
}

所以,ReentrantLock 在 ThreadPoolExecutor 中发挥了很重要的作用,用于保证线程池状态和队列访问的线程安全。

ConcurrentHashMap中的应用

ConcurrentHashMap 也广泛使用了 ReentrantLock 来实现线程安全。主要使用在:

  1. 分段锁 - ConcurrentHashMap 使用 16 个锁来控制对 hash 表的访问,这 16 个锁就是 ReentrantLock 实例。
  2. 重构链表时的锁 - 当两个线程并发扩容时,如果发现 hash 冲突,需要对相应的链表进行重构。这个过程使用的就是 ReentrantLock。
  3. 读写锁 - 除了分段锁外,ConcurrentHashMap 还提供了读写锁来实现对 get 操作的优化。读锁就是 ReentrantReadWriteLock 的读锁。

下面是 ConcurrentHashMap 中使用 ReentrantLock 的部分示例代码:

代码语言:javascript
复制
/**
 * The segments, each of which is a specialized hash table.
 */ 
final Segment<K,V>[] segments;

static final class Segment<K,V> extends ReentrantLock {...}

final ReentrantLock putLock = new ReentrantLock();  

public V put(K key, V value) {
  Segment<K,V> s;
  if (value == null)
    throw new NullPointerException();
  int hash = hash(key);
  int j = (hash >>> segmentShift) & segmentMask;
  s = ensureSegment(j);
  s.lock(); // lock the segment 
  try { 
    // ...
  } finally { 
    s.unlock(); // unlock the segment
  }
}

final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock rl = rwl.readLock(); 
private final ReentrantReadWriteLock.WriteLock wl = rwl.writeLock();

public V get(Object key) { 
  rl.lock(); // lock for reading  
  try {
    // ... 
  } finally {
    rl.unlock();
  }
}

通过代码我们可以看出,ReentrantLock 被广泛用于 ConcurrentHashMap 的各个方面,起到了保证线程安全的关键作用。

小结

综上,我们分析了 ReentrantLock 在 ThreadPoolExecutor 和 ConcurrentHashMap 两个重要开源框架中的应用。可以看出:

  1. ReentrantLock 用于保证并发环境下对关键数据结构的访问线程安全。
  2. 相比 synchronized,ReentrantLock 提供更加灵活的锁机制,如可中断锁、超时锁、公平锁等,这为框架的并发控制提供了更精细的权衡。
  3. ReentrantLock 通过 CAS 操作来高效地实现锁的获取和释放,这也是它比 synchronized 性能更高的原因之一。

熟悉这些开源框架的实现原理,不仅可以让我们在使用时更加得心应手,也可以在我们自己设计框架时学以致用。ReentrantLock 的应用就是一个很好的参考范例。

ReentrantLock 在实践中的运用

在实际开发中,我们也常常会使用 ReentrantLock 来实现高效的并发控制。这里给出几个示例:

自定义链表的线程安全实现

我们可以使用 ReentrantLock 来实现一个线程安全的链表:

代码语言:javascript
复制
public class ThreadSafeLinkedList {
    private Node head;
    private ReentrantLock lock = new ReentrantLock();
    
    public void add(int value) {
        Node node = new Node(value);
        lock.lock();
        try {
            node.next = head;
            head = node;
        } finally {
            lock.unlock();
        }
    }
    
    public void remove() {
        lock.lock();
        try {
            head = head.next;
        } finally {
            lock.unlock();
        }
    }
    
    private class Node {
        int value;
        Node next;
        
        public Node(int value) {
            this.value = value;
        }
    }
}

我们在添加节点和删除节点时获取 ReentrantLock 的锁,这样可以确保在并发环境下链表操作的线程安全。

实现一个阻塞队列

我们可以使用 ReentrantLock 与 Condition 对象来实现一个阻塞队列:

代码语言:javascript
复制
public class BlockingQueue {
    private LinkedList<Integer> queue = new LinkedList<>();
    private ReentrantLock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    
    public void put(Integer value) {
        lock.lock();
        try {
            queue.add(value);
            notEmpty.signal(); // 唤醒等待的线程
        } finally {
            lock.unlock();
        }
    }
    
    public Integer take() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await(); // 等待
            }
            return queue.removeFirst();
        } catch (InterruptedException e) {
            // ...
        } finally {
            lock.unlock();
        }
    }
}

put 方法用于添加元素到队列,并唤醒等待的消费线程。take 方法用于消费元素,如果队列为空则等待生产者的通知。ReentrantLock 与 Condition 配合使用,很好地实现了阻塞队列的功能。

其他

  • 锁超时:使用 ReentrantLock 的 tryLock(long time, TimeUnit unit) 方法实现锁的超时获取。
  • 锁中断:通过 ReentrantLock 的 lockInterruptibly() 方法获取锁,如果线程等待锁的过程中被中断,则会响应中断异常。
  • 公平锁:使用 ReentrantLock(true) 构造方法创建公平锁实现,可以避免饥饿问题的发生。
  • Condition:ReentrantLock 提供的 Condition对象可以实现线程之间复杂的协作。

综上,ReentrantLock 在实践中有很强的实用性,我们可以运用它来实现定制化的并发控制和复杂的协作逻辑。

总的来说,优先考虑使用 ReentrantLock,而不是 synchronized,这可以使我们的程序更加灵活高效。

ReentrantLock总结

  1. ReentrantLock 的原理:基于volatile变量和FIFO队列实现,支持可重入。
  2. ReentrantLock 的使用:lock()获取锁,unlock()释放锁,提供了更加灵活的锁机制。
  3. ReentrantLock 的公平性选择:公平锁和非公平锁,需要根据具体场景来选择。
  4. ReentrantLock 底层使用 CAS 操作来实现高效的锁获取和释放。
  5. ReentrantLock 在 ThreadPoolExecutor 和 ConcurrentHashMap 等开源框架中的应用,用于保证并发环境下的数据结构和状态的线程安全。
  6. ReentrantLock 在实践中的几个运用示例:自定义线程安全链表、阻塞队列的实现、锁超时和中断、公平锁等。

ReentrantLock 是一个功能强大且高效的锁实现,它应该是我们实现并发控制的首选方案。熟练掌握 ReentrantLock 有助于我们设计出更加健壮的多线程程序。

ReentrantLock FAQ

这里总结一些关于 ReentrantLock 常见的问题.

为什么ReentrantLock是可重入的?

可重入意味着同一个线程可以多次获取同一把锁。对于 ReentrantLock 来说,这是实现隐式锁的必要功能。

举个例子,如果一个类中的方法获取了锁,然后再调用同一个类的另一个方法,第二个方法也会自动获取锁。如果锁不可重入,那么第二个方法将永远等待锁,导致死锁。

所以,可重入的锁一定要在实现隐式锁机制的锁中实现,ReentrantLock做了这点考虑,使其成为更易用的锁。

与synchronized相比,ReentrantLock有什么优势?

相比 synchronized,ReentrantLock 有以下优势:

  1. 灵活性:ReentrantLock 提供了更加灵活的锁机制,如可中断锁、超时锁、公平锁等。这在许多场景下都很有用。
  2. 性能:基于CAS实现,ReentrantLock的性能通常优于synchronized。尤其是在高并发的场景下,优势更加明显。
  3. 可中断:获取 ReentrantLock 锁的线程可以响应中断,这在调试死锁时很有帮助。而synchronized的锁获取不可中断。
  4. 超时:ReentrantLock可以设置锁的最大等待时间,这可以快速响应获取锁失败的情况。synchronized 的锁一直等待,容易发生死锁。
  5. 公平性:可以选择公平锁或非公平锁。synchronized 的锁始终是非公平的。
  6. 条件变量:ReentrantLock 提供的 Condition可以实现复杂的线程协作,而synchronized很难实现这点。

所以,总体来说,ReentrantLock提供了更强大和灵活的锁机制。如果需要这些功能,那么选择 ReentrantLock 是更好的选择。当然,在某些极简单的场景下,synchronized 也完全能够满足需求。

Condition和wait/notify对比?

Condition 是 JDK5 中引入的替代 Object 的 wait/notify 的一个工具,它依赖于 Lock 对象。主要有以下差异:

  1. wait/notify必须和同步块一起使用,Condition可以和任何具有锁的对象一起使用。
  2. wait/notify只能在同步方法或同步块内调用,而Condition可以在任何地方调用。
  3. wait/notify唤醒的线程是随机的,而Condition可以精确地唤醒指定数量的线程。
  4. Condition可以提供更加强大的线程通信机制。

所以,总体来说,Condition的功能更加强大和灵活。对于复杂的线程协作逻辑,Condition是更好的选择。

不过,Condition依赖于Lock对象,使用略微复杂。而wait/notify是Object的内置方法,使用简单,适用于基本的线程通信场景。

所以,根据具体需求选择对应的工具就可以了。

ReentrantLock和synchronized的锁释放是否一定成功?

不一定。释放锁的操作并非原子操作,所以锁释放可能会失败。

对于 synchronized 来说,锁的释放依赖于 JVM,如果 JVM 在锁释放的时刻进行了线程切换,那么就可能导致锁释放失败。不过 JVM 总体来说会保证锁一定能被成功释放。

而对于 ReentrantLock 来说,它是通过 CAS 操作来释放锁的。如果 CAS 操作失败,那么锁的释放也就失败了。不过 ReentrantLock 提供了一套机制来保证锁最终一定会成功释放:

  1. 当释放锁失败时,锁持有线程会自旋一段时间再重试。
  2. 如果重试多次仍未成功,则会加入一个释放锁的队列,并停止自旋。
  3. 锁对象会定期重试那些在释放锁队列中的线程。
  4. 为了避免重试过于频繁影响性能,释放锁操作会有一个最大自旋时间和最大重试次数。
  5. 如果超过最大重试次数仍未成功,则会采取更加激进的措施来释放锁,比如通过 Thread.stop() 强制终止锁持有线程。

所以,总体来说,虽然锁释放可能失败,但是 JVM 和 ReentrantLock 都提供了相应的机制来保证锁最终一定会被成功释放,对程序的正确性影响不大。但频繁的释放失败会影响程序的性能,所以我们应该尽量减少造成这种情况的可能。

公平锁与非公平锁的优缺点?

公平锁与非公平锁的主要区别在于锁的分配方式不同:

  • 公平锁:以 FIFO 的方式依次分配锁给等待线程。这保证了所有的线程获取锁的机会都是公平的。
  • 非公平锁:不依赖等待线程的队列顺序来分配锁。有可能刚请求的线程先获取到锁,这就是非公平的。

两者有以下优缺点:

公平锁:

优点:防止饥饿,保证每个线程获取锁的机会。 缺点:公平策略会降低吞吐量。

非公平锁:

优点:通常可以提高吞吐量。 缺点:可能会导致某些线程长期无法获取锁(饥饿)。

所以,选择哪种锁需要根据具体场景来权衡:

如果对吞吐量要求较高,可以选择非公平锁,并采取其他手段防止饥饿。

如果需要严格的公平性,以防止某些线程无法获取锁,可以选择公平锁,并容忍一定程度的吞吐量降低。

如果同时需要高吞吐量和公平性,可以选择自适应的锁,它可以根据当前情况动态选择公平锁或非公平锁。

综上,没有一种锁可以绝对优于另一种,我们需要根据具体应用场景选择最合适的锁。

如何解决ReentrantLock带来的死锁问题?

ReentrantLock 虽然提供了比 synchronized 更强大的锁机制,但是也带来了更复杂的死锁问题。主要有以下几点需要注意:

  1. 避免嵌套锁:一个线程获取多个锁时,必须遵循获取锁的顺序,否则很容易出现死锁。
  2. 不要在锁内调用可被其他锁调用的方法:如果在锁内调用其它锁保护的方法,那么可能会出现死锁。
  3. 使用 lockInterruptibly() 方法:这样可以在等待锁的线程中响应中断,提高死锁发生时的可调试性。
  4. 使用 tryLock() 设置超时:如果在指定超时时间内无法获取锁,线程可以选择放弃获取,避免死锁发生。
  5. 分层加锁:按照锁的粒度从大到小依次加锁,这样可以避免加锁顺序错误导致的死锁。
  6. 使用 Thread.join() 代替同步:在某些场景下,join() 可以代替同步,而 join() 不会有加锁操作,所以也就不存在死锁问题。
  7. 选择非阻塞算法:在并发程序设计过程中,尽量选择非阻塞的数据结构和算法,这样可以避免加锁产生死锁。
  8. 合理设置同步范围:同步范围应尽可能小,只在真正需要同步的地方添加锁,这样可以减少加锁操作带来的死锁风险。
  9. 检查加锁顺序:对复杂的并发程序来说,最好能检查加锁顺序,避免加锁顺序的错误配置导致死锁发生。这通常需要对程序加锁逻辑进行静态检查。
  10. 使用 jps 和 jstack 检查死锁:如果发生死锁,可以使用这两个工具来分析线程 Dump,找到死锁的根源,然后修复代码。

总之,解决 ReentrantLock 带来的死锁问题主要依靠灵活的使用它提供的功能,遵循良好的加锁规则与并发设计原则,选择合理的线程协作方式,并在发生死锁时能够快速找到问题所在。

ReentrantLock 中的 Condition 如何唤醒指定数量的线程?

ReentrantLock 提供的 Condition 对象允许我们唤醒指定数量的线程。这是它相比 Object 的 wait/notify 具有的优势之一。

Condition 定义了几个方法来唤醒线程:

  • signal():唤醒一个等待线程
  • signalAll():唤醒所有等待线程
  • await():使当前线程等待,直到被唤醒

那么如何实现唤醒指定数量的线程呢?主要靠循环调用 signal() 方法来实现:

代码语言:javascript
复制
// 唤醒 n 个线程 
for (int i = 0; i < n; i++) {
    condition.signal();
}

每调用一次 signal(),就会从等待队列中唤醒一个线程。所以通过循环调用,我们可以精确地唤醒想要的线程数量。

例如,我们可以实现一个生产者-消费者场景,生产者负责唤醒指定数量的消费线程:

代码语言:javascript
复制
public class ProducerConsumer {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    
    private int queueSize = 0;
    
    public void produce(int num) {
        lock.lock();
        try {
            // 生产 num 个商品
            queueSize += num;  
            // 唤醒 num 个消费者线程
            for (int i = 0; i < num; i++) {
                condition.signal();
            }
        } finally {
            lock.unlock();
        }
    }
    
    public void consume() {
        lock.lock();
        try {
            while (queueSize == 0) {  
                // 如果没有商品,等待生产者的通知
                condition.await(); 
            } 
            // 消费一个商品
            queueSize--;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

在这个例子中,生产者线程会调用 produce() 方法生产商品,并唤醒指定数量的消费者线程。消费者线程调用 consume() 方法消费商品,如果商品为空会等待生产者的通知。

所以,通过 Condition 的 signal() 方法循环调用,我们可以精确地唤醒指定数量的线程,这在实现复杂的线程协作逻辑时很有用。

这个例子可以帮你加深对 Condition 的理解,在并发编程中运用自如。Condition 是一个很强大的工具,它配合 ReentrantLock可以实现各种复杂的线程间协作与控制。

ReentrantLock 的读写锁如何实现?

ReentrantLock 本身不提供读写锁的功能,但是我们可以通过它提供的锁与条件变量来简单实现一个读写锁。

读写锁主要有以下几个方法:

  • readLock():获取读锁,多个线程可以同时持有读锁
  • writeLock():获取写锁,只有一个线程可以获取写锁
  • readUnlock():释放读锁
  • writeUnlock():释放写锁

读写锁还需要遵循以下规则:

  1. 同一时刻只允许有一个写锁,或者多个读锁
  2. 写锁不能与其他锁(读锁或写锁)同时持有
  3. 读锁之间可以同时被多个线程持有

基于此,我们可以这样实现一个简单的读写锁:

代码语言:javascript
复制
public class ReadWriteLock {
    private ReentrantLock lock = new ReentrantLock();
    private Condition readCondition = lock.newCondition();
    private Condition writeCondition = lock.newCondition();
    private int readCount = 0;
    private boolean writing = false;
    
    public void readLock() {
        lock.lock();
        try {
            while (writing) {  // 如果有线程持有写锁,等待
                readCondition.await(); 
            }
            readCount++;      // 增加读锁持有数
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
    public void readUnlock() {
        lock.lock();
        readCount--;           // 减少读锁持有数
        if (readCount == 0) { // 如果没有线程持有读锁
            writeCondition.signal(); // 唤醒等待的写线程
        }
        lock.unlock();
    }
    
    public void writeLock() {
        lock.lock();
        try {
            while (readCount > 0 || writing) { // 如果有读锁或写锁,等待
                writeCondition.await();   
            }
            writing = true;                     // 标记有线程持有写锁
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
    public void writeUnlock() {
        lock.lock();
        writing = false;                   // 标记写锁已经释放
        readCondition.signalAll();        // 唤醒所有等待的读线程
        writeCondition.signal();          // 唤醒一个等待的写线程
        lock.unlock();
    }
}

通过 ReentrantLock 与 Condition,我们实现了一个简单的读写锁。读锁通过读锁持有数 readCount 来实现多个线程同时获取,写锁通过 writing 标记来保证同一时间只有一个线程获取。

读写锁是一个比较复杂的并发工具,上面这个实现还比较简单,但已经能够满足基本的需求。如果需要一个更为高级的读写锁,可以参考 JDK 中的 ReadWriteLock 接口的实现。

通过这个例子可以加深你对 ReentrantLock 的理解,以及如何运用它来实现各种并发控制工具。读写锁只是其中一个例子,我们还可以实现各种锁,信号量,栅栏等并发工具。

ReentrantLock 的锁降级会带来什么问题?

锁降级指的是线程持有较高级别的锁(例如写锁),之后又获取较低级别的锁(例如读锁)的场景。对于 ReentrantLock 来说,这会带来一些问题:

  1. 无法实现锁降级:ReentrantLock 自身并不支持锁降级功能。如果一个线程持有写锁,之后再获取读锁,这两个操作是互斥的,所以无法实现锁降级。
  2. 可能导致死锁:如果实现锁降级,必须严格遵守加锁与解锁的顺序。否则很容易产生加锁顺序错误,导致死锁。例如: 线程1获取写锁 -> 线程2获取读锁 -> 线程1尝试获取读锁,等待线程2释放读锁 线程2尝试获取写锁,等待线程1释放写锁 -> 死锁
  3. 需要额外的读写状态标记:要实现锁降级,需要维护读写状态,以判断线程是否可以从写锁降级到读锁。这会增加实现的复杂度。
  4. 锁消除带来的并发问题:锁降级其实是一种锁消除的手段,它可以提高并发度。但是,并不一定是越高的并发度越好。过高的并发也会带来令人难以应对的并发问题。

所以,对 ReentrantLock 来说,锁降级是一个比较复杂的特性,它需要解决许多并发方面的困难才能实现。通常只有在读写锁中才会提供锁降级的功能,而唯独会要求使用者遵守一定的加锁顺序与规则,避免死锁等问题的发生。

对于一般使用 ReentrantLock 的场景,不建议去实现锁降级。ReentrantLock 本身支持可重入,但这仅限于同一种锁(写锁或读锁)。如果我们需要锁降级功能,可以选择 JDK 中的 ReadWriteLock 等并发工具。

在并发领域,锁降级是一个比较高级的功能,建议我们在熟练掌握 ReentrantLock 的基本使用后才去尝试实现。

自旋锁和互斥锁的区别与适用场景?

自旋锁:

  • 不会阻塞线程,通过忙等待的方式实现同步。
  • 在线程数少、同步段极短或预期锁很快就会释放的场景下,自旋锁的性能会更好,因为它避免了阻塞带来的上下文切换开销。
  • 如果锁被占用的时间较长,那么自旋会消耗较多 CPU 资源,效率会变低。

互斥锁:

  • 会阻塞请求锁而获取不到的线程。
  • 可以避免自旋锁消耗 CPU 资源的问题,因为阻塞的线程会被切换出 CPU。
  • 上下文切换 also 会带来开销,如果锁被占用时间较短,那么互斥锁的性能可能会较差。

所以,两者适用的场景也不同:

自旋锁:适用于锁被占用时间极短的场景,这时自旋可以避免上下文切换带来的开销,获得更好的性能。

互斥锁:适用于锁被占用时间较长,同步段较长的场景。这时互斥锁可以避免自旋消耗过多 CPU 资源的问题。

实际上,我们很难准确判断锁被占用的时间,所以两种锁都采取一定的自适应策略:

  • 自旋锁会设置一个循环次数上限,超过后会使用互斥锁。
  • 互斥锁在线程阻塞前,也会先进行少量的自旋,以避免立即阻塞带来的上下文切换开销。

ReentrantLock 就采用了自适应的策略,它会在一定次数的自旋后采用互斥锁阻塞线程。所以,我们使用 ReentrantLock就无需过于关注自旋锁与互斥锁的区别,ReentrantLock 会根据当前情况选择最优的策略。

总之,自旋锁与互斥锁各有优缺点,我们需要根据锁被占用的时间长短选择最合适的同步手段。但在实际开发中,很难准确判断这个时间,所以 ReentrantLock 等并发工具采用自适应策略,这也是它们性能强劲的原因之一。

CountDownLatch与CyclicBarrier的区别?

CountDownLatch 和 CyclicBarrier 都是用于线程协作的工具类,但有以下主要区别:

CountDownLatch:

  • 一次性的,计数器的值只能在构造方法中初始化一次,之后只能通过 countDown() 方法减 1 。
  • 主要用于某些线程正在 doSomething,另一些线程需要在 doSomething 完成后才能继续做其他事情。
  • 当计数器的值减到 0 时,所有调用 await() 方法而在等待的线程才会继续执行。

CyclicBarrier:

  • 可循环使用,计数器的值可以在构造方法中初始化,之后在每次调用 await() 方法之后加 1。
  • 主要用于一组线程互相等待,只有当所有线程都到达一个屏障点之后才继续执行。
  • 调用 await() 方法的线程会阻塞,直到所有的线程都调用 await() 方法。一旦计数器的值加到 parties 的值,所有线程就被释放,继而继续执行。

所以主要区别在于:

CountDownLatch 是一次性的,主要用于一个线程等待多个线程。

CyclicBarrier 可循环使用,主要用于多个线程相互等待。

示例代码:

CountDownLatch:

代码语言:javascript
复制
CountDownLatch countDownLatch = new CountDownLatch(3);

new Thread(() -> {
    System.out.println("子线程1执行完毕");
    countDownLatch.countDown();
}).start();

new Thread(() -> {
    System.out.println("子线程2执行完毕");
    countDownLatch.countDown();  
}).start();

countDownLatch.await();  
System.out.println("所有子线程执行完毕");  // 等待子线程执行完,然后打印

CyclicBarrier:

代码语言:javascript
复制
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);

new Thread(() -> {
    System.out.println("子线程1到达栅栏");
    cyclicBarrier.await();
}).start();

new Thread(() -> {
    System.out.println("子线程2到达栅栏");
    cyclicBarrier.await();
}).start();

cyclicBarrier.await();  
System.out.println("所有子线程到达栅栏");  // 等待所有子线程到达,然后同时继续

所以根据实际需要,选择使用 CountDownLatch 还是 CyclicBarrier。它们是 JDK 中实现线程协作的两个很有用的工具。

Java中的阻塞队列你了解哪些?各自的特点是什么?

Java 中提供了以下几种阻塞队列:

  1. ArrayBlockingQueue:基于数组的阻塞队列,遵循FIFO原则。
    • 可以指定最大容量,如果塞满则插入操作会阻塞等待。
    • 吞吐量通常要高于LinkedBlockingQueue。
  2. LinkedBlockingQueue:基于链表的阻塞队列,遵循FIFO原则。
    • 可以指定最大容量,若不指定则为Integer.MAX_VALUE。
    • 对入队和出队操作的吞吐量较低,但对插入和移除操作的吞吐量较高。
  3. PriorityBlockingQueue:具有优先级的阻塞队列。
    • 不允许放入null元素,按元素自然顺序或Comparator定义的顺序排序。
    • 无最大容量,会一直扩容。
    • 对入队和出队操作的吞吐量较低,时间复杂度为O(logN)。
  4. DelayQueue:具有延时性的阻塞队列。
    • 队列元素必须实现Delayed接口。
    • 出队时会等待队头元素的延时时间到期。
    • 无最大容量,会一直扩容。
  5. SynchronousQueue:不存储元素的阻塞队列。
    • 每一个put操作必须等待一个take操作,否则将会阻塞。
    • 与其他阻塞队列不同,它是一个不存储元素的队列。
    • 吞吐量很高,可以当作线程间传递数据的通道。

除此之外,还有LinkedTransferQueue、LinkedBlockingDeque等。

阻塞队列提供了阻塞的插入、移除操作,它们在并发编程中非常有用。我们可以根据实际场景选择合适的阻塞队列。它们有各自的特点,需要权衡吞吐量、排序、延时等因素来决定用哪个队列。

CountDownLatch和Semaphore的区别与应用场景?

CountDownLatch 和 Semaphore 都是用于控制线程并发访问的工具类,但有以下主要区别:

CountDownLatch:

  • 用于使一个或多个线程等待其他线程完成各自的工作后再执行。
  • 内部维护一个计数器,每次调用countDown()方法计数器的值减 1。
  • 调用await()方法的线程会一直等待,直到计数器的值为 0。
  • 一次性的,计数器的值不能被重置。

Semaphore:

  • 用于控制对某组资源的访问权限。
  • 内部维护一个计数器,每次调用acquire()方法计数器的值减 1;调用release()方法计数器的值加 1。
  • 调用acquire()方法的线程会等待,直到有许可证可以获得。
  • 可重用,计数器的值可以被重置。

主要应用场景:

CountDownLatch:

使一个线程等待多个线程完成各自工作后再继续执行。例如,主线程等待多个子线程完成初始化工作。

Semaphore:

  • 用于限制可以访问某组资源的线程数量。例如,限制文件IO与数据库连接池中的连接数。
  • 可以用于实现资源池等。

示例代码:

CountDownLatch:

代码语言:javascript
复制
CountDownLatch countDownLatch = new CountDownLatch(3);

new Thread(() -> {   // 子线程1
    System.out.println("子线程1开始执行");
    countDownLatch.countDown();
    System.out.println("子线程1执行完毕");
}).start();

new Thread(() -> {   // 子线程2
    System.out.println("子线程2开始执行");
    countDownLatch.countDown();   
    System.out.println("子线程2执行完毕");
}).start(); 

countDownLatch.await();   // 主线程等待
System.out.println("所有子线程执行完毕");

Semaphore:

代码语言:javascript
复制
Semaphore semaphore = new Semaphore(2);  // 许可证数量为2

new Thread(() -> {
    try {
        semaphore.acquire();   // 获取一个许可证
        System.out.println(Thread.currentThread().getName() + "获取到许可证");
        TimeUnit.SECONDS.sleep(2);
        semaphore.release();   // 释放许可证
    } catch (InterruptedException e) {
        e.printStackTrace();
    } 
}).start();

new Thread(() -> {   // 其他线程也申请许可证
    try {
        semaphore.acquire();
        System.out.println(Thread.currentThread().getName() + "获取到许可证");
        TimeUnit.SECONDS.sleep(2);
        semaphore.release();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } 
}).start(); 

所以根据需要控制线程启动顺序还是资源访问,选择使用 CountDownLatch 或 Semaphore。它们都是 Java 并发包中比较基本但非常有用的工具。

Java中的阻塞算法你了解哪些?

Java 中提供了几种常见的阻塞算法:

  1. 互斥锁(Mutual Exclusion Lock):用于保证同一时间只有一个线程可以访问共享资源。例如 ReentrantLock。
  2. 读写锁(Read-Write Lock):用于解决读写操作的互斥问题。在同一时间可以允许多个读线程或一个写线程访问数据。例如 ReadWriteLock。
  3. 信号量(Semaphore):用于控制可以访问某组资源的线程数量。它内部维护一个计数器,每调用一次 acquire() 方法计数器减 1,调用 release() 方法计数器加 1。
  4. 屏障(Barrier):用于使多个线程相互等待,只有当所有线程都到达屏障点时才可以继续执行。例如 CyclicBarrier。
  5. 栅栏(Latch):用于使一个或多个线程等待其他线程完成工作后才可以继续执行。区别于 Barrier,栅栏是一次性的。例如 CountDownLatch。
  6. 阻塞队列(Blocking Queue):用于不同线程之间通过队列进行数据传递,当队列为空或满时会使生产者或消费者线程等待。
  7. 选择器(Selector):基于事件驱动的 I/O 复用模型。用于监听多个网络连接的就绪事件,包括连接就绪、读就绪和写就绪等。
  8. 线程池(ThreadPoolExecutor):用于创建一定数量的线程,重复使用以便需要时创建新线程。它内部维护着一组线程,等待监督管理者分派可执行的任务。

这些都是 Java 并发包中比较基本与常用的阻塞算法与工具。通过它们,我们可以实现各种线程间的协作与并发控制。

互斥锁用于独占资源,读写锁用于读多写独占,信号量用于资源池,屏障与栅栏用于线程协调,阻塞队列用于生产者消费者,选择器用于 I/O 多路复用,线程池用于线程复用。掌握它们的原理与用法,是成为并发编程高手的基础。

阻塞算法虽然会使线程等待,但通过 Thread.sleep() 等方式实现的等待不属于阻塞,因为它会释放 CPU 使用权。阻塞算法会使线程在等待过程中保持活跃状态,这是一个重要的区别。

常见的并发容器与并发集合?

Java 提供了几种并发容器与集合,主要有:

  1. ConcurrentHashMap: 线程安全的 HashMap,通过分段锁实现高效并发。
    • 支持高并发的插入、删除和获取操作。
    • 键值对数量过多时(超过 Share Segment)才会带来锁的竞争开销,所以在特定场景下性能优于 HashMap。
    • 可以指定初始容量与负载因子,默认为 16 的并发级别。
  2. CopyOnWriteArrayList: 读写分离的线程安全 List,采用写入时复制的思想实现。
    • 适合读多写少的场景,因为写入时会进行复制。
    • 在迭代的过程中对列表进行修改会抛出 ConcurrentModificationException。
    • 不支持 addIfAbsent、removeIf 等原子条件操作。
  3. BlockingQueue: 线程安全的队列,常用于生产者消费者场景。例如 ArrayBlockingQueue,LinkedBlockingQueue 等。
    • 支持阻塞的插入、移除等操作。
    • 不同的实现各有特点,需要根据实际需要选择合适的队列。
  4. ConcurrentSkipListMap: 线程安全的有序 Map,通过跳表数据结构实现。
    • 键值对会按键自动排序,支持 Range 查询与迭代。
    • 除了有序性,其他方面与 ConcurrentHashMap 相似。
    • 支持较高的并发性,性能也不错。
  5. CopyOnWriteArraySet: 与 CopyOnWriteArrayList 类似,采用写入时复制的 Set。
  • 读多写少场景,迭代时快照数据,不会抛出并发修改异常。
  • 不支持条件操作,添加、删除单个元素操作时需要遍历并复制数据。

理解并发容器与集合的特性,选择合适的集合来解决问题,这些都是并发编程的重要技能。它们可以有效解决多线程环境下的线程安全问题,并且性能较好。并发程序设计的难点在于处理好同步与互斥,选择高效的同步手段与数据结构。并发集合正是在这方面提供了很好的支持。

Java 8中新增的并发API有哪些?

  1. StampedLock:乐观读锁与悲观写锁相结合的锁算法。它可以提高读操作的效率,适用于读多写少的场景。
    • readLock():获取读锁,锁住后可以安全读,但不保证读取的数据一定是最新。
    • writeLock():获取写锁,获取后可以安全读写,但会阻塞其他读写操作。
    • tryOptimisticRead():尝试获取乐观读锁,锁住后可以读取但不保证一定是最新数据,返回戳记。
    • validate(stamp):验证戳记,如果失败说明数据已被修改,需要重试。
  2. LongAdder:高并发场景下统计值的累加器,比 AtomicLong 性能更好。
    • increment():增加值,线程安全。
    • increment():增加值,线程安全。
  3. CompletableFuture:代表异步计算结果的完整的 Future。它提供了很丰富的 API 来组合(thenCombine() 等)执行多个 Future,对它们的结果进行处理(thenApply() 等)。
    • supplyAsync():异步提供结果的 Future。
    • thenApply():接收上一步的结果并处理,返回一个新的 Future。
    • thenCombine():合并两个 Future 的结果,并处理,返回一个新的 Future。
    • thenAccept():接收上一步的结果并处理,但没有返回值。
    • exceptionally():处理 Future 产生的异常。
    • get():获取最终结果。
  4. CompletionStage:与 CompletableFuture 差不多,但 API 更简单。常与 Stream 搭配使用。
    • thenApply():接收上一步的结果并处理,返回一个新的 Stage。
    • thenCombine():合并两个Stage的结果,并处理,返回一个新的Stage 。
    • exceptionally():处理Stage产生的异常。
    • toCompletableFuture():转换为CompletableFuture。

这些 API 使异步编程变得更简单,也为我们提供更多的并发工具。虽然学习成本也有所提高,但使用它们可以轻松编写出高性能的并发程序。Java 一直在着力丰富其并发支持与工具。理解并熟练使用这些 API 有助于我们编写出更高效的多线程程序。这也是成为并发编程高手必须掌握的技能。

JDK 中的并发工具类有哪些?你最常用的有哪些?

JDK 中提供了许多并发工具类,主要有:

  1. Executor 和 ExecutorService:用于线程池与任务执行。这是我使用最频繁的工具类之一。
  2. CountDownLatch 和 CyclicBarrier:用于线程同步协作。CountDownLatch 更常用。
  3. Semaphore:用于控制对某组资源的访问权限。用来实现资源池等机制。
  4. BlockingQueue:线程安全的队列,常用于生产者消费者场景。ArrayBlockingQueue 和 LinkedBlockingQueue 使用较多。
  5. Lock 和 ReentrantLock:用于显式锁定,替代 synchronized。ReentrantLock 是我最常使用的锁实现。
  6. Atomic 系列类:用于原子操作,线程安全。AtomicInteger,AtomicBoolean 和 AtomicReference 使用较频繁。
  7. ConcurrentHashMap:线程安全的 HashMap。这可能是我使用最多的并发集合了。
  8. CopyOnWriteArrayList:读写分离的线程安全 List,适用于读多写少的场景。偶尔会使用。
  9. CompletableFuture:代表异步计算结果的 Future,可以串行执行任务和处理计算结果。Java 8 中使用较频繁。
  10. ForkJoinPool:Java 7 加入的分治框架,用来并行执行任务。适用于可以分治的 CPU 密集型计算。

除此之外,还有 ConcurrentSkipListMap、CopyOnWriteArraySet、StampedLock 等并发类。

这些并发工具类和集合覆盖了并发编程中最主要与常用的功能。掌握它们的特性与用法,可以有效编写出高效与正确的多线程程序。

这些类可以解决线程池、线程同步、互斥与协作、资源控制、原子操作、线程安全集合与异步编程等方面的问题。所以对我来说,它们是最重要与不可或缺的并发工具。

本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-05-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JAVA并发万字长文从ReentrantLock到juc框架
    • ReentrantLock 的原理
      • ReentrantLock 的使用
        • ReentrantLock 的公平性选择
          • CAS操作
            • 参考框架中的ReentrantLock使用
              • ThreadPoolExecutor 中的应用
              • ConcurrentHashMap中的应用
              • 小结
            • ReentrantLock 在实践中的运用
              • 自定义链表的线程安全实现
              • 实现一个阻塞队列
              • 其他
            • ReentrantLock总结
              • ReentrantLock FAQ
                • 为什么ReentrantLock是可重入的?
                • 与synchronized相比,ReentrantLock有什么优势?
                • Condition和wait/notify对比?
                • ReentrantLock和synchronized的锁释放是否一定成功?
                • 公平锁与非公平锁的优缺点?
                • 如何解决ReentrantLock带来的死锁问题?
                • ReentrantLock 中的 Condition 如何唤醒指定数量的线程?
                • ReentrantLock 的读写锁如何实现?
                • ReentrantLock 的锁降级会带来什么问题?
                • 自旋锁和互斥锁的区别与适用场景?
                • CountDownLatch与CyclicBarrier的区别?
                • Java中的阻塞队列你了解哪些?各自的特点是什么?
                • CountDownLatch和Semaphore的区别与应用场景?
                • Java中的阻塞算法你了解哪些?
                • 常见的并发容器与并发集合?
                • Java 8中新增的并发API有哪些?
                • JDK 中的并发工具类有哪些?你最常用的有哪些?
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
            http://www.vxiaotou.com