1. synchronized

1.1. synchronized 缺陷

1.1.1. 缺陷一

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1. 获取锁的线程执行完了,然后释放锁。
  2. 线程执行发生异常,自动释放锁。

根据上边两点,可以看出synchronized线程如果被阻塞了,会导致其他线程便只能干巴巴地等待

1.1.2. 缺陷二

当多个线程同时进行读写操作时,为了防止读写出现安全问题,通常会对读写操作都加上 synchronized 以保证线程安全。但是会产生以下两种情况:

  1. 写线程加 synchronized 是没有问题的,这保证了每次只能一个线程去写。
  2. 但是读数据时,往往是需要支持多个线程同时读的,一旦加了 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 和 Lock_加锁

上边的结果看得出来,不加读锁,会导致读到了正在修改中的数据,但是加了读锁 synchronized 又导致每次只能一个线程去读,这明显看得出 synchronized 满足不了多线程的读写需求。

1.2. Synchronized 和 Lock 区别

  1. synchronized是java内置关键字,Lock是个java类;
  2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  3. synchronized会自动释放锁,Lock需在finally中手工释放锁;
2. Lock 锁

什么是 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() 加锁打印结果如下:
Synchronized 和 Lock_抛出异常_02

使用 lockInterruptibly() 加锁打印结果如下:
Synchronized 和 Lock_读锁_03

3. ReadWriteLock 读写锁

什么是 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();
        });
    }
}

Synchronized 和 Lock_公平锁_04

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接口。