0
点赞
收藏
分享

微信扫一扫

Semaphore与ReentrantLock——Java并发控制的利器

1.Java锁概述

1.1 锁的作用与重要性

在多线程编程中,锁是用于控制多个线程对共享资源的访问,确保了线程安全,防止数据的不一致和破坏。锁提供了两个主要功能:互斥和可见性。互斥即一次只允许一个线程持有锁,以避免多线程同时修改同一数据;可见性保证一个线程对共享数据的修改,能够被接下来获得锁的其他线程看到。

1.2 Java中锁的类型简介

Java提供了多种锁机制,包含内置锁synchronized、显式锁Lock、以及其他的协作对象,如Semaphore、CountDownLatch、CyclicBarrier等。每种锁都有其特定的应用场景。

2.ReentrantLock详解

2.1 Lock接口的主要方法

Lock接口是提供了比synchronized关键字更灵活的锁操作机制,它允许更复杂的条件同步控制。Lock接口提供的主要方法如下:

public interface Lock {
    void lock(); // 获取锁
    boolean tryLock(); // 尝试非阻塞地获取锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 尝试在给定时间内阻塞地获取锁
    void unlock(); // 释放锁
    Condition newCondition(); // 返回绑定到此Lock实例的新Condition实例
    void lockInterruptibly() throws InterruptedException; // 获取锁,除非当前线程被中断
}

2.2 ReentrantLock的特性

ReentrantLock作为Lock接口的一个实现类,在多线程处理中提供了高度的支持。

2.2.1 非公平锁与公平锁的区别

在构造ReentrantLock的时候可以指定一个boolean值来决定锁的公平性:ReentrantLock fairLock = new ReentrantLock(true); 非公平锁的优点在于吞吐量比公平锁大。对于非公平锁,当锁变得可用时,锁将尝试给予等待时间最长的线程,但不会像公平锁那样保证这一点。公平锁则是完全按照等待的先后顺序,先来的线程先获得锁。

2.2.2 ReentrantLock与synchronized的对比

synchronized是依赖于JVM实现锁的功能,它只有简单的锁定和释放功能。而ReentrantLock则提供了丰富的功能,如可中断的锁获取、锁投票、定时锁等候以及公平性选项。 来看一个简单的ReentrantLock例子:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
    private final Lock lock = new ReentrantLock();
    private int count = 0;
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
    public int getCount() {
        return count;
    }
}

在这个例子中,我们用ReentrantLock确保每次只有一个线程能够进入增量部分的临界区。

2.3 ReentrantLock的内部实现机制

ReentrantLock的内部实现基于AQS(AbstractQueuedSynchronizer)这一同步器框架。AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

2.4 Condition类的使用与特点

Condition接口提供了类似于Object监视器方法的功能,如等待通知机制的wait、notify和notifyAll。不同的是,一个Lock能够绑定多个Condition对象。

2.4.1 Condition与Object类锁方法的区别

用Condition实现的生产者/消费者模式比synchronized方式能更好地控制线程的唤醒。Condition能够为线程指定不同的等待集合(条件队列),而synchronized方式下所有线程都是在一个等待集合中。来看一个Condition的使用实例:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull  = lock.newCondition(); 
    final Condition notEmpty = lock.newCondition(); 
    final Object[] items = new Object[100];
    int putptr, takeptr, count;
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

在这个有界队列的例子中,notFull和notEmpty分别对应着队列的不满和不为空的条件。当队列满或空时,生产者和消费者线程会分别等待,直到队列不满或不为空才会继绀执行。

2.5 tryLock、lock与lockInterruptibly的使用场景与区别

tryLock() 方法提供一种限时等待来尝试获取锁。如果锁在指定的等待时间内没有被获取,当前线程可以做一些其他事情,这通常用于响应外部条件的改变。 lock() 方法会阻塞线程直到获取锁。如果锁已被其他线程获取,当前线程将处于等待状态。 lockInterruptibly() 允许在等待锁的过程中响应中断。如果当前线程被中断,则不会获取锁。 每种获取锁的方式都有其适应的场景,开发者应根据实际情况选择恰当的锁策略。

3.Semaphore信号量机制

3.1 Semaphore基本概念与原理

Semaphore信号量是用于控制同时访问特定资源的线程数量,它通过维护一组许可证来实现。Semaphore可以用来实现资源池,或者用于并发任务限流。Semaphore主要方法介绍如下:

public class Semaphore {
    public Semaphore(int permits) { ... } // 初始化许可证数量的构造器
    public void acquire() { ... } // 获取许可证,如果无可用许可证则阻塞
    public void release() { ... } // 释放许可证,增加可用许可证数量
    public boolean tryAcquire() { ... } // 尝试非阻塞的获取许可证
}

3.2 通过Semaphore实现互斥锁(计数器设置为1)

将Semaphore的许可证数量设置为1,使得任何时刻只允许一个线程执行操作,与互斥锁相似。以下是示例代码:

import java.util.concurrent.Semaphore;
public class Mutex {
    private final Semaphore semaphore = new Semaphore(1);
    public void accessResource() {
        try {
            semaphore.acquire();
            // 访问临界区资源
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release();
        }
    }
}

以上代码确保了每次只有一个线程能够进入到临界区。

3.3 Semaphore的高级用法与案例

Semaphore除了基本的互斥作用,还可以实现其他复杂的同步需求,如限流器(Rate Limiter)。

3.4 Semaphore与ReentrantLock的使用对比

  • Semaphore允许多个线程同时访问某一个资源,而ReentrantLock通常用于资源的互斥使用。
  • ReentrantLock可以完全保证公平性,而Semaphore则不行。 Semaphore可用于不同的场景,如限制对集合访问的并发度,实现简单的线程池等。

4.原子类操作的锁机制

4.1 AtomicInteger的使用与实现原理

在Java的java.util.concurrent.atomic包中,AtomicInteger是一个提供原子操作的整型值。原子类通过底层硬件指令来实现无锁的线程安全操作,大多数情况下比锁有着更好的性能。以下是AtomicInteger的一个基础示例:

import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger();
    public void increment() {
        count.getAndIncrement(); // 原子性地增加1
    }
    public int getCount() {
        return count.get(); // 获取当前的计数值
    }
}

这种方式无需担心线程安全问题,因为getAndIncrement()方法会保证原子性操作。

4.2 如何使用原子类确保线程安全

原子类背后的主要机制是CAS(Compare-And-Swap),这是一种硬件对并发操作共享数据的支持。CAS操作包含3个参数:内存位置、预期原值及新值。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值,这个操作是原子的。 使用原子类可以避免synchronized和Lock需要的上下文切换和调度延迟,从而在高并发环境中提供更好的性能。

5.可重入锁(递归锁)的深入理解

5.1 可重入锁在Java中的表现

可重入锁,也称递归锁,指的是同一个线程多次获得同一把锁。在Java中,ReentrantLock和synchronized都提供了可重入性。这种机制保证了当一个线程已经持有锁时,它可以再次获取该锁而不会产生死锁。

5.2 可重入锁的实战应用案例

在多线程环境中处理递归调用时,可重入锁显得尤为重要。以下是一个ReentrantLock的使用案例,其中显示了可重入特征:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class RecursiveLock {
    private final Lock lock = new ReentrantLock();
    private int count = 0;
    public void outer() {
        lock.lock();
        try {
            count++;
            inner(); // 可重入锁可以在这里再次被获取
        } finally {
            lock.unlock();
        }
    }
    public void inner() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}

在这个例子中,当执行到inner()方法时,当前线程不需要重新争取锁,它可以直接再次获取到锁,并增加计数。 递归锁功能同样适用于synchronized,它是通过线程持有的锁计数来实现的。当线程进入同步块时,锁计数会增加,当退出时减少。计数为0时锁被释放。

举报

相关推荐

0 条评论