1.引子
昨天是1024程序员节日,看到很多朋友发了朋友圈,在这里我想到了问一个问题:1024是2的多少此方?关于这个问题,如果你还需要默算而不是靠潜意识就能回答上来,那说明你不是一个合格的程序员,开个玩笑!
下面进入正题,今天开始在高级并发编程系列中,我们正式进入第二个小节系列:锁 。说起锁你一定不会陌生。日常生活中,我们经常谈及的很多都与锁有关,你比如说:出门的时候记得把门锁 好、锁 保险柜、装个防盗锁 等等。这些都是我们生活中的锁,锁好门、锁好保险柜都是为了安全 ;它其实与我们程序中锁目的是一致的,在程序中加锁的目的也是为了安全,这又是一个证明世间万物道理想通的铁证。
我们暂且记住,加锁的目的是为了:安全。这个系列中我们准备分享这么几个内容:
- 锁定义
- 常见锁分类
- Lock接口和它的常见实现类分析
- ReentrantLock使用案例
- ReentrantReadWriteLock使用案例
今天这一篇,让我们先从入门开始,那么来吧
bash
#考考你:
1.你能用自己的话,结合你的理解给锁下一个定义吗
2.你能说出常见的锁分类有哪些吗
2.案例
2.1.第一个加锁案例
我们先看一个大家都熟悉,而可能又陌生的案例操作。在看之前,你需要先思考一下:在大多数编程语言中,都提供了:++自曾的操作,关于这个操作它是线程安全的吗?
2.1.1.案例版本一:不加锁
2.1.1.1.任务线程
csharp
/**
* 任务线程
*/
class AddITask implements Runnable{
@Override
public void run() {
// for循环,让add_i变量自增操作:10000次
for (int i = 0; i < 10000; i++){
NeedDoLockDemo.addI();
}
}
}
2.1.1.2.主程序
csharp
/**
* 演示需要加锁的基本操作
*
* @author ThinkPad
* @version 1.0
* @date 2020/10/24 21:35
*/
public class NeedDoLockDemo {
/**
* 自曾操作变量add_i
*/
public static int add_i = 0;
/**
* addI方法,实现add_i变量自曾操作
* 注意:这里没有加锁
*/
public static void addI(){
add_i ++;
}
public static void main(String[] args) {
// 创建任务线程对象
Runnable r1 = new AddITask();
// 用20个线程,并行执行自增操作
for(int i = 0; i < 20; i++){
new Thread(r1).start();
}
// 循环等待子线程执行结束
while (Thread.activeCount() > 2){
;
}
// 打印add_i变量值,预期结果:200000
System.out.println("20个线程并行自增后,结果:" + add_i);
}
}
2.1.1.3.执行结果
结果分析:
-
add_i变量初始值是:0
arduinopublic static int add_i = 0
-
通过20个线程,并行执行自增操作
scss// 用20个线程,并行执行自增操作 for(int i = 0; i < 20; i++){ new Thread(r1).start(); }
-
每个线程,自增10000次
ini// for循环,让add_i变量自增操作:10000次 for (int i = 0; i < 10000; i++){ NeedDoLockDemo.addI(); }
-
预期结果:20 * 10000 = 200000
-
实际结果:198359
-
执行结果与预期结果不符,发生了并发不安全问题
2.1.2.案例版本二:加锁
在案例版本一中,由于没有加锁,发生了并发不安全的问题。这里有一个小知识点:对于jvm来说,++操作不是一个线程安全的操作,因为自增++操作,它不是一个原子性的操作。
那如何实现自增++的放心操作,人畜无害呢?答案是:加锁。我们再来看加锁的版本。
2.1.2.1.方法addI加锁
arduino
/**
* addI方法,实现add_i变量自曾操作
* 注意:这里的synchronized关键字
*/
public synchronized static void addI(){
add_i ++;
}
2.1.2.2.执行结果
结果分析:
- 加锁后,add_i变量的最终执行结果是:200000,符合预期
2.2.锁定义及常见锁分类
2.2.1.锁定义
我们知道了,在多线程并发操作下,需要考虑线程安全的问题。这是因为有共享资源的存在,所谓共享资源即是每个线程都会去操作的资源,你需要,我也需要,可以这么去理解 ,比如说上面案例中的add_i变量。
那么问题来了,在并发操作的情况下,是不管先来后到的,即便后来,但是可能却先把活干了。这样下去,世界就乱了,没有规则没有秩序,重新陷入混沌状态!
所以需要一种规则,建立一套秩序,让程序世界不能乱!锁就是编程世界中,解决混乱的规则和秩序,它的本质是在整体并行执行的应用程序中,实现局部串行有序执行。
到了这里,我们可以用一句简洁的话来描述锁:锁是一种工具,用于控制对共享资源访问的工具。
2.2.2.锁分类
关于锁的分类,如果要细分会非常多,非常难记,且没有必要。我们关注几类常见的吧:
-
乐观锁与悲观锁
-
乐观锁
bash#1.所谓乐观锁,它是典型的乐天派,认为在操作共享资源的时候,永远都只有自己一个人(一个线程)在操作,不会有其它线程与自己争 #2.因此乐观锁本质上不加锁,而是引入了冲突检测的机制。关于冲突检测机制,在后面分享CAS的时候,我们在详细讨论 #3.乐观锁适用于:读多写少,冲突小的业务场景
-
悲观锁
bash#1.所谓悲观锁,它是典型的悲观派,时时刻刻都认为:在操作共享资源的时候,永远都有别人(别的线程),在跟自己抢! #2.因此不管三七二十一,先锁了再说,让你抢不着 #悲观锁适用于:写多读少,容易发生冲突的业务场景
-
-
可重入锁与非可重入锁
-
可重入锁
bash#1.所谓可重入锁,一个线程拿到该锁以后,在释放以前,可以重复多次获取到该锁 #2.在juc中提供的ReentrantLock即是可重入锁
-
非可重入锁
bash#1.所谓非可重入锁,一个线程拿到该锁以后,在释放以前,不能重复多次获取到该锁 #2.在jdk中提供的synchronized锁,即是非可重入锁
-
-
共享锁与排它锁
-
共享锁
bash#1.所谓共享锁,表示该锁比较大方,独乐乐不如众乐乐,一个锁可以被多个线程同时获取拥有 #2.在juc中提供的ReentrantReadWriteLock锁中的读锁,即是共享锁
-
排它锁
bash#1.所谓排它锁,表示有你没我,有我没你。即一个锁被一个线程获取以后,便不能再被其他线程获取 #2.我们平常见的较多的锁,都是排它锁。比如ReentrantLock、synchronized、ReentrantReadWriteLock中的写锁等
-