并发编程 锁机制、线程池机制和AQS

Java并发编程是Java编程中非常重要的一部分,它允许开发者编写能够处理多个任务同时执行的应用程序。Java提供了一系列的工具和机制来支持并发编程,包括锁机制、线程池机制和AQS(AbstractQueuedSynchronizer)等。下面是对这些机制的总结:

锁机制

Java提供了多种锁机制来确保多线程之间的同步和互斥。

1.内置锁(synchronized):

-是Java中最基本的锁机制。
-可以用于方法或代码块。
-可重入(Reentrant):一个线程可以多次获得同一个锁。
-隐式锁获取和释放。

以下是Java代码示例,展示了内置锁的用法:

  1. 用于方法:
public synchronized void printMessage() {
    // 该方法的代码块是被内置锁保护的
    System.out.println("Hello, world!");
}

在上面的示例中,使用了synchronized关键字修饰了printMessage方法。这意味着当一个线程进入该方法时,它会获取内置锁并执行方法体中的代码。其他线程在同一时间只能一个接一个地进入该方法。

  1. 用于代码块:
public void doSomething() {
    // 其他代码

    synchronized (this) {
        // 在这里执行需要被保护的代码块
    }

    // 其他代码
}

在上面的示例中,synchronized关键字被用于代码块。代码块的作用范围是synchronized关键字括号内的部分。当一个线程进入该代码块时,它会获取内置锁并执行代码块中的代码。其他线程在同一时间只能一个接一个地进入该代码块。

需要注意的是,上述两个示例中的锁是通过关键字synchronized来实现的,而不需要显式地创建锁对象。这是因为内置锁是Java中的一种特殊机制,它会自动关联到正在执行的对象上。

当一个线程已经获得了一个锁之后,它可以再次获得同一个锁,而不会被阻塞。也就是说,如果一个线程已经持有了某个锁,并且在拥有锁的情况下执行一段需要同一个锁的代码时,它可以继续执行而不会被阻塞。

这种情况被称为锁的可重入性(Reentrant),也被称为线程的递归进入。当线程已经获得了锁时,再次进入需要同一个锁的代码块时,它可以继续执行,而不会被阻塞或等待。

可重入性是内置锁的一个重要特性,它允许线程在同一个锁上进行嵌套的同步调用,从而避免了死锁的问题。例如,在一个方法中调用另一个需要同一个锁的方法,由于可重入性,线程可以继续执行而不会被阻塞。

需要注意的是,可重入性仅适用于同一个线程对同一个锁的多次获取,而不适用于不同线程之间对同一个锁的获取。不同线程之间的锁获取仍然会遵循同步机制,需要等待锁的释放。

2.ReentrantLock:

-是Java.util.concurrent.locks包下的一个类,提供了比synchronized更灵活的锁机制。
-显式锁获取(lock())和释放(unlock())。
-支持公平锁和非公平锁。
-支持可中断的获取锁尝试。
-支持尝试获取锁(tryLock())。

ReentrantLock提供了一些其他的特性,比如支持公平锁和非公平锁的选择,默认是非公平锁。公平锁会按照线程请求的顺序分配锁,而非公平锁则允许插队,即新请求锁的线程有可能在等待队列中的线程之前获取到锁。

此外,ReentrantLock还支持可中断的获取锁尝试,即在等待锁的过程中,线程可以被中断。它也支持尝试获取锁的操作,即尝试获取锁,如果锁已被其他线程持有,则返回false,而不是一直等待。

需要注意的是,与内置锁不同,使用ReentrantLock时需要手动释放锁,因此在finally块中确保调用unlock()方法是重要的,以防止锁一直保持而导致死锁的问题。

ReentrantLock类提供了在构造函数中选择公平锁(fairness)和非公平锁的机制。

当使用公平锁时,多个线程按照它们尝试获取锁的顺序进行竞争。即先来先服务的原则,不会出现饥饿现象。

当使用非公平锁时,多个线程尝试获取锁的顺序是不确定的。这种方式可能会导致某些线程一直无法获取到锁,从而出现饥饿现象。

让我们通过代码来演示这一点:

import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    private static ReentrantLock lock = new ReentrantLock(true); // true表示使用公平锁

    public static void main(String[] args) {
        Runnable runnable = () -> {
            String name = Thread.currentThread().getName();
            lock.lock();
            try {
                System.out.println(name + " acquired the lock.");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(name + " released the lock.");
            }
        };

        // 创建多个线程并启动
        Thread thread1 = new Thread(runnable, "Thread 1");
        Thread thread2 = new Thread(runnable, "Thread 2");
        Thread thread3 = new Thread(runnable, "Thread 3");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

在上面的示例中,我们创建了3个线程并使用相同的Runnable实例来尝试获取锁。针对ReentrantLock的构造函数,我们传入了true来表示使用公平锁。

运行这段代码,你会发现输出的顺序是按照线程的启动顺序来竞争锁。这就是公平锁的作用,确保线程按照获取锁的顺序进行竞争,不会出现饥饿现象。

如果将ReentrantLock的构造函数中的参数改为false,则表示使用非公平锁。运行代码,你会发现输出的顺序是不确定的,即线程之间的竞争是随机的。这可能会导致某些线程一直无法获取到锁,从而出现饥饿现象。

在使用ReentrantLock类时,我们可以通过tryLock()方法来尝试获取锁。tryLock()方法会立即尝试获取锁,如果当前锁没有被其他线程占用,则获取成功并返回true;如果当前锁被其他线程占用,则获取失败并立即返回false,而不会阻塞当前线程。

另外,ReentrantLock还支持可中断的获取锁尝试。也就是说,当一个线程在尝试获取锁时,如果被其他线程中断了,它可以通过中断异常来终止获取锁的操作。

让我们通过代码来演示这两个功能:

import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            String name = Thread.currentThread().getName();

            // 尝试获取锁
            if (lock.tryLock()) {
                try {
                    System.out.println(name + " acquired the lock.");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                    System.out.println(name + " released the lock.");
                }
            } else {
                System.out.println(name + " failed to acquire the lock.");
            }
        }, "Thread 1");

        Thread thread2 = new Thread(() -> {
            String name = Thread.currentThread().getName();

            // 尝试获取锁,带有超时时间
            try {
                if (lock.tryLock(2000)) {
                    try {
                        System.out.println(name + " acquired the lock.");
                    } finally {
                        lock.unlock();
                        System.out.println(name + " released the lock.");
                    }
                } else {
                    System.out.println(name + " failed to acquire the lock within timeout.");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "Thread 2");

        thread1.start();
        thread2.start();
        
        // 中断Thread 2
        thread2.interrupt();
    }
}

在上面的示例中,我们创建了两个线程并尝试获取ReentrantLock的锁。在第一个线程中,我们使用tryLock()方法来尝试获取锁,如果获取成功则等待3秒后释放锁,否则输出获取失败的信息。在第二个线程中,我们使用带有超时时间的tryLock()方法来尝试获取锁,如果在指定的时间内获取成功则输出获取成功的信息,否则输出获取失败超时的信息。

运行这段代码,你会发现第一个线程成功获取到了锁并等待3秒后释放,而第二个线程在2秒的超时时间内没有获取到锁,输出了获取失败超时的信息。另外,我们通过中断Thread 2来演示可中断的获取锁尝试,你会发现Thread 2在被中断后立即输出了获取失败的信息。

总结来说,ReentrantLock类提供了tryLock()方法来尝试获取锁,如果获取失败则立即返回false。同时,它还支持可中断的获取锁尝试,当一个线程在尝试获取锁时被中断时,它可以通过中断异常来终止获取锁的操作。

3.读写锁(ReadWriteLock):

读写锁(ReadWriteLock)是一种特殊类型的锁,它允许多个线程同时读取共享资源,但在写线程访问时,其他线程无法读取或写入该共享资源。这种锁的设计可以提高读取操作的并发性能,因为多个线程可以同时读取数据而不会发生冲突。

Java中提供了ReadWriteLock接口和ReentrantReadWriteLock实现类来支持读写锁的使用。ReentrantReadWriteLock实现了ReadWriteLock接口,并提供了良好的性能和可扩展性。

下面是一个使用读写锁的简单示例:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {
    private static ReadWriteLock lock = new ReentrantReadWriteLock();
    private static int value = 0;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            String name = Thread.currentThread().getName();
            lock.readLock().lock(); // 获取读锁
            try {
                System.out.println(name + " read value: " + value);
            } finally {
                lock.readLock().unlock(); // 释放读锁
            }
        }, "Reader Thread");

        Thread thread2 = new Thread(() -> {
            String name = Thread.currentThread().getName();
            lock.writeLock().lock(); // 获取写锁
            try {
                value = 42;
                System.out.println(name + " write value: " + value);
            } finally {
                lock.writeLock().unlock(); // 释放写锁
            }
        }, "Writer Thread");

        thread1.start();
        thread2.start();
    }
}

在上面的示例中,我们创建了一个ReadWriteLock实例,然后创建了一个读取线程和一个写入线程。读取线程通过调用readLock()方法获取读锁,并在读取完共享资源后释放读锁。写入线程通过调用writeLock()方法获取写锁,并在写入完共享资源后释放写锁。

运行上述代码,你会看到读取线程获取到读锁后可以同时读取共享资源。而在写入线程获取写锁期间,读取线程无法访问共享资源。

总结来说,读写锁允许多个线程同时读取共享资源,但在写入线程访问时,其他线程无法读取或写入该共享资源。使用Java的ReadWriteLock接口和ReentrantReadWriteLock实现类,我们可以轻松管理读写锁的获取和释放,从而提高并发性能。

4.StampedLock:

StampedLock是Java 8引入的一种新型锁,它提供了更细粒度的读写控制。与传统的读写锁不同,StampedLock允许多个读线程、一个写线程或乐观读线程同时访问共享资源。

StampedLock的主要特点包括:

  1. 乐观读锁(Optimistic Read Lock):与传统的读锁不同,乐观读锁不会阻塞其他线程的写操作。乐观读锁获取的是一个标记(stamp),并不会阻塞其他线程的读或写操作。在使用乐观读锁之后,需要校验是否有写操作发生。

  2. 悲观读锁(Read Lock):与传统的读锁类似,悲观读锁会阻塞其他线程的写操作。悲观读锁的特点是可以升级为写锁。

  3. 写锁(Write Lock):与传统的写锁类似,写锁会独占资源,并阻塞其他线程的读或写操作。

下面是一个使用StampedLock的乐观读锁的示例:

import java.util.concurrent.locks.StampedLock;

public class StampedLockDemo {
    private static StampedLock lock = new StampedLock();
    private static int value = 0;

    public static void main(String[] args) {
        Thread readerThread = new Thread(() -> {
            long stamp = lock.tryOptimisticRead(); // 尝试获取乐观读锁
            int currentValue = value;

            if (!lock.validate(stamp)) { // 检查是否有写操作发生,如果有,需要获取悲观读锁
                stamp = lock.readLock();
                try {
                    currentValue = value;
                } finally {
                    lock.unlockRead(stamp);
                }
            }

            System.out.println("Reader: value = " + currentValue);
        });

        Thread writerThread = new Thread(() -> {
            long stamp = lock.writeLock(); // 获取写锁
            try {
                value = 42;
                System.out.println("Writer: value = " + value);
            } finally {
                lock.unlockWrite(stamp); // 释放写锁
            }
        });

        readerThread.start();
        writerThread.start();
    }
}

在这个示例中,读取线程首先尝试使用tryOptimisticRead()方法获取乐观读锁,并记录当前的value值。然后,通过调用validate()方法来检查在乐观读锁获取后是否有写操作发生。如果有写操作发生,读取线程需要重新获取悲观读锁,并读取最新的value值。如果没有写操作发生,读取线程可以直接使用之前记录的value值。

乐观读锁的特点是不会阻塞其他线程的读或写操作,但它的缺点是无法保证数据的一致性。因此,当使用乐观读锁时,需要通过validate()方法来检查数据是否仍然有效,如果无效则需要获取悲观读锁来确保数据的一致性。

StampedLock提供了更细粒度的读写控制,由于它的乐观读锁不会阻塞其他线程的读或写操作,因此在某些情况下可以提供更好的性能。然而,使用StampedLock也需要注意避免加锁的顺序引发死锁的问题。

线程池机制

Java线程池机制通过重用已创建的线程来减少线程创建和销毁的开销,提高系统性能。

1.ThreadPoolExecutor:

ThreadPoolExecutor是Java线程池框架的核心类。它实现了ExecutorService接口,为线程的执行提供了管理和控制的功能。

通过ThreadPoolExecutor,可以创建一个线程池,并设置不同的参数来控制线程池的行为。以下是常用的参数:

  • 核心线程数(corePoolSize):线程池中保留的核心线程数,即使线程处于空闲状态也不会被销毁。
  • 最大线程数(maximumPoolSize):线程池中允许创建的最大线程数,包括核心线程和非核心线程。
  • 线程空闲时间(keepAliveTime):非核心线程的空闲时间超过该值时,会被销毁。
  • 任务队列(workQueue):用于存放待执行的任务的队列,可以选择不同的实现类来满足不同的需求。
  • 拒绝策略(rejectedExecutionHandler):当任务无法被执行时,采取的处理策略,如丢弃任务、抛出异常、在调用者线程中直接执行等。

以下是一个使用ThreadPoolExecutor的示例:

import java.util.concurrent.*;

public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {
        // 创建一个线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                60, // 线程空闲时间
                TimeUnit.SECONDS, // 时间单位
                new ArrayBlockingQueue<>(10), // 任务队列
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        // 提交任务给线程池
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " executed by thread " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // 模拟任务执行时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个线程池,并设置核心线程数为2,最大线程数为4,线程空闲时间为60秒,任务队列为有界的ArrayBlockingQueue,并采用CallerRunsPolicy拒绝策略。然后,我们提交了10个任务给线程池进行执行。

线程池会根据核心线程数和任务数量来创建和销毁线程,同时根据线程空闲时间来控制线程的生命周期。如果任务数量超过了核心线程数且任务队列已满,线程池会根据拒绝策略来处理无法执行的任务。

ThreadPoolExecutor类提供了以下几种拒绝策略:

  1. AbortPolicy(默认策略):当任务无法被执行时,会抛出RejectedExecutionException异常。

  2. CallerRunsPolicy:当任务无法被执行时,会在调用者线程中直接执行该任务。这个策略可以用于降低对系统的影响,但可能会导致任务执行速度变慢。

  3. DiscardPolicy:当任务无法被执行时,会丢弃该任务,不做任何处理。

  4. DiscardOldestPolicy:当任务无法被执行时,会丢弃最早提交的任务,然后尝试再次提交当前任务。

除了这些默认的拒绝策略,你也可以实现RejectedExecutionHandler接口来自定义拒绝策略。自定义拒绝策略可以根据具体的业务需求来处理无法执行的任务。

以下是一个使用自定义拒绝策略的示例:

import java.util.concurrent.*;

public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义处理逻辑,例如将任务添加到其他队列中等待执行
        System.out.println("Task rejected: " + r.toString());
    }
}

public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {
        // 创建一个线程池,并设置自定义的拒绝策略
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, 4, 60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10),
                new CustomRejectedExecutionHandler()
        );

        // 提交任务给线程池
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " executed by thread " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个自定义的拒绝策略CustomRejectedExecutionHandler,并将其设置为线程池的拒绝策略。当任务无法被执行时,我们在自定义的拒绝策略中输出一条消息。

2.Executors

该工具类是Java提供的一个用于创建线程池的工具类,它提供了一些静态方法来方便创建不同类型的线程池。以下是一些常用的方法:

  1. newFixedThreadPool(int nThreads):创建一个固定大小的线程池,该线程池中的线程数量始终为指定大小。如果任务数量超过线程池大小,多余的任务会在队列中等待。

  2. newCachedThreadPool():创建一个可以根据需要自动扩展的线程池。线程池中的线程数量会根据任务的数量自动增减。如果线程池中没有可用线程,将创建新的线程来执行任务。

  3. newSingleThreadExecutor():创建一个只有一个线程的线程池。所有任务都会按照顺序在同一个线程中执行,保证任务的顺序性。

  4. newScheduledThreadPool(int corePoolSize):创建一个支持定时和周期性任务执行的线程池。可以用于定时任务的执行,例如在固定的时间间隔内执行某个任务。

这些方法返回的都是ThreadPoolExecutor线程池的实例,你可以根据具体的需求选择适合的线程池类型。当你需要创建线程池时,使用Executors工具类可以简化代码的编写,避免手动配置ThreadPoolExecutor的繁琐过程。

以下是一个使用Executors工具类创建固定大小线程池的示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建固定大小的线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 提交任务给线程池
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " executed by thread " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们使用Executors工具类创建了一个固定大小为5的线程池,然后提交了一些任务给线程池执行。每个任务执行时会输出执行的线程名称。最后,我们调用线程池的shutdown()方法来关闭线程池。

AQS(AbstractQueuedSynchronizer)

AQS是Java并发包java.util.concurrent.locks下的一个框架,用于构建依赖先进先出(FIFO)等待队列的阻塞锁和相关的同步器。

AQS维护了一个FIFO队列,用于管理等待获取资源的线程。每个线程都可以通过AQS的方法来尝试获取资源,如果资源已被占用,则线程会被加入到等待队列中。

AQS还提供了一个int类型的状态变量state,用于表示同步状态。根据具体的使用场景,可以自定义state的含义。例如,对于互斥锁来说,state=0表示锁可用,state=1表示锁被占用;对于信号量来说,state表示可用的许可数量。

AQS提供了一些用于操作state的方法,包括获取当前状态getState()、原子地设置状态setState(int newState)以及比较并交换状态compareAndSetState(int expect, int update)等。

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class MutexLock {

    private static class Sync extends AbstractQueuedSynchronizer {

        private static final long serialVersionUID = 1L;

        // 获取锁
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                // 设置当前线程为独占线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 释放锁
        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException("Lock is not held by current thread");
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // 是否是独占状态
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }

    private final Sync sync = new Sync();

    // 获取锁
    public void lock() {
        sync.acquire(1);
    }

    // 释放锁
    public void unlock() {
        sync.release(1);
    }

    // 判断锁是否被占用
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}
 

通过对state的操作,AQS提供了一种灵活且可扩展的机制来实现各种同步器,如ReentrantLock、CountDownLatch等。

下面我将分别举例说明ReentrantLock、CountDownLatch和Semaphore的实现。

1. ReentrantLock的实现:

ReentrantLock是可重入锁,同一个线程在获取到锁之后,可以多次重复获取锁,而不会发生死锁。这种机制可以避免同一个线程在执行递归或嵌套调用时出现死锁情况。

public class ReentrantLock {
    private Sync sync = new Sync();

    public void lock() {
        sync.lock();
    }

    public void unlock() {
        sync.unlock();
    }

    private static class Sync extends AbstractQueuedSynchronizer {
        protected boolean tryAcquire(int arg) {
            // 尝试获取锁
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        protected boolean tryRelease(int arg) {
            // 释放锁
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        protected boolean isHeldExclusively() {
            // 判断锁是否被当前线程持有
            return getState() == 1 && getExclusiveOwnerThread() == Thread.currentThread();
        }
    }
}

ReentrantLock通过继承AQS并重写tryAcquire、tryRelease等方法来实现自定义的同步器,即Sync类。tryAcquire方法尝试获取锁,如果state为0,则将state设置为1,并将当前线程设置为独占线程;tryRelease方法释放锁,将state设置为0,并将独占线程设置为null。isHeldExclusively方法判断锁是否被当前线程持有。

2. CountDownLatch的实现:

CountDownLatch主要用于解决一个或多个线程等待其他线程完成操作的场景,常见的应用场景包括主线程等待多个工作线程完成任务、多个工作线程等待某个共享资源初始化完毕等。通过CountDownLatch,可以实现线程之间的协调和同步。

public class CountDownLatch {
    private final Sync sync;

    public CountDownLatch(int count) {
        if (count < 0) {
            throw new IllegalArgumentException("count < 0");
        }
        this.sync = new Sync(count);
    }

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    public void countDown() {
        sync.releaseShared(1);
    }

    private static class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            // 如果计数器的值为0,则允许等待的线程继续执行
            return (getState() == 0) ? 1 : -1;
        }

        protected boolean tryReleaseShared(int releases) {
            // 递减计数器的值
            for (;;) {
                int count = getState();
                if (count == 0) {
                    return false;
                }
                int nextCount = count - 1;
                if (compareAndSetState(count, nextCount)) {
                    return nextCount == 0;
                }
            }
        }
    }
}

CountDownLatch通过继承AQS并重写tryAcquireShared、tryReleaseShared等方法来实现自定义的同步器,即Sync类。tryAcquireShared方法判断计数器的值是否为0,如果为0,则允许等待的线程继续执行;tryReleaseShared方法递减计数器的值,如果递减后的值为0,则返回true,表示计数器已归零。

3.Semaphore实现并发控制:

Semaphore是Java中的一个同步工具类,它可以控制同时访问某个资源的线程数量。Semaphore的作用类似于一个计数器,它维护一个许可数量,线程可以通过获取许可来访问资源,当许可数量为0时,线程必须等待其他线程释放许可后才能继续访问。
以下是一个简单的Java代码示例,演示了如何通过继承AQS(AbstractQueuedSynchronizer)并实现自定义的Semaphore:

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class MySemaphore {
    private final Sync sync;

    public MySemaphore(int permits) {
        sync = new Sync(permits);
    }

    public void acquire() throws InterruptedException {
        sync.acquireShared(1);
    }

    public void release() {
        sync.releaseShared(1);
    }

    private static class Sync extends AbstractQueuedSynchronizer {
        Sync(int permits) {
            setState(permits); // 设置初始许可数量
        }

        @Override
        protected int tryAcquireShared(int arg) {
            while (true) {
                int current = getState(); // 获取当前许可数量
                int newCount = current - arg; // 计算新的许可数量
                if (newCount < 0 || compareAndSetState(current, newCount)) {
                    /*尝试获取许可的线程在更新许可数量时,如果发现新的许可数量小于0,
                    那么表示当前许可数量已经不足,无法获取更多许可,此时返回的值为负数表示获取失败。
                    另外,如果CAS操作成功,表示当前线程成功地将许可数量从当前值更新为新的值,即成功获取到了许可。
                    CAS操作是一种原子操作,它可以确保在多线程环境下,只有一个线程能够成功修改许可数量。
                    如果CAS操作失败,表示有其他线程先于当前线程获取到了许可,当前线程需要继续尝试获取许可或者等待。*/
                    return newCount; // 返回剩余许可数量
                }
            }
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            while (true) {
                int current = getState(); // 获取当前许可数量
                int newCount = current + arg; // 计算新的许可数量
                if (compareAndSetState(current, newCount)) {
                    // CAS操作成功,表示成功释放了许可
                    return true;
                }
            }
        }
    }
}
 

以上示例说明了通过继承AQS并重写其提供的方法来实现自定义同步器的过程。具体的实现方式可以根据具体的需求和场景进行调整和扩展。

总结

Java并发编程提供了丰富的工具和机制来支持多线程编程。锁机制用于确保线程之间的同步和互斥,线程池机制用于优化系统性能,而AQS则是构建高性能同步器的基础框架。当选择并发编程的工具和机制时,可以根据以下几个方面来考虑:

1.需要实现的功能:不同的工具和机制适用于不同的功能需求。例如,如果需要实现互斥访问共享资源,可以选择使用synchronized关键字或ReentrantLock;如果需要等待多个线程完成后再继续执行,可以选择使用CountDownLatch;如果需要限制同时访问某个资源的线程数量,可以选择使用Semaphore。

2.性能需求:有些工具或机制可能会对性能产生较大影响,因此需要根据实际场景来评估性能需求。例如,synchronized关键字是Java内置的锁机制,使用简单但可能存在性能损失,而ReentrantLock相对更灵活但性能稍佳。

3.安全性要求:一些工具或机制在处理并发问题时可能会引发死锁、竞态条件等安全性问题。因此,在选择工具和机制时,需要考虑其在安全性方面的表现。例如,ReentrantLock提供了可重入性和公平性的控制,能够更好地保证线程安全。

4.可扩展性和灵活性:一些工具和机制提供了更灵活的扩展性,能够满足更复杂的并发编程需求。例如,AQS提供了一种灵活的机制来实现各种同步器,如ReentrantLock、CountDownLatch等,能够根据具体需求自定义状态和操作。

综上所述,选择合适的并发编程工具和机制需要考虑功能需求、性能要求、安全性要求以及扩展性和灵活性等方面。在实际应用中,可以根据具体情况进行评估和选择。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值