头图

线程锁在分布式应用中是重中之重,当谈论线程锁时,通常指的是在多线程编程中使用的同步机制,它可以确保在同一时刻只有一个线程能够访问共享资源,从而避免竞争条件和数据不一致性问题。

在 Java 中,有多种方式可以实现线程锁,其中最常见的是使用 synchronized 关键字和 java.util.concurrent.locks 包中的锁类(如 ReentrantLock)。

使用 synchronized 关键字

synchronized 关键字可以用来保护代码块或方法,确保同一时刻只有一个线程可以进入同步区域。当一个线程进入同步区域时,其他线程会被阻塞,直到该线程执行完毕并释放锁。

public class WeigeSynchronizedExample {
    private int count = 0;

    // 同步方法
    public synchronized void increment() {
        count++;
    }

    // 同步代码块
    public void incrementWithSyncBlock() {
        synchronized (this) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

在上面的示例中,increment() 方法和 incrementWithSyncBlock() 方法都是同步的,它们确保了对 count 变量的操作是线程安全的。

使用 ReentrantLock

ReentrantLockjava.util.concurrent.locks 包中的一种锁实现,它提供了比 synchronized 更多的灵活性和功能,例如可中断锁、尝试获取锁和定时获取锁等。

我们来看一个示例:

import java.util.concurrent.locks.ReentrantLock;

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

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

    public int getCount() {
        return count;
    }
}

在上面的示例中,increment() 方法使用了 ReentrantLock 来保护对 count 变量的操作,确保了线程安全。

那是选择 synchronized 还是 ReentrantLock

V哥这么说吧:

  • synchronized 更简单易用,但灵活性较差。
  • ReentrantLock 提供了更多功能,但使用时需要显式地获取和释放锁,因此容易出错。
  • 一般来说,如果只需简单的同步控制,使用 synchronized 就可以了。但如果需要更复杂的同步需求,比如可中断锁、尝试获取锁或定时获取锁,就可以选择 ReentrantLock
  • 无论使用哪种方式,都要确保正确地处理锁的获取和释放,以避免死锁等问题。当使用线程锁时,我们需要考虑一些关键概念和最佳实践,以确保代码的正确性和性能。

下面V哥从以下不同的点来详细介绍一下线程锁,老铁们坐稳~要发车了。

1. 锁的粒度

锁的粒度指的是锁保护的范围。通常情况下,我们希望尽量缩小锁的范围,以减少线程之间的竞争,从而提高性能。例如,如果只有少量代码需要同步,那么可以将同步块或方法的范围限制在这些代码周围,而不是整个方法或对象。

下面我将提供一个简单的代码示例来解释锁的粒度问题。我们将使用 Java 的synchronized关键字来展示细粒度锁和粗粒度锁的应用。

细粒度锁示例

假设我们有一个简单的Bank类,其中每个账户(Account)都有自己的锁。

public class Account {  
    private double balance;  
    private final Object lock = new Object(); // 每个账户有一个独立的锁  
  
    public Account(double balance) {  
        this.balance = balance;  
    }  
  
    public void withdraw(double amount) {  
        synchronized (lock) { // 使用账户的锁  
            if (balance >= amount) {  
                balance -= amount;  
                System.out.println("Withdraw successful. Remaining balance: " + balance);  
            } else {  
                System.out.println("Insufficient funds");  
            }  
        }  
    }  
  
    public double getBalance() {  
        return balance;  
    }  
}  
  
public class Bank {  
    private Account[] accounts;  
  
    public Bank(int numAccounts, double initialBalance) {  
        accounts = new Account[numAccounts];  
        for (int i = 0; i < numAccounts; i++) {  
            accounts[i] = new Account(initialBalance);  
        }  
    }  
  
    public void withdrawFromAccount(int accountIndex, double amount) {  
        accounts[accountIndex].withdraw(amount); // 每个账户的withdraw方法使用其自己的锁  
    }  
}

在这个例子中,每个Account对象都有一个私有的lock对象,用于同步对balance的访问。多个线程可以同时对不同的账户进行取款操作,因为每个账户都有自己的锁。这就是细粒度锁的应用,它提高了并发性能。

粗粒度锁示例

如果我们使用粗粒度锁,整个Bank类可能只有一个锁来保护所有账户。

public class Bank {  
    private Account[] accounts;  
    private final Object lock = new Object(); // 银行有一个共享的锁  
  
    public Bank(int numAccounts, double initialBalance) {  
        accounts = new Account[numAccounts];  
        for (int i = 0; i < numAccounts; i++) {  
            accounts[i] = new Account(initialBalance);  
        }  
    }  
  
    public void withdrawFromAccount(int accountIndex, double amount) {  
        synchronized (lock) { // 使用银行的共享锁  
            Account account = accounts[accountIndex];  
            if (account.getBalance() >= amount) {  
                account.setBalance(account.getBalance() - amount);  
                System.out.println("Withdraw successful. Remaining balance: " + account.getBalance());  
            } else {  
                System.out.println("Insufficient funds");  
            }  
        }  
    }  
}  
  
// Account类不需要锁了,因为所有的同步都在Bank类中处理  
public class Account {  
    private double balance;  
  
    public Account(double balance) {  
        this.balance = balance;  
    }  
  
    public void setBalance(double balance) {  
        this.balance = balance;  
    }  
  
    public double getBalance() {  
        return balance;  
    }  
}

在这个粗粒度锁的示例中,整个Bank类只有一个lock对象,用于同步所有对账户的访问。这意味着,如果有一个线程正在对某个账户进行取款操作,其他线程将不能对任何账户进行取款操作,即使它们尝试访问的是不同的账户。这降低了并发性能,但简化了同步机制。

解释

  • 细粒度锁:提供了更高的并发性,因为不同的线程可以同时访问不同的资源(在这里是不同的账户)。但是,它增加了同步的复杂性,因为需要管理更多的锁,并且需要小心避免死锁等问题。
  • 粗粒度锁:简化了同步机制,因为只需要管理一个锁。但是,它降低了并发性能,因为对任何资源的访问都会阻塞对其他所有资源的访问。

在实际应用中,选择哪种锁的粒度取决于具体的应用场景和需求。在需要高并发性的情况下,通常会使用细粒度锁;而在对同步机制有简化需求或资源访问冲突不频繁的情况下,可能会选择粗粒度锁。

2. 避免死锁

死锁是多线程编程中常见的问题,它发生在两个或多个线程相互等待对方持有的资源而无法继续执行的情况下。为了避免死锁,我们需要确保线程获取锁的顺序是一致的,并且避免持有一个锁的同时去请求另一个锁。

示例:

public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {
            // do something
            synchronized (lock2) {
                // do something
            }
        }
    }

    public void method2() {
        synchronized (lock2) {
            // do something
            synchronized (lock1) {
                // do something
            }
        }
    }
}

死锁是并发编程中常见的问题,它指的是两个或更多线程在等待对方释放资源,从而导致它们都无法继续执行的情况。解决死锁问题通常涉及检测死锁的存在,并采取措施避免或解决它。

检测死锁:

检测死锁通常依赖于检测循环等待条件。一种常见的方法是使用资源分配图或银行家算法来检查是否有可能发生死锁。然而,在运行时检测死锁通常代价高昂,并且可能导致额外的性能开销。

避免死锁:

避免死锁通常涉及以下几种策略:

  • 避免嵌套锁:尽量减少在同一方法中获取多个锁的情况,因为这增加了死锁的风险。
  • 锁顺序:如果多个线程需要获取多个锁,确保它们总是以相同的顺序请求锁,这有助于防止循环等待条件。
  • 超时:为锁的获取设置超时,如果线程不能在规定时间内获得锁,则放弃并稍后重试。
  • 使用死锁检测工具:一些并发库和工具提供了死锁检测功能,可以在运行时监视和报告潜在的死锁情况。

3. 使用 tryLock() 避免死锁

ReentrantLocktryLock() 方法可以尝试获取锁,如果获取失败则立即返回,而不是一直等待。这个特性可以用来避免死锁,通过尝试获取锁并在失败后释放已经获取的锁,可以防止死锁的发生。来看一个示例:

import java.util.concurrent.locks.ReentrantLock;

public class TryLockExample {
    private final ReentrantLock lock1 = new ReentrantLock();
    private final ReentrantLock lock2 = new ReentrantLock();

    public void method1() {
        while (true) {
            if (lock1.tryLock()) {
                try {
                    if (lock2.tryLock()) {
                        try {
                            // do something
                            break;
                        } finally {
                            lock2.unlock();
                        }
                    }
                } finally {
                    lock1.unlock();
                }
            }
            // 锁获取失败,稍微等待再尝试
            Thread.sleep(100);
        }
    }

    // method2 类似
}

4. 使用 Lock 接口的 Condition 来实现线程间通信

Condition 接口提供了一种在锁保护下等待和唤醒线程的机制,可以用来实现复杂的线程间通信。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean flag = false;

    public void waitForFlag() throws InterruptedException {
        lock.lock();
        try {
            while (!flag) {
                condition.await(); // 等待 flag 变为 true
            }
            // 执行需要在 flag 变为 true 后才能进行的操作
        } finally {
            lock.unlock();
        }
    }

    public void setFlag() {
        lock.lock();
        try {
            flag = true;
            condition.signalAll(); // 通知等待线程 flag 变为 true
        } finally {
            lock.unlock();
        }
    }
}

这些是关于线程锁的一些基本概念、最佳实践和常见问题的解决方法。在实际开发中,正确地使用线程锁可以确保多线程程序的正确性和性能。

5. 使用 ReentrantLock 的 lockInterruptibly() 实现可中断的锁获取

ReentrantLocklockInterruptibly() 方法可以用来获取锁,但是它允许线程在等待锁的过程中响应中断,如果其他线程中断了当前线程,那么当前线程会抛出 InterruptedException 异常,从而提供了可中断的锁获取操作。

import java.util.concurrent.locks.ReentrantLock;

public class InterruptibleLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void performTask() throws InterruptedException {
        try {
            lock.lockInterruptibly(); // 可中断地获取锁
            // 执行需要同步的任务
        } finally {
            lock.unlock();
        }
    }
}

6. 使用 ReentrantLock 的 tryLock(long timeout, TimeUnit unit) 实现定时锁获取

ReentrantLocktryLock(long timeout, TimeUnit unit) 方法可以尝试在指定的时间内获取锁,如果在指定时间内未能获取到锁,则返回 false,从而避免了无限期地等待锁的情况,来看一个示例:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TimedLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void performTaskWithTimeout() throws InterruptedException {
        if (lock.tryLock(10, TimeUnit.SECONDS)) {
            try {
                // 执行需要同步的任务
            } finally {
                lock.unlock();
            }
        } else {
            // 在指定时间内未能获取到锁
            // 执行相应的处理逻辑
        }
    }
}

7. 使用 ReentrantLock 的条件变量来管理线程等待和唤醒

ReentrantLock 的条件变量(Condition)提供了更灵活的线程等待和唤醒机制,它可以替代传统的 Object.wait()Object.notify() 方法,同时与 ReentrantLock 结合使用,提供了更强大的线程通信机制。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionVariableExample {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean flag = false;

    public void waitForFlagChange() throws InterruptedException {
        lock.lock();
        try {
            while (!flag) {
                condition.await(); // 等待条件变量满足
            }
            // 执行需要在条件满足后才能进行的操作
        } finally {
            lock.unlock();
        }
    }

    public void setFlagAndSignal() {
        lock.lock();
        try {
            flag = true;
            condition.signalAll(); // 通知等待线程条件已经满足
        } finally {
            lock.unlock();
        }
    }
}

8. 避免使用 synchronized(this),而是使用私有锁对象

在使用 synchronized 关键字时,最好避免使用 synchronized(this),而是使用私有的锁对象来确保锁的粒度和安全性。因为使用 synchronized(this) 可能会导致不必要的竞争和性能问题。

public class SynchronizedExample {
    private final Object lock = new Object();

    public void synchronizedMethod() {
        synchronized (lock) {
            // 同步代码块
        }
    }
}

这些是关于线程锁的一些进阶技巧和最佳实践,它们可以帮助你更好地使用线程锁来确保多线程程序的正确性、性能和可维护性。

9. 可重入性

在 Java 中,synchronized 关键字和 ReentrantLock 类都支持可重入性,也就是说,线程可以重复地获取同一把锁,而不会导致死锁或其他问题。可重入性使得线程可以在持有锁的情况下调用同步方法或进入同步代码块,从而简化了编程模型。换句话说,如果一个线程已经持有了某个锁,那么在没有释放该锁之前,它可以多次获取相同的锁而不会被阻塞。这种机制使得编程更加方便,允许线程在执行一段同步代码时,调用另一个需要获取同一把锁的同步方法。

下面是一个简单的代码示例,演示了锁的可重入性:

import java.util.concurrent.locks.ReentrantLock;

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

    public static void main(String[] args) {
        new Thread(ReentrantLockExample::outerMethod).start();
    }

    public static void outerMethod() {
        lock.lock();
        try {
            System.out.println("Outer method is executing.");
            innerMethod(); // 调用内部方法
        } finally {
            lock.unlock();
        }
    }

    public static void innerMethod() {
        lock.lock(); // 内部方法再次获取同一把锁
        try {
            System.out.println("Inner method is executing.");
        } finally {
            lock.unlock();
        }
    }
}

在这个示例中,outerMethod() 方法首先获取了锁,然后调用了 innerMethod() 方法。在 innerMethod() 方法内部,它又尝试获取相同的锁。由于锁是可重入的,因此这种嵌套调用并不会导致死锁,内部方法能够正常执行。

小结:

  • 锁的可重入性允许同一个线程在持有锁的情况下再次获取同一个锁。
  • 这种机制使得线程可以在执行同步代码的过程中调用其他同步方法,而不用担心死锁。
  • 可重入性是锁的一个重要特性,简化了编程复杂性,提高了程序的可靠性和可维护性。

10. 公平性

ReentrantLock 类提供了公平锁和非公平锁两种模式。在公平模式下,线程获取锁的顺序与它们等待锁的顺序一致;而在非公平模式下,线程可以在锁可用时立即获取锁,不考虑其他线程的等待顺序。如果一个锁是公平的,那么所有等待锁的线程都将按照它们发出请求的顺序获取锁,而不会有线程被饿死(永远无法获取到锁)的情况发生。

下面是一个简单的代码示例,演示了使用公平锁的情况:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class FairnessExample {
    private static final Lock fairLock = new ReentrantLock(true); // 使用公平锁

    public static void main(String[] args) {
        Runnable runnable = () -> {
            try {
                fairLock.lock();
                System.out.println(Thread.currentThread().getName() + " has acquired the lock.");
            } finally {
                fairLock.unlock();
            }
        };

        // 创建多个线程并启动
        for (int i = 0; i < 5; i++) {
            new Thread(runnable).start();
        }
    }
}

在这个示例中,我们使用 ReentrantLock 创建了一个公平锁(通过构造函数的参数指定为 true)。然后创建了多个线程,它们竞争获取这个公平锁。由于锁是公平的,因此线程获取锁的顺序将按照它们发出请求的顺序进行。

小结:

  • 公平性是指锁的获取顺序按照线程发出请求的顺序来进行,即先到先得。
  • 公平锁能够避免线程被饿死,确保所有等待锁的线程都能按照公平的原则获取锁资源。
  • 使用公平锁可能会降低程序的性能,因为需要维护一个线程队列来记录等待锁的顺序。
  • 在一些情况下,公平性可能并不重要,可以使用非公平锁来提高程序的性能。

11. 使用并发集合类

Java 提供了许多并发集合类,并发集合类是 Java 并发包提供的线程安全的集合类,用于在多线程环境下操作共享数据。这些集合类提供了对于常见数据结构如列表、映射和队列等的线程安全实现,避免了在多线程环境下可能出现的竞态条件和数据不一致性问题。(如 ConcurrentHashMapCopyOnWriteArrayList 等)有了这些类,可以方便咱们简化多线程的编程。

以下是一个简单的示例,演示了如何使用 ConcurrentHashMap 进行并发安全的键值对操作:

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentCollectionExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();

        // 多个线程同时向 ConcurrentHashMap 中添加元素
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                concurrentMap.put("Key" + i, i);
            }
        };

        // 创建多个线程并启动
        Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(task);
            threads[i].start();
        }

        // 等待所有线程执行完成
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 输出 ConcurrentHashMap 中的元素数量
        System.out.println("Size of ConcurrentHashMap: " + concurrentMap.size());
    }
}

在这个示例中,我们创建了一个 ConcurrentHashMap 实例 concurrentMap,然后创建了多个线程,并使用这些线程并发地向 concurrentMap 中添加键值对。由于 ConcurrentHashMap 是线程安全的,因此可以安全地在多个线程之间共享并操作这个集合,而不需要额外的同步手段。

小结:

  • 并发集合类提供了线程安全的数据结构,可以在多线程环境下安全地操作共享数据。
  • 使用并发集合类可以避免手动实现同步机制,减少了编程的复杂性,并提高了代码的可读性和可维护性。
  • Java 并发包提供了丰富的并发集合类,如 ConcurrentHashMapConcurrentLinkedQueueCopyOnWriteArrayList 等,可以根据具体的需求选择合适的集合类来使用。

12. 线程间通信

线程锁通常用于协调多个线程之间的操作,例如,一个线程可能需要等待另一个线程完成某个任务才能继续执行。在这种情况下,可以使用等待/通知机制或者使用 java.util.concurrent 包中提供的高级工具来实现线程间通信,来看一个示例:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class MessagePassingExample {
    private static final BlockingQueue<String> queue = new ArrayBlockingQueue<>(10); // 消息队列

    public static void main(String[] args) {
        Thread producer = new Thread(() -> {
            try {
                System.out.println("Producer thread starts producing...");
                // 模拟生产过程
                // 将消息放入队列中
                queue.put("Message");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread consumer = new Thread(() -> {
            try {
                // 从队列中获取消息
                String message = queue.take();
                System.out.println("Consumer thread starts consuming: " + message);
                // 开始消费消息
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        producer.start();
        consumer.start();
    }
}

在这个示例中,生产者线程通过 put() 方法将消息放入阻塞队列中,而消费者线程通过 take() 方法从队列中获取消息进行消费。

小结

  • 线程间通信是多线程编程中非常重要的一个概念,它通过共享内存或者消息传递等方式实现线程之间的数据交换和同步执行。
  • 共享内存方式适用于需要频繁读写共享数据的场景,而消息传递方式适用于数据交换不频繁但需要解耦的场景。
  • 在使用共享内存方式时,需要注意共享数据的同步和线程安全问题;而在使用消息传递方式时,需要注意消息的发送和接收的顺序以及消息队列的容量限制等问题。

13. 使用 volatile 关键字

volatile 关键字可以确保多个线程之间对变量的修改可见性,即当一个线程修改了 volatile 变量的值,其他线程能够立即看到最新的值。volatile 变量通常用于状态标志位和双重检查锁定等场景。

public class VolatileExample {
    private volatile boolean flag = false;

    public void toggleFlag() {
        flag = !flag; // 修改 volatile 变量的值
    }

    public boolean getFlag() {
        return flag; // 读取 volatile 变量的值
    }
}

14. 使用原子类

Java 提供了一系列原子类(如 AtomicIntegerAtomicBoolean 等),它们提供了一种线程安全的操作数的方式,无需使用显式的锁机制。原子类的操作是不可分割的,从而避免了竞态条件。

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // 原子性地增加计数器的值
    }

    public int getCount() {
        return count.get(); // 获取计数器的值
    }
}

15. 使用并行流和并行编程

Java 8 引入了并行流和并行编程的支持,可以轻松地将某些操作并行化,从而充分利用多核处理器的性能。但在使用并行编程时需要注意线程安全性,避免数据竞争和数据不一致性。

import java.util.List;
import java.util.stream.Collectors;

public class ParallelStreamExample {
    public List<Integer> doubleList(List<Integer> list) {
        return list.parallelStream()
                   .map(i -> i * 2) // 使用并行流并行地对列表中的元素进行处理
                   .collect(Collectors.toList());
    }
}

通过理解和应用这些额外的概念和技术,你可以更好地设计和编写高效、安全的多线程 Java 应用程序。同时,还可以充分利用现代计算机的硬件资源,提高程序的性能和吞吐量。

16. 使用读写锁(ReentrantReadWriteLock)

读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这样可以提高读取操作的并发性能,适用于读操作远远多于写操作的场景。

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    private int data;

    public int readData() {
        readLock.lock();
        try {
            return data;
        } finally {
            readLock.unlock();
        }
    }

    public void writeData(int newData) {
        writeLock.lock();
        try {
            data = newData;
        } finally {
            writeLock.unlock();
        }
    }
}

解释:

  • ReentrantReadWriteLock分为读锁和写锁。多个线程可以同时持有读锁,但只有一个线程可以持有写锁。
  • 当一个线程持有写锁时,其他线程无法获取读锁或写锁。
  • 当一个线程持有读锁时,其他线程可以获取读锁,但不能获取写锁。

小结:

  • synchronized是Java内置的锁机制,简单易用,但功能有限。ReentrantLock提供了更灵活的锁控制,但需要手动管理锁的获取和释放。
  • ReentrantReadWriteLock适用于多读少写的场景,提高了并发性能。
  • 在选择锁机制时,应根据具体的应用场景和需求来决定。在大多数情况下,synchronized已经足够满足需求,但在需要更精细的锁控制时,可以考虑使用ReentrantLock或ReentrantReadWriteLock。

17. 使用 ThreadLocal

ThreadLocal 是一种线程封闭技术,它提供了线程本地变量,使得每个线程都拥有一份独立的变量副本。这样可以避免线程安全问题,并提高程序的并发性能。

public class ThreadLocalExample {
    private static final ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);

    public static void increment() {
        int value = threadLocalValue.get();
        threadLocalValue.set(value + 1);
    }

    public static int getValue() {
        return threadLocalValue.get();
    }
}

18. 使用并发工具类

除了锁之外,Java还提供了其他并发工具,如Semaphore(信号量)、CountDownLatch(计数器)、CyclicBarrier(循环屏障)等,这些工具可以帮助实现更复杂的并发控制逻辑。

示例代码:
使用Condition实现等待/通知机制。

import java.util.concurrent.locks.Condition;  
import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;  
  
public class BoundedBuffer<T> {  
    private final Lock lock = new ReentrantLock();  
    private final Condition notFull = lock.newCondition();  
    private final Condition notEmpty = lock.newCondition();  
    private final T[] items;  
    private int putPtr, takePtr, count;  
  
    @SuppressWarnings("unchecked")  
    public BoundedBuffer(int capacity) {  
        items = (T[]) new Object[capacity];  
    }  
  
    public void put(T 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 T take() throws InterruptedException {  
        lock.lock();  
        try {  
            while (count == 0) {  
                notEmpty.await(); // 等待数据可用  
            }  
            T x = items[takePtr];  
            if (++takePtr == items.length) takePtr = 0;  
            --count;  
            notFull.signal(); // 通知等待放数据的线程  
            return x;  
        } finally {  
            lock.unlock();  
        }  
    }  
}

这个示例展示了如何使用ReentrantLockCondition来实现一个有界缓冲区(Bounded Buffer),它允许生产者线程向其中放入数据,消费者线程从中取出数据。通过使用Condition,生产者线程在缓冲区满时会等待,消费者线程在缓冲区空时会等待,从而实现了线程间的同步和协调。

19. 读写锁(ReadWriteLock)

读写锁允许多个线程同时读取共享资源,但在写线程访问时,所有读线程和其他写线程均被阻塞。它提供了更高的并发性能,因为读操作通常不会修改数据,因此可以安全地并发执行。

import java.util.concurrent.locks.ReadWriteLock;  
import java.util.concurrent.locks.ReentrantReadWriteLock;  
import java.util.HashMap;  
import java.util.Map;  
  
public class SharedMap<K, V> {  
    private final Map<K, V> map = new HashMap<>();  
    private final ReadWriteLock lock = new ReentrantReadWriteLock();  
  
    public V get(K key) {  
        lock.readLock().lock(); // 获取读锁  
        try {  
            return map.get(key);  
        } finally {  
            lock.readLock().unlock(); // 释放读锁  
        }  
    }  
  
    public void put(K key, V value) {  
        lock.writeLock().lock(); // 获取写锁  
        try {  
            map.put(key, value);  
        } finally {  
            lock.writeLock().unlock(); // 释放写锁  
        }  
    }  
}

在上面的示例中,SharedMap 类使用ReentrantReadWriteLock来实现读写锁。多个线程可以同时调用get方法读取数据,但在调用put方法写入数据时,其他所有读写操作都会被阻塞,直到写操作完成。

20. 锁分离(Lock Striping)

锁分离是一种技术,它将一个大锁分解为多个小锁,每个小锁控制数据的一个子集。这样可以提高并发性能,因为不同的线程可以并发地访问不同的数据子集。

示例:

假设我们有一个大型数组,我们想要对其进行并发访问。我们可以使用锁分离来为每个数组段分配一个单独的锁。

import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;  
  
public class StripedArray {  
    private final Lock[] locks;  
    private final int[] array;  
    private final int stripeSize;  
  
    public StripedArray(int size, int stripeSize) {  
        this.stripeSize = stripeSize;  
        this.locks = new ReentrantLock[size / stripeSize + 1];  
        for (int i = 0; i < locks.length; i++) {  
            locks[i] = new ReentrantLock();  
        }  
        this.array = new int[size];  
    }  
  
    public void set(int index, int value) {  
        int lockIndex = index / stripeSize;  
        locks[lockIndex].lock();  
        try {  
            array[index] = value;  
        } finally {  
            locks[lockIndex].unlock();  
        }  
    }  
  
    public int get(int index) {  
        int lockIndex = index / stripeSize;  
        locks[lockIndex].lock();  
        try {  
            return array[index];  
        } finally {  
            locks[lockIndex].unlock();  
        }  
    }  
}

在上面的示例中,StripedArray 类将一个大数组分割成多个条纹,并为每个条纹分配一个单独的锁。这样,当多个线程尝试访问数组的不同部分时,它们可以并发地执行,而不会互相阻塞。

21. 无锁编程(Lock-Free Programming)

无锁编程是一种不使用显式锁的并发编程技术。它通常依赖于原子操作来实现线程安全。无锁数据结构通常具有更高的性能,但设计和实现起来也更加复杂。

示例:

无锁编程通常涉及到复杂的算法和底层原子操作,不适合用简单的示例来说明。然而,Java 的 java.util.concurrent.atomic 包提供了一些原子变量类,如 AtomicInteger,这些类可以在一定程度上实现无锁编程。

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

在上面的示例中,Counter 类使用 AtomicInteger 来实现一个线程安全的计数器。

22. 锁的公平性与饥饿问题

锁的公平性是指锁分配是否按照请求的顺序进行。如果锁是公平的,那么等待时间最长的线程将优先获得锁。相反,如果锁是非公平的,那么锁的分配可能会受到线程调度和其他因素的影响,导致某些线程长时间得不到执行,即出现饥饿问题。

示例:

在Java中,ReentrantLock类允许我们指定锁是公平的还是非公平的。默认情况下,它是非公平的,但可以通过构造函数指定为公平的。

import java.util.concurrent.locks.ReentrantLock;  
  
public class FairLockExample {  
    private final ReentrantLock fairLock = new ReentrantLock(true); // 公平的锁  
    // ... 其他代码 ...  
  
    public void someMethod() {  
        fairLock.lock(); // 请求锁  
        try {  
            // 访问共享资源  
        } finally {  
            fairLock.unlock(); // 释放锁  
        }  
    }  
}

在上面的示例中,我们创建了一个公平的ReentrantLock。这意味着当多个线程请求锁时,等待时间最长的线程将优先获得锁,从而减少了饥饿问题的可能性。然而,公平锁通常比非公平锁具有更低的吞吐量,因为线程调度和上下文切换的开销较大。

23. 锁与内存一致性模型

在并发编程中,内存一致性模型定义了多线程环境下读写操作的可见性和顺序性。不同的硬件和编译器可能会采用不同的内存一致性模型,这可能会影响到并发程序的行为。锁通常与特定的内存一致性模型一起使用,以确保数据的一致性。

在Java中,volatile关键字和锁都提供了内存可见性和有序性的保证。当使用锁时,Java内存模型确保了在锁释放之前对共享变量的修改对其他线程是可见的,并且在锁获取之后,线程将看到其他线程在锁释放之前所做的修改。

示例:

public class SharedCounter {  
    private int count = 0;  
    private final Object lock = new Object();  
  
    public void increment() {  
        synchronized (lock) { // 获取锁,保证内存可见性和有序性  
            count++;  
        }  
    }  
  
    public int getCount() {  
        synchronized (lock) { // 获取锁,保证内存可见性和有序性  
            return count;  
        }  
    }  
}

在上面的示例中,我们使用synchronized块来获取和释放锁。这确保了increment方法中对count的修改在方法结束后对其他线程是可见的,并且getCount方法将看到increment方法所做的最新修改。

24. 锁与性能调优

在性能敏感的应用中,锁的使用需要仔细调优。锁的粒度、锁的类型、锁的争用情况等因素都会影响到程序的性能。

示例:

减少锁的粒度是提高并发性能的一种常见方法。通过将一个大锁分解为多个小锁,可以减少线程之间的争用,从而提高吞吐量。

import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;  
import java.util.concurrent.ConcurrentHashMap;  
  
public class ConcurrentMapExample {  
    private final ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();  
    private final Lock lock = new ReentrantLock(); // 大锁  
  
    // 使用大锁的版本,性能可能较差  
    public void putBigLock(String key, Object value) {  
        lock.lock();  
        try {  
            map.put(key, value);  
        } finally {  
            lock.unlock();  
        }  
    }  
  
    // 使用ConcurrentHashMap内置的分段锁的版本,性能更好  
    public void putFineGrainedLock(String key, Object value) {  
        map.put(key, value); // ConcurrentHashMap内部使用了分段锁  
    }  
}

在上面的示例中,putBigLock方法使用了一个大锁来保护整个map的访问,这可能导致较高的锁争用。而putFineGrainedLock方法则利用了ConcurrentHashMap内部实现的分段锁,每个段有自己的锁,从而减少了锁争用,提高了并发性能。

最后要切记:

在使用锁时,V哥提醒需要注意锁的性能开销。虽然锁是确保线程安全的重要工具,但过度使用锁可能会降低程序的性能。因此,在设计多线程程序时,需要综合考虑线程安全性和性能之间的平衡。


威哥爱编程
183 声望16 粉丝

华为开发者专家(HDE)、TiDB开发者官方认证讲师、Java畅销书作者、HarmonyOS应用开发高级讲师