悲观锁和乐观锁
悲观就是任何事都认为会往坏处发生,乐观就是认为任何事都会往好处发生。
打个比方,假如一个公司里只有一台打印机,如果多个人同时打印文件,可能出现混乱的问题,他的资料打印在了我的资料上,悲观锁就是我认为一定会出问题,所以我在使用打印机的时候直接把打印机霸占了,不允许其他人打印文件。这就是悲观锁。
同样的,我认为即使是多个人一起打印,打印机也不会出现混乱打印的情况,所以我不对打印机做出处理,而是确认下每次打印的东西即可。
Java如何实现
悲观锁
悲观锁实现主要靠 synchronized 和 ReentrantLock 给资源加锁,确保资源只分配给一个人,比如我独占打印机。等我打印完了才放开。
synchronized 可以修饰方法体,对象和变量,表示同时只有一个线程能访问。
public void performSynchronisedTask() { synchronized (this) { // 需要同步的操作 } } private Lock lock = new ReentrantLock(); lock.lock(); try { // 需要同步的操作 } finally { lock.unlock(); }
乐观锁
乐观锁可以通过 版本号机制和CAS 算法实现。
版本号机制,可以在数据库添加一个数据版本号,当对数据进行修改时同时修改版本号,比如我对这条数据进行了修改,同时对版本号进行+1,可以表示这条数据被修改了几次。
举个例子,假如线程 A 对数据进行修改,首先先读取数据库的版本号,假设此时为1,然后执行修改操作,此时线程 B 也要对数据库进行修改,获取到版本号也为1,然后线程 A 完成了修改,并将版本号修改为 2。之后线程B完成修改时用获取到的版本号和现在的版本号进行对比,结果不用同,所以线程B 对数据库的修改回滚。
CAS 算法
CAS 算法全称是 Compare And Swap(比较与交换),是一个原子操作,当操作一旦开始就不能被打断。
CAS 主要包含三个操作数:V 要更新的变量值,E 预期值,N 要写入的新值
举个例子。假设线程 A 对数据进行修改,想要将数字 6 修改为2,那么此时获取到 CAS 的三个操作数为:V=6,E=6,N=2。此时线程 B 也想对其进行修改,将 6 改为 4,那么它获取到的三个操作数分别为:V=6,E=6,N=4。线程A 首先完成了对数据的修改,获取新的 V,然后用 E 和 V 进行比较,如果相等,则表示没有被修改,所以可以继续完成修改,将 6 改为 2。之后 线程B 也要对其进行修改,获取到新的 V 是 2,但他的预期值是 6,二者不等,所以认定为被其他线程修改了,所以此次修改失败,可以重试,也可以放弃。
具体是实现是通过 Atomic 原子类进行操作的,可以去详细看一下。
问题
悲观锁
悲观锁加锁,独占打印机就会导致其他人不能打印资料,如果要打印的人很多,这就会造成阻塞问题。
乐观锁
版本号机制,高并发情况下,会导致版本号频繁变动,需要额外的开销去维护版本号。
CAS ,最常见的问题就是 ABA 问题,即当线程 A 进行修改时,其他的线程将这个元素改成了其他数字又改了回来。解决思路就是在变量前面加上版本号或者时间戳。