为什么需要线程同步? 当多个线程访问和修改共享资源(同一个变量、集合、文件等)时,如果没有正确的同步,可能会导致数据不一致、脏读、幻读等问题。同步的目的是确保在任一时刻,只有一个线程可以执行特定的代码段(临界区)。
一、 基本的Java同步机制
1、synchronized 关键字
最经典、最基础的同步方式。它提供了一种内置锁(Intrinsic Lock)或监视器锁(Monitor Lock),确保线程互斥地执行代码块或方法。
- 同步代码块:更灵活,可以指定锁对象。
private final Object mLock = new Object(); // 专门的锁对象
private int mCount = 0;
public void increment() {
synchronized (mLock) { // 线程进入这里会获取mLock的锁
mCount++; // 临界区
} // 线程执行完会自动释放锁
}
- 同步实例方法:锁是当前对象实例(this)。
public synchronized void increment() {
mCount++;
}
- 同步静态方法:锁是当前类的Class对象。
public static synchronized void staticIncrement() {
staticCount++;
}
2、volatile 关键字
确保变量的可见性,但不保证操作的原子性。
作用:当一个线程修改了volatile变量,新值会立即被刷新到主内存。当其他线程读取该变量时,它会从主内存重新加载,而不是使用自己工作内存中的缓存值。
适用场景:标志位(boolean flag),或者该变量不参与依赖于其当前值的复合操作(例如 count++ 不是原子操作,volatile 无法保证其线程安全)。
private volatile boolean isRunning = false;
public void start() {
isRunning = true;
// 其他线程可以立即看到isRunning变为true
}
二、 java.util.concurrent (JUC) 包中的高级工具
这是Java提供的更强大、更现代、性能更高的并发工具包。
1、ReentrantLock (可重入锁)
synchronized的增强版,提供了更多高级功能。
- 基本用法:
private final ReentrantLock mLock = new ReentrantLock();
private int mCount = 0;
public void increment() {
mLock.lock(); // 获取锁
try {
mCount++; // 临界区
} finally {
mLock.unlock(); // 必须在finally块中释放锁!
}
}
2、CountDownLatch (倒计时门闩)
允许一个或多个线程等待其他线程完成操作。
典型场景:主线程等待所有子线程初始化完成。
// 主线程
CountDownLatch latch = new CountDownLatch(3); // 计数器初始化为3
// 启动3个工作线程,每个线程完成任务后调用 latch.countDown()
startWorkerThread(latch);
startWorkerThread(latch);
startWorkerThread(latch);
latch.await(); // 主线程在此阻塞,直到计数器减为0
proceed(); // 所有工作线程完成后,继续执行
// 工作线程
public void run() {
try {
// ... 执行任务
} finally {
latch.countDown(); // 完成后将计数器减1
}
}
3、Semaphore (信号量)
控制同时访问某个特定资源的线程数量,用于流量控制。
典型场景:限制同时下载的线程数、数据库连接池。
private final Semaphore mDownloadSemaphore = new Semaphore(3); // 只允许3个线程同时执行
public void startDownload() {
try {
mDownloadSemaphore.acquire(); // 获取一个许可
// ... 执行下载任务(临界区)
} finally {
mDownloadSemaphore.release(); // 释放许可
}
}
三、 Android特有的机制
1、Handler + Looper + MessageQueue
这是Android最核心的线程间通信机制,本质上是一个单线程消息处理模型。它通过将任务(Message或Runnable)投递到目标线程的MessageQueue中,由该线程的Looper按顺序取出并执行,从而实现了将代码切换到特定线程(通常是主线程)执行的效果,避免了多线程并发访问UI的问题。
- 作用:线程切换和任务同步。
// 在后台线程中
Handler mainHandler = new Handler(Looper.getMainLooper()); // 获取主线程的Handler
mainHandler.post(new Runnable() {
@Override
public void run() {
// 这段代码将在主线程执行,可以安全更新UI
mTextView.setText("Update from background thread");
}
});
2、AsyncTask (已废弃)
底层基于Handler和线程池。它被设计用来执行短暂的异步任务并在完成后更新UI。注意:在Android 11 (API 30) 中已被正式废弃,推荐使用 kotlin协程、RxJava 或 ListenableFuture。
四、 现代Kotlin推荐方式
1、Mutex (互斥锁)
Kotlin协程世界中的ReentrantLock,但更轻量、更协程友好。它使用lock和unlock,但更推荐使用withLock扩展函数。
val mutex = Mutex()
var counter = 0
fun increment() = runBlocking {
mutex.withLock { // 挂起协程,而不是阻塞线程
counter++
}
}