多线程·锁池与等待池(jdk1.8);在乐观锁下,真的有必要关心CAS中的ABA问题吗???

目录

进程、线程、协程

线程的目标

线程的切换

线程的管理

乐观锁策略


本来不想聊关于线程的话题,主要原因在于自己对计算机原理相关的知识了解太浅,怕耽搁大家。

可是网络上的文章太多东拼西凑、莫名其妙,最近在读一些博文的时候被气到了,于是勉强阐述一些个人在线程方面的理解。

 

进程、线程、协程

当说起线程的时候,总是避不开进程和协程,较官方些的定义如下:

进程:操作系统分配资源的基本单位,有独立的内存空间,通信效率低,切换开销大

线程:CPU调度的基本单位,共享父进程的资源,通信效率高,切换开销小

协程:数据安全,可由程序控制调度

 

在具体的使用过程中,进程相对于线程来说最主要的特点是安全,安全指的是线程存在于进程中,进程挂掉意味着所有的线程也一起销毁。比如nginx服务器,典型的多进程+多路复用,master进程负责接收请求、管理worker进程;worker进程处理请求,多路复用的目的是为了减少线程创建的开销;还有cache进程等。

协程相对于线程来说特点有两个,安全、效率。两个特点来自于协程的实现,协程使用同步实现异步,同步就意味着没有资源竞争,没有锁开销,没有切换开销;调度策略由开发者自行控制,可以回忆一下关于asm编程时的中断。

而在java中,使用最多的是线程,所以java程序员了解最多的也是线程。

 

线程的目标

首先必须要清楚使用线程的原因,最简单的,使用线程是为了提高程序性能。而线程解决的是IO密集型问题与CPU利用率问题。

IO密集指的是有大量的操作需要读/写,而此时的CPU处于等待状态,这种情况就是通常所说的BIO模式。

而我们作为一个残忍的剥削者,对于CPU老实的恶劣行径,是万万不能容忍的。这个时候就需要给他下达命令:“你先处理其他的事情吧,等会儿IO完成了再回来搞”,CPU于是把此时的IO操作先放在一边,屁颠屁颠地做其他的事情去了。

但是呢,CPU在处理其他事情的时候脑子里还记着刚才的IO操作,他可不能把这茬给忘了,所以这个做事的效率就差了一些,这个就叫存储上下文信息,即线程切换时的开销。

 

与之相对的就是计算密集,单CPU多线程不能解决计算密集问题,效果甚至比单线程更差。

比如今天你有两个计划:1.工作2.吃鸡

如果你花10s工作,再花10s吃鸡,如此反复,最终不光工作一塌糊涂,吃鸡估计也是吃不到了。

但是如果这个时候你的好哥们到你家来了,诶,现在相当于有两个CPU了。你认真工作,然后跟你哥们说:“你上我的号,帮我吃鸡吧,晚上请你吃饭”,这下就完美了,谁也不闲着,且都能专心做,这个就叫CPU充分利用。

 

线程的切换

线程的切换带来的另一个问题是共享资源的控制。

假设现在有一个餐厅,餐厅里有一张餐桌,厨师每做出一盘菜就端到餐桌上,但是呢,餐厅中没有灯,黑漆麻乌的啥也看不见。

在这里餐桌就是共享资源,餐桌上的菜就是资源的值,黑漆麻乌表示在操作资源前不知道资源的状态。

人们到了餐桌前徒手一扒拉,不论能否拿到餐盘,扒拉完一次就得走。如果有的人啥也没拿到,你说他下次还会光临吗?

后来啊,餐厅老板意识到了这个问题,于是在门口贴了一个二维码,每个客人就餐前需要扫码登记,登记完喊到谁的名字就让谁进,于是每出一盘菜老板就根据统计表里的人喊一嗓子,这个被喊到的人就能进入餐厅吃饭了,这个就是线程锁。

 

你觉得现在已经都搞定了,然而太天真了!

有的客人被翻到了牌子,进入餐厅正准备吃食,万事俱备之际,该客人突然闹肚子,遂狂奔茅厕。难道其他客人都等着他?不可能的,效率太低了!老板在该客人蹲坑的时候把他的名字记到小本本上,等他出来了,把他的名字再次登记一遍,这段时间不影响其他客人就餐,这个就叫线程的挂起与唤醒。

 

在java中,有两个概念,锁池与等待池(synchronized):

锁池:某个线程已经拥有了某个对象的锁,其他想要获取该对象的锁的线程就会进入该对象的锁池中

等待池:已经拥有某个对象的锁的线程调用了wait()方法,该线程将进入该对象的等待池中,等待池中的线程不会竞争该对象的锁

 

当执行该对象的notify()方法时,随机将等待池中的一个线程移到锁池中;当执行该对象的notifyAll()方法时,将等待池中的所有线程移动到锁池中。

锁池中的线程可以竞争该对象的锁。

 

wait()、notify()、notifyAll()实质上就是线程在锁池与等待池之间的移动,每个锁对象都有自己的锁池和等待池。

 

线程的管理

即便线程能够通过锁来控制合理的执行,但线程仍然不能够无限地创建,就算你不在乎线程切换的开销,但计算机的内存毕竟是有限的。

餐厅老板不可能让等待就餐的顾客从餐厅门口排队排到法国(麻烦法国的那位哥们帮我带瓶红酒回来,谢谢)。

 

线程池由此诞生,其最主要的作用我认为就是对于线程数量的控制:

1.降低资源消耗

2.提高响应速度

3.线程可管理性

 

在java中,常见的线程池有四种:

1. newCachedThreadPool()

2. newFixedThreadPool()

3. newScheduledThreadPool()

4. newSingleThreadExecutor()

四种线程池都是由ThreadPoolExecutor实现的,该类除了线程数以外,还有两个地方需要注意,一个是构造器参数中的taskQueue,如果定义的是一个*队列,当线程处理速度小于task添加速度时,很容易造成内存泄漏;第二个是关闭线程池时的两个方法,shutdown()和shutdownNow(),相关资料请自行查阅。

 

乐观锁策略

上面所述都是基于synchronized关键字来讲的,可以看到synchronized是悲观锁,在每次操作数据前必须先拿到锁。

与悲观锁相对的是乐观锁,乐观锁就是在每次操作数据前都假设没有冲突,当执行过程中遇到了再做处理。

 

在java中,AutomicXx和ReentrantLock都是通过CAS实现的,CAS即CompareAndSwap,一种同步非阻塞的无锁算法,通过阅读源码,很容易理解。

 

多线程·锁池与等待池(jdk1.8);在乐观锁下,真的有必要关心CAS中的ABA问题吗???

多线程·锁池与等待池(jdk1.8);在乐观锁下,真的有必要关心CAS中的ABA问题吗???

 

具体过程有3个步骤:

1. 从内存中获取旧的值V

2. 通过旧的值V计算出新的值A

3. 将内存中的值B与V比较,如果相同,则将其修改成A,否则跳到第1步

 

CAS操作可以通过CPU的单条指令来完成,理论上来说效率是非常高的,但是在大量的线程频繁修改的情况下,单条线程长时间循环比较,此时CAS的表现就不尽人意了。

另外可以看到,CAS保证的是单个变量的原子性。

还有很多文章在聊AtomicXx的时候说到ABA问题,ABA问题简单来说就是一个值从A修改成B,再从B修改回A。而某些场景下,一个合理的操作不仅依赖于结果,同时依赖于过程。单独谈CAS的时候,提起ABA是没有问题的,可是在阐述AtomicXx等内容时,莫名其妙来这么一句,让人不知所以。我倒是觉得可以将CAS理解为一个最终一致性的乐观锁。

 

其实在ABA问题上,java也提供了支持,与AtomicReference相对的AtomicStampedReference,通过一个版本号来保证了执行顺序。

 

有任何问题或者发现文章中的错误之处,欢迎私信或者邮箱过来,谢谢。

 

公主号搜索:以镒称铢

多线程·锁池与等待池(jdk1.8);在乐观锁下,真的有必要关心CAS中的ABA问题吗???