Java进阶篇之并发控制:掌握锁、信号量与屏障的艺术

系列文章目录

第一章 Java进阶篇之并发基础

第二章 Java进阶篇之并发控制:掌握锁、信号量与屏障的艺术

第三章 Java进阶篇之并发工具类:深入Atomic、Concurrent与BlockingQueue

第四章 Java进阶篇之线程池(Executor框架)深度解析

第五章 Java进阶篇之Fork/Join 框架:并行处理的艺术

第六章 Java进阶篇之并发实践:优化与挑战


目录

前言

一、锁(Locks)

synchronized关键字

ReentranLock

 总结

二、信号量(Semaphores)

使用Semaphore

三、屏障(Barriers)

CyclicBarrier

CountDownLatch

 四、比较

(1)锁(Locks) vs. 信号量(Semaphores)

1. 用途与控制粒度:

2. 可重入性与公平性:

3. API与使用方式:

(2)屏障(Barriers) vs. 锁与信号量

1. 目的与场景:

2. 可重用性与一次性:

3. 行为特性:

(3)总结

结语


前言

        在多线程编程的世界里,Java提供了丰富的工具和API来帮助我们控制并发,确保数据的一致性和程序的正确性。本文将深入探讨Java中用于并发控制的关键概念——锁、信号量以及屏障,这些是Java并发编程中的高级主题,也是构建高效、健壮的多线程应用程序的基石。


一、锁(Locks)

synchronizedReentrantLock都是Java中用于线程同步的重要机制,它们允许在一个多线程环境中控制对共享资源的访问,从而避免竞态条件和数据不一致性问题。尽管它们都能实现线程同步,但在使用方式、灵活性和性能上有明显的差异。

synchronized关键字

synchronized是Java语言的关键字,它可以直接作用于实例方法、静态方法或代码块。当一个线程进入synchronized代码块或方法时,它会自动获取锁,并在退出时自动释放锁。这种机制简化了代码,但也可能导致以下局限性:

  • 自动获取和释放锁synchronized关键字不需要显式的调用锁的获取和释放操作,这简化了使用,但也意味着如果发生异常,锁会在异常抛出时自动释放,避免死锁。
  • 非公平锁synchronized总是实现非公平锁,即新来的线程可能在等待队列中的线程之前获取锁。
  • 没有超时机制synchronized没有提供尝试获取锁的超时选项,如果无法立即获取锁,线程将一直等待。
  • 缺乏中断响应:在等待锁的过程中,线程不能响应中断请求。
  • 性能:在Java早期版本中,synchronized的性能较差,因为它依赖于JVM级别的实现,但随着JDK版本的更新,synchronized的性能得到了很大提升,尤其是在JDK 6之后。
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

ReentranLock

ReentrantLockjava.util.concurrent.locks包中提供的一个接口实现,它提供了比synchronized更强大的锁机制。ReentrantLock允许更细粒度的控制:

  • 手动获取和释放锁:使用ReentrantLock时,你需要显式地调用lock()方法获取锁,以及unlock()方法释放锁。这增加了灵活性,但也要求程序员必须小心处理,以避免死锁。
  • 公平锁或非公平锁ReentrantLock允许你选择是否使用公平锁,公平锁按照线程请求锁的顺序分配锁,而非公平锁可能会让新来的线程抢先获取锁。
  • 超时机制ReentrantLock提供了尝试获取锁的超时选项,如果在指定时间内未能获取锁,则返回失败,而不是无限期等待。
  • 响应中断:在等待锁的过程中,ReentrantLock可以响应中断请求,使线程能够提前退出等待状态。
  • Condition支持ReentrantLock支持Condition对象,可以更精细地控制线程的等待和唤醒,而synchronized只支持单一的wait()notify()notifyAll()
  • 性能:在某些情况下,ReentrantLock的性能优于synchronized,尤其是当需要更复杂的锁机制时。
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}

 总结

synchronizedReentrantLock各有优缺点,选择使用哪一个取决于具体的应用场景。如果你需要更简单的同步代码,并且对性能的要求不是非常高,synchronized可能是一个合适的选择。然而,如果你需要更复杂的锁行为,如公平锁、响应中断、超时机制或使用Condition,那么ReentrantLock可能是更好的选择。在实际开发中,理解和评估这些差异对于编写高效、安全的多线程代码至关重要。

二、信号量(Semaphores)

信号量是一种控制多个线程对共享资源访问的同步工具。在Java中,java.util.concurrent.Semaphore类提供了一个信号量的实现。

使用Semaphore

当我们需要限制同时访问某个资源的线程数量时,信号量就非常有用。例如,限制网络连接数或数据库连接数。

import java.util.concurrent.Semaphore;

public class ConnectionPool {
    private final Semaphore semaphore = new Semaphore(10); // 限制最大连接数为10

    public void acquireConnection() throws InterruptedException {
        semaphore.acquire();
    }

    public void releaseConnection() {
        semaphore.release();
    }
}

三、屏障(Barriers)

 屏障允许一组线程相互等待,直到每个线程都到达了屏障点。Java中java.util.concurrent.CyclicBarrierjava.util.concurrent.CountDownLatch是两种常用的屏障类。

CyclicBarrier

CyclicBarrier支持重用,一旦所有线程到达屏障点后,它们将被释放并可以继续执行,而屏障则会被重置以供下一轮使用。

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.BrokenBarrierException;

public class Worker implements Runnable {
    private final CyclicBarrier barrier;

    public Worker(CyclicBarrier barrier) {
        this.barrier = barrier;
    }

    @Override
    public void run() {
        System.out.println("任务启动");
        // 执行一些任务...
        try {
            barrier.await(); // 等待所有线程到达
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
        System.out.println("所有工作任务结束");
    }
}

CountDownLatch

CountDownLatch是一个一次性使用的屏障,当计数器达到零时,所有等待的线程都会被释放。

import java.util.concurrent.CountDownLatch;

public class Worker implements Runnable {
    private final CountDownLatch latch;

    public Worker(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        System.out.println("任务启动");
        // 执行一些任务...
        latch.countDown(); // 减少计数器
    }
}

 四、比较

(1)锁(Locks) vs. 信号量(Semaphores)

1. 用途与控制粒度

  • 主要用于保护临界区,确保同一时刻只有一个线程可以访问共享资源。锁通常用于细粒度的同步控制,比如保护一个变量或一段代码。
  • 信号量则用于控制对一组相关资源的访问,它允许一定数量的线程同时访问资源。信号量适用于更粗粒度的资源管理,例如限制系统中的文件句柄数量或数据库连接数。

2. 可重入性与公平性

  • 锁可以是可重入的,这意味着已经拥有锁的线程可以再次获取同一个锁而不导致死锁。ReentrantLock支持设置为公平或非公平锁。
  • 信号量也支持公平性设置,但通常不涉及可重入的概念,因为其主要关注的是资源的数量而非同一线程多次获取资源的问题。

3. API与使用方式

  • 锁通常通过try/finally块来保证即使发生异常也能释放锁,防止死锁。
  • 信号量的acquire()release()方法分别用于获取和释放资源,其中acquire()可能阻塞直到有足够的资源可用。

(2)屏障(Barriers) vs. 锁与信号量

1. 目的与场景

  • 信号量主要用于同步线程对共享资源的访问,避免竞态条件。
  • 屏障(如CyclicBarrierCountDownLatch)则用于协调多个线程的执行顺序,让一组线程在继续之前必须等待所有线程到达某个点。

2. 可重用性与一次性

  • 锁和信号量在释放后可以被再次获取,因此它们是可重用的。
  • CyclicBarrier可以在每次所有线程到达屏障后自动重置,因此它可以重复使用。而CountDownLatch在计数器到达零后不能重置,只能作为一次性屏障使用。

3. 行为特性

  • 锁和信号量的行为更加直接,主要是控制访问权限。
  • 屏障除了控制线程执行外,还可能触发某些动作(如CyclicBarrierbarrierAction),并且它们的使用通常与特定的多线程算法或模式紧密相关。

(3)总结

  • 适用于细粒度的资源保护,确保原子性和排他性。
  • 信号量适合于管理一组有限资源的访问,提供了一种定量控制并发访问的方法。
  • 障碍物(如CyclicBarrierCountDownLatch)用于线程间的协调,特别适合于需要同步开始或结束的多线程场景。

结语

        锁、信号量和屏障是Java并发编程中不可或缺的工具。理解并熟练运用它们,可以显著提高你的多线程应用程序的性能和稳定性。希望本文能帮助你更好地掌握Java的并发控制技术,为你的项目带来更高的并发处理能力。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

捡破烂的小码农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值