写点什么

Java 常见的锁及同步机制

作者:echoes
  • 2023-10-24
    海南
  • 本文字数:8153 字

    阅读完需:约 27 分钟

Java常见的锁及同步机制

Java 常见的锁

一、synchronized 关键字

它是 Java 的内置锁机制,可以用于同步代码块或方法。通过 synchronized,你可以锁定对象或类。

synchronized (object) {    // 同步的代码块}
复制代码

1.1 使用规则:

synchronized 方法:你可以在方法前面加上,synchronized 关键字来锁定整个方法,确保同一时刻只有一个线程可以执行这个方法。这通常用于保护对象的状态。例如:

public synchronized void mySynchronizedMethod() {    // 同步的方法体}
复制代码

synchronized 代码块:你也可以使用 synchronized 代码块,锁定特定的对象,而不是整个方法。这样可以更精确地控制同步。例如:

public void myMethod() {    synchronized (myObject) {        // 同步的代码块    }}
复制代码

1.2 使用场景:

1、多线程协同工作synchronized 适用于需要多个线程协同工作,共享某个资源的情况。例如,在多线程环境下访问共享的数据结构时,你可以使用 synchronized 来确保数据的一致性。

多个线程在执行过程中相互配合以完成某项任务。这种协同工作通常涉及多个线程之间的数据共享和互斥访问,因此需要合适的同步机制来确保线程安全。

比如生产者-消费者模型:

这是一个经典的多线程协同工作场景。生产者线程负责生产数据,消费者线程负责消费数据。两者需要协同工作以确保生产和消费的顺序和数据完整性。你可以使用 synchronized 来实现这种模型。

public class SharedResource {    private int data;    private boolean isProduced;
    public synchronized void produce(int value) {        while (isProduced) {            try {                wait();            } catch (InterruptedException e) {                Thread.currentThread().interrupt();            }        }        data = value;        isProduced = true;        notify();    }
    public synchronized int consume() {        while (!isProduced) {            try {                wait();            } catch (InterruptedException e) {                Thread.currentThread().interrupt();            }        }        isProduced = false;        notify();        return data;    }}
复制代码

2、单例模式:在创建单例对象时,你可能需要确保只有一个对象被实例化。你可以使用 synchronized 来实现线程安全的单例模式。

public class Singleton {    private static Singleton instance;        private Singleton() {}        public static synchronized Singleton getInstance() {        if (instance == null) {            instance = new Singleton();        }        return instance;    }}
复制代码

3、线程安全的数据结构

Java 标准库提供了线程安全的数据结构,如 Vector,HashTable,Collections.synchronizedList 等,它们在内部使用 synchronized 来确保线程安全。

4、等待-通知机制:当一个线程等待另一个线程的通知时,你可以使用 synchronized 来创建一个监视器对象,以控制线程的等待和通知。

synchronized (monitor) {    // 等待    monitor.wait();    // 通知    monitor.notify();}
复制代码

synchronized (monitor) {     // 等待     monitor.wait();     // 通知     monitor.notify(); }

需要注意的是,虽然 synchronized 是最简单的同步机制,但它可能会导致性能问题,因为只有一个线程能够执行同步块。在高度并发的情况下,你可能需要考虑使用更高级的锁机制,如 ReentrantLock,以提高性能和灵活性。

二、ReentrantLock

java.util.concurrent.locks.ReentrantLock 是 Java 提供的显式锁。它提供了更多的灵活性,如可中断、超时等等。 与 synchronized 关键字不同,ReentrantLock 提供了更多的灵活性和控制,特别是在多线程环境下。

2.1 使用规则:

获取锁:使用 lock()方法来获取锁。如果锁已被其他线程获取,当前线程会阻塞直到锁可用

ReentrantLock lock = new ReentrantLock();lock.lock(); // 获得锁try {    // 执行同步的代码块} finally {    lock.unlock(); // 释放锁}
复制代码

释放锁:确保在合适的地方释放锁,通常在 finally 块中释放锁,以确保异常情况下锁一定会被释放。

2.2 使用场景:

  • 替代 synchronized:当需要更精确的控制线程的锁定行为时,ReentrantLock 可以替代 synchronized。

  • 条件等待:ReentrantLock 可以通过 Condition 对象来实现线程的等待和唤醒,以实现更复杂的同步机制,这通常在一些高级的多线程应用中非常有用。

以下是一个简单的示例,演示如何使用 ReentrantLock Condition 来控制两个线程的通信。

import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;
public class SharedData {    private int data;    private boolean isDataAvailable = false;
    private Lock lock = new ReentrantLock();    private Condition condition = lock.newCondition();
    public void produce(int value) {        lock.lock();        try {            // 如果数据可用,等待消费者消费            while (isDataAvailable) {                try {                    condition.await();                } catch (InterruptedException e) {                    Thread.currentThread().interrupt();                }            }            data = value;            isDataAvailable = true;            System.out.println("Produced: " + data);            condition.signal();        } finally {            lock.unlock();        }    }
    public int consume() {        lock.lock();        try {            // 如果数据不可用,等待生产者生产            while (!isDataAvailable) {                try {                    condition.await();                } catch (InterruptedException e) {                    Thread.currentThread().interrupt();                }            }            isDataAvailable = false;            System.out.println("Consumed: " + data);            condition.signal();            return data;        } finally {            lock.unlock();        }    }}
复制代码

在上述示例中,ReentrantLock 和 Condition 用于实现生产者和消费者之间的同步。produce 方法生产数据,如果数据已经可用,它会等待;consume 方法消费数据,如果数据不可用,它也会等待。这种方式确保了生产者和消费者之间的协同工作,避免了竞态条件。

  • 公平锁与非公平锁:ReentrantLock 允许选择是否使用公平锁,公平锁会按照线程请求锁的顺序获得锁,而非公平锁不做这样的保证,可能导致某些线程在其他线程之前获得锁。下面是一个简单的示例来说明这两者之间的差异。

import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;
public class FairnessExample {    public static void main(String[] args) {        Lock fairLock = new ReentrantLock(true); // 使用公平锁        Lock unfairLock = new ReentrantLock(false); // 使用非公平锁
        Runnable fairTask = createTask(fairLock, "Fair Lock");        Runnable unfairTask = createTask(unfairLock, "Unfair Lock");
        Thread fairThread1 = new Thread(fairTask);        Thread fairThread2 = new Thread(fairTask);        Thread unfairThread1 = new Thread(unfairTask);        Thread unfairThread2 = new Thread(unfairTask);
        fairThread1.start();        fairThread2.start();        unfairThread1.start();        unfairThread2.start();    }
    private static Runnable createTask(Lock lock, String name) {        return () -> {            for (int i = 0; i < 5; i++) {                lock.lock();                try {                   ystem.out.println(name + " acquired lock in thread: " + Thread.currentThread().getName()); Thread.sleep(100); // 模拟线程执行任务需要一些时间                } finally {                    lock.unlock();                }            }        };    }}
复制代码

在这个示例中,我们创建了一个使用公平锁和一个使用非公平锁的 ReentrantLock 。然后,我们创建了两个线程分别使用这两个锁执行任务。你会注意到,使用公平锁的情况下,线程按顺序获得锁,而使用非公平锁的情况下,线程之间的获锁顺序是不确定的。

2.3 优缺点:

优点:

  • 更灵活的线程控制:ReentrantLock 提供了一系列灵活的方法,如超时获取锁、尝试获取锁等。

  • 更细粒度的锁控制:可以精确控制锁的获取和释放。

  • 可中断:在等待锁的过程中,可以响应中断。

缺点:

  • 复杂性:相较于 synchronized,ReentrantLock 使用起来较为复杂。

  • 需要手动释放锁:使用 synchronized 时,锁的释放由 Java 虚拟机自动处理,而 ReentrantLock 需要手动释放,容易忘记。

二、ReadWriteLock

(读写锁)是 Java 并发编程中的一个重要概念,它是一个接口,通常由两个具体的类来实现:ReentrantReadWriteLock 和 StampedLock。ReadWriteLock 的主要特点是它分离了读锁和写锁,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。

  • 读锁:多个线程可以同时获取读锁,用于读取共享资源,读锁之间不互斥。

  • 写锁:只允许一个线程获取写锁,用于修改共享资源,写锁会阻塞其他读锁和写锁。

3.1 使用规则:

  • 在使用 ReadWriteLock 时,通常需要创建一个 ReadWriteLock 实例,然后根据需要获取读锁或写锁。

  • 读锁的获取使用 readLock()方法,写锁的获取使用 writeLock()方法。

  • 一般的使用规则是,读操作可以同时进行,而写操作会排他执行,即写锁会阻塞其他写锁和读锁。

3.2 使用场景:

  • 适用于读多写少的场景,当共享资源被频繁读取而很少被修改时,使用读写锁可以提高并发性能。

  • 适用于需要降低锁竞争的情况,因为读锁之间不互斥,可以提高并发性。

3.3 优缺点:

  • 写锁的等待可能导致读锁的饥饿:如果写锁一直有请求,读锁可能一直等待。

  • 不能替代所有锁:虽然读写锁可以提高并发性能,但并不是所有情况下都适用,某些场景下仍需要使用普通锁。

下面是一个使用 ReadWriteLock 的简单示例:

import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample { private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private int sharedResource = 0;
public int readSharedResource() { readWriteLock.readLock().lock(); try { return sharedResource; } finally { readWriteLock.readLock().unlock(); } }
public void writeSharedResource(int value) { readWriteLock.writeLock().lock(); try { sharedResource = value; } finally { readWriteLock.writeLock().unlock(); } }}
复制代码

这个示例中,readSharedResource 方法获取读锁来读取 sharedResource,writeSharedResource 方法获取写锁来修改 sharedResource。这样,多个线程可以同时读取,但只有一个线程可以修改。

四、Semaphore

java.util.concurrent.Semaphore 是一个计数信号量,它可以控制同时访问资源的线程数。

4.1 使用规则:

  • 使用 acquire()方法获取许可证。

  • 使用 release()方法释放许可证。

  • 可以通过构造函数指定 Semaphore 的许可证数量。

4.2 使用场景:

Semaphore 通常用于以下情况:

  • 控制资源访问:限制同时访问资源的线程数量,例如控制数据库连接池的访问。

  • 并发任务控制:限制同时执行的任务数量,以免系统过载。

  • 流量控制:控制访问服务器的并发连接数,以维护系统的稳定性。

  • 同步问题的解决:在某些同步问题中,Semaphore 可以用于实现更复杂的同步方案。

4.3 优缺点:

优点:

  • 可以有效控制资源的并发访问,避免资源竞争问题。

  • 提供了一种机制,可以限制同时执行的任务数量。

缺点:

  • 如果使用不当,可能会引入死锁问题。

  • 需要仔细考虑许可证的数量和资源的特性,否则可能导致系统性能问题。

示例代码:

下面是一个简单的 Semaphore 示例,演示了如何使用 Semaphore 来控制对共享资源的访问:

import java.util.concurrent.Semaphore;
public class SemaphoreExample { private Semaphore semaphore = new Semaphore(3); // 假设有3个许可证,表示同时最多允许3个线程访问资源
public void accessResource() { try { semaphore.acquire(); // 获取许可证 System.out.println("Resource accessed by " + Thread.currentThread().getName()); Thread.sleep(1000); // 模拟资源访问 } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); // 释放许可证 } }}
复制代码

五、CyclicBarrier

java.util.concurrent.CyclicBarrier

CyclicBarrier 是一个同步辅助类,它允许一组线程在达到一个共同的屏障点之前相互等待。一旦所有线程都达到这个屏障点,它们可以继续执行。CyclicBarrier 的一个重要特点是,它可以在多个线程间创建一个屏障,当所有线程都到达这个屏障后,它们可以同步执行下一阶段的任务。

5.1 使用规则:

  • 创建 CyclicBarrier 时,需要指定参与线程的数量和在屏障处执行的动作(可选)。

  • 调用 await()方法,当线程到达屏障点时,它会等待其他线程也到达,然后一起继续执行。

5.2 使用场景:

CyclicBarrier 通常用于以下情况:

  • 分布式系统中的任务协同:多个节点的任务必须协同工作,等待所有节点准备好后再一起执行下一步操作。

  • 数据分析:多个数据处理任务在某一阶段合并结果后,继续进行下一阶段的分析。

  • 多阶段计算:任务分成多个阶段,每个阶段的任务需要等待其他任务完成后才能继续。

5.3 优缺点:

优点:

  • 提供了一种同步机制,允许线程在某个屏障点同步等待。

  • 可以用于实现多阶段任务的同步。

缺点:

  • 一旦屏障打破,就无法重置。如果需要多次使用,必须重新创建新的 CyclicBarrier 实例。

  • 可能引入线程竞争和死锁问题,因此需要小心使用。

示例代码:

下面是一个简单的 CyclicBarrier 示例,演示了如何使用 CyclicBarrier 来控制一组线程在达到屏障点后同步执行:

import java.util.concurrent.BrokenBarrierException;import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample { private static final int NUM_THREADS = 3;
public static void main(String[] args) { CyclicBarrier barrier = new CyclicBarrier(NUM_THREADS, () -> { System.out.println("All threads have reached the barrier. Continue execution."); });
for (int i = 0; i < NUM_THREADS; i++) { Thread thread = new Thread(() -> { System.out.println("Thread started."); try { barrier.await(); // 线程到达屏障点 } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } System.out.println("Thread continued execution."); }); thread.start(); } }}
复制代码

在这个示例中,我们创建了一个 CyclicBarrier,它需要三个线程同时到达屏障点才会执行屏障后的动作。每个线程在到达屏障点后会等待其他线程,然后一起继续执行。CyclicBarrier 在多阶段任务中同步线程的执行非常有用。

特别备注:

对于 CyclicBarrier 来说,确实需要在构造时指定的参与线程数是设置的屏障点的倍数,这样可以确保所有线程都能继续执行。如果参与线程数不是屏障点的倍数,有可能导致某些线程无法继续执行,或者某些线程会永久地等待。

比如: 参与线程数为 10,CyclicBarrier 设置屏障点为 3;

第一组:1 等待,2 等待,3 到达屏障,1、2、3 继续执行。

第二组:4 等待,5 等待,6 到达屏障,4、5、6 继续执行。

第三组:7 等待,8 等待,9 到达屏障,7、8、9 继续执行。

第四组:10 等待

因为第四组只有 1 个,无法到达屏障点,只能一次等待

六、CountDownLatch

CountDownLatch 是 Java 中的一个同步工具类,它允许一个或多个线程等待其他线程完成操作。它的工作方式是,创建一个 CountDownLatch 实例时,通过指定一个计数器来初始化它。这个计数器代表需要等待的事件数量。每当一个事件完成,就会通过 countDown() 方法将计数器减一。当计数器的值达到零时,等待的线程可以继续执行。

6.1 使用规则:

  1. 创建 CountDownLatch 对象并初始化计数器,通常设置计数器的值为需要等待的线程数量。

  2. 在需要等待的线程中,在适当的位置(通常是事件完成的地方)调用 countDown() 方法,表示一个事件完成。

  3. 其他等待的线程(通常是主线程)可以调用 await() 方法来等待计数器的值达到零。await() 方法将会阻塞当前线程,直到计数器的值为零,然后继续执行。

6.2 使用场景:

CountDownLatch 通常用于以下场景:

  • 启动多个线程,等待所有线程都完成后再继续主线程的执行。

  • 实现一个并发控制,确保一组任务都完成后再执行下一组任务。

  • 等待多个网络请求完成后再进行某项操作。

6.3 优点和缺点:

优点:

  • 简单易用,易于理解。

  • 可以用于控制多个线程的同步。

  • 灵活性强,可以根据需要等待不同数量的事件。

缺点:

  • 一旦计数器达到零,就无法重置,需要创建新的 CountDownLatch 实例。

  • CountDownLatch 只能用于一次性的事件,不能用于周期性的事件等。

以下是一个简单的示例,演示了 CountDownLatch 的基本用法:

CountDownLatch latch = new CountDownLatch(3); // 等待 3 个线程完成 // 在各个线程中执行 countDown(),当 3 个线程都执行到此时,等待的线程可以继续执行。



import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3);
Runnable task = () -> { // 模拟任务的执行 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task completed."); latch.countDown(); // 事件完成,计数器减一 };
// 启动三个线程执行任务 new Thread(task).start(); new Thread(task).start(); new Thread(task).start();
// 等待三个任务完成 latch.await(); System.out.println("All tasks completed. Continue..."); }}

复制代码

在这个示例中,我们创建了一个 CountDownLatch,计数器的初始值为 3。然后,我们启动了三个线程执行任务,并在每个任务完成时调用 countDown() 方法。最后,主线程通过 await() 方法等待计数器的值为零,然后继续执行。

发布于: 刚刚阅读数: 3
用户头像

echoes

关注

探索未知,分享收获 2018-04-25 加入

同步公众号《码猿同学》

评论

发布
暂无评论
Java常见的锁及同步机制_echoes_InfoQ写作社区