什么是锁
假设世界上只有一个人,那么锁是不会出现的。因为锁是为了防范外人的不法行为,如果只有一个人,那就谈不上外人,那当然锁就没有必要。即使有多个人,如果这两个人井水不犯河水,也没有提防的必要。可是现实确实,人是一个社会性的存在,人与人之间不可避免的存在各种依赖于竞争等各种情况,所以就需要制定一定的规则对其进行协调。
同样在软件开发中的锁,也是一种在多线程下的对资源的访问进行协调的手段。
协调与同步基本是中同义替换,这就不难理解在 Java 中写多线程程序使用锁时会使用 synchronized 了。示例代码如下:
Object monitor = new Object();
void doSomething(){
sychronized(monitor){
//业务逻辑省略
}
}
让我们详细考察下上述示例代码。
既然是锁是一个协调者,而我们的口头禅都是“从中协调”。由此可见,协调者是一个中间方。而在上述代码中的 monitor 就是这样一个中间方,它夹在两个(或多个)线程之间做协调工作。上述代码的工作方式有个学名叫做监视器模式(其实英文名就是 monitor),它工作的原理是,在 sychronized 代码块的入口和出口处,分别执行 monitorenter 和monitorexit 两条字节码指令,本质上,就是在执行代码之前,在 monitor里写入当前线程的 id,其它线程看到 monitor 已经有了线程,而且不是自己,就会进入等待,知道当前线程退出(正常执行完毕或者异常),等待的线程才会被唤醒,再次进入锁的竞争过程中。
让我们再来看看,锁它锁的究竟是什么。既然锁是一种多线程的协调,那就是说,锁是在对线程运行本身进行协调。至于线程本身执行的是什么逻辑线程并不关心。也就是说,锁并不直接保证程序(或者说数据)的一致性问题。这个问题由开发者自己保证。
锁保证的是,当前只有一个线程执行示例中的代码块。sychronized 包围的代码也有一个学名就 “临界区”。也就是说,统一时刻只有一个线程在执行临界区代码。一句话概括就是:锁的是一段代码,而不是共享的资源。锁保证共享自己安全的方式,就是保证临界区代码是访问共享资源的唯一路径。
锁的分类
谈了锁的作用,那么有一个问题,为什么会存在这么多的锁的种类呢?如乐观锁,悲观锁,读写锁,重入锁等。这么多锁出现的动因是什么呢?
锁是一种对并发程序协调的方式,用于保证程序的正确性(一致性)。而显然,这种协调对性能是有损的。可以想象多了一个协调者,系统就变复杂了,程序需要多出来一些协调的工作。而这么多种的锁的出现的原因,无非是在对锁带来的新的问题。其中最主要的就是性能问题。
也就是说,各种锁的出现,都是针对不同的场景,在性能与一致性上做的取舍。
乐观 VS 悲观
既然锁是一种协调,有人就想了,不加锁可不可以实现同一目的呢?这些人就是所谓的“本质思考者”。他们在想,既然锁是一种协调机制,可不可以去掉中间的协调方呢?这就是乐观锁了。乐观锁实际就是根本不加锁,在程序执行时自己对当前状态做判断,是否合适进行操作。
全局锁 vs 局部锁
全局锁有一个STW(stop the world)的时间。只要有一个线程在执行,所有线程都得等着。这就相当于,张三在睡觉的时候,全世界所有人都不能睡。其它人就怒了:我他妈找谁惹谁了?是啊,只有张三睡的不是别人的床,为什么别人不能谁。
类比到线程里,只有线程之间的逻辑没有依赖关系,完全可以一起执行嘛。也就是说,我们不能全部一棒子打死,要把大的逻辑拆分成小逻辑,没有依赖的线程完全可以并行执行。
比如在 JUC中的ConcurrentHashmap就是利用了分段锁这种局部锁,降低了锁的力度,如果两个线程不是操作同一数据元素,完全可以并发运行嘛。
数据库中也有这个概念。下面以 mysql 为例。
mysql中的锁
mysql 是插件式架构。在MyIsam中只有一种锁,那就是表锁。在Innodb中存在另外一种锁,那就是行锁,这在保证一致性的同时也保证了一定的并发性能。我想这是Innodb越来越被广泛采纳的原因之一吧。
mysql 根据在不同的查询条件下,是通过选择不同的加锁算法(记录锁,行锁,间隙锁,临键锁)来实现局部锁的。