一、锁的概念与种类
锁通常用于保证多线程访问共享资源的安全性,它能有效地避免资源竞争,确保多线程之间的数据同步。Java中的锁可以分为以下几种:
1. synchronized锁
public synchronized void method() {
// critical section
}
使用synchronized关键字修饰的方法或代码块,即可将其变成同步方法(或同步代码块)。它是Java中最基本的锁实现方式。
2. ReentrantLock锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
ReentrantLock是一个可重入锁,支持公平性和非公平性操作,并提供了比synchronized更加灵活的锁控制。
3. ReadWriteLock锁
ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
try {
// read
} finally {
lock.readLock().unlock();
}
lock.writeLock().lock();
try {
// write
} finally {
lock.writeLock().unlock();
}
ReadWriteLock允许多个线程同时读取共享资源,但只能由一个线程修改资源。因此,它的读写锁分别有readLock和writeLock两种。
二、死锁与活锁
1. 死锁
死锁是指多个线程在获取锁的过程中,由于互相等待对方释放锁而陷入阻塞的状态,无法继续执行下去。如下代码就存在死锁的风险:
public class Deadlock {
private static final Object obj1 = new Object();
private static final Object obj2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (obj1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
System.out.println("Thread1 finished");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (obj2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1) {
System.out.println("Thread2 finished");
}
}
});
t1.start();
t2.start();
}
}
以上代码中,t1和t2先后获取到obj1和obj2,但又同时等待对方释放锁,从而导致死锁的产生。
2. 活锁
活锁是指多个线程在获取锁的过程中,由于过度地释放锁或者过度地交换执行权,导致线程一直在重复执行同一段代码,无法继续向前执行。
三、锁的优化
1. 锁的粗化
锁的粗化是指将一些连续的同步操作拼接在一起,减少线程竞争的次数。比如以下代码:
public synchronized void method() {
// step1
// step2
// step3
// step4
}
以上代码中,step1 ~ step4是一系列的同步操作,如果不进行粗化处理,则在每个步骤之间都需要获取和释放锁,造成较大的时间和性能消耗。可以将代码粗化,将所有同步操作都放在一个同步块中:
public void method() {
synchronized (this) {
// step1
// step2
// step3
// step4
}
}
2. 锁的细化
锁的细化是指将原本的锁拆解成多个锁,从而减少竞争的粒度。比如以下代码:
public synchronized void method() {
// step1
// step2
// step3
// step4
}
以上代码中,如果step1 ~ step4之间存在比较重的计算操作,会导致该方法在执行时无法同时处理其它的请求。可以将锁进行细化,将步骤拆解出来,从而减少竞争的粒度:
private final Object lock1 = new Object();
private final Object lock2 = new Object();
private final Object lock3 = new Object();
private final Object lock4 = new Object();
public void method() {
synchronized (lock1) {
// step1
}
synchronized (lock2) {
// step2
}
synchronized (lock3) {
// step3
}
synchronized (lock4) {
// step4
}
}
3. 乐观锁
乐观锁是一种无锁的实现方式,它假设并发的冲突非常少,并通过CAS操作来检查和更新数据。对于不频繁的冲突,乐观锁可以在多线程并发中提供良好的性能和吞吐量。Java中的Atomic类就是乐观锁的一种实现方式。
四、锁的使用场景
1. 高并发访问的共享资源
在高并发访问下,共享资源容易被多个线程同时访问,导致数据不一致等问题。这时可以使用锁来保证数据的一致性。例如收银台模拟程序中,多个售票员同时向一个账户扣款,需要使用锁保证操作的原子性。
2. 多线程间的通信
在多线程间的通信中,常常需要一个线程等待另一个线程执行完毕,在该线程继续执行。这时可以使用锁来实现线程之间的互斥和同步。例如生产者和消费者模型中,需要用锁机制来实现对共享资源(缓冲区)的同步访问。
3. 避免数据竞争
数据竞争是指当两个或多个线程同时访问某个变量,并且至少其中一个线程对变量进行了写操作,此时可能会导致变量的值不确定性,出现严重错误。这时可以使用锁来避免数据竞争的产生。
五、案例演示
以下是一个使用ReentrantLock锁的示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final Lock lock = new ReentrantLock();
private int count = 0;
public int incrementAndGet() {
lock.lock();
try {
Thread.sleep(100); // 模拟长时间计算
return ++count;
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable runnable = () -> {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ": " + counter.incrementAndGet());
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("count: " + counter.count);
}
}
运行以上代码,可以看到输出结果:
Thread-0: 1
Thread-0: 2
Thread-0: 3
Thread-1: 4
Thread-1: 5
Thread-1: 6
count: 6
以上代码中,counter是一个计数器,在incrementAndGet方法中使用了ReentrantLock锁,来保证count变量的线程安全。在main方法中,开启两个线程同时访问incrementAndGet方法,从而验证锁的效果。