转载

Java并发编程那些事儿(二)——锁

原创:花括号MC(微信公众号:huakuohao-mc)。关注JAVA基础编程及大数据,注重经验分享及个人成长。

这是并发编程系列的第二篇文章。上一篇介绍了线程和任务的关系,以及如何创建线程。这篇说一下多线程如何正确的访问共享可变资源。

所谓的共享可变资源就是每个线程都可以读也都可以写的资源。如何让多个线程正确的修改以及读取共享变量是一门学问。

问题引入

如下段代码实现了一个线程计数器功能,也就是统计一下有多少个线程执行了任务。

首先定义一个任务

使用线程驱动任务

上面的示例代码,如果你需要多执行几次,就会发现得到的结果是不一样的,可见并发程序的错误是多么隐蔽。我在本地环境拿到的结果为1W或者9999,如下所示:

Java并发编程那些事儿(二)——锁

问题分析

理论上,我们启动了 1W 个线程,但结果却有可能是 9999 。这个问题的原因有两个,第一个原因是两个线程可以同时修改和读取变量 count 。第二个原因是在 java 里面的自增 ++ 操作不是原子操作。

当执行 count++ 操作的时候,实际上是三个动作,先读取 count 的当前值,然后将 count1 ,最后将结果写入 count

假设第一个线程读取 count 之后,得到 count 的值为 0 ,然后执行自增操作的过程中,第二个线程也来读取 count 的值,那么第二个线程得到的值还是 0 ,然后第二个线程也基于 0 做自增操作,这样两个线程执行完得到的结果都是 1 ,并不是 2

解决方案

其实上面的问题可以概括为“多线程如何正确的使用共享可变资源"的问题,这也是并发编程最为核心的问题。对于这个问题,通常有两种解决方案。

第一种方案就是对共享资源进行加锁同步处理,锁可以保证同一时刻只有一个线程在使用共享资源;

第二种方案就是不共享变量,比如每个线程都持有一份共享变量的Copy,或者只有一个线程可以修改共享变量,其他线程只读。

锁的介绍

当我们对一个资源或者一段代码进行加锁处理的时候,表示同一时刻只有一个线程可以执行该段代码,当一个线程执行完并释放锁资源之后,其他线程才有机会获取该资源继续执行。

这个过程好比多个人在争抢一个卫生间的坑位,当卫生间被你抢到之后,立刻把卫生间锁住,这样其他人就没办法影响你使用了,如果你不加锁,就会很多人不断的把门拉开,对你产生影响。

锁分类

从使用方式来看, Java 提供了两种锁,第一种锁称为内制锁也就是大家熟悉的 synchronized 。第二种锁称为显示锁也就是 ReentrantLock

内置锁-synchronized

我们可以通过使用 synchronizedincrease() 方法进行加锁同步处理,这样可以保证同一时刻只有一个线程使用共享资源 count

java 中每个对象或者类都含有一个单一的内置锁,也叫做监视器锁。线程进入同步代码块时会自动获取锁,离开时会自动释放锁。

如果一个对象中有多个方法都是加锁的,那么他们共享同一把锁,假设一个对象包含 publicsynchronizedvoidf() 方法,以及 publicsynchronizedvoidg(); 方法,如果某个线程调用了 f() ,那么其他线程必须等 f() 结束并释放锁之后,才能继续调用 f() 或者 g()

内置锁的重入

一个线程想获取一个由其他线程持有的锁时会发生阻塞,但是一个线程可以重新获得由他自己持有的锁。比如一个子类改写父类的 synchronized 修饰的方法,然后再次调用父类中的方法,如果没有锁重入机制,那么将发生死锁。

临界区

除了上面的锁住整个方法以外,还可以锁住部分代码块。这被称为同步控制块,也叫临界区。这样做的目的可以显著提高程序性能,因为缩小了锁粒度。

显示锁-ReentrantLock

对于上面的任务计数器代码,除了内置锁以外,还可以使用显示锁 ReentrantLock 来实现。示例代码如下

对于显示的锁,在上面的代码量明显比内制锁要多,因为显示锁除了要自己声明锁以外,还要自己手动释放锁,如果忘记释放锁,那将会是灾难的。

但是显示锁也有自己的特点,比如更加灵活,你可以在发生异常时,清理线程资源。但是如果是内制锁,你能做的恐怕就不多了。

除此之外,使用显示锁对资源进行获取时,可以指定时间范围,比如通过 tryLock(longtimeout,TimeUnitunit) 方法,如果在指定时间内没有获取,线程可以去执行一些其他事情,不用长时间处于阻塞状态。

显示锁-读写分离锁

从名字可以看出这是两把锁,一个是读锁,一个是写锁。读写锁允许多个读线程同时执行,但是当有写线程操作的时候还是只有一个线程可以操作。 读写锁在读多写少的情况下,可以显著提高性能,因为多个读操作时并行执行。

一个典型的读多写少的应用场景就是缓存。下面的代码示例分别使用显示锁和读写分离锁来实现两个不同的缓存。可以明显感受到两个缓存的性能区别。

抽象类 DemoCache ,定义了缓存的基本操作,显示锁实现的缓存和读写分离锁实现的缓存都继承自该类。

DemoLockCache ,使用显示锁实现的缓存,性能比较差。

DemoReadWriteLockCache , 使用读写锁实现的缓存,性能较好。

创建两个任务,一个用于读操作,一个用于写操作。

用于读操作的 DemoCacheReadTask

用于写操作的 DemoCacheWriteTask

测试类 DemoCacheTest

结束

这篇文章主要介绍了,如何通过加锁的方式,实现共享可变资源的正确访问。其中包括内置锁,显示锁,读写锁。在一般情况下建议大家使用内置锁,如果内置锁不能满足要求可以考虑使用显示锁,但一定不要忘记手动释放锁。在读多写少的场景,可以考虑使用读写分离锁提高性能。

下一篇介绍不使用锁的情况下,如何做到正确的访问共享可变资源。

推荐阅读:

1. Java并发编程那些事儿(一) ——任务与线程

2. Java8的Stream流真香,没体验过的永远不知道

3. Awk这件上古神兵你会用了吗

4. 手把手教你搭建一套ELK日志搜索运维平台

·END·

花括号MC

Java·大数据·个人成长

Java并发编程那些事儿(二)——锁

微信号:huakuohao-mc

点一下你会更好看耶

Java并发编程那些事儿(二)——锁

原文  http://mp.weixin.qq.com/s?__biz=MzUzMzE4MDY0Nw==&mid=2247483967&idx=1&sn=e2edfe9456c165cbd40bb2c0aa6bcbbd
正文到此结束
Loading...