同步机制(synchronized 关键字、ReentrantLock 类)
synchronized 关键字和 ReentrantLock 类都是用来实现同步机制的,以确保多线程环境下的数据一致性和线程安全。
一、synchronized 关键字
synchronized 可以用于方法 或 方法体中的代码块
。
- 当用于方法时,可使其成为同步方法
- 当用于方法体中的代码块 时,可使其成为同步代码块
1. synchronized 用于方法
当synchronized
用于方法时,它会将整个方法变为同步方法。这意味着在同一时间内,只有一个线程可以执行该方法。对于实例方法,锁是当前实例对象(this
);对于静态方法,锁是当前类的Class
对象。
示例:同步实例方法
public class SynchronizedMethodExample {
private int count = 0;
// 同步实例方法
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) {
SynchronizedMethodExample example = new SynchronizedMethodExample();
// 创建多个线程来测试同步
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 预期输出为2000,因为increment方法是同步的
System.out.println("Final count: " + example.getCount());
}
}
示例:同步静态方法
public class SynchronizedStaticMethodExample {
private static int count = 0;
// 同步静态方法
public static synchronized void increment() {
count++;
}
public static int getCount() {
return count;
}
public static void main(String[] args) {
// 创建多个线程来测试同步
Thread t1 = new Thread(SynchronizedStaticMethodExample::increment);
Thread t2 = new Thread(SynchronizedStaticMethodExample::increment);
// 启动1000次每个线程
for (int i = 0; i < 1000; i++) {
new Thread(SynchronizedStaticMethodExample::increment).start();
new Thread(SynchronizedStaticMethodExample::increment).start();
}
// 等待所有线程完成(这里只是简单示例,实际应使用更好的线程管理方式)
try {
Thread.sleep(1000); // 等待足够的时间以确保所有线程完成
} catch (InterruptedException e) {
e.printStackTrace();
}
// 预期输出接近2000(可能由于线程调度而略有不同),因为increment方法是同步的
System.out.println("Final count: " + getCount());
}
}
2. synchronized 用于方法体中的代码块
当synchronized
用于方法体中的代码块时,它允许更细粒度的同步控制。您可以指定一个锁对象,并且只有持有该锁的线程才能进入并执行同步代码块内的代码。
示例
public class SynchronizedBlockExample {
private final Object lock = new Object();
private int count = 0;
// 非同步方法,但包含同步代码块
public void increment() {
// 其他非同步代码
// ...
// 同步代码块
synchronized (lock) {
count++;
}
// 其他非同步代码
// ...
}
public int getCount() {
return count;
}
public static void main(String[] args) {
SynchronizedBlockExample example = new SynchronizedBlockExample();
// 创建多个线程来测试同步
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 预期输出为2000,因为increment方法中的同步代码块是同步的
System.out.println("Final count: " + example.getCount());
}
}
在这些示例中,synchronized
关键字确保了increment
方法的同步执行或同步代码块内的代码只能被一个线程同时访问,从而避免了并发修改导致的竞态条件。
二、ReentrantLock 类
- ReentrantLock 是 Java 中 java.util.concurrent.locks 包提供的一个可重入的互斥锁,它提供了与 synchronized 关键字类似的同步功能,但更加灵活和强大。
- ReentrantLock 提供了显式锁定和解锁的操作,以及中断正在等待获取锁的线程的能力。
- 在使用 ReentrantLock 时,必须确保在 finally 块中释放锁,以避免死锁的发生。
只能用于方法体中的代码块哦!!!
知识小贴士:
锁的可重入性:特指同一个线程能够多次获得同一把锁的特性,而不会引起死锁。(场景发生:递归)
互斥性:同一时间只有一个线程可以访问共享资源
示例:ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
private int count = 0;
// 使用ReentrantLock进行同步的方法
public void increment() {
lock.lock(); // 获取锁
try {
count++;
// 可以在这里添加其他需要同步的代码
} finally {
lock.unlock(); // 确保在方法结束时释放锁
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
// 创建多个线程来测试ReentrantLock的同步
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
t1.start(); // 启动线程t1
t2.start(); // 启动线程t2
try {
t1.join(); // 等待线程t1完成
t2.join(); // 等待线程t2完成
} catch (InterruptedException e) {
e.printStackTrace();
}
// 预期输出为2000,因为increment方法使用了ReentrantLock进行同步
System.out.println("Final count: " + example.getCount());
}
}
三、锁的类型
1. synchronized 类锁(即静态锁)
1. 直接在静态方法上使用
synchronized
关键字(隐式声明)这种方式是隐式声明类锁。当您在静态方法上使用
synchronized
关键字时,Java会隐式地使用该类的Class
对象作为锁。Java例子:
public class MyClass { public static synchronized void staticMethod() { // 需要同步的代码 } }
在这个例子中,
staticMethod
是一个静态同步方法,它隐式地使用了MyClass.class
作为锁对象。
2. 在同步块中使用
ClassName.class
作为锁对象(显式声明)这种方式是显式声明类锁。您可以在同步块中直接使用类的
Class
对象作为锁。Java例子:
public class MyClass { public static void staticMethodWithBlock() { synchronized (MyClass.class) { // 需要同步的代码 } } }
在这个例子中,
staticMethodWithBlock
方法中的同步块显式地使用了MyClass.class
作为锁对象。
3. 使用自定义的静态锁对象(显式声明)
这种方式也是显式声明类锁,但使用了一个自定义的静态对象作为锁。这种方式提供了更高的灵活性和可读性。
Java例子:
public class MyClass { private static final Object classLock = new Object(); public static void staticMethodWithCustomLock() { synchronized (classLock) { // 需要同步的代码 } } }
在这个例子中,
classLock
是一个自定义的静态锁对象,staticMethodWithCustomLock
方法中的同步块显式地使用了这个锁对象。
2. synchronized 对象锁
1. 在实例方法上使用synchronized关键字(隐式声明)
public class MyObject { private int count = 0; // 隐式地使用当前对象实例作为锁 public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }
在这个例子中,
increment
和getCount
方法都是同步的,它们隐式地使用了调用它们的MyObject实例作为锁。
2. 在同步块中使用对象实例作为锁对象(显式声明)
public class MyObject { private int count = 0; private final Object lock = new Object(); // 自定义锁对象 // 显式地使用自定义锁对象作为锁 public void incrementWithBlock() { synchronized (lock) { count++; } } public int getCountWithBlock() { synchronized (lock) { return count; } } }
在这个例子中,
incrementWithBlock
和getCountWithBlock
方法中的同步块显式地使用了lock对象作为锁。
这种方式提供了更高的灵活性和可读性,因为您可以自定义锁对象的名称,并在需要的地方重复使用它。
3. ReentrantLock 类锁(即静态锁)
ReentrantLock 都是显式声明的
public class MyClass { private static final Lock classLock = new ReentrantLock(); public static void staticMethod() { classLock.lock(); try { // 需要同步的代码 } finally { classLock.unlock(); } } }
在这个例子中,classLock是一个静态的ReentrantLock对象,它在静态方法staticMethod中被用来实现类级别的同步控制。
4. ReentrantLock 对象锁
ReentrantLock 都是显式声明的
public class MyObject { private int count = 0; private final Lock objectLock = new ReentrantLock(); public void increment() { objectLock.lock(); try { count++; } finally { objectLock.unlock(); } } public int getCount() { objectLock.lock(); try { return count; } finally { objectLock.unlock(); } } }
在这个例子中,
objectLock
是一个ReentrantLock
对象,它在实例方法increment
和getCount
中被用来实现对象级别的同步控制。
四、锁在开发过程中的选取(重点)
在Java中,当需要在循环调用的情况下保证同步性时,首先要考虑Bean的注入方式
Bean的注入方式:
- 注解注入:如果您是通过Spring等框架使用注解(如
@Autowired
)来注入Bean的,那么这些Bean通常是单例的(默认情况下为(@Scope(“singleton”)))。这意味着在整个应用程序中,注入的Bean实例是唯一的。因此,当您在这些Bean的方法上使用synchronized
关键字或ReentrantLock
时,实际上是在对该单例对象进行同步控制。@Autowired这种情况下,使用实例级别
的锁(隐式或显式)就足够了。- 构造器注入/new关键字:如果您是通过构造器或
new
关键字来创建Bean的实例,那么每次调用都会创建一个新的对象。在这种情况下,如果您需要在多个实例之间保持同步性(即类级别的同步),则需要使用类锁。类锁通常是通过在静态方法中使用synchronized
关键字或静态的ReentrantLock
对象来实现的。
归纳总结:
当在循环调用中需要保证同步性时,在大多数情况下,如果您正在使用Spring等框架进行依赖注入,并且Bean是单例的,那么使用实例级别的锁(隐式或显式)就足够了。
如果您需要在多个实例之间保持同步性,那么您可能需要使用类锁。
五、synchronized 与 ReentrantLock 的差异
synchronized
和ReentrantLock
在可重入性上没有本质区别,它们的主要差异体现在锁的获取和释放方式上。
锁的获取和释放
-
synchronized:
- 锁的获取是隐式的。当线程进入
synchronized
修饰的方法或代码块时,会自动获取锁。 - 锁的释放也是隐式的。当线程退出
synchronized
修饰的方法或代码块时,锁会自动释放。
- 锁的获取是隐式的。当线程进入
-
ReentrantLock:
- 锁的获取是显式的。需要手动调用
lock()
方法来获取锁。 - 锁的释放也是显式的。需要手动调用
unlock()
方法来释放锁。因此,在使用ReentrantLock
时,通常会在finally
块中释放锁,以确保在发生异常时也能正确释放锁,避免死锁。
- 锁的获取是显式的。需要手动调用
其他差异
除了锁的获取和释放方式外,synchronized
和ReentrantLock
还有以下差异:
-
锁的公平性:
synchronized
只能是非公平锁,即线程获取锁的顺序不是按照等待时间的长短来决定的。ReentrantLock
默认是非公平锁,但也可以设置为公平锁。通过构造函数的参数可以指定锁的公平性。当设置为公平锁时,线程会按照等待时间的长短来获取锁。
-
锁的等待和通知机制:
synchronized
使用Object
类的wait()
、notify()
和notifyAll()
方法来实现线程的等待和通知。这些方法必须在synchronized
代码块或synchronized
修饰的方法中调用。ReentrantLock
提供了Condition
类来实现线程的等待和通知。Condition
类提供了更灵活的线程等待和通知机制,可以避免“伪唤醒”问题。
-
锁的可中断性:
synchronized
不支持正在等待锁的线程被中断。线程会一直等待直到获取到锁。ReentrantLock
提供了lockInterruptibly()
方法支持等待锁的线程被中断。如果线程在等待锁的过程中被中断,会抛出InterruptedException
异常并释放当前获得的锁。
-
锁的超时获取:
synchronized
不支持设置超时时间来尝试获取锁。ReentrantLock
提供了tryLock(long timeout, TimeUnit unit)
方法,可以设置超时时间来尝试获取锁。如果超时时间到了还没有获取到锁,会立即返回一个布尔值表示是否成功获取锁。