Lock 那点事儿

项目经理今天又接了一个客户需求,又要折磨我们这些程序员屌丝了。这个需求说起来很简单,做起来非常容易出错。我先简单描述一下:

这是一个在线文件编辑器。同一份文件,一个人在读的时候,其他人不能写;同理,一个人在写的时候,其他人也不能读。也就是说,要么读,要么写,这两件事情不能同时进行。

项目经理跟客户讲,“这个很容易实现的,我们是可以做的。”。什么都可以做,做不出来说是我们程序员能力不行,他一点责任都没有。领导发话了,不管怎么样,事情还是要做的。

看了一下需求,有两个问题,我得先问清楚,否则到时候做得不对,他又把负责推给我,我们项目经理经常搞这些让我背黑锅的事情。

“多人同时读可以吗?”

“当然可以啦!多少人来读都没关系,文件的内容不要变就行。”。

“多人同时写可以吗?”

“当然不行啦!你写别人也会写,文件不知道以哪份数据为准了。”。

他态度极其恶劣,算了,不跟他计较了,我的项目奖金还在他手里。赶紧完工,下班了还要回家抱小孩。

根据多年的项目实战经验,我写了一个超牛逼的 Data 类,来封装文件的数据。看起来是这样的:

01 public class Data {
02  
03     private final char[] buffer;
04  
05     public Data(int size) {
06         this.buffer = new char[size];
07         for (int i = 0; i < size; i++) {
08             buffer[i] = '*';
09         }
10     }
11  
12     public String read() {
13         StringBuilder result = new StringBuilder();
14         for (char c : buffer) {
15             result.append(c);
16         }
17         sleep(100);
18         return result.toString();
19     }
20  
21     public void write(char c) {
22         for (int i = 0; i < buffer.length; i++) {
23             buffer[i] = c;
24             sleep(100);
25         }
26     }
27  
28     private void sleep(long ms) {
29         try {
30             Thread.sleep(ms);
31         catch (InterruptedException e) {
32             e.printStackTrace();
33         }
34     }
35 }

稍微解释一下:

  1. Data 类中封装了一个 char 数组类型的 buffer 成员变量。
  2. 在构造器中传入一个 size,表示 buffer 的长度,并在其中创建并初始化这个 buffer,使其每个字符都为“*”。
  3. 提供两个方法,一个负责读取,另一个负责写入。在读取方法中只需遍历 buffer,将结果不断 append 到一个 StringBuilder 中,最终将其转为 String 并返回。
  4. 在写入方法中传入一个字符,仍然是遍历 buffer,赋值 buffer 中的每个字符,这样可以使 buffer 中每个字符都是相同的。
  5. 故意在读写方法中加入了一个 sleep() 方法,让程序运行慢一点,模拟比较耗时的操作。而且故意让写入比读取慢一点,因为将 sleep() 方法放入了 write() 方法的循环体中,而 read() 方法却没有。

当然了,以上这个示例跑通了,我想项目经理那个需求也不难实现。这也是我们平时做开发的一种习惯,先快速地写个 Demo 出来,让领导们看看,技术上走通了,我们再实现具体的需求。

好了,不就是要同时读写吗?这不就是一个典型的多线程使用场景吗?于是我快速地写了一个读取线程,让它拼命地去读取 Data 中的数据。

01 public class ReaderThread extends Thread {
02  
03     private final Data data;
04  
05     public ReaderThread(Data data) {
06         this.data = data;
07     }
08  
09     @Override
10     public void run() {
11         while (true) {
12             String result = data.read();
13             System.out.println(Thread.currentThread().getName() + " => " + result);
14         }
15     }
16 }

在 ReaderThread 中通过一个死循环去不断地读取 Data 中的数据,并将结果打印出来。

再来一个写入线程,让它使劲地向 Data 中写入数据。

01 public class WriterThread extends Thread {
02  
03     private final Data data;
04     private final String str;
05     private int index = 0;
06  
07     public WriterThread(Data data, String str) {
08         this.data = data;
09         this.str = str;
10     }
11  
12     @Override
13     public void run() {
14         while (true) {
15             char c = next();
16             data.write(c);
17         }
18     }
19  
20     private char next() {
21         char c = str.charAt(index);
22         index++;
23         if (index >= str.length()) {
24             index = 0;
25         }
26         return c;
27     }
28 }

一次性可以传入一个字符串到 WriterThread 中,它将不断获取下一个字符(请见 next() 方法),并将该字符写入 Data 中。

如果让 ReaderThread 与 WriterThread 同时工作会怎样?不妨写了一个简单的 Client 类运行试试看。

01 public class Client {
02  
03     public static void main(String[] args) {
04         Data data = new Data(10);
05  
06         new ReaderThread(data).start();
07         new ReaderThread(data).start();
08         new ReaderThread(data).start();
09         new ReaderThread(data).start();
10         new ReaderThread(data).start();
11  
12         new WriterThread(data, "ABCDEFGHI").start();
13         new WriterThread(data, "012345789").start();
14     }
15 }

我开启了 5 个 ReaderThread 与 2 个 WriterThread,模拟读得多写得少的情况,并将不同的数据写入 Data 中。

运行一下!

...
Thread-1 => AA0A0A00A0
Thread-4 => AA0A0A00A0
Thread-3 => AA0A0A00A0
Thread-2 => AA0A0A00A0
Thread-0 => AA0A0A00A0
...

为何每次读取出来的数据不一致呢?应该是输出 10 个相同的字符才对啊!Data 的 buffer 中每个字符不是应该相同吗?

如果把这个结果给项目经理看,他肯定要搞死我的。

哦!想到了!在多线程开发中,资源的访问一定要做到“共享互斥”,也就是说要“上锁”,这招还是架构师前几天才教我的,我怎能不用?

于是我用了 Java 多线程中超牛逼的 synchronized 关键字,将它放到了 read() 与 write() 方法上,这样就可以保证 synchronized 方法在同一时刻只能被一个线程调用了,其他线程将会阻挡在外。

废话少说,赶紧加两个 synchronized 运行看看吧。

01 public class Data {
02  
03     ...
04  
05     public synchronized String read() {
06         ...
07     }
08  
09     public synchronized void write(char c) {
10         ...
11     }
12  
13     ...
14 }

再运行一把!

...
Thread-0 => 1111111111
Thread-4 => CCCCCCCCCC
Thread-3 => CCCCCCCCCC
Thread-2 => CCCCCCCCCC
Thread-1 => CCCCCCCCCC
...

终于搞定啦!这下子项目经理应该满意了吧?

“不错!这效果很好啊,同时写同时读,而且每次读出来的数据都一样,技术上应该是走通了,这个需求应该可以实现了吧?” 项目经理问。

“没问题啊!小意思!” 我高兴的答。

“这是一个在线文件编辑器,你考虑过性能问题吗?” 架构师突然问了一句。

“性能很好啊!”

“你可以在 ReaderThread 中每调用 10 次 read() 方法,就打印 1 次所耗时间看看。”

“好啊!”

这还不简单,我快速地给 ReaderThread 的 run() 方法中加了几行代码,测试一下运行所消耗的时间。

01 public class ReaderThread extends Thread {
02  
03     ...
04  
05     @Override
06     public void run() {
07         while (true) {
08             long begin = System.currentTimeMillis();
09             for (int i = 0; i < 10; i++) {
10                 String result = data.read();
11                 System.out.println(Thread.currentThread().getName() + " => " + result);
12             }
13             long time = System.currentTimeMillis() - begin;
14             System.out.println(Thread.currentThread().getName() + " -- " + time + "ms");
15         }
16     }
17 }

跑起来吧!

...
Thread-2 => IIIIIIIIII
Thread-2 -- 24802ms
Thread-3 => IIIIIIIIII
Thread-3 -- 24901ms
Thread-4 => IIIIIIIIII
Thread-4 -- 25001ms
Thread-0 => 3333333333
...
Thread-0 => 1111111111
Thread-0 -- 55305ms
Thread-4 => CCCCCCCCCC
Thread-3 => CCCCCCCCCC
Thread-2 => CCCCCCCCCC
Thread-1 => CCCCCCCCCC
Thread-1 -- 58705ms
Thread-2 => CCCCCCCCCC
...

我随意挑选了其中这 5 个 ReaderThread 所消耗的时间,平均值是:37742.8 毫秒,折合 37.8 秒。

我心里也没谱了,这性能到底是否需要优化呢?于是我带着测试结果,去向架构师请教。

他看到了这样的结果,微笑着摇了摇头。从他鄙视而又猥琐的表情上,我可以推测,这次他又要在我面前露一手了。

来吧,我给你写一个 ReadWriteLock,你自己去看吧。

随后,架构师用他熟练的手指,疯狂地在键盘上敲了一堆让我一知半解的东西。

01 public class ReadWriteLock {
02  
03     private int readThreadCounter = 0;      // 正在读取的线程数(0个或多个)
04     private int waitingWriteCounter = 0;    // 等待写入的线程数(0个或多个)
05     private int writeThreadCounter = 0;     // 正在写入的线程数(0个或1个)
06     private boolean writeFlag = true;       // 是否对写入优先(默认为是)
07  
08     // 读取加锁
09     public synchronized void readLock() throws InterruptedException {
10         // 若存在正在写入的线程,或当写入优先时存在等待写入的线程,则将当前线程设置为等待状态
11         while (writeThreadCounter > 0 || (writeFlag && waitingWriteCounter > 0)) {
12             wait();
13         }
14         // 使正在读取的线程数加一
15         readThreadCounter++;
16     }
17  
18     // 读取解锁
19     public synchronized void readUnlock() {
20         // 使正在读取的线程数减一
21         readThreadCounter--;
22         // 读取结束,对写入优先
23         writeFlag = true;
24         // 通知所有处于 wait 状态的线程
25         notifyAll();
26     }
27  
28     // 写入加锁
29     public synchronized void writeLock() throws InterruptedException {
30         // 使等待写入的线程数加一
31         waitingWriteCounter++;
32         try {
33             // 若存在正在读取的线程,或存在正在写入的线程,则将当前线程设置为等待状态
34             while (readThreadCounter > 0 || writeThreadCounter > 0) {
35                 wait();
36             }
37         finally {
38             // 使等待写入的线程数减一
39             waitingWriteCounter--;
40         }
41         // 使正在写入的线程数加一
42         writeThreadCounter++;
43     }
44  
45     // 写入解锁
46     public synchronized void writeUnlock() {
47         // 使正在写入的线程数减一
48         writeThreadCounter--;
49         // 写入结束,对读取优先
50         writeFlag = false;
51         // 通知所有处于等待状态的线程
52         notifyAll();
53     }
54 }

我看出来了,架构师特意写了很多注释,免得我总是去烦他。

代码不解释了,看看注释吧,有疑问可以给我留言哦!

此时,Data 类还需要稍作改写。

01 public class Data {
02  
03     ...
04  
05     private final ReadWriteLock lock = new ReadWriteLock(); // 创建读写锁
06  
07     ...
08  
09     public String read() throws InterruptedException {
10         lock.readLock(); // 读取上锁
11         try {
12             return doRead(); // 执行读取操作
13         finally {
14             lock.readUnlock(); // 读取解锁
15         }
16     }
17  
18     public void write(char c) throws InterruptedException {
19         lock.writeLock(); // 写入上锁
20         try {
21             doWrite(c); // 执行写入操作
22         finally {
23             lock.writeUnlock(); // 写入解锁
24         }
25     }
26  
27     private String doRead() {
28         StringBuilder result = new StringBuilder();
29         for (char c : buffer) {
30             result.append(c);
31         }
32         sleep(100);
33         return result.toString();
34     }
35  
36     private void doWrite(char c) {
37         for (int i = 0; i < buffer.length; i++) {
38             buffer[i] = c;
39             sleep(100);
40         }
41     }
42  
43     ...
44 }

同样的 Client 类,我再运行一把试试看,性能是否有提高呢?

...
Thread-1 => 4444444444
Thread-2 -- 14000ms
Thread-0 -- 14001ms
Thread-3 -- 14000ms
Thread-4 -- 14000ms
Thread-1 -- 14001ms
Thread-4 => IIIIIIIIII
...

平均下来是 14000.4 毫秒,折合 14.0 秒,比以前快了 63%,而且输出的结果都比以前平稳(以前忽高忽低的)。

果然是架构师,真让我们这些程序员崇拜啊!

最后架构师过来,看到我在那里得意地笑。他拍拍我的肩,对我说:“别乐了,其实 JDK 1.5 中已经有 ReadWriteLock 了,我这个只不过是一个精简版而已,去看看 java.util.concurrent.locks.ReadWriteLock 吧,你一定会震精!”。

看来我真是孤陋寡闻啊,打开 JDK API 看到了 ReadWriteLock:

1 public interface ReadWriteLock {
2  
3     Lock readLock();
4  
5     Lock writeLock();
6 }

可以通过 ReadWriteLock 接口来获取 ReadLock 与 WriteLock,它们都是 Lock 对象,这也是一个接口。

官方提供了一个 ReadWriteLock 接口的实现类 java.util.concurrent.locks.ReentrantReadWriteLock。

01 public interface Lock {
02  
03     void lock();
04  
05     void lockInterruptibly() throws InterruptedException;
06  
07     boolean tryLock();
08  
09     boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
10  
11     void unlock();
12  
13     Condition newCondition();
14 }

该接口中,有两个非常重要的方法:lock() 与 unlock(),分别表示“上锁”与“解锁”。

尝试用一下 JDK 的 ReadWriteLock 吧。

01 public class Data {
02  
03     ...
04  
05     private final ReadWriteLock lock = new ReentrantReadWriteLock(); // 创建读写锁
06     private final Lock readLock = lock.readLock();    // 获取读锁
07     private final Lock writeLock = lock.writeLock();  // 获取写锁
08  
09     ...
10  
11     public String read() throws InterruptedException {
12         readLock.lock(); // 读取上锁
13         try {
14             return doRead(); // 执行读取操作
15         finally {
16             readLock.unlock(); // 读取解锁
17         }
18     }
19  
20     public void write(char c) throws InterruptedException {
21         writeLock.lock(); // 写入上锁
22         try {
23             doWrite(c); // 执行写入操作
24         finally {
25             writeLock.unlock(); // 写入解锁
26         }
27     }
28  
29     ...
30 }

再次运行一把看看效果。

使用了 JDK 的 ReadWriteLock,性能与自己实现的 ReadWriteLock 差不多,大家不妨自己试一下吧。

此外 JDK 还提供了一个更加简单的 ReentrantLock,它可以取代 synchronized,确保获取更高的吞吐率,一般可以这样来做:

以前的做法:

1 public synchronized void foo() {
2     ...
3 }

现在的做法:

01 private final Lock lock = new ReentrantLock();
02  
03 public void foo() {
04     lock.lock();
05     try {
06         ...
07     finally {
08         lock.unlock();
09     }
10 }
这里提供两张 synchronized 与 Lock 的性能测试对比:

参考:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html



总结

当系统中出现不同的读写线程同时访问某一资源时,需要考虑共享互斥问题,可使用 synchronized 解决次问题。若对性能要求较高的情况下,可考虑使用 ReadWriteLock 接口及其 ReentrantReadWriteLock 实现类,当然,自己实现一个 ReadWriteLock 也是一种解决方案。此外,为了在高并发情况下获取较高的吞吐率,建议使用 Lock 接口及其 ReentrantLock 实现类来替换以前的 synchronized 方法或代码块。

关于 Lock 那点事儿当然还不止这些,今天先写到这里吧,以上内容是否对大家有用,敬请点评!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值