多线程2之线程同步
线程同步
线程同步的概念:
线程同步是指多个线程在访问共享资源时,通过一定的机制来协调它们的执行顺序和访问方式,以确保共享资源在同一时刻只能被一个线程访问,或者在多个线程访问时能够保证数据的一致性和正确性。在多线程编程环境中,由于线程的执行是并发的,如果没有适当的同步机制,可能会导致数据竞争(data race)、死锁(deadlock)等问题。
为什么要线程同步:
数据竞争问题:
当多个线程同时访问和修改共享数据时,由于线程执行的顺序是不确定的,可能会导致数据不一致。例如,假设有两个线程同时对一个共享变量count
进行自增操作。如果没有同步机制,可能会出现以下情况:线程 1 读取count
的值为 5,在它即将写入新值(6)之前,线程 2 也读取了count
的值为 5,然后两个线程分别将自己计算的值(6)写入count
,最终count
的值为 6,而不是正确的 7。
死锁问题:
在多个线程互相等待对方释放资源的情况下,会导致程序无法继续执行,形成死锁。例如,线程 A 持有资源 R1 并且等待资源 R2,而线程 B 持有资源 R2 并且等待资源 R1,这样两个线程都无法继续执行,程序陷入僵局。
线程同步之Synchronized
Synchronized 是 Java 中的一个关键字,用于实现方法或代码块的同步。它提供了一种简单且有效的方式来控制多个线程对共享资源的访问,确保在同一时刻只有一个线程能够执行被 Synchronized 修饰的方法或代码块。其底层原理是基于对象的内部锁(也称为监视器锁,monitor lock)。
Synchronized的两种用法
1.修饰方法:
public class SynchronizedMethodExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
increment
方法被 Synchronized 关键字修饰。当一个线程调用increment
方法时,它首先需要获取对象的内部锁。如果此时没有其他线程持有该锁,那么这个线程就可以顺利执行increment
方法中的代码,即对count
变量进行自增操作。如果有其他线程已经持有了这个锁,那么当前线程将被阻塞,直到持有锁的线程释放锁。这样就保证了在任何时候只有一个线程能够执行increment
方法,从而避免了多个线程同时修改count
变量可能导致的数据竞争问题。
2.修饰代码块:
public class SynchronizedBlockExample {
private Object lockObject = new Object();
private int count = 0;
public void increment() {
synchronized (lockObject) {
count++;
}
}
public int getCount() {
return count;
}
}
这里使用 Synchronized 关键字修饰了一个代码块,并且指定了一个锁对象lockObject
。当一个线程进入这个被 Synchronized 修饰的代码块时,它需要获取lockObject
的锁。这种方式比直接修饰方法更加灵活,因为可以根据具体的需求选择不同的锁对象。例如,如果有多个方法需要对不同的共享资源进行同步操作,就可以分别为它们指定不同的锁对象,而不是将整个方法都进行同步。
Synchronized 的优缺点
优点
简单易用:作为 Java 内置的同步机制,使用 Synchronized 关键字来实现线程同步非常方便,不需要额外引入复杂的库或工具。对于初学者或者简单的多线程同步场景,很容易理解和掌握。
语义清晰:其语义明确,表示在同一时刻只有一个线程能够执行被修饰的方法或代码块,能够有效地避免数据竞争,保证数据的一致性和正确性。
缺点
灵活性相对较差:虽然可以通过修饰代码块来指定锁对象,但在一些复杂的场景下,可能需要更精细的锁控制策略。例如,对于读多写少的场景,Synchronized 无法区分读操作和写操作,所有的访问都需要获取相同的锁,这可能会导致不必要的性能损耗。
性能问题:在高并发的场景下,尤其是多个线程竞争激烈时,Synchronized 的性能可能会受到影响。因为当锁升级为重量级锁后,涉及到操作系统的内核态和用户态的切换,这种切换会带来较大的开销。而且,当一个线程持有锁时,其他等待的线程会被阻塞,不能有效地利用 CPU 资源。
线程同步之Lock
-
Lock 接口概述
- 在 Java 中,
Lock
接口提供了比synchronized
关键字更灵活的线程同步机制。Lock
接口位于java.util.concurrent.locks
包中,它定义了一系列用于控制多个线程对共享资源访问的方法。与synchronized
基于对象内部锁不同,Lock
接口通过显式的锁操作来实现线程同步,提供了更多的功能和更高的灵活性。
- 在 Java 中,
-
Lock 接口的主要实现类 - ReentrantLock
可重入性
ReentrantLock
是Lock
接口最常用的实现类,它具有可重入性。这意味着一个线程可以多次获取同一个锁。例如,如果一个线程在一个已经获取了ReentrantLock
锁的方法中调用了另一个同样需要获取该锁的方法,线程不会被阻塞,而是可以成功获取锁,就像使用synchronized
关键字时的情况一样。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
首先创建了一个ReentrantLock
对象lock
。在increment
方法中,通过lock.lock()
来获取锁,然后在try - finally
块中执行对共享资源(count
变量)的操作。在finally
块中,通过lock.unlock()
来释放锁。这种方式确保了无论在操作共享资源的过程中是否发生异常,锁都会被释放,从而避免了死锁的情况。
Lock 接口提供的其他功能
- 公平锁与非公平锁
- 公平锁(Fair Lock):
ReentrantLock
可以通过构造函数来指定是否为公平锁。公平锁遵循先来先得的原则,即等待时间最长的线程会最先获取锁。例如,在一个多线程环境下,如果有多个线程在等待获取锁,那么这些线程会按照请求锁的先后顺序来获取锁。公平锁可以保证线程获取锁的顺序性,但可能会导致性能下降,因为需要维护一个等待队列,每次释放锁时都要检查队列中是否有等待时间最长的线程。 - 非公平锁(Unfair Lock):默认情况下,
ReentrantLock
是一个非公平锁。非公平锁在释放锁后,不会考虑等待线程的等待时间,任何一个等待线程都有可能获取到锁。这样可能会导致某些线程长时间等待,但在高并发场景下,非公平锁通常比公平锁具有更好的性能,因为它减少了线程切换和等待队列维护的开销。
- 公平锁(Fair Lock):
-
Lock 与 Synchronized 的比较
- 灵活性:
Lock
接口提供了更多的功能,如公平锁 / 非公平锁的选择、可中断锁以及更灵活的条件变量。而synchronized
关键字相对简单,使用时基于对象的内部锁,没有这些额外的功能选项。 - 性能:在低并发场景下,
synchronized
和Lock
的性能差异不大。但在高并发场景下,ReentrantLock
(非公平锁)可能会因为减少了一些不必要的等待队列维护和线程切换开销而具有更好的性能。不过,synchronized
在 JDK 的不断优化下,性能也在逐步提升。 - 使用的便利性:
synchronized
关键字使用起来更加简单,它是 Java 语言的内置特性,语法简洁,对于简单的线程同步场景,代码量较少。而Lock
接口需要显式地进行锁的获取和释放操作,代码结构相对复杂一些,但在复杂的多线程场景下,能够提供更精细的控制。
- 灵活性: