线程锁好比传统线程模型中的 synchronized 技术,但是比 synchronized 方式更加面向对象,与生活中的锁类似,锁本身也应该是个对象。两个线程执行的代码片段如果要实现同步互斥的效果,它们必须用用一个锁对象。锁是上在代表要做操的资源的类的内部方法中,而不是线程代码中。这篇文章主要总结一下线程锁技术中 Lock锁、ReadWriteLock 锁的使用。
1. Lock的简单使用
有了synchronized 的基础,Lock 就比较简单了,首先看一个实例:
public class LockTest { public static void main(String[] args) { new LockTest().init(); } private void init() { final Outputer outputer = new Outputer(); // 线程1打印:duoxiancheng new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(5); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } outputer.output("duoxiancheng"); } } }).start(); ; // 线程2打印:eson15 new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(5); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } outputer.output("eson15"); } } }).start(); ; } // 自定义一个类,保存锁和待执行的任务 static class Outputer { Lock lock = new ReentrantLock(); //定义一个锁,Lock是个接口,需实例化一个具体的Lock //字符串打印方法,一个个字符的打印 public void output(String name) { int len = name.length(); lock.lock(); try { for (int i = 0; i < len; i++) { System.out.print(name.charAt(i)); } System.out.println(""); } finally { lock.unlock(); //try起来的原因是万一一个线程进去了然后挂了或者抛异常了,那么这个锁根本没有释放 } } }
这个例子和前面介绍 synchronized 的例子差不多,区别在于将 synchronized 改成了 lock。从程序中可以看出,使用 Lock 的时候,需要先 new 一个 Lock 对象,然后在线程任务中需要同步的地方上锁,但是一定要记得放锁,所以使用 try 块去处理了一下,将放锁的动作放在 finally 块中了。
这是一个线程任务的情况,如果两个线程任务也不麻烦,还是在这个类中新建一个任务方法,因为 Lock 是这个类的成员变量,还是可以用这个 lock,而且必须用这个 lock,因为要实现同步互斥,必须使用同一把锁。
2. 读写锁的妙用
锁又分为读锁和写锁,读锁与读锁不互斥,读锁与写锁互斥,写锁与写锁互斥,这是由 jvm 自己控制的。这很好理解,读嘛,大家都能读,不会对数据造成修改,只要涉及到写,那就可能出问题。我们写代码的时候只要在挣钱的位置上相应的锁即可。读写锁有个接口叫 ReadWriteLock,我们可以创建具体的读写锁实例,通过读写锁也可以拿到读锁和写锁。下面看一下读写锁的例子。
2.1 读写锁的基本用法
public class ReadWriteLockTest { public static void main(String[] args) { final Queue3 q3 = new Queue3(); //封装共享的数据、读写锁和待执行的任务的类 for (int i = 0; i < 3; i++) { new Thread() { // 开启三个线程写数据 public void run() { while (true) { q3.put(new Random().nextInt(10000)); } } }.start(); new Thread() { // 开启三个线程读数据 public void run() { while (true) { q3.get(); } } }.start(); } } } class Queue3 { private Object data = null; // 共享的数据 private ReadWriteLock rwl = new ReentrantReadWriteLock();// 定义读写锁 // 读取数据的任务方法 public void get() { rwl.readLock().lock(); // 上读锁 try { System.out.println(Thread.currentThread().getName() + ":before read: " + data); // 读之前打印数据显示 Thread.sleep((long) (Math.random() * 1000)); // 睡一会儿~ System.out.println(Thread.currentThread().getName() + ":after read: " + data); // 读之后打印数据显示 } catch (InterruptedException e) { e.printStackTrace(); } finally { rwl.readLock().unlock();// 释放读锁 } } // 写数据的任务方法 public void put(Object data) { rwl.writeLock().lock(); // 上写锁 try { System.out.println(Thread.currentThread().getName() + ":before write: " + this.data); // 读之前打印数据显示 Thread.sleep((long) (Math.random() * 1000)); // 睡一会儿~ this.data = data; //写数据 System.out.println(Thread.currentThread().getName() + ":after write: " + this.data); // 读之后打印数据显示 } catch (InterruptedException e) { e.printStackTrace(); } finally { rwl.writeLock().unlock();// 释放写锁 } } }
为了说明读锁和写锁的特点(读锁和读锁不互斥,读锁与写锁互斥,写锁与写锁互斥),我先把上面两个任务方法中上锁和放锁的四行代码注释掉,来看一下运行结果。
其实不管是注释调读锁还是注释调写锁,还是全注释掉,都会出问题,写的时候会有线程去读。那么将读写锁加上后,再看一下运行结果。
可以看出,有了读写锁,各个线程运行有序,从结果来看,也印证了读锁和读锁不互斥,写锁与读锁、写锁都互斥的特点。