多线程与高并发基本概念

1.同步(Synchronous)与异步(Asynchronous)

    同步和异步通常形容一次方法的调用。同步方法调用开始后调用者必须等到方法调用返回才能进行后续行为。异步方法则像一个消息的传递,调用方法后立即返回而方法体则在后台继续运行,调用者无需等待继续后续操作。

多线程与高并发基本概念

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

    并发和并行都能表示两个或多个任务一起执行,但是并发偏重于任务交替执行,多个任务之间很可能是串行的,而并行是真正意义上的"同时执行"。

多线程与高并发基本概念

    严格的来说并行的多个任务是真实的同时执行,而并发是交替的,有系统在任务之间进行切换,即多个任务之间是串行并发,但对人来说会造成多任务是并行的错觉。

3.临界区

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

4.阻塞(Blocking)和非阻塞(Non-Blocking)

    阻塞和非阻塞通常用来形容多线程的互相影响。当一个线程占用了临界区资源,那么其他需要这个资源的线程必须在这个临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。非阻塞与之相反,它强调没有一个线程可以妨碍其他线程执行,所有线程都会不断尝试向前执行。

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

    死锁、饥饿和活锁都属于多线程活跃性问题。

    死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。

    饥饿是指某一个或多个线程因为优先级等原因导致其无法获得所需要的资源,最终导致一直无法执行。

    活锁则是线程之间一直将资源释放给其他线程,这会使资源不断在两个线程中跳动,而没有一个线程可以同时拿到所有资源而正常执行,这种情况就是活锁。

并发级别

    由于临界区的存在,多线程之间的并发必须受到控制,根据并发的策略大致将并发的级别进行分为阻塞,无饥饿,无障碍,无锁,无等待几种。

1.阻塞(Blocking)

    一个线程是阻塞的那么在其他线程释放资源前,当前线程无法继续执行。当使用synchonized关键字或者重入锁时,得到的就是阻塞的线程。

    无论是synchonized关键字或者重入锁都会试图在执行后续代码前得到临界区的锁,如果得不到则会挂起等待,直到占有了所需要的资源为止。

2.无饥饿(Starvation-Free)

    如果线程之间有优先级,那么在线程调度的时候总会倾向于高优先级的线程。如果没有优先级,满足先来后到,则不会产生饥饿。

3.无障碍(Obstruction-Free)

    无障碍是最弱的的非阻塞调度。两个线程如果都是无障碍的执行,那么他们不会因为临界区的问题导致一方挂起。如果阻塞的控制方式是悲观策略,那么相对来说非阻塞的调度是一种乐观的策略。一种可行的无障碍实现是以来一个“一致性标记”来实现。线程在操作前先读取并保存这个标记,在操作完成后再读取检查这个标记是否被更改如果是一致的则表示资源没有访问冲天,否则表示有其他线程对此资源有过修改需要重试操作。而任何对资源有修改的线程在更改数据前需要更新这个一致性标记,表示数据不安全。

4.无锁(Lock-Free)

    无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区的访问,但是无锁的并发保证必然有一个线程能在有限步内完成操作离开临界区。(但是有可能总是不成功会出现类似饥饿的现象)

    在无锁的调用中,典型的特点是包含一个无穷循环,在这个循环中线程会不断尝试修改共享变量。如果没有冲突修改成功则程序退出,否则继续尝试修改。

4.无等待(Wait-Free)

    无等待是在无锁的基础上更近一步进行扩充,它要求所有线程都必须在有限步内完成,这样就不会引起饥饿问题。

    典型的无等待结构就是RCU(Rrad-Copy-Update)。它的基本思想是对数据的读不进行控制,所以所有的读线程都是无等待。但是写数据是会先取得源数据的副本,然后只修改副本数据,修改完成后在合适的时机回写数据。

有关并行的两个重要定律

1.Amdahl定律

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

多线程与高并发基本概念

    根据Amdahl定律使用多核CPU对系统进行优化,优化的效果取决于CPU的熟练以及串行化程序的比重,CPU越多,串行化比重越低,则优化效果越好。仅提高CPU数量而不降低程序的串行化比重也无法提高系统性能。

2.Gustafson定律

    多线程与高并发基本概念

如果串行化比例小,并行化比例大,那么加速比则是处理器个数。只要不断累加处理器就能更快。

Amdahl强调的是:当串行化比例一定时,加速比是有上限的,不管堆叠多少个CPU进行计算都不能突破上限。

Gustafson关心的是:如果可悲 并行化代码比重足够多,那么加速比能随着CPU的数量线性增长。

    这两个定律并不矛盾,从极端角度来看,如果系统中没有可被串行化代码(F=1)那么对于两个定律来说加速比都是1。反之两个定律得到的加速比为n(处理器个数)。

Java:JVM

1.原子性(Atomicity)

    原子性是指一个操作不可中断。即使是多个线程一起执行的时候,一个操作一旦开始就不会被其它线程干扰。

如赋值操作int i=10;但是在32位机中的long类型不能保证原子性。

2.可见性(Visibility)

    可见性是指当一个线程修改了某一个共享变量的值,其它线程是否能立即知道这个修改。在串行程序中可见性问题不存在,因为在任何一个操作中修改了某个变量那么在后续的步骤中读取这个变量的值一定是修改后的新值。

3.有序性(Ordering)

    有序性问题的原因是因为程序在执行时会进行指令重排,重排后的指令与原指令的顺序未必一致。指令重排序在串行代码中能保证语义一致,但在多线程中并不保证。

4.那些指令不能重排:Happen-Before规则

1.程序顺序原则:一个线程内保证语义的串行性

2.volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性

3.锁规则:解锁必然发生在随后的加锁钱

4.传递性:A先于B,B先于C,则A先必然于C

5.线程的start()方法先于它的每一个动作

6.线程的所有操作先于线程的终结(Thread.join())

7.线程的中断(interrupt())先于被中断线程的代码

8.对象的构造函数执行,先于finalize()方法

 

原文资料《实战Java高并发程序设计》