.util.concurrent.lock
中的Lock
框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为Lock
的多种实现留下了空间,各种实现可能有不同的调度、性能特性或者锁定语义。
ReentrantLock
类实现了Lock
,它拥有与synchronized
相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
class Outputter1 { private Lock lock = new ReentrantLock();// 锁对象 public void output(String name) { lock.lock(); // 得到锁 try { for(int i = 0; i < name.length(); i++) { System.out.print(name.charAt(i)); } } finally { lock.unlock();// 释放锁 } } }
区别:
需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!!
上例中展示的是和synchronized相同的功能,那Lock的优势在哪里?
例如一个类对其内部共享数据data提供了get()和set()方法,如果用synchronized,则代码如下:
class syncData { private int data;// 共享数据 public synchronized void set(int data) { System.out.println(Thread.currentThread().getName() + "准备写入数据"); try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } this.data = data; System.out.println(Thread.currentThread().getName() + "写入" + this.data); } public synchronized void get() { System.out.println(Thread.currentThread().getName() + "准备读取数据"); try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "读取" + this.data); } }
然后写个测试类来用多个线程分别读写这个共享数据:
public static void main(String[] args) {
// final Data data = new Data(); final syncData data = new syncData(); // final RwLockData data = new RwLockData(); //写入 for (int i = 0; i < 3; i++) { Thread t = new Thread(new Runnable() { public void run() { for (int j = 0; j < 5; j++) { data.set(new Random().nextInt(30)); } } }); t.setName("Thread-W" + i); t.start(); } //读取 for (int i = 0; i < 3; i++) { Thread t = new Thread(new Runnable() { public void run() { for (int j = 0; j < 5; j++) { data.get(); } } }); t.setName("Thread-R" + i); t.start(); } }运行结果:
Thread-W0准备写入数据 Thread-W0写入0 Thread-W0准备写入数据 Thread-W0写入1 Thread-R1准备读取数据 Thread-R1读取1 Thread-R1准备读取数据 Thread-R1读取1 Thread-R1准备读取数据 Thread-R1读取1 Thread-R1准备读取数据 Thread-R1读取1 Thread-R1准备读取数据 Thread-R1读取1 Thread-R2准备读取数据 Thread-R2读取1 Thread-R2准备读取数据 Thread-R2读取1 Thread-R2准备读取数据 Thread-R2读取1 Thread-R2准备读取数据 Thread-R2读取1 Thread-R2准备读取数据 Thread-R2读取1 Thread-R0准备读取数据 //R0和R2可以同时读取,不应该互斥! Thread-R0读取1 Thread-R0准备读取数据 Thread-R0读取1 Thread-R0准备读取数据 Thread-R0读取1 Thread-R0准备读取数据 Thread-R0读取1 Thread-R0准备读取数据 Thread-R0读取1 Thread-W1准备写入数据 Thread-W1写入18 Thread-W1准备写入数据 Thread-W1写入16 Thread-W1准备写入数据 Thread-W1写入19 Thread-W1准备写入数据 Thread-W1写入21 Thread-W1准备写入数据 Thread-W1写入4 Thread-W2准备写入数据 Thread-W2写入10 Thread-W2准备写入数据 Thread-W2写入4 Thread-W2准备写入数据 Thread-W2写入1 Thread-W2准备写入数据 Thread-W2写入14 Thread-W2准备写入数据 Thread-W2写入2 Thread-W0准备写入数据 Thread-W0写入4 Thread-W0准备写入数据 Thread-W0写入20 Thread-W0准备写入数据 Thread-W0写入29现在一切都看起来很好!各个线程互不干扰!等等。。读取线程和写入线程互不干扰是正常的,但是两个读取线程是否需要互不干扰??
我们可以用读写锁ReadWriteLock实现:
class Data { private int data;// 共享数据 private ReadWriteLock rwl = new ReentrantReadWriteLock(); public void set(int data) { rwl.writeLock().lock();// 取到写锁 try { System.out.println(Thread.currentThread().getName() + "准备写入数据"); try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } this.data = data; System.out.println(Thread.currentThread().getName() + "写入" + this.data); } finally { rwl.writeLock().unlock();// 释放写锁 } } public void get() { rwl.readLock().lock();// 取到读锁 try { System.out.println(Thread.currentThread().getName() + "准备读取数据"); try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "读取" + this.data); } finally { rwl.readLock().unlock();// 释放读锁 } } }
运行结果
Thread-W1准备写入数据
Thread-W1写入9 Thread-W1准备写入数据 Thread-W1写入24 Thread-W1准备写入数据 Thread-W1写入12 Thread-W0准备写入数据 Thread-W0写入22 Thread-W0准备写入数据 Thread-W0写入15 Thread-W0准备写入数据 Thread-W0写入6 Thread-W0准备写入数据 Thread-W0写入13 Thread-W0准备写入数据 Thread-W0写入0 Thread-W2准备写入数据 Thread-W2写入23 Thread-W2准备写入数据 Thread-W2写入24 Thread-W2准备写入数据 Thread-W2写入24 Thread-W2准备写入数据 Thread-W2写入17 Thread-W2准备写入数据 Thread-W2写入11 Thread-R2准备读取数据 Thread-R1准备读取数据 Thread-R0准备读取数据 Thread-R0读取11 Thread-R1读取11 Thread-R2读取11 Thread-W1准备写入数据 Thread-W1写入18 Thread-W1准备写入数据 Thread-W1写入1 Thread-R0准备读取数据 Thread-R2准备读取数据 Thread-R1准备读取数据 Thread-R2读取1 Thread-R2准备读取数据 Thread-R1读取1 Thread-R0读取1 Thread-R1准备读取数据 Thread-R0准备读取数据 Thread-R0读取1 Thread-R2读取1 Thread-R2准备读取数据 Thread-R1读取1 Thread-R0准备读取数据 Thread-R1准备读取数据 Thread-R0读取1 Thread-R2读取1 Thread-R1读取1 Thread-R0准备读取数据 Thread-R1准备读取数据 Thread-R2准备读取数据 Thread-R1读取1 Thread-R2读取1 Thread-R0读取1与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)
从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。
在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。——例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁定的理想候选者。