【Java 基础篇】Java 线程的同步与互斥详解

2023-10-12 16:33:00 浏览数 (2)

多线程编程是一种常见的编程模型,它可以提高程序的性能和响应速度。然而,多线程编程也伴随着一些挑战,其中一个最重要的挑战是确保线程安全。线程安全是指多个线程访问共享资源时不会引发不确定的行为或错误。为了实现线程安全,Java提供了许多同步和互斥机制,本文将详细介绍这些机制。

什么是线程安全?

在多线程环境下,如果多个线程同时访问共享的数据或资源,可能会导致以下问题:

  • 竞态条件(Race Condition):多个线程在不同的时刻访问同一个资源,导致数据不一致或错误的结果。
  • 数据争用(Data Race):多个线程同时读写共享数据,可能导致数据的不一致性。
  • 死锁(Deadlock):多个线程互相等待对方释放资源,导致所有线程无法继续执行。
  • 饥饿(Starvation):某些线程无法获得所需的资源,导致一直无法执行。

线程安全的代码是指在多线程环境下,不管多少线程并发访问,都能保证程序的正确性和一致性。线程安全的代码不会出现上述问题。

Java中的线程同步

Java提供了多种机制来实现线程同步,主要包括:

  • synchronized关键字:通过在方法或代码块前加上synchronized关键字,可以确保同一时刻只有一个线程可以执行被同步的代码块或方法。
  • ReentrantLock类ReentrantLock是一个可重入锁,它提供了更灵活的锁定机制,可以用于替代synchronized
  • volatile关键字volatile关键字用于修饰变量,确保变量的可见性,但不能保证原子性。
  • Atomic包:Java提供了一组原子操作类,如AtomicIntegerAtomicLong等,用于执行具有原子性要求的操作。
  • wait()和notify()方法:这两个方法通常与synchronized一起使用,用于线程之间的协作和通信。
  • CountDownLatch和CyclicBarrier:这两个类用于实现线程的等待和同步。
  • SemaphoreSemaphore是一个计数信号量,用于控制同时访问某个资源的线程数量。
  • Condition接口Condition接口通常与ReentrantLock一起使用,用于实现更复杂的线程等待和通知机制。
  • Concurrent包:Java的java.util.concurrent包提供了大量线程安全的数据结构和工具类,如ConcurrentHashMapCopyOnWriteArrayList等。

下面我们将详细介绍synchronized关键字和ReentrantLock类,它们是实现线程同步的两种主要方式。

synchronized关键字

synchronized是Java中最常用的线程同步机制之一,它可以用来修饰方法或代码块。当一个线程访问一个被synchronized修饰的方法或代码块时,其他试图访问该方法或代码块的线程将被阻塞,直到当前线程执行完毕释放锁。

同步方法

可以使用synchronized关键字修饰方法,将方法变成同步方法,确保同一时刻只有一个线程可以执行该方法。例如:

代码语言:javascript复制
public synchronized void synchronizedMethod() {
    // 同步代码块
    // ...
}

上面的代码中,synchronizedMethod方法被修饰为同步方法,只允许一个线程同时执行该方法。

同步代码块

除了同步方法,还可以使用synchronized关键字修饰代码块,以实现更细粒度的同步。语法如下:

代码语言:javascript复制
synchronized (锁对象) {
    // 同步代码块
    // ...
}

在上面的代码中,锁对象通常是一个对象,多个线程可以根据锁对象的不同实例来实现同步。

下面是一个示例,演示了如何使用同步方法和同步代码块来保证线程安全:

代码语言:javascript复制
public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        count  ;
    }

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

上述示例中,increment方法是一个同步方法,只允许一个线程同时执行,而performTask方法使用同步代码块,锁定的是当前对象(this),也确保了线程安全。

需要注意的是,虽然synchronized是一种简单且常用的线程同步方式,但过度使用它可能导致性能下降。因为每次访问同步方法或同步代码块时,都需要获取锁并释放锁,这会增加线程的竞争和上下文切换的开销。因此,在设计多线程程序时,需要权衡性能和线程安全性。

ReentrantLock类

ReentrantLock是Java提供的一种可重入锁,它相比synchronized更加灵活,提供了更多的控制和扩展功能。使用ReentrantLock可以实现与synchronized相同的线程同步效果,但更多情况下,它用于解决synchronized无法满足的复杂同步问题。

基本用法

ReentrantLock的基本用法如下:

代码语言:javascript复制
import java.util.concurrent.locks.ReentrantLock;

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

    public void performTask() {
        lock.lock(); // 获取锁
        try {
            // 同步代码块
            // ...
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}

在上面的代码中,通过lock()方法获取锁,然后在try-finally块中执行同步代码,最后使用unlock()方法释放锁。与synchronized不同,ReentrantLock的锁定和解锁操作是显式的,这使得代码的逻辑更加清晰。

可重入性

ReentrantLock支持可重入性,即同一个线程可以多次获取同一个锁而不会死锁。这使得在一个方法中调用另一个使用同一把锁的方法成为可能。例如:

代码语言:javascript复制
public class ReentrantLockExample {
    private ReentrantLock lock = new ReentrantLock();

    public void outerMethod() {
        lock.lock(); // 第一次获取锁
        try {
            innerMethod(); // 调用内部方法
        } finally {
            lock.unlock(); // 第一次释放锁
        }
    }

    public void innerMethod() {
        lock.lock(); // 第二次获取锁
        try {
            // 同步代码块
            // ...
        } finally {
            lock.unlock(); // 第二次释放锁
        }
    }
}

上述示例中,outerMethodinnerMethod都使用了相同的ReentrantLock实例,且innerMethodouterMethod中被调用,但由于可重入性,它们都能正常工作。

公平锁和非公平锁

ReentrantLock可以是公平锁(Fair Lock)或非公平锁(Nonfair Lock)。默认情况下,ReentrantLock 是非公平锁,即锁的获取是无序的,不保证等待时间最长的线程最先获取锁。而公平锁会按照线程的等待时间来获取锁,等待时间最长的线程会最先获取锁。

要创建一个公平锁,可以在创建ReentrantLock实例时传入true作为参数,如下所示:

代码语言:javascript复制
ReentrantLock fairLock = new ReentrantLock(true);

需要注意的是,公平锁会增加一些额外的性能开销,因此只有在确实需要时才使用它。

条件变量

ReentrantLock还提供了条件变量(Condition)的支持,用于实现更复杂的线程等待和通知机制。条件变量通常与await()signal()方法一起使用。

下面是一个简单的示例,演示了如何使用条件变量等待某个条件的发生:

代码语言:javascript复制
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void waitForCondition() throws InterruptedException {
        lock.lock();
        try {
            while (!conditionIsMet()) {
                condition.await(); // 等待条件变量
            }
            // 条件满足,继续执行
        } finally {
            lock.unlock();
        }
    }

    public void signalCondition() {
        lock.lock();
        try {
            // 修改条件
            modifyCondition();
            condition.signal(); // 通知等待线程条件已发生变化
        } finally {
            lock.unlock();
        }
    }

    private boolean conditionIsMet() {
        // 检查条件是否满足
        // ...
        return true;
    }

    private void modifyCondition() {
        // 修改条件
        // ...
    }
}

在上述示例中,waitForCondition方法等待条件变量的发生,如果条件不满足,则调用await()方法使线程进入等待状态。signalCondition方法负责修改条件并通知等待线程条件已发生变化。

总结一下,ReentrantLock相比synchronized提供了更灵活的线程同步机制,并且支持可重入性、公平锁、条件变量等特性,使得多线程编程更加方便和可控。

总结

线程同步是多线程编程中的重要问题,Java提供了多种机制来实现线程同步,包括synchronized关键字和ReentrantLock类。选择合适的线程同步方式取决于具体的需求和性能考虑。无论使用哪种方式,都需要小心设计,以确保线程安全性和程序的正确性。

0 人点赞