首页 > concurrent

 

CAS:Compare and Swap, 翻译成比较并与交换。

 

CAS的定义是这样的,看看就好,我也是百度复制的,懒得自己描述了。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

以上就是CAS定义。

 

看完CAS的定义后,就可以说乐观锁机制了,在软件开发之中,遇到并发问题时大量的场景使用到了乐观锁机制。

与乐观锁机制相对应的,即为悲观锁, 乐观和悲观这俩词体现在哪儿呢,既然知道一点:场景为并发,那么就可以得知乐观和悲观就体现在认为并发操作是否真的有存在。

 

首先,只要是并发修改的场景,为了保证并发安全,加锁机制是肯定需要的, 就看你用个啥解决方案。

若果说我认为并发场景时时有,每秒钟都有大量的线程同时请求。 那么可以说 我比较悲观。

如果说我觉得并发并不是那么多,可能半天才有几个并发,而且操作一般还很快速,那么可以说 我比较乐观。

 

既然得知了以上的悲观乐观概念,那么解决方案也就浮现而出了

悲观锁:我觉得你并发多,那我就直接用 阻塞/拒绝等 方式来控制你的请求,让数据只可能被一个线程同时操作。

乐观锁:我认为你没啥并发 干脆就不加锁了,你要修改啥玩意都直接改,不过为了保证并发安全,要有个判定机制。 一般来说那就是CAS。

 

悲观锁这玩意就不具体说了,就是为了控制临界区只能同时被一个线程访问设计出来的,遇到并发就线程阻塞/请求失败,效率低,并且加解锁这套操作指不定要调用什么消耗资源的命令,比如java中的 synchronized关键字(1.6以前) 就是一个标准的悲观锁,加解锁时还要使用操作系统的命令导致cpu用户态与内核态的切换,加解锁的时间指不定比你执行的临界区代码时间还多。

 

下面就主要说下乐观锁在哪些地方有用到

 

如果是数据库的并发场景:

这里我说的数据库并发场景只讨论”锁” 如何控制并发问题, 不讨论脏读幻读不可重复读这些由于事务隔离级别产生的并发不安全结果。

比如MySQL,MySQL为了并发安全自然里面内置了锁的机制,不过MySQL实现的锁机制是悲观锁(我这说的是写锁啊),增删改的时候默认就给你带上了。MyISAM存储引擎就是使用表锁,你增删改的时候一次锁你一张表,InnoDB存储引擎 使用的就是行锁,只锁你的操作数据行。

行锁表锁好处和坏处也很简单,因为加锁耗时间,所以MyISAM表锁开销小、加锁快,就是锁范围大了,容易发送锁冲突,并发度低。InnoDB行锁开销大、加锁慢。锁的范围小,不易发生锁冲突,并发度高。

 

而乐观锁这玩意 MySQL 是没给你实现的,不过乐观锁机制说简单也简单,无脑CAS就完事了,数据库的操作想使用乐观锁机制可以从表设计和代码逻辑上实现

既然乐观锁机制的核心就是CAS操作,修改的值是可以确定好的,就是我执行SQL语句要改啥嘛,那么就差个所谓的内存位置和预期原值了。

内存位置和预期原值这俩词听起来挺抽象,其实就是一个更改的基准判断,在数据库场景中只要在表里加个字段就完事了,一般来说是version或者timestamp,以version版本举例的话那就是每次修改的时候都先查出来我要改的这个字段的版本号,然后在执行 SQL 语句进行修改,不过 SQL 中需要带一个 WHERE 子句,条件为 version = {查出来的版本},如果是 UPDATE 语句的话则同时将版本号+1 

当当当,一个标准的CAS操作出现了,标准的比较交换流程,比较版本号,若和我之前查询出来的一致则交换,代表此时并没有人修改此字段。

若是我在修改前 此版本号已经被别人修改,那么由于此SQL带了判定的WHERE子句,直接将导致此SQL不成立。

一个乐观锁就这么实现了,而且数据库的这种场景还不用考虑ABA的问题,反正版本在每次修改时都不会一样。 然后在代码逻辑里根据SQL的返回值判定结果,成功就继续,失败要不触发失败操作,要不自旋抢锁。

 

如果是java程序当中的并发场景:

java当中 synchronized 关键字的锁膨胀阶段中 偏向锁、轻量级锁  都可以说是一个乐观锁(这里说的是经过1.6优化之后的)。

偏向锁 意思是偏向于第一个获取此锁的线程,获取锁的操作也是CAS操作,这里对java对象头不详细说,总之只要将java对象头中偏向线程ID以CAS操作改成自己的成功了,就代表获取到锁了。

如果存在并发情况,第二个线程试图获取锁的时候就会等所有线程到达全局安定点后将其升级为轻量级锁,这个轻量级锁就是一个典型的CAS+自旋操作 乐观锁。

轻量级锁情况下线程想要获取锁会直接在自己的线程栈中创建一个锁记录,然后将锁对象的对象头中markwork复制到自己的锁记录里面来,再试图将此对象对象头的markwork指针直接指向自己的锁记录。这套CAS流程要是失败,就自旋再次重复。经典的乐观锁机制。

 

 

CAS和乐观锁这个东西应该这篇文章算是解释清楚了,还举了几个例子比如数据库如何实现乐观锁、java中有哪些地方在使用乐观锁,synchronized在1.6之后的优化没仔细说,锁膨胀的过程只是简单地过了下,因为这个文章主要还是讲讲CAS和使用CAS实现乐观锁机制的,synchronized底层不在文章讨论范围之内,而且这玩意搜索引擎里边资料到处是,也没啥必要细说。

阅读全文

这书怎么说呢,讲的挺浅的,而且主要是无意义的代码片段贴的过多了。

很多知识点比如说书中的synchronized关键字讲解、wait notify等方法讲解,明明梳理知识点讲完就完事了,偏偏要贴几十页代码,有水字数嫌疑。

贴代码也就算了吧,还贴的是eclipse的截图代码,也不知道该说什么。

 

本来看synchronized关键字的时候还以为会讲讲jvm实现方式、对象头储存的数据、monitorenter、monitorexit指令之类的。

结果啥都没讲。Lock类也就讲ReentrantLock、ReentrantReadWriteLock的使用,我也以为会有实现、机制、AQS的讲解来着。

每天早上起床看半小时书,就这本看完感觉没学到啥= =

我估摸着是给没怎么接触过java多线程的人群看的,可能不太适合有多线程编程经验的职业人士。

 

最后在吐槽一句: 这些多线程相关的类的使用方法,我不会看API嘛,闲得蛋疼慢慢看这书

阅读全文

看下这样一段代码:

我在一个方法中建立了个死循环,循环的判断条件为一个boolean类型的成员变量。

然后在main线程中,创建了一个新的名为 “t1” 的线程,去执行这个方法。

等待一秒后,mian线程自身,将该成员变量的值改为false,试图使其不满足条件从而循环终止。

按照正常的逻辑来说,按照脑海中预演的情况来说,应该是没问题的。

可是执行后却没有得到想要的结果,控制台输出 “start” 后再无反应,程序一直没有终止,即 死循环没有退出。

 

想要让程序正确执行的话,将定义语句 boolean flag = true; 改为 volatile boolean flag = true;  程序就可以正确执行,这是为什么呢?

 

这里要涉及一点 java 内存模型,JMM。

JMM 里头,他有分配一块内存,叫主内存。类比于操作系统中的主存,主要存放线程间的共享数据(除去方法参数、局部变量)。

然后,每个线程在执行的过程当中,都有一个WorkingMemory,是主内存中部分数据的副本,只能该线程访问,类比于硬件的高速缓存。线程对数据的访问通常只能在workingMemory中进行,不能直接与主存交互。

比如cpu运行多个不同的线程的话,每个线程都有一个WorkingMemory,cup就是将主内存里边的数据读过来读到WorkingMemory里,然后再进行修改,比如+1+1+1+1,修改完后,再给他写回内存去。

 

我上边写的代码,这几个线程是怎么执行的呢?

在我的代码中,flag 是存在于堆内存中的 v 对象中。

当线程 t1 开始跑后,它首先会从主内存中把这个 flag 变量给它 copy 到自己的工作内存里边,然后开始运行。在cpu处理的过程之中,由于这cpu处理这个线程的部分他非常的忙,一直在while循环嘛,就不再去读主内存里边的数据了,一直在读自己的缓冲区(线程的工作内存)。

main 线程,改flag的参数,也是把 flag 参数 true 给读到自己的工作内存里边,然后进行修改,修改完事后,发现自己工作内存中的数据和主存中不一致,于是将自己工作内存中的数据给他刷新到主存中去。

这个时候就有问题了。第一个线程没有在主存里重新读啊?所以,这时候第一个线程就结束不了。

 

 

volatile 关键字功能及作用:

简单的来说,volatile 关键字,主要功能是使一个变量在多个线程间可见。

volatile关键字修饰的变量一旦在主存中被改变时,就会通知别的使用到该变量的线程:你们的缓冲区中的内容过期了,需要再重新刷新一下。

以上面那个代码例子来说 ,这样定义语句 volatile boolean flag = true;

当 main 线程修改主内存中 flag 的值时。这个时候, t1 线程,就会去重新读一遍主存,刷新自己的缓冲区(工作空间)。此时 t1 线程中的 flag 刷新为 false ,故循环停止。程序运行完毕。

 

虽然 volatile 用起来比较简单,但是该关键字背后代表的逻辑还是很深的。

 

 

还有个点要注意:

volatile并不能保证多个线程共同修改某一变量时所带来的不一致问题,也就是说 volatile 不能代替 synchronized。

当然, volatile 的效率高 synchronized 很多倍。但是该上锁的时候也只能上锁。

synchronized 是 既有可见性,又保证原子性。而 volatile 只保证可见性。synchronized的实现也是比 volatile 更重的。

 

 

关于 synchronized  本来也有一篇文章要写的。想想还是算了,本来想写点加锁释放锁的原理、可重入性质和可见性和原理啥的。

不过我过了归纳的那一段时间了,现在想归纳总结一通怪麻烦的。反正这些知识脑子里都有,就不记录了。

 

阅读全文

CyclicBarrier这个类的字面意思是循环屏障,跟CountDownLatch有些像,但不一样。关于CountDownLatch我在该爬虫项目中使用过,没有单独的文章进行讲解。

CyclicBarrier跟CountDownLatch的区别是:
CountDownLatch只计数1次
CyclicBarrier可以通过reset()重置计数,实现更复杂的业务,也会在其等待完毕释放锁后重置计数。

说明:
CyclicBarrier有2个构造
CyclicBarrier(int parties) 设置一个任务的参与数
CyclicBarrier(int parties,Runnable barrierAction) 设置一个任务的参与数和优先执行的barrierAction

步骤:
1、设置任务参与数,每个参与数达到相应阶段后,执行await方法。
2、当最后一个参与者进入await的阶段后,则停止阻塞

注意: barrierAcction屏障动作是由最后一个达到的子线程执行的

 

可以看我下面的代码,以学生去郊游为例,展示了其线程阻塞和可循环使用的功能:

 

输出语句为:

12 学生 已达到学校门口
13 学生 已达到学校门口
11 学生 已达到学校门口
14 学生 已达到学校门口
15 学生 已达到学校门口
15~~大家都到齐了,出发干下件事咯
15 学生 已经坐上 旅游大巴
13 学生 已经坐上 旅游大巴
14 学生 已经坐上 旅游大巴
11 学生 已经坐上 旅游大巴
12 学生 已经坐上 旅游大巴
12~~大家都到齐了,出发干下件事咯
12 学生到达景点
13 学生到达景点
15 学生到达景点
11 学生到达景点
14 学生到达景点
14~~大家都到齐了,出发干下件事咯触发

 

的确实现了线程等待其他线程完成的功能

注意观察线程ID,可以发现每次触发 barrierAction 屏障动作都是由最后一个线程来执行的

阅读全文

在实际业务中进行部分功能的开发时,不可避免的会遇上算数运算、计数等操作。

最典型的表现就是代码中一个又一个的 i++ (或者 num++ 之类的) 、i – – 等自增自减运算。

 

在普通的应用中当然可以这样。但是如果是一个上线的业务,一定会遇到并发访问的情况。

或许是多个线程(比如web请求)对某一方法中的公有变量进行增减。

也或者是实现功能的线程需要对某个值进行动态更改并且获取。

 

如果只是使用普通的加减运算,数量较少还好。但是如果进行的计算比较多、线程也比较多的话,那么普通的加减运算则不能做到足够精准。

java 便在 java.util.concurrent.atomic 包下提供了几个原子操作类比如 AtomicBoolean、AtomicLong、AtomicInteger。

这里主要说 AtomicInteger,其他的也差不了多少,去翻一翻 API 就好啦

 

JDK1.6 的 API 对该类的描述(最新的汉化版就是 1.6的 啦) 可以看一下:

可以用原子方式更新的 int 值。有关原子变量属性的描述,请参阅 java.util.concurrent.atomic 包规范。AtomicInteger 可用在应用程序中(如以原子方式增加的计数器),并且不能用于替换 Integer。但是,此类确实扩展了 Number,允许那些处理基于数字类的工具和实用工具进行统一访问。

 

AtomicInteger类主要方法:

 

建议在多线程环境下将

int i = 1;

i++;

替换为

AtomicInteger atomicInteger = new AtomicInteger(1);

atomicInteger.getAndIncrement();

 

阅读全文
EA PLAYER &

历史记录 [ 注意:部分数据仅限于当前浏览器 ]清空

      00:00/00:00