JAVA高并发学习笔记(一)

版权声明:本文为博主原创文章,博客地址:http://blog.csdn.net/a67474506?viewmode=contents https://blog.csdn.net/a67474506/article/details/48266653

1. 前言

1.1. 为什么需要并行

① 业务需要

业务模型需要一个逻辑执行的执行单元

让不同线程承担不同的业务工作

简化任务调度

适合计算密集型

② 性能问题

多线程的程序在多核cpu上面性能要好一些

cpu单核频率性能已经基本上无法提升了

通过多核来提升处理能力

1.2. 概念

1.2.1. 同步(synchronous)和异步(asynchronous)

1.2.1.1. 同步

等待方法执行结束开始执行下一个方法

1.2.1.2. 异步

在后台启动一个线程执行该方法,不用等待该方法执行结束

 

1.2.2. 并发(Concurrency)和并行(Parallelism)

1.2.2.1. 并行

2个线程同时执行

1.2.2.2. 并发

一个线程分别执行AB,每个时间段执行对象可能是不同的

有调度的过程

 

并发,就像一个人(cpu)喂2个孩子(程序),轮换着每人喂一口,表面上两个孩子都在吃饭。并行,就是2个人喂2个孩子,两个孩子也同时在吃饭。

详细解释文章:

http://3961409.blog.51cto.com/3951409/759708

1.2.3. 临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。

详细例子:

http://ifeve.com/race-conditions-and-critical-sections/

1.2.4. 阻塞(Blocking)和非阻塞(Non-blocking)

阻塞和非阻塞通常用来形容多线程间的相互影响。

① 阻塞

一个线程占用了临界区资源,那么其它所有需要这个资源的线程就必须在这个临界区中进行等待,等待会导致线程在操作系统层面被挂起。这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其它所有阻塞在这个临界区上的线程都不能工作

 

如果一个线程被挂起,则需要8万个时钟周期,并且需要进行上下文切换

 

除了直接的上下文切换性能成本,新线程一般会使用来自前一个线程的不同数据。内存访问比处理器时钟慢得多,所以现代系统会在处理器核心与主要内存之间使用多层缓存。尽管比主要内存快得多,但缓存的容量也小得多(一般而言,缓存越快,容量越小),所以任何时刻只能在缓存中保存总内存的小部分。发生线程切换且一个核心开始执行一个新线程时,新线程需要的内存数据可能不在缓存中,所以该核心必须等待该数据从主要内存加载。

 

组合的上下文切换和内存访问延迟,会导致直接的显著性能成本。

 

上下文切换:

一个线程阻塞时,以前执行该线程的处理器核心会转而执行另一个线程。以前执行的线程的执行状态必须保存到内存中,并加载新线程的状态。这种将核心从运行一个线程切换到运行另一个线程的操作称为上下文切换

② 非阻塞

允许多个线程同时进入临界区

  

1.2.5. 死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)

1.2.5.1. 死锁

两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

 

为什么会产生死锁:

① 因为系统资源不足。

② 进程运行推进的顺序不合适。    

③ 资源分配不当。

 

产生死锁的条件有四个:

① 互斥条件:所谓互斥就是进程在某一时间内独占资源。

② 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

③ 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。

④ 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

 

避免死锁:

 

避免嵌套封锁:这是死锁最主要的原因的,如果你已经有一个资源了就要避免封锁另一个资源。如果你运行时只有一个对象封锁,那是几乎不可能出现一个死锁局面的。

 

只对有请求的进行封锁:你应当只想你要运行的资源获取封锁,比如在上述程序中我在封锁的完全的对象资源。但是如果我们只对它所属领域中的一个感兴趣,那我们应当封锁住那个特殊的领域而并非完全的对象。

 

避免无限期的等待:如果两个线程正在等待对象结束,无限期的使用线程加入,如果你的线程必须要等待另一个线程的结束,若是等待进程的结束加入最好准备最长时间。

 

1.2.5.2. 饥饿

一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

 

解决饥饿的方案被称之为公平性” – 即所有线程均能公平地获得运行机会。

 

Java中导致饥饿的原因:

① 高优先级线程吞噬所有的低优先级线程的CPU时间。

② 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。

③ 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法),因为其他线程总是被持续地获得唤醒。

 

Java中实现公平性方案,需要:

① 使用锁,而不是同步块。

② 公平锁。

③ 注意性能方面。

详细学习文章

饥饿和公平:

http://ifeve.com/starvation-and-fairness/#header

1.2.5.3. 活锁

任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

 

 活锁死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

 

活锁的例子:

单一实体的活锁

例如线程从队列中拿出一个任务来执行,如果任务执行失败,那么将任务重新加入队列,继续执行。假设任务总是执行失败,或者某种依赖的条件总是不满足,那么线程一直在繁忙却没有任何结果。

协同导致的活锁

生活中的典型例子: 两个人在窄路相遇,同时向一个方向避让,然后又向另一个方向避让,如此反复。

通信中也有类似的例子,多个用户共享信道(最简单的例子是大家都用对讲机),同一时刻只能有一方发送信息。发送信号的用户会进行冲突检测, 如果发生冲突,就选择避让,然后再发送。 假设避让算法不合理,就导致每次发送,都冲突,避让后再发送,还是冲突。

计算机中的例子:两个线程发生了某些条件的碰撞后重新执行,那么如果再次尝试后依然发生了碰撞,长此下去就有可能发生活锁。

 

活锁的解决方法:

解决协同活锁的一种方案是调整重试机制。

比如引入一些随机性例如如果检测到冲突,那么就暂停随机的一定时间进行重试。这回大大减少碰撞的可能性。 典型的例子是以太网的CSMA/CD检测机制。

另外为了避免可能的死锁,适当加入一定的重试次数也是有效的解决办法。尽管这在业务上会引起一些复杂的逻辑处理。

比如约定重试机制避免再次冲突。 例如自动驾驶的防碰撞系统(假想的例子),可以根据序列号约定检测到相撞风险时,序列号小的飞机朝上飞, 序列号大的飞机朝下飞。

1.2.6. 并发的级别

 

1.2.6.1. 阻塞

当一个线程进入临界区后,其他线程必须等待

1.2.6.2. 无障碍(Obstruction-Free

无障碍是一种最弱的非阻塞调度

自由出入临界区

无竞争时,有限步内完成操作

有竞争时,回滚数据


无障碍是最弱的非阻塞调度阻塞调度是一种悲观的策略,认为大家一起修改数据是有可能吧数据改坏,所以每次允许一个人修改,而非阻塞调度是比较乐观的,认为如果大家一起修改未必会吧数据改坏,所以放开让大家进入,但是他是宽进严出的策略,允许所有的线程进入临界区,但是出来的话,发现一个线程在临界区的操作遇到了数据竞争,和别的线程产生了冲突,他就会回滚这条数据,重试这个操作.

 

在无障碍的调度方式中,所有的线程都相当于在拿取一个系统当前的快照,一直尝试知道拿到的快照是有效的为止

1.2.6.3. 无锁(Lock-Free

是无障碍的

保证有一个线程可以胜出

所有的线程都是能进入临界区,如果发生了竞争,不能保证临界区中的线程能够顺利胜出,如果线程发现自己每次对临界区的数据进行读取或者操作,总是和别人发生冲突,然后就开始不停的重试,多个线程互相干扰操作,所以无锁首先是无障碍的,但是每次的竞争有个线程是必然能胜出.如果有一个线程能胜出,就不会造成死锁.

 

在无锁的情况下如果方法执行过程是需要同步的,则每次执行的时候都只会有一个线程获得资源,完成执行。这样其他线程不得不从头开始执行,这样线程越多对程序的性能影响越大

java中比较典型的无锁计算的代码:

while(!atomicVar.compareAndSet(localVar,localVar+1))
{
       localVar=atomicVar.get();
}


1.2.6.4. 无等待(Wait-Free

无锁的

要求所有的线程都必须在有限步内完成

无饥饿的

无等待首先要求是无锁的,保证线程都能进入临界区,并且每次竞争都有一个线程能胜出,在有限步骤内离开临界区.因为所有的线程都能在有限的步骤中离开临界区完成操作,所以无等待也是无饥饿的.

 

无等待的典型案例:

只对数据做读的操作,任何线程都不对数据做写的操作,必然是无等待的

 

如果里面有写的操作可以先拷贝一份原始数据出来,在处理完拷贝的数据之后,将数据会写,这个操作是非常快的,会写数据只是一个引用地址的改变,所以数据必然是安全的。

无等待无锁详细介绍文章:

http://ifeve.com/why-is-wait-free-so-important/ (为什么无等待如此重要)

1.3. 有关并行的2个重要定律

1.3.1. Amdahl定律(阿姆达尔定律)

定义了串行系统并行化后的加速比的计算公式和理论上限

加速比定义:加速比=优化前系统耗时/优化后系统耗时

 


增加CPU处理器的数量并不一定能起到有效的作用提高系统内可并行化的模块比重,合理增加并行处理器数量,才能以最小的投入,得到最大的加速比

其他资料:

http://ifeve.com/amdahls-law/

1.3.2. Gustafson定律(古斯塔夫森)

说明处理器个数,串行比例和加速比之间的关系



只要有足够的并行化, 那么加速比和CPU个数成正比


猜你喜欢

转载自blog.csdn.net/a67474506/article/details/48266653