【多线程02】关于JDK中线程同步类的学习

1,213 阅读6分钟

本文主要从锁的分类来分别描述JDKReentrantLock、Semaphore、ReentrantReadWrite类的使用,本文主要有以下内容:

  • 可重入锁
  • 悲观锁和乐观锁
  • 读写锁
  • 公平锁

注: 锁只是一种性质,并不互斥,即一个锁可以是可重入锁,同时也可以是悲观锁,或者互斥锁或者独占锁。

首先明确以下概念:

可重入锁: 当一个锁被线程持有后,该锁仍能被当前线程成功申请。即线程可以重复获得同一个锁。Java中的内置锁都是可重入锁如synchronized修饰的代码块,ReentrantLock类等。

悲观锁: 一个线程进入临界资源,会造成"自己"状态的变化,因此不允许其他线程进入,只有等到当前线程出了临界区(释放掉锁资源)之后,才允许其他线程进入。也可以称之为独占锁。如synchronized 修饰的代码 或者ReentrantLock修饰的代码

乐观锁:指的是临界区的代码允许多个线程同时访问,不担心会对我的状态发生变化,从而导致的线程安全问题。如读写锁。允许多个线程同时读。

读写锁:从对状态的操作上来定义,如去访问一个值不会修改这个值这样的行为称为,为这种行为加的锁叫读锁,反之当去修改一个值称为写操作,加的锁为写锁

读写锁具有如下性质:

  • 读读操作不阻塞
  • 读写操作会阻塞,写会阻塞读操作
  • 写写操作也会堵塞

公平锁: 按照先来后到的顺序依次获得临界资源

ReentrantLock

这一部分主要涉及以下内容

  • synchronized

  • ReentrantLock

    • lock(): 获得锁,得不到则等待
    • unlock():释放锁
    • lockInterruptibly():获得锁,得不到则等待,优先响应中断
    • tryLock():尝试去获得锁,立马返回结果,申请成功则返回true,否则返回false
    • tryLock(timeout,TimeUnit):在指定时间内去尝试申请锁,申请返回true否则返回false

需要关注的是:申请锁之后必须手动调用unlock()去执释放锁,否则该锁得不到释放,其他线程无法获取资源

ReentrantLock::lock/unlock

不得不说的Java线程中的那些事一文中,描述了synchronized关键字的作用

  • 保证了可见性,原子性,以及有序性。

在这里做一个补充说明,synchronized可用于修饰如下代码

  • 修饰非静态方法,此时这个锁对象是调用方法的实例
  • 修饰静态方法,此时锁对象是静态方法所在类
  • 出现在方法内部,修饰的局部代码段,此时锁对象是传入的对象

以修饰非静态方法为例演示"重入"现象

public class ReentrantDemo {
    public static void main(String[] args) {
        new ReentrantDemo().sayHello();
    }
    public synchronized void sayHello(){
        System.out.println("hello hi");
        sayHi();
    }
    public synchronized void sayHi(){
        System.out.println("hi hello");
    }
    // output:hello hi 
    //         hi hello
}

运行结果表明:synchronized关键字修饰两个方法如果是不可从重入的,则在打印完hello hi之后,线程就应该阻塞,而不应该打印出hi hello,下面以ReentrantLock为例:

public class ReentrantLockDemo implements Runnable {
​
    public static ReentrantLock reentrantLock = new ReentrantLock();
    public static int i = 0;
    @Override
    public void run() {
        try {
            reentrantLock.lock();
            reentrantLock.lock();
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        } finally {
            reentrantLock.unlock();
            reentrantLock.unlock();
        }
    }
}
public static void main(String[] args) {
    ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
    Thread threadA = new Thread(reentrantLockDemo);
    Thread threadB = new Thread(reentrantLockDemo);
    threadA.start();
    threadB.start();
    try {
        threadA.join();
        threadB.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(ReentrantLockDemo.i); 
    //output:200000
}

从运行结果200000可以看到reentrantLock.lock();出现了两次,也表明ReentrantLock也是一个可重入锁。

synchronizedReentrantLock具有可重入锁这个性质,在JDK1.5以前ReentrantLock的性能要优于synchronized,但是在JDK6.0之后,synchronized得到很大的优化,使得二者性能相差不大。

ReentrantLock::LockInterruptibly

lockInterruptibly方表示在申请的过程中,优先响应中断,以如下代码为例

public class ReentrantLockInterruptDemo implements Runnable {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    private int flag;
    public ReentrantLockInterruptDemo(int flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        if (flag == 1) {
            try {
                lock1.lockInterruptibly();
                Thread.sleep(3000);
                try {
                    lock2.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + " my job done");
                } finally {
                    lock2.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock1.unlock();
            }
        } else {
            try {
                lock2.lockInterruptibly();
                Thread.sleep(3000);
                try {
                    lock1.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + " my job done");
                } finally {
                    lock1.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock2.unlock();
            }
        }
​
    }
    public static void main(String[] args) throws InterruptedException {
        ReentrantLockInterruptDemo reentrantLockInterruptDemo = new ReentrantLockInterruptDemo(1);
        ReentrantLockInterruptDemo reentrantLockInterruptDemo2 = new ReentrantLockInterruptDemo(2);
        Thread thread = new Thread(reentrantLockInterruptDemo,"first");
        Thread thread2 = new Thread(reentrantLockInterruptDemo2,"second");
        thread.start();
        thread2.start();
        Thread.sleep(1000);
        thread2.interrupt();
    }
}
​

线程先申请lock1然后睡眠3s在去申请lock2,反之线程2先申请lock2在申请lock1,这样就构造出了一个死锁条件。此时中断thread1得到如下输出

lockInterruptibly.png

可以看到线程2得到成功运行,线程1响应中断。线程1并没有输出 first my job done

ReentrantLock::tryLock

Reentrant::tryLock尝试去获取一个锁,立马返回结果

public class ReentrantLockTryLockDemo implements Runnable {
    private static ReentrantLock lock1 = new ReentrantLock();
    private static ReentrantLock lock2 = new ReentrantLock();
    private int flag;
    public ReentrantLockTryLockDemo(int flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
​
        if (flag == 1) {
            if (lock1.tryLock()) {
                System.out.println(Thread.currentThread().getName() + "得到lock1");
                try {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    boolean b = lock2.tryLock();
                    System.out.println(Thread.currentThread().getName() + "lock2的申请结果:" + b);
                    if (b) {
                        try {
                            System.out.println(Thread.currentThread().getName() + " my job done");
                        } finally {
                            lock2.unlock();
                            System.out.println(Thread.currentThread().getName() + "释放lock2");
                        }
                    }
                } finally {
                    System.out.println(Thread.currentThread().getName() + "释放lock1");
                    lock1.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放lock1");
                }
​
            }
        } else {
​
            if (lock2.tryLock()) {
                System.out.println(Thread.currentThread().getName() + "得到lock2");
                try {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    boolean b = lock1.tryLock();
                    System.out.println(Thread.currentThread().getName() + "lock1的申请结果:" + b);
                    if (b) {
                        try {
                            System.out.println(Thread.currentThread().getName() + " my job done");
                        } finally {
                            lock1.unlock();
                            System.out.println(Thread.currentThread().getName() + "释放lock1");
                        }
​
                    }
                } finally {
                    System.out.println(Thread.currentThread().getName() + "释放lock2");
                    lock2.unlock();
                }
​
            }
        }
    }
    public static void main(String[] args) {
        ReentrantLockTryLockDemo lockDemo1 = new ReentrantLockTryLockDemo(1);
        ReentrantLockTryLockDemo lockDemo2 = new ReentrantLockTryLockDemo(2);
​
        Thread thread = new Thread(lockDemo1);
        Thread thread1 = new Thread(lockDemo2);
        thread.start();
        thread1.start();
    }
​
}

运行结果如下

tryLock.png

可以看出并没有因为没得锁资源而导致线程阻塞。如果将trylock()换成lock()则会导致线程死锁,两个线程都将阻塞在这里。

ReentrantLock的实现既具有悲观锁、重入锁、独占锁的性质、同时也实现了公平锁和非公平锁,默认是非公平锁,可通过构造函数来构造公平锁

// 构造函数
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
 public static void main(String[] args) {
     ReentrantLock lock = new ReentrantLock(true);// 指定为公平锁
 }
​

ReentrantLock::newCondition

ReentrantLock中还提供了Condition,他的用法和Object类中的api相似即Object::wait(),Object::singal()

  • await()
  • awaitUninterruptibly()
  • awaitNanos(timeout)
  • awaitUntil(date deadline)
  • singal()
  • singalAll()

上面的await开头方法会使当前线程释放当前锁,进入等待状态,和Object::wait()方法类似,而且Condition中的方法需要在获得ReentrantLock锁之后才能运行,否则会出现java.lang.IllegalMonitorStateException异常

public class ConditionDemo implements Runnable{
​
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();
​
    @Override
    public void run() {
        try {
            lock.lock();
            condition.await(); // 释放锁
            System.out.println("going on");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ConditionDemo demo = new ConditionDemo();
        Thread thread = new Thread(demo);
        thread.start();
        Thread.sleep(2000);
        lock.lock(); // 得到锁
        condition.signal(); // 唤醒其他线程
        lock.unlock();// 释放锁
        // output: going on
    }
}

Semaphore

信号量:信号量为javad的线程同步提供了更强大的方法,信号量允许多个线程同时访问一个资源,类似于读锁。主要方法如下

  • acquire():获取一个许可,响应中断
  • acquire(int): 获取指定数目的许可
  • acquireUniterruptibly():获取许可不响应中断
  • tryAcquire():尝试获取一个许可,如果成功返回true否则返回false,不会进行线程阻塞
  • tryAcquire(timeout,unit):指定时间内获取许可,成功返回true
  • release():释放许可

需要注意的是:获取了几个许可,就要释放几个许可,这样才能保证程序的正确性,

public class SemaphoreDemo implements Runnable{
    public Semaphore semaphore = new Semaphore(5);
​
    @Override
    public void run() {
        try {
            semaphore.acquire(2);
            System.out.println("线程:" + Thread.currentThread().getName() + "得到许可");
​
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.out.println(Thread.currentThread().getName() + "释放许可");
            semaphore.release(2);
            // semaphore.release();
        }
    }
​
    public static void main(String[] args) {
        SemaphoreDemo demo = new SemaphoreDemo();
        ExecutorService service = Executors.newFixedThreadPool(10);
        for (int i = 0; i <10; i++) {
            service.submit(demo);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

如果注释掉semaphore.release(2);打开其下面的注释代码,运行结果将如下图所示

semaphore_less_release.png

运行不会结束,也没有后续输出。这是因为这几个线程都获取了2个信号量,但是只释放了一个信号量,导致后面的线程获取不到足够的信号量来运行程序。

ReentrantReadWriteLock

读写锁:把线程的操作分为读逻辑和写逻辑,读是获取值,写是修改值

  • 读操作和读操作不会阻塞
  • 读操作和写操作会阻塞
  • 写操作和写操作相互阻塞

一个线程要么进行读操作,要么进行写操作,不能同时进行读写操作。会阻塞

public class ReentrantReadWriteLockDemo implements Runnable {
​
    public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public static Lock readLock = lock.readLock();
    public static Lock writeLock = lock.writeLock();
    public static void main(String[] args) {
        ReentrantReadWriteLockDemo runnable = new ReentrantReadWriteLockDemo();
        ExecutorService service = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            service.submit(runnable);
        }
    }
    @Override
    public void run() {
        handleLock();
    }
    public void handleLock(){
        try {
            // readLock.lock();
            writeLock.lock();
            // System.out.println(Thread.currentThread().getName() + " 读readLock " + LocalDateTime.now());
            System.out.println(Thread.currentThread().getName() + " 写writeLock " + LocalDateTime.now());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        finally {
            // readLock.unlock();
            writeLock.unlock();
        }
​
    }
}

注释掉writeLock,打开readLock相关代码,可以得到如下的运行结果:

readLock.png

可以看到,在同一时刻,线程都得到了运行,相互之间没有阻塞。

反之得到这样的结果

writeLock.png

注意不能同时打开readLock和writeLock如果这样做,将得不到任何输出。

参考资料:

  • Java高并发程序设计
  • Java并发实战