Java基础教程(138)线程同步:深入剖析Java线程同步,从竞态条件到锁的终极指南(附完整示例代码)

一、 问题的根源:竞态条件(Race Condition)

当多个线程同时访问和修改同一共享资源时,最终的执行结果依赖于线程执行的精确时序。这种不确定性往往会导致错误的发生,这就是竞态条件。

示例:一个不安全的计数器

public class UnsafeCounter {
    private int count = 0;

    public void increment() {
        count++; // 这并非原子操作
    }

    public int getCount() {
        return count;
    }
}

count++这行代码看似简单,但实际上是一个“读取-修改-写入”的复合操作。如果两个线程同时读取到相同的值,分别增加后写入,就会导致一次增加被丢失,最终结果会小于预期。

二、 同步的核心武器:synchronized

Java提供了内置的synchronized关键字来实现同步,它是一种互斥锁,确保同一时刻最多只有一个线程可以执行某个方法或代码块。

1. 同步方法(Synchronized Methods)

public class SynchronizedCounter {
    private int count = 0;

    // 同步方法,锁是当前实例对象(this)
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

原理:线程在进入同步方法前必须获得当前对象实例(this)的锁。如果锁被其他线程持有,当前线程将被阻塞,直到锁被释放。

2. 同步代码块(Synchronized Blocks)
同步方法会锁住整个方法,有时粒度太粗,影响性能。同步代码块可以更精细地控制锁的范围,并允许选择不同的锁对象(监视器)。

public class FineGrainedSynchronization {
    private final Object lock = new Object(); // 专门的锁对象
    private int count = 0;

    public void increment() {
        // 只同步关键的代码部分,性能更好
        synchronized (lock) {
            count++;
        }
        // 这里可以执行其他非线程安全的操作
    }
}

使用一个专门的私有锁对象(private final Object lock)是更好的实践,它可以防止外部代码意外获取你的锁,导致死锁。

三、 更现代的选择:Lock接口

Java 5引入了java.util.concurrent.locks包,提供了更灵活、更强大的锁操作,主要是ReentrantLock

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

public class LockCounter {
    private final Lock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock(); // 手动获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 必须在finally块中确保释放锁
        }
    }
}

相比于synchronized的优势:

  • 尝试非阻塞获取锁:tryLock()方法可以立即返回或在一定时间内尝试获取锁,避免无限期阻塞。
  • 可中断的锁获取:lockInterruptibly()方法允许在等待锁时响应中断。
  • 公平锁:ReentrantLock可以设置为公平锁(先等待的线程先获得锁),虽然可能会降低吞吐量。
四、 实战示例:生产者-消费者模型

这是一个经典的线程同步问题,完美展示了wait(), notify(), notifyAll()(通常与synchronized配合)或Condition(与Lock配合)的使用。

使用Lock和Condition实现:

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

public class ProducerConsumerDemo {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int capacity;
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();  // 队列未满条件
    private final Condition notEmpty = lock.newCondition(); // 队列非空条件

    public ProducerConsumerDemo(int capacity) {
        this.capacity = capacity;
    }

    public void produce(int value) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await(); // 队列已满,生产者等待
            }
            queue.add(value);
            System.out.println("Produced: " + value);
            notEmpty.signalAll(); // 生产后通知消费者可以消费了
        } finally {
            lock.unlock();
        }
    }

    public void consume() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await(); // 队列为空,消费者等待
            }
            int value = queue.poll();
            System.out.println("Consumed: " + value);
            notFull.signalAll(); // 消费后通知生产者可以生产了
        } finally {
            lock.unlock();
        }
    }
}

这个例子展示了如何利用多个Condition对象精确地唤醒特定类型的线程(只唤醒生产者或只唤醒消费者),比使用synchronizednotifyAll()更高效。

五、 总结与选型建议
  • synchronized:优先考虑。语法简单,JVM会自动释放锁,不易出错。在大多数情况下性能已经足够好(JVM对其有大量优化,如锁升级)。
  • Lock:当你需要尝试获取锁、可中断、公平锁等高级功能时使用。但要切记手动在finally中释放锁。

核心思想:同步的本质是缩小临界区(被锁保护的代码范围),在保证线程安全的前提下,最大限度地提高程序的并发性能和吞吐量。选择最适合业务场景的同步工具,是构建高效、稳定并发应用的关键。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

值引力

持续创作,多谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值