1.1. synchronized 缺陷
1.1.1. 缺陷一
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
- 获取锁的线程执行完了,然后释放锁。
- 线程执行发生异常,自动释放锁。
根据上边两点,可以看出synchronized线程如果被阻塞了,会导致其他线程便只能干巴巴地等待。
1.1.2. 缺陷二
当多个线程同时进行读写操作时,为了防止读写出现安全问题,通常会对读写操作都加上 synchronized 以保证线程安全。但是会产生以下两种情况:
- 写线程加 synchronized 是没有问题的,这保证了每次只能一个线程去写。
- 但是读数据时,往往是需要支持多个线程同时读的,一旦加了 synchronized 会导致每次读数据时,只能有一条线程去读数据。
根据上边的情况,会对读锁产生疑问:为什么读线程要加锁?读线程又不修改数据,只需要加个 volatile 关键字保证可见性不行吗?
至于读线程要不要加锁,可以看下边的代码。
public class Test {
volatile static int num = 0;
static final int max = 2;
public synchronized static void write() {
num++;
if (num > max) {
num = 0;
}
}
public static void read() {
if (num > max) {
System.err.println(String.format("数据异常,ThreadName=%s, num = %s", Thread.currentThread().getName(), num));
System.exit(0);
}
System.out.println(String.format("ThreadName=%s, num = %s", Thread.currentThread().getName(), num));
}
public static void main(String[] args) {
// 循环 3 次,创建 3个写线程,3个读线程
IntStream.range(0, 3).forEach(i -> {
new Thread(() -> {
while (true) {
write();
}
}, "write" + i).start();
new Thread(() -> {
while (true) {
read();
}
}, "read" + i).start();
});
}
}
上边的结果看得出来,不加读锁,会导致读到了正在修改中的数据,但是加了读锁 synchronized 又导致每次只能一个线程去读,这明显看得出 synchronized 满足不了多线程的读写需求。
1.2. Synchronized 和 Lock 区别
- synchronized是java内置关键字,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁,Lock需在finally中手工释放锁;
什么是 Lock?
Lock 是一个接口主要方法有以下
public interface Lock {
// 用来获取锁。如果锁已被其他线程获取,则进行等待。
void lock();
// 用来获取锁。但是可以被interrupt()方法中断获取锁,直接抛出异常。
void lockInterruptibly() throws InterruptedException;
// 用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false
boolean tryLock();
// 和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法
Condition newCondition();
}
lock() 和 unlock() 方法
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
采用Lock,必须主动去释放锁。一般来说,使用Lock必须在try{}catch{}中执行,并且将unlock放在finally中,以保证锁一定被被释放。
通常使用Lock是以下面这种形式去使用的:
Lock lock = new ReentrantLock();// ReentrantLock是可重入锁
lock.lock();// 获得锁
try{
//处理任务
}finally{
lock.unlock(); //释放锁
}
tryLock() 方法
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。
一般情况下tryLock获取锁时是这样使用的:
Lock lock = new ReentrantLock();// ReentrantLock是可重入锁
if(lock.tryLock()) {
try{
//处理任务
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
lockInterruptibly() 方法
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则调用interrupt()方法能够中断线程的等待状态(中断等待获取锁的状态)。
由于lockInterruptibly()方法中申明了抛出异常,所以lock.lockInterruptibly()必须放在try块中或者在调用方法的中声明抛出InterruptedException。
因此lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
或者
public void method() {
try {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
} catch (InterruptedException e) {
//...
}
}
lockInterruptibly() 加锁和 lock() 加锁被 interrupt() 中断对比
lock() 方法会忽略interrupt()方法的中断请求,继续等待获取锁直到成功,成功获取锁之后再抛出异常。
lockInterruptibly() 和 lock() 不同,lockInterruptibly() 直接抛出中断异常立即响应中断
public class Test {
static Lock lock = new ReentrantLock();
public static void write() {
try {
lock.lock(); // 使用 lock 加锁
// lock.lockInterruptibly(); // 使用 lockInterruptibly 加锁
try {
System.out.println(Thread.currentThread().getName() + ">>>得到锁");
Thread.sleep(3000);
} catch (Exception e) {
System.out.println(Thread.currentThread().getName() + "===先得到锁,再抛出异常");
}finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "<<<释放锁");
}
} catch (Exception e1) {
System.out.println(Thread.currentThread().getName() + "+++不等待获取锁了,直接抛出异常");
}
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
write();
}, "t1");
Thread t2 = new Thread(() -> {
write();
}, "t2");
t1.start();
Thread.sleep(10);
t2.start();
t2.interrupt();// 中断 t2 线程
}
}
使用 lock() 加锁打印结果如下:
使用 lockInterruptibly() 加锁打印结果如下:
什么是 ReadWriteLock?
ReadWriteLock 是读写锁,也是一个接口,在它里面只定义了两个方法
public interface ReadWriteLock {
// 读锁
Lock readLock();
// 写锁
Lock writeLock();
}
readLock() 和 writeLock() 的使用方式
-
写锁:写锁只能有一个线程写,当 write1 线程占用写锁之后,其他写线程 write* 以及读线程 read* 都需要等待,只有当 write1 线程释放锁之后,其他的 write* 以及 read* 线程才能尝试获取锁。
-
读锁:读锁可以多个线程同时读,当 read1 线程占用读锁之后,其他读线程 read* 也能同时进入锁中读取数据。但是在 read1 线程释放锁之前,所有写线程 write* 都需要等待。
上边已经说过 synchronized 满足不了多线程的读写需求。这次可以使用 ReadWriteLock 来解决多线程的读写问题。
由下边的程序和结果看得出来,加读写锁之后,多线程读写数据不会存在任何安全问题。
public class Test {
static volatile int num = 0;
static final int max = 2;
static ReadWriteLock rwl = new ReentrantReadWriteLock(); // 可重入读写锁
public static void write() {
rwl.writeLock().lock();// 写锁,每次只能一个线程写
try {
num++;
if (num > max) {
num = 0;
}
} finally {
rwl.writeLock().unlock();
}
}
public static void read() {
rwl.readLock().lock();// 读锁,可以多个线程同时读
try {
if (num > max) {
System.err.println(String.format("数据异常,ThreadName=%s, num = %s", Thread.currentThread().getName(), num));
System.exit(0);
}
System.out.println(String.format("ThreadName=%s, num = %s", Thread.currentThread().getName(), num));
} finally {
rwl.readLock().unlock();
}
}
public static void main(String[] args) {
// 循环 3 次,创建 3个写线程,3个读线程
IntStream.range(0, 3).forEach(i -> {
new Thread(() -> {
while (true) {
write();
}
}, "write" + i).start();
new Thread(() -> {
while (true) {
read();
}
}, "read" + i).start();
});
}
}
4. 锁相关概念
4.1. 可重入锁
像synchronized、ReentrantLock、ReentrantReadWriteLock都是可重入锁。
举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
4.2. 可中断锁
顾名思义,就是可以响应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果线程A获取了锁,线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。
4.3. 公平/非公平锁
比如同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
- synchronized:非公平锁
- ReentrantLock:可以非公平锁,也可以公平锁
- ReentrantReadWriteLock:可以非公平锁,也可以公平锁
ReentrantLock 和 ReentrantReadWriteLock 怎么实现公平锁?
ReentrantLock 和 ReentrantReadWriteLock 要实现公平锁,可以看它们的源码,在创建对象时可以指定使用公平锁还是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
从上边的构造方法可以看出,创建公平锁可以使用以下方式ReentrantLock lock = new ReentrantLock(true);
另外在ReentrantLock类中定义了很多方法,比如:
isFair() //判断锁是否是公平锁
isLocked() //判断锁是否被任何线程获取了
isHeldByCurrentThread() //判断锁是否被当前线程获取了
hasQueuedThreads() //判断是否有线程在等待该锁
在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要注意,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。