笔记:《实战JAVA高并发程序设计》

第1章 走入并行世界

1.2.1 同步和异步

同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。

异步方法像消息传递,会在另外一个线程中执行

1.2.2 并发和并行

它们都表示两个或者多个任务一起执行,但是偏重点不同。

并发偏重于多个任务交替执行,而多个任务之间还可能是串行的。

并行是真正意义上的“同时执行”

并发的最终效果可能和并行是一样的。

1.2.3 临界区

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

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

阻塞和非阻塞通常用来形容多线间的相互影响。一个线程占用了临界区的资源,那么其他所有需要这个资源的线程就必须在这个临界区等待。等待会导致线程挂起,这种情况就是阻塞

非阻塞,强调没有一个线程可以妨碍其他线程执行。所有的线程都会尝试不断前向执行。

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

如果发生了上述情况,那么相关线程可能就不再活跃,很难再继续往下执行。

死锁,不同线程之间相互占用了对方所需的资源,导致其中的每个线程都无法继续执行。

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

饥饿可能由以下情况产生,1 资源不够用 2 某个线程一直占着资源不放

活锁 两个线程之间由于都主动将资源释放给他人使用,那么久会出现资源不断在两个线程中跳动,而没有一个线程可以同时拿到所有资源而正常执行。

1.3 并发级别

根据控制并发的策略,我们可以把并发的级别进行分类,大致上可以分为阻塞、无饥饿、无障碍、无锁、无等待几种。

1.3.1 阻塞(Blocking)

  • 解释:当线程处于阻塞的时候,该线程就相当于暂停状态。直到被分配资源后才可以继续执行。

用synchronized或者重入锁,都会试图在执行后续代码前,得到临界区的锁,如果得不到,线程就会被挂起等待,知道占有了所需资源为止。

1.3.2 无饥饿(Starvation-Free)

  • 解释:如果线程资源分配满足先来后到,则不会不会导致低优先级的线程产生饥饿。

如果线程之间是有优先级的,那么线程调度的时候总是会倾向于满足高优先级的线程。也就是,对于同一个资源的分配,是不公平的。对于非公平的锁来说,系统允许高优先级的线程插队。导致低优先级的线程产生饥饿。如果锁是公平的,满足先来后到,不会产生饥饿。

1.3.3 无障碍(Obstruction-Free)

  • 解释:两个线程无障碍执行,可以同时进入临界区,如果大家一起修改共享数据,那么无障碍线程就会立刻回滚操作,保证数据安全。

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

阻塞的控制方式是悲观策略,任务两个线程之间很有可能发生不幸的冲突。

非阻塞的调度就是一种乐观的策略。认为多个线程之间冲突的概率不大。检测到冲突就回滚。

一种可行的无障碍实现,可以依赖一个“一致性标记”来实现。

线程操作前,先读取并保存这个标记,在操作完成之后,再次读取,检查这个标记是否被更改过,如果两者是一致的,则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突,需要重试操作。而任何线程在修改资源前,都要更新这个一致性标记,表示数据不再安全。

1.3.4 无锁(Lock-Free)

  • 解释:所有的线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区

无锁的并行都是无障碍的。

无锁的调用中,一个典型的特点是可能包含一个无穷循环。在这个循环之中,线程会不断尝试修改共享变量。如果没有冲突,修改成功,否则继续尝试。无锁的并行总能保证有一个线程是胜出的。如果运气不好,线程总是尝试不成功,则会出现类似饥饿的线程,线程会停止不前。

1.3.5 无等待(Wait-Free)

  • 解释:无锁只要求有一个线程可以在有限步内完成操作,而无等待则在无锁的基础上更进一步进行扩展。它要求所有的线程必须在有限步内完成,这样就不会引起饥饿问题。

一种典型的无等待结构就是 RCU(Read-Copy-Update),它的基本思想是,对读不加控制。但是写数据的时候,先取得原始数据的副本,接着只修改副本数据,修改完成后,在合适的时机回写数据

1.4 有关并行的两个重要定律

1.4.1 Amdahl定律

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

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

加速比越高,表面优化效果越明显。

Tn = T1 ( F + 1/n * (1-F) )

n: 处理器个数

F: 串行比例

1-F: 并行比例

Tn: 优化前耗时

T1: 优化后耗时

提高可并行化的模块比重,在此基础上合理增加并行处理器的数量,才能获得最大加速比。

1.4.2 Gustafson定律

执行时间: a + b

总执行时间: a + nb

加速比: (a=nb) / (a+b)

定义: F = a/(a+b)

加速比: S(n) = (a=nb)/(a+b) = n - F(n-1)

如果串行化比例很小,加速比就是CPU的个数。

1.4.3 Amdahl 和 Gustafson 是否矛盾

Amdahl强调:当串行比例一定时,加速比是有上限的。

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

1.5 JAVA:JMM

JAVA的内存模型 JMM

1.5.1 原子性(Atomicity)

原子性是指一个操作是不可中断的。

对于32位系统来说,long型数据的读写不是原子的,因为long有64位。

如果两个线程同时对long进行写入的话(或者读取),对线程之间的结果是有干扰的。

1.5.2 可见性(Visibility)

可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

缓存优化、硬件优化、指令重拍、编辑器的优化都会导致一个线程的修改不会立即被其他线程察觉。

1.5.3 有序性(Ordering)

有序性问题的原因是程序在执行时,可能会进行指令重排。

一条指令的执行分为很多步骤:

  1. 取指IF
  2. 译码和取寄存器操作数 ID
  3. 执行或者有效地址计算EX
  4. 存储器访问MEM
  5. 写回WB

指令1    IF ID EX MEM WB

指令2        IF ID  EX      MEM WB

流水线技术使得多条指令可以同时执行

但是多条指令同时执行时,有时需要等待上一条指令执行完毕,造成延迟

ADD SUB 都要等待上一条指令的结果

指令重排能够尽量减少中断的流水线,消除停顿

1.5.4 哪些指令不能重排

  1. 程序顺序原则:一个线程内保证语义的串行性
  2. volatile规则:volatile的写先发生于读
  3. 锁规则:unlock在lock前
  4. 传递性原则:A先于B,B先于C,则A先于C
  5. 线程的start()方法先于它的每个动作
  6. 线程的所有中断先于线程的终结(Thread,join())
  7. 线程的中断(interrupt())先于被中断线程的代码
  8. 对象的构造函数执行、结束先于finalize()方法

第2章 Java并行程序基础

2.1 有关线程你必须知道的事

JAVA Thread 生命周期:

    NEW                           新建

    RUNNABLE                运行时

    BLOCKED                 阻塞状态

    WAITING                   等待特殊事件,如:notify() 

    TIMED_WAITING       限时等待

    TERMINATED            终止

2.2 初始线程:线程的基本操作

2.2.1 新建线程

2.2.2 终止线程

stop() 会导致线程强行终止。

应该调用自定义的终止函数,设置终止变量,从而在run()方法的主循环里判断终止变量是否存在,从而正确结束线程。

2.2.3 线程中断

Thread.interrupt() 中断,它通知目标线程中断,需要线程主动检测该标志位,Thread.isInterruped()。

Thread.sleep()方法由于中断而抛出异常,它会清除中断标记,所以在catch里再次执行中断函数,置标记。

2.2.4 等待(wait)和通知(notify)

wait() 和 notify() 必须先获得object的监视器(synchronized)。

object.wait()使当前线程进入object的等待队列。object.notify()则从队列里随机唤醒一个线程。notifyAll()则全部唤醒。

2.2.5 挂起(suspend)和继续执行(resume)线程

线程在挂起的时候,其占用的锁不会被释放。线程状态仍然是Runnable

废弃方法,不推荐使用。

2.2.6 等待线程结束(join)和谦让(yield)

join()会一直阻塞当前线程,直到目标线程执行完毕。

join(long millis)则限制了最大等待时间。

它让调用线程在当前线程对象上进行等待。被等待的线程会在退出前调用notidyAll()通知所有等待线程。

yield() 是一个静态方法,它会使当前线程让出CPU,重新参与CPU资源的争夺。

2.3 volatile与Java内存模型(JMM)

volatile关键字能够确保变量被修改后,所以线程都能够“看到”这个改动,能够保证变量的可见性。

2.4 分门别类的管理:线程组

ThreadGroup 类

activeCount() 可以获得活动线程的总数

list() 可以打印这个线程组的所有线程信息

stop() 停止组里所有的线程

2.5 驻守后台:守护线程(Daemon)

守护线程在后台完成一些系统性的任务,比如垃圾回收线程, JIT线程等。

用户线程是系统的工作线程,它完成业务操作。

当一个Java应用内,只有守护进程时,Java虚拟机就会退出。

setDaemon(true) 将线程设置为守护线程,必须在start()之前设置

2.6 先干重要的事:线程优先级

JAVA的线程有优先级。

不同优先级在竞争资源时会产生不可预测的结果,可能会产生饥饿。因此在要求严格的场合,要自己在应用层解决线程调度问题。

setPriority(Thread.MAX_PRIORITY)    设置优先级,值越大、优先级越大。

2.7 线程安全的概念与synchronized

程序并行化之后,要保证执行结果的正确性。

比如:两个线程同时对i进行累加操作,如果仅将i声明为volatile变量,仍会发生错误。

因为两个线程可能读取到相同的值,从而写入相同的+1后的值,造成写入冲突。(少加了一个)

关键字synchronized的工作是对同步的代码加锁,使每次只有一个线程能进入同步块。

  • 指定加锁对象:进入前要获得指定对象的锁
  • 直接作用与实例方法:进入前要获得当前实例的锁
  • 直接作用于静态方法:进入前要获得当前类的锁

2.8 程序中的幽灵:隐蔽的错误

不提示的错误。

2.8.1 无提示的错误案例

这里给出一个系统运行错误,缺没有任何提示的案例。

int ave=(v1+v2)/2;

当值太大时会导致int溢出,从而造成错误的结果。

2.8.2 并发下的ArrayList

ArrayList在多线程同时添加元素时,可能会出现错误。

  • ArrayIndexOutOfBoundsException异常,因为ArrayList在扩容过程中,内部一致性被破坏。
  • 元素数量少于预期,因为多个线程可能在同一位置赋值,造成覆盖。

最简单的方法是使用Vector

2.8.3 并发下诡异的HashMap

多个线程同时对HashMap进行put()操作,可能会出现错误。

  • 程序正常结束,但是结果不符合预期,小于预期数字。与ArrayList类似,可能在同一位置赋值。
  • 程序无法结束,因为多线程冲突,链表可能会形成一个环,造成死循环。但是JDK8修复了这个问题。

最简单的方法是使用ConcurrentHashMap

2.8.4 初学者常见问题:错误的加锁

在进行多线程同步时,加锁是保证现场安全的重要手段之一。

把synchronized加到Integer类型对象时,会出现错误。

因为Integer是不可变对象。比如:i++实际是Integer.valueOf(i.inValue() + 1)返回了一个新的Integer对象。

所以每次锁住的是不同的对象。

第3章 JDK并发包

本章

介绍有关同步控制的工具

介绍JDK对线程池的支持

介绍JDK的一些并发容器

3.1 多线程的团队协作:同步控制

3.1.1 synchronized的功能扩展:重入锁

synchronized关键字是一种最简单的控制方法,控制临界区的访问。

重入锁使用 java.util.concurrent.locks.ReentrantLock类来实现。

重入锁使用lock()和unlock()手动指定加锁和释放锁。

Re-Entrant-Lock 翻译为 重入锁 ,因为这种锁可以反复进入。局限于一个线程。

还有一些高级功能

  • 中断响应

如果一个线程在等待锁,它仍然可以收到一个通知,被告知无需等待时,可以停止工作。

lockInterruptibly() 可中断锁住,收到中断会返回。

  • 锁申请等待限时

除了等待外部通知,还可以通过限时等待避免死锁。

tryLock()方法进行一次限时的等待

两个参数,一个表示等待时长,一个表示计时单位。

也可以不带参数执行,申请成功会返回true,否则不会等待,直接返回false

  • 公平锁

重入锁允许设置公平性,有一个构造函数:

public ReentrantLock(boolean fair)

参数fair为true时,表示锁是公平的。

实现公平锁必然要维护一个有序队列,因此成本较高,性能相对低下,因此默认情况,锁是非公平的。

重要方法整理:

lock()

lockInterruptibly()

tryLock()

tryLock(long time, TimeUnit unit)

unlock()

重入锁的实现,主要包含三个要素:

第一:原子状态                使用CAS操作存储锁的状态

第二:等待队列                所有没有请求到锁的线程会进入等待队列

第三:阻塞原语                park()和unpark()用来挂起和恢复线程,没得到锁的线程会被挂起

3.1.2 重入锁的好搭档:Condition条件

Lock类的newCondition()可以生成一个与当前重入锁绑定的Condition实例。

  • await() 方法使当前线程等待,同时释放当前锁,当其他线程中使用signal()或者signalAll()方法时,线程会重新获得锁并继续执行。
  • awaitUninterruptibly() 方法与await()类似,但不会在等待时响应中断。
  • singal() 方法用于唤醒一个在等待中的线程。
  • signalAll() 方法唤醒所有在等待中的线程。

3.1.3 允许多个线程同时访问:信号量(Semaphore)

信号量允许多个线程同时访问某一个资源

构造函数

public Semaphore(int permits)

public Semaphore(int permits, boolean fair)

构造时需要指定信号量的准入数

public void acquire()                            获得一个准入许可

public void acquireUninterruptibly()    获得一个准入许可,但不响应中断

public boolean tryAcquire()                尝试获得一个许可,成功返回true,失败返回false,不等待

public boolean tryAcquire(long timeout, TimeUnit unit)    

public void release()                            释放许可

3.1.4 ReadWriteLock 读写锁

ReadWriteLock是JDK5中提供的读写分离锁。

读写锁允许多个线程同时读,但是写写操作和读写操作间依然需要相互等待和持有锁。

    读-读不互斥

    读-写互斥

    写-写互斥

如果系统中,读操作次数远远大于写操作,则读写锁可以发挥最大的功效。

Lock readLock = readWriteLock.readLock()

Lock writeLock = readWriteLock.writeLock()

3.1.5 倒计时器:CountDownLatch

构造函数,参数为计数个数

public CountDownLatch(int count)

调用 CountDownLatch.await() 等待指定数量任务完成

调用 CountDownLatch.countdown() 方法通知 CountDownLatch 一个线程已经完成任务,倒计时器减1

当所有任务完成时,await()的线程才能继续执行。

3.1.6 循环栅栏:CyclicBarrier

和 CountDownLatch 类似,可以实现线程间的计数等待,

CyclicBarrier 更强大,可以接收一个参数作为barrierAction。在一次计数完成后,系统会执行barrierAction。

构造函数:

public CyclicBarrier(int parties, Runnable barrierAction);

CyclicBarrier.await() 会等待所有士兵线程到齐,再次调用会进行下一次计数

CyclicBarrier.await() 可能会抛出 InterruptedException和BrokenBarrierException异常。

InterruptedException        使得线程在等待时可以响应外部的紧急事件

BrokenBarrierException    表示CyclicBarrier.已经破损

3.1.7 线程阻塞工具类:LockSupport

它可以在线程内任意位置让线程阻塞

和Object.wait()相比,它不需要先获得某个对象的锁,也不会抛出InterruptedException异常

LockSupport 的静态方法 park() 可以阻塞当前线程,类似还有 parkNanos() parkUntil() 等,它们实现了限时等待。

LockSupport 类使用类似信号量的机制,它为每个线程准备了一个许可,如果许可可用,则park()函数会立即返回。所以即使unpark() 操作发生在 park() 之前,park()操作也能够返回。

park() 挂起状态的线程是WAITING状态。

LockSupport.park() 还支持中断响应,但是它不会抛出 InterruptedException 异常,只会默默的返回。可用从Thread.interrupted()获得中断标记(是否被中断)。

3.2 线程复用:线程池

3.2.2 不要重复发明轮子:JDK对线程池的支持

JDK提供了一套Executor框架,在java.util.concurrent包

Executor类扮演线程池工厂角色,通过Executor可以取得一个特定功能的线程池。

ThreadPoolExecutor类实现了Executor接口,通过该接口,任何Runnable对象都可以被ThreadPoolExecutor线程池调度。

Executor有以下工厂方法:

newFixedThreadPool()                            固定线程数量的线程池    LinkedBlockingQueue

newSingleThreadExecutor()                    只有一个线程的线程池

newCachedThreadPool()                         可自行调整线程数量的线程池    SynchronousQueue

newSingleThreadScheduledExecutor()    定时执行任务的单线程池

newScheduledThreadPool()                    可指定线程数量,执行定时任务的线程池

3.2.3 刨根究底:核心线程池的内部实现

这些线程池都是ThreadPoolExecutor类的封装

public ThreadPoolExecutor(int corePoolSize,

                              int maximumPoolSize,

                              long keepAliveTime,

                              TimeUnit unit,

                              BlockingQueue<Runnable> workQueue,

                              ThreadFactory threadFactory,

                              RejectedExecutionHandler handler)

corePoolSize                指定了线程池中的线程数量

maximumPoolSize        指定了线程池中的最大线程数量

keepAliveTime               当线程池线程数量超过corePoolSize时,多余的空闲线程的存活时间。

unit                                keepAliveTime的单位

workQueue                    任务队列,被提交但尚未被执行的任务。

threadFactory                线程工厂,用于创建线程,一般用默认的即可

handler                          拒绝策略。任务太多来不及处理,如何拒绝任务

ThreadPoolExecutor的构造函数可以使用一下几种BlockingQueue

直接提交队列    SynchronousQueue

没有容量,每一个插入操作都要等待一个删除操作。任务不会被保存,总是提交给线程执行,若没有空闲进程,则尝试创建新的进程,否则执行拒绝策略。

有界的任务队列    ArrayBlockingQueue

一个参数,capacity代表队列的最大容量,若小于maximumPoolSize大于corePoolSize则创建新的线程,大于maximumPoolSize则执行拒绝策略。

无界的任务队列    LinkedBlockingQueue

不存在入队失败的情况,线程数小于corePoolSize时,会生成新的线程执行任务,否则会进入队列等待。

优先任务队列        PriorityBlockingQueue

可以控制执行先后顺序,是一个特殊的无界队列

3.2.4 超负载了怎么办:拒绝策略

JDK内置四种拒绝策略

AbortPolicy 策略                    直接抛出异常,阻止系统正常工作

CallerRunsPolicy 策略            只要线程池未关闭,直接在调用者线程中运行当前被丢弃的任务

DiscardOledestPolicy 策略     丢弃最老的一个请求

DiscardPolicy 策略                 丢弃无法处理的任务,这可能是一种最好的方案

扩展 RejectedExecutionHandler 接口

3.2.5 自定义线程创建:ThreadFactory

ThreadFactory接口只有一个方法,用来创建线程:

Thread newThread(Runnable r);

3.2.6 我的应用我做主:扩展线程池

ThreadPoolExecutor 是一个可扩展的线程池,提供了beforeExecute()、afterExecute()、terminated()三个接口。

3.2.7 合理的选择:优化线程池线程数量

《Java Concurrency in Practice》中给出了一个估算线程池大小的经验公式:

Ncpu = CPU的数量

Ucpu = 目标CPU的使用率,0<=Ucpu<=1

W/C = 等待时间与计算时间的比率

为了保持处理器达到期望的使用率,最优的池的大小等于:

Nthreads = Ncpu * Ucpu * (1 + W/C)

Runtime.getRuntime().availableProcessors()    取得可用的CPU数量

3.2.8 堆栈去哪里了:在线程池中寻找堆栈

线程池可能会忽略掉程序抛出的异常,导致我们对程序的错误一无所知。

最简单的方法是

  1. 放弃submit() 改为 execute()
  2. Future re = pools.submit(new DivTask(100, i));re.get();

以上两种方法都能得到堆栈信息,但是无法知道是哪里抛出的信息。

因此我们使用自定义的wrap()方法包装Runnable,在wrap方法里的用try catch包围task.run()并放入外层Runnable中。

3.2.9 分而治之:Fork/Join框架

JDK中,给出了一个ForkJoinPool线程池

每个线程都有一个任务队列,并且自己的任务执行完,会获取其他线程的任务执行。

执行自己的任务从顶部拿,获取其他线程的任务从底部开始拿。

public <T> ForkJoinTask<T> submit (ForkJoinTask<T> task)

ForkJoinTask有两个子类

    RecursiveAction        没有返回值的任务

    RecursiveTask            可以携带返回值的任务

fork() join() 两个操作

ForkJoinTask可以通过fork()无限分解,需要在父线程join()等待结果,实现大数据分治计算。

注意 一、线程数量积累太多,导致性能严重下降 二、函数调用太深,栈溢出

3.3 不要重复发明轮子:JDK的并发容器

3.3.1 超好用的工具类:并发集合简介

JDK提供的容器大部分在java.util.concurrent包中。

    ConcurrentHashMap            这是一个高效的并发HashMap

    CopyOnWriteArrayList         在读多写少的场合,这个List性能非常好

    ConcurrentLinkedQueue     线程安全的LinkedList

    BlockingQueue                    这是一个接口,表示阻塞队列。

    ConcurrentSkipListMap        跳表的实现

还有 java.util下的Vector是线程安全的。

3.3.2 线程安全的HashMap

可以使用 Collections.synchronizedMap()

synchronizedMap里面包装了一个Map

里面用Object对象 mutex 和 synchronized ,实现互斥。

适合并发级别不高的情况。

3.3.3 有关List的线程安全

ArrayList和Vector都使用数组实现,但是Vector是线程安全的。

LinkedList使用链表实现List。

ArrayList和LinkedList都是可以使用 Collections.synchronizedList() 包装,达到线程安全的目的。

3.3.4 高效读写的队列:深入剖析 ConcurrentLinkedQueue

JDK提供 ConcurrentLinkedQueue 实现高并发队列。这个队列使用链表作为其数据结构。

线程安全完全由CAS操作和队列的算法来保证。

3.3.5 高效读取:不变模式下的 CopyOnWriteArrayList

CopyOnWriteArrayList 写入不会阻塞读取操作,只有写入和写入之间需要进行同步等待。

因为当List需要修改时,不会修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本中。

修改后,读取线程可以立即“察觉”到这个修改,因为array变量是volatile类型。

3.3.6 数据共享通道:BlockingQueue

BlockingQueue是一个接口,这里主要介绍两个实现: ArrayBlockingQueue 和 LinkedBlockingQueue 。

ArrayBlockingQueue基于数组实现,LinkedBlockingQueue基于链表。

ArrayBlockingQueue 适合做有界队列,LinkedBlockingQueue 元素可以动态添加,适合做无界队列。

压入元素可以使用offer()和put()方法

offer()            如果队列满了,会立即返回false

put()              如果队列满了,会移植等待

弹出元素可以使用poll()和take()方法

poll()            如果队列为空,直接返回null

take()            如果队列为空,会一直等待

内部使用 ReentranrLock 和 Condition 实现线程等待和通知。

3.3.7 随机数据结构:调表(SkipList)

跳表的时间复杂度是O(logn)

高并发情况下,平衡树需要一个全局锁,而跳表只需要部分锁。

使用跳表实现Map,所有元素是排序的。哈希算法实现Map,里面的元素时无序的。

对Node的所有操作,使用CAS方法

第4章 锁的优化及注意事项

4.1 有助于提高“锁”性能的几点建议

4.1.1 减小锁持有时间

4.1.2 减少锁粒度

ConcurrentHashMap 在内部,细分成了若干个HashMap,称之为段(SEGMENT)

通常,一个 ConcurrentHashMap 被细分为16个段,在put() 时仅对该段加锁

4.1.3 读写分离锁来替换独占锁

4.1.4 锁分离

LinkedBlockingQueue 使用 takeLock 和 putLock 两把锁,实现了取数据和写数据的分离,实现真正意义上的可并发操作。

4.1.4 锁粗化

虚拟机遇到一连串地连续请求和释放同一锁时,会把所有的锁操作合成对锁的一次请求,从而减少同步次数,这个操作叫着锁的粗化。

4.2 Java虚拟机对锁优化所做的努力

这里介绍几种JDK内部的“锁”优化策略。

4.2.1 锁偏向

如果一个线程获得了锁,那么锁就进入偏向模式。当线程再次请求锁时,无需再做任何同步操作。

对于几乎没有锁竞争的场合,偏向锁效果较好。竞争激烈的场合,可能每次是来自不同线程的请求,偏向模式会失效。

-XX:+UseBiasedLocking 开启偏向锁

4.2.2 轻量级锁

如果偏向锁失败,虚拟机不会立即挂起线程。转为轻量级锁。

它将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。

如果线程获得轻量级锁成功,则可以顺利进入临界区。

如果失败,其他线程先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁。

4.2.3 自旋锁

锁膨胀之后,虚拟机会尝试自旋锁。系统假设在短时间内,线程可以获得这把锁,因此虚拟机会让线程做接空循环,若可以得到锁,则会顺利进入临界区。若不能,则会将线程在操作系统层面挂起。

4.2.4 锁消除

Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。节省请求锁的时间。

由于JDK的内置API,比如StringBuffer、Vector等,在使用的时候,可能是在一个不存在并发竞争的场合,所以会被虚拟机消除锁。

锁消除涉及的一项关键技术为逃逸分析。

逃逸分析:观察某一个变量是否会逃出某一个作用域。

逃逸分析必须在-server模式下进行。

-XX:+DoEscapeAnalysis    打开逃逸分析

-XX:+EliminateLocks         打开锁消除

4.3 人手一只笔:ThreadLocal

4.3.1 ThreadLocal 的简单使用

为每个线程分配不同的对象

4.3.2 ThreadLocal 的实现原理

set() 时,获得当前线程对象,然后通过getMap()拿到线程的 ThreadLocalMap ,将值设入 ThreadLocalMap 中。

get() 时,取得当前线程的 ThreadLocalMap 对象,然后将自己作为 key 取得内部的实际数据。

ThreadLocalMap 的实现使用了弱引用 WeakReference 。

弱引用在垃圾回收时,会被立刻回收。

当 ThreadLocal 的外部强引用被回收时,ThreadLocalMap 中的key就会变成null。

4.3.3 对性能有何帮助

如果共享对象对于竞争的处理容易引起性能损失,我们还是应该考虑使用 ThreadLocal 为每个线程分配单独的对象。

4.4 无锁

对于并发控制而言,锁是一种悲观的策略。

无锁是一种乐观的策略,它假设对资源的访问是没有冲突的。

无锁的策略使用一种叫做 比较交换的技术(CAS Compare And Swap)来鉴别线程冲突

4.4.1 与众不同的并发策略:比较交换(CAS)

CAS算法的过程:它包含三个参数 CAS(V,E,N)。V 表示要更新的变量,E 表示预期值,N 表示新值。仅当 V 值 等于 E 值时,才会将 V 值设为 N 。否则什么都不做,CAS 返回当前 V 的真实值。

大部分的现代处理器都已经支持原子化的 CAS 指令,在 JDK 5.0 以后,虚拟机便可以使用这个指令。

(汇编指令 cmpxchg)

4.4.2 无锁的线程安全整数:AtomicInteger

JDK 并发包中有一个 atomic 包,里面实现了一些直接使用 CAS 操作的线程安全的类型。

AtomicInteger 可以看成一个整数,但是与 Integer 不同,它是可变的。并且是线程安全。对其进行修改等任何操作,都是用 CAS 指令进行的。

4.4.3 Java中的指针:Unsafe类

AtomicInteger 里面有一个 sun.misc.Unsafe 类型的变量 unsafe

Unsafe 封装了一些类似指针的操作。虽然JAVA抛弃了指针,但是在关键时刻,类似指针的技术还是不可少的。

它是一个JDK内部使用的专属类。用户程序不可用。

getUnsafe()函数会判断这个类的 ClassLoader 是否为 null ,不为 null 抛出异常。

因为引用程序的类有 App Loader 架子啊,而系统核心类,如 rt.jar 中的类由 Bootstrap 类加载器加载。 Bootstrap 加载器没有 Java 对象,试图获得这个类加载器会返回 null 。

4.4.4 无锁的对象引用:AtomicReference

AtomicReference 是对普通对象的引用, AtomicInteger 是对整数的封装。

它可以保证你在修改对象引用时的线程安全性。

但是有可能出现对象的值被修改了两次,当前线程误以为这个对象没有被修改,在某些情况下会出错。

使用 AtomicStampedReference 可以很好的解决这些问题。

4.4.5 带有时间戳的对象引用: AtomicStampedReference

我们只要能够记录对象在修改过程中的状态值,就可以很好地解决对象呗反复修改,导致线程无法正确判断对象状态的问题。

AtomicStampedReference 就是这么做的,它内部不仅维护了对象值,还维护了一个时间戳。当 AtomicStampedReference 对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。

当 AtomicStampedReference 设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。

public boolean compareAndSet (V expectedReference, V newReference, int expectedStamp, int newStamp)                            //比较设置 参数:期望值 写入新值 期望时间戳 新时间戳

public V getReference()        //获得当前对象引用

public int getStamp()            // 获得当前时间戳

public void set(V newReference, int newStamp)    //设置当前对象引用和时间戳

4.4.6 数组也能无锁: AtomicIntegerArray

当前可用的原子数组有: AtomicIntegerArray 、 AtomicLongArray 、 AtomicReferenceArray ,

分别表示整数数组、long型数组和普通的对象数组。

AtomicIntegerArray 本质上是对 int[] 类型的封装,使用 Unsafe 类通过 CAS 的方式控制 int[] 在多线程下的安全性。

4.4.7 让普通变量也享受原子操作: AtomicIntegerFieldUpdater

AtomicIntegerFieldUpdater 可以让你在不改动(或极少改动)原有代码的基础上,让普通的变量也享受 CAS 操作带来的线程安全性。

根据数据类型不同,有三种 Updater 。分别是 AtomicIntegerFieldUpdater 、 AtomicLongFieldUpdater 、 AtomicReferenceFieldUpdater 。

注意事项:

  1. Updater 只能修改可见范围内的对象
  2. 为了确保变量被正确读取,必须是 volatile 类型
  3. CAS 操作不支持 staitc 字段,因为 Unsafe.objectFieldOffset() 不支持静态变量

4.4.8 挑战无锁算法:无锁的 Vector 实现

向大家介绍一种使用无锁方式实现的 Vector 。

将这个无锁的 Vector 称为 LockFreeVector 。

使用二维数组,方便增加新的元素。

定义了一个 Descriptor元素 ,它使用 CAS 操作写入新数据。

4.4.9 让线程之间互相帮助:细看 SynchronousQueue 的实现

4.5 有关死锁的问题

死锁就是两个或者多个线程,互相占用对方需要的资源,都不进行释放,导致彼此之间互相等待对方释放资源,产生了无限制等待的现象。

想要避免死锁,除了使用无锁的函数外,还可以使用重入锁。

第5章 并行模式与算法

发表评论

邮箱地址不会被公开。 必填项已用*标注