跳转至

多线程编程:线程同步

在多线程编程中,线程同步是一个非常重要的概念。当多个线程同时访问共享资源时,如果没有适当的同步机制,可能会导致数据不一致或其他不可预见的错误。本文将详细介绍如何使用Java中的同步机制来控制线程,确保线程安全。

1. 什么是线程同步?

线程同步是指多个线程在访问共享资源时,通过某种机制确保同一时间只有一个线程可以访问该资源。这样可以避免多个线程同时修改共享资源而导致的数据不一致问题。

1.1 为什么需要线程同步?

在多线程环境中,多个线程可能会同时访问和修改同一个共享资源。如果没有同步机制,可能会导致以下问题:

  • 竞态条件(Race Condition):多个线程同时修改共享资源,导致最终结果依赖于线程执行的顺序。
  • 数据不一致:由于线程之间的竞争,共享资源的状态可能会变得不一致。

1.2 线程同步的基本概念

  • 临界区(Critical Section):一段代码,同一时间只能有一个线程执行。
  • 锁(Lock):一种同步机制,用于控制对临界区的访问。
  • 监视器(Monitor):Java中的一种同步机制,通过synchronized关键字实现。

2. Java中的线程同步机制

Java提供了多种线程同步机制,包括synchronized关键字、ReentrantLock类、volatile关键字等。本文将重点介绍synchronized关键字和ReentrantLock类。

2.1 使用synchronized关键字

synchronized关键字可以用来修饰方法或代码块,确保同一时间只有一个线程可以执行被修饰的代码。

2.1.1 同步方法

class Counter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

public class SynchronizedMethodExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // 创建两个线程
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        // 启动线程
        t1.start();
        t2.start();

        // 等待线程执行完毕
        t1.join();
        t2.join();

        // 输出最终结果
        System.out.println("Final Count: " + counter.getCount());
    }
}

解释: - increment()方法被synchronized修饰,确保同一时间只有一个线程可以执行该方法。 - 两个线程t1t2分别对Counter对象进行1000次递增操作。 - 最终输出的count值应为2000,因为synchronized确保了线程安全。

2.1.2 同步代码块

class Counter {
    private int count = 0;
    private final Object lock = new Object();

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

    public int getCount() {
        return count;
    }
}

public class SynchronizedBlockExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // 创建两个线程
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        // 启动线程
        t1.start();
        t2.start();

        // 等待线程执行完毕
        t1.join();
        t2.join();

        // 输出最终结果
        System.out.println("Final Count: " + counter.getCount());
    }
}

解释: - increment()方法中的同步代码块使用了一个lock对象作为锁。 - 与同步方法类似,同步代码块确保同一时间只有一个线程可以执行该代码块。 - 最终输出的count值应为2000。

2.2 使用ReentrantLock

ReentrantLock是Java提供的一个可重入锁,与synchronized相比,它提供了更灵活的锁机制。

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

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

    public void increment() {
        lock.lock();  // 获取锁
        try {
            count++;
        } finally {
            lock.unlock();  // 释放锁
        }
    }

    public int getCount() {
        return count;
    }
}

public class ReentrantLockExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // 创建两个线程
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        // 启动线程
        t1.start();
        t2.start();

        // 等待线程执行完毕
        t1.join();
        t2.join();

        // 输出最终结果
        System.out.println("Final Count: " + counter.getCount());
    }
}

解释: - ReentrantLock提供了与synchronized类似的锁机制,但更加灵活。 - lock()方法用于获取锁,unlock()方法用于释放锁。 - 最终输出的count值应为2000。

3. 练习题

3.1 简单练习

题目:修改以下代码,使其线程安全。

class UnsafeCounter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

提示:使用synchronized关键字或ReentrantLock

3.2 中等练习

题目:编写一个程序,模拟多个线程同时访问一个银行账户,并进行存款和取款操作。确保线程安全。

提示:使用synchronized关键字或ReentrantLock来保护账户余额。

3.3 复杂练习

题目:实现一个生产者-消费者模型,使用ReentrantLockCondition来控制线程的等待和唤醒。

提示ConditionReentrantLock提供的一个条件变量,可以用于线程间的协调。

4. 总结

  • 线程同步是确保多个线程安全访问共享资源的关键机制。
  • synchronized关键字可以用于同步方法或代码块,确保同一时间只有一个线程执行。
  • ReentrantLock提供了更灵活的锁机制,支持可重入锁和条件变量。
  • 在多线程编程中,选择合适的同步机制非常重要,以确保程序的正确性和性能。

通过本文的学习,你应该已经掌握了Java中线程同步的基本概念和使用方法。希望你能通过练习题进一步巩固所学知识,并在实际项目中灵活运用这些技术。