本文主要是介绍多线程·锁池与等待池(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,一种同步非阻塞的无锁算法,通过阅读源码,很容易理解。
具体过程有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问题吗???的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!