文章目录
- 1. 线程同步与共享资源保护概述
- 1.1 多线程安全问题
- 1.2 解决方案:线程同步
- 2. 线程同步的常用方式
- 2.1 使用 synchronized 关键字
- 2.2 使用 ReentrantLock
- 3. 实践:多线程计数器示例
- 4. 实践说明与运行步骤
- 5. 总结与思考
- 6.今日生词
1. 线程同步与共享资源保护概述
1.1 多线程安全问题
- 数据竞争(Race Condition) :当多个线程同时访问共享变量且至少有一个线程进行写操作时,就可能发生数据竞争,导致最终结果不正确。
- 共享资源问题: 例如多个线程同时对同一个计数器进行自增操作,如果没有同步保护,可能会出现丢失更新,计数结果最终不正确。
1.2 解决方案:线程同步
通过引入同步措施(例如 synchronized
关键字或Lock
接口实现)可以确保在同一时刻只有一个线程访问共享资源,从而保证数据一致性。
2. 线程同步的常用方式
2.1 使用 synchronized 关键字
-
原理:synchronized 利用对象的内部锁(Monitor)来确保同步,只有获得锁的线程才能进入临界区。
-
用法:
- 同步方法:在方法声明上添加 synchronized。
- 同步代码块:在代码块前使用 synchronized(锁对象){ … },锁对象通常使用 this 或其他共享对象。
-
优点:语法简单,适合大部分场景。
-
缺点:颗粒度较粗,灵活性不如 Lock。
2.2 使用 ReentrantLock
- 原理:ReentrantLock 提供了与 synchronized 相同的互斥保护,同时支持更多高级功能,例如公平锁、可中断锁申请等。
- 用法:
- 通过 lock() 获取锁,在 finally 块中确保调用 unlock() 释放锁。
- 优点:可中断锁,尝试锁(tryLock)等高级特性。
- 缺点:需要手动释放锁,代码稍显繁琐。
3. 实践:多线程计数器示例
我们通过一个简单示例来比较两种同步方式在多线程计数器实现中的使用。
示例一:使用 synchronized 关键字
创建一个使用 synchronized 保护自增操作的计数器,确保在多个线程中并发更新时结果正确。
// SynchronizedCounter.java
public class SynchronizedCounter {
private int count = 0; // 共享计数器
// synchronized方法保证同一时刻只有一个线程进入
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
int numThreads = 10; // 启动10个线程
int incrementsPerThread = 1000; // 每个线程计数1000次
Thread[] threads = new Thread[numThreads];
// 创建并启动线程
for (int i = 0; i < numThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
counter.increment();
}
}, "Thread-" + i);
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread t : threads) {
t.join();
}
// 最终计数应为:10 * 1000 = 10000
System.out.println("Final count (synchronized): " + counter.getCount());
}
}
示例二:使用 ReentrantLock
使用 ReentrantLock 来保护临界区,实现同样的多线程计数器。
// ReentrantLockCounter.java
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockCounter {
private int count = 0; // 共享计数器
private final ReentrantLock lock = new ReentrantLock(); // 创建锁对象
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 确保释放锁
}
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockCounter counter = new ReentrantLockCounter();
int numThreads = 10; // 启动10个线程
int incrementsPerThread = 1000; // 每个线程计数1000次
Thread[] threads = new Thread[numThreads];
// 创建并启动线程
for (int i = 0; i < numThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
counter.increment();
}
}, "Thread-" + i);
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread t : threads) {
t.join();
}
// 最终计数应为:10 * 1000 = 10000
System.out.println("Final count (ReentrantLock): " + counter.getCount());
}
}
4. 实践说明与运行步骤
1.新建项目:
- 在 IntelliJ IDEA 中创建一个新的 Java 项目,命名(如:
ThreadSyncDemo
)。
2.创建类文件: - 在
src
下分别创建两个 Java 文件(例如:SynchronizedCounter.java
和ReentrantLockCounter.java
),粘贴上述各自代码。
3.编译运行: - 右键点击任一文件的
main
方法所在的编辑器窗口,选择 Run ‘SynchronizedCounter.main()’ 或 Run ‘ReentrantLockCounter.main()’。 - 观察控制台输出,验证最终计数是否为预期的 10000。
5. 总结与思考
-
线程安全问题:多个线程并发修改共享变量时,若无同步保护,可能会引发数据竞争,导致数据不准确。
-
synchronized 与 ReentrantLock 的选择:
- synchronized:简单易用,但锁机制较为简单,无法中断等待线程。
- ReentrantLock:更为灵活,支持公平性设置、中断锁定等高级功能,但使用时要注意确保在 finally 块中释放锁,避免死锁风险。
-
实践意义:
- 理解和掌握基本的线程同步机制,是编写正确的多线程程序的基础。
- 可根据应用场景选择合适的同步策略,以保证程序在并发环境下数据的一致性与正确性。
6.今日生词
1.resident 2.pose 3.compound 4.consumption 5.facility