《Java并发编程的艺术》笔记

电子书:

链接: https://pan.baidu.com/s/1hQecjJw7SHQcophVIY-miw 提取码: 4ava 

简介

这是一本适合入门以及深入Java并发编程的书籍,“介绍了Java并发框架、线程池的实现原理。但不仅局限于Java层面,而是深入到JVM、甚至CPU层面从底层看并发技术”。

但书中的JDK版本基于1.5 1.6 1.7,并且个人认为阅读前需要了解一定的数据结构、操作系统、JVM等相关知识

(摘自前言)

阅读本书之前,希望读者必须有一定的Java基础和开发经验,最好还有一定的并发编程基础。如果读者是一名并发编程初学者,建议读者按照顺序阅读本书,并按照书中的例子进行编码和实战。如果读者是有一定的并发编程经验,可以把本书当做一个手册,直接看需要学习的章节。以下是各章节的基本介绍:

第一章 Java并发编程的挑战,会向读者说明进入并发编程的世界里,你们可能会遇到哪些问题,以及如何解决。

第二章 Java并发编程的底层实现原理,介绍在CPU和JVM这个层面是如何帮助Java实现并发编程的。

第三章 详细深入介绍了Java的内存模型。Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,本章试图揭开Java内存模型神秘的面纱。

第四章 从介绍多线程技术带来的好处开始,讲述了如何启动和终止线程以及线程的状态,详细阐述了多线程之间进行通信的基本方式和等待/通知经典范式。

第五章 介绍Java并发包中与锁相关的API和组件,以及这些API和组件的使用方式和实现细节。

第六章 介绍了Java中的大部分并发容器,并深入剖析其实现原理,让读者领略大师的设计技巧。

第七章 介绍了Java中的原子操作类,并给出一些实例。

第八章 介绍了Java中提供的很多并发工具类,相信这个是并发编程中的瑞士军刀。

第九章 介绍了Java中的线程池实现原理和使用建议。

第十章 介绍了Executor框架的整体结构和成员组件。

第十一章 介绍几个并发编程的实战,以及如何排查并发编程造成的问题。

目录(仅是笔记目录)

第一章 并发编程的挑战

第二章 Java并发机制的底层实现原理

2.1 volatile(可参考 volatile)

2.2 synchronized(可参考 synchronized)

2.3 原子操作的实现原理

第三章 Java内存模型(JMM)

3.1 JMM基础

3.2 重排序

3.3 volatile的内存语义

3.4 锁的内存语义

3.5 happens-before

3.6 双重检查锁定与延迟初始化

第四章 Java并发编程的基础

4.1 线程生命周期与创建方法

4.2 线程常用方法

4.3 Daemon Thread

4.4 ThreadLocal

第五章 Java中的锁

5.1 Lock

5.2 队列同步器(AQS)

5.3 重入锁

5.4 读写锁

5.5 LockSupport工具

5.6 Condition接口

第六章 Java并发容器和框架

6.1 concurrentHashMap

6.2 ConcurrentLinkedQueue


第一章 并发编程的挑战

1.1 上下文切换会影响执行效率,如何减少上下文切换

  • 无锁并发模式,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
  • CAS算法(Compare And Swap)
  • 使用最少线程,如减少JBOSS工作线程数,使WAITING线程尽可能少,减少上下文切换的次数
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

1.2 死锁,如何避免死锁

  • 避免一个线程同时获取多个锁
  • 尽量保证每个锁只占一个资源
  • 尝试使用定时锁

第二章 Java并发机制的底层实现原理

2.1 volatile(可参考 volatile

2.1.1 volatile如何实现可见性

通过Lock前缀的指令。将当前处理器缓存的数据写会系统内存,并使在其他CPU里缓存了该内存地址的数据无效(每个处理器会通过嗅探在总线上传播的数据来检查自己缓存的值是否过期)

2.1.2 volatile的使用优化 —— 追加64字节(JDK7,仅限64字节缓存行处理器)

如果队列的头节点和尾节点都不足64字节,处理器会将他们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头尾节点;当一个处理器试图修改头节点时,会导致其他处理器不能访问高速缓存中的尾节点,从而影响入队和出队效率

2.2 synchronized(可参考 synchronized

2.2.1 Synchronized锁分类

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的Class对象
  • 对于同步方法块,锁是Synchronized括号里配置的对象

2.2.2 Synchronized锁的实现原理

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。

monitorenter指令是在编译后插入到同步代码块开始的位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之匹配。任何对象都有一个monitor与之关联,并且一个monitor被持有后将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象对应的monitor的所有权,即对象的锁

2.2.2 Java对象头

synchronized用的是存在Java象头里的。Java对里的Mark Word里默储对象的HashCode、分代年锁标记32JVM 的Mark Word的默储结构如表2-3所示

 
在运行期 Mark Word 里存 的数据会随着 锁标 志位的 化而 化。 Mark Word 可能 变化为 以下 4 种数据,如 表2-4 所示。
 
 
 
64 位虚 机下, Mark Word 64bit 大小的,其存 储结 构如 2-5 所示
 
 

2.2.3 锁的升级与对比(这节很重要!!!)

锁的状态按级别分类:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态

偏向锁

大多数情况,锁不仅不存在多线程竞争,而且总是由同 一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出 同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否 存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需 要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则 使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
 
偏向锁的撤销:需要等待全局安全点(在这个时间点上没有正在执行的字节码),会暂停拥有偏向锁的线程
 

轻量级锁

1 级锁

线程在行同步之前,JVM会先在当前线程的栈桢建用于存储锁记录的空,并将对中的Mark Word复制到锁记录中,官方称Displaced Mark Word。然后线尝试使用CAS将中的Mark Word换为指向锁记录的指。如果成功,当前线,如果失败,表示其他线,当前线程便尝试使用自旋来

2 级锁

锁时,会使用原子的CAS操作将Displaced Mark Word回到,如果成功,则表示没有生。如果失,表示当前存在争,就会膨成重量级锁2-2是两个线程同夺锁的流程

锁的优缺点对比

2.3 原子操作的实现原理

2.3.1 处理器如何实现原子操作

1)使用总线锁原子性

2)使用缓存锁原子性

2.3.2 Java如何实现原子操作

1)锁

2)使用循环CAS实现原子操作

第三章 Java内存模型(JMM)

3.1 JMM基础

3.1.1 JMM的抽象结构

3.1.2 从源代码到指令序列的重排序

行程序 了提高性能, 编译 器和 理器常常会 指令做重排序。重排序分 3 类型。
  1. 编译器优化的重排序 编译 器在不改 变单线 程程序 语义 的前提下,可以重新安排 句的执 序。
  2. 指令级并行的重排序如果不存在数据依 性, 理器可以改 变语 对应机器指令的执 序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上 去可能是在乱序执 行。

Java源代到最终实际执行的指令序列,会分别经历下面3种重排序:

上述的1属于编译器重排序,23属于理器重排序。些重排序可能会致多线程程序出现内存可问题编译器,JMM编译器重排序规则会禁止特定型的编译器重排序(不是所有的编译器重排序都要禁止)。理器重排序,JMM理器重排序规则会要求Java编译器在生成指令序列,插入特定型的内存屏障Memory BarriersIntel称之为 Memory Fence)指令,通内存屏障指令来禁止特定型的理器重排序。

3.1.3 内存屏障

了保内存可性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的理器重排序。JMM把内存屏障指令分4,如下表所示。(Load和Store可以和read/write类比,Load相当于将主内存数据加载到工作内存,而store是将工作内存数据刷新到主内存

3.1.4 happens-before 简介

JSR-133使用happens-before的概念来述操作之的内存可性。在JMM中,如果一个操作执行的果需要另一个操作可,那么两个操作之要存在happens-before关系。

3.2 重排序

3.2.1 数据依赖性

3.2.2 as-if-serial

as-if-serial语义的意思是:不管怎么重排序(编译器和理器了提高并行度),(单线程) 程序的执果不能被改编译器、runtime理器都必遵守as-if-serial语义。

3.3 volatile的内存语义

3.3.1 volatile内存语义的实现

前文提到重排序分为编译器重排序和理器重排序。实现volatile内存语义JMM 会分别限制两种型的重排序型。表3-5JMM针对编译器制定的volatile重排序规则表。

从表3-5可以看出:

 
  • 当第二个操作是 volatile ,不管第一个操作是什么,都不能重排序。 规则 确保 volatile写之前的操作不会被编译器重排序到volatile写之后。
  • 当第一个操作是 volatile 读时 ,不管第二个操作是什么,都不能重排序。 规则 确保 volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 当第一个操作是 volatile 写,第二个操作是 volatile 读时 ,不能重排序。

实现volatile的内存语义编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的理器重排序。编译器来发现一个最布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。(保证在volatile写之前的所有普通写操作已经对处理器可见了
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。(避免在volatile写之后,可能有volatile读/写重排序
  • 在每个volatile操作的后面插入一个LoadLoad屏障。(避免volatile读与之后的普通读重排序
  • 在每个 volatile 操作的后面插入一个 LoadStore 屏障。( 避免volatile读与之后的普通写重排序

volatile写/读指令序列示意图

3.4 锁的内存语义

ReentrantLock为例,依赖于AQS(Java同步器框架)。AQS使用一个整形的volatile变量(state)来维护同步状态

对于公平锁:获取锁时,首先回去读state变量;释放锁时,最后写state。

对于非公平锁:获取锁时,会用CAS更新volatile变量;释放锁时,最后写state。

3.5 happens-before

3.5.1 happens-before 的定义

JSR-133:Java Memory Model and Thread Specificationhappens-before关系的定如下:

1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的序排在第二个操作之前。

2)两个操作之存在happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执行。如果重排序之后的果,与按happens-before关系来执行的果一致,那么种重排序并不非法(也就是JMM许这种重排序)。

3.5.2 happens-before 规则

JSR-133:Java Memory Model and Thread Specification》定了如下happens-before规则:

1)程序规则:一个线程中的每个操作,happens-before该线程中的任意后操作。

2监视锁规则一个的解happens-before于随后对这的加

3volatile规则一个volatile域的写,happens-before于任意后续对这volatile域的读。

4传递性:如果A happens-before B,且B happens-before C,那么A happens-before C

5start()规则:如果线A行操作ThreadB.start()(启动线B),那么A线程的 ThreadB.start()操作happens-before线B中的任意操作。

6join()规则:如果线A行操作ThreadB.join()并成功返回,那么线B中的任意操作

happens-before线AThreadB.join()操作成功返回。

3.6 双重检查锁定与延迟初始化

3.6.1 Double-Checked Locking 多线程不一定安全

原因:重排序

解决方法:将单例实例变量定义为volatile,或使用静态内部类实现单例

第四章 Java并发编程的基础

4.1 线程生命周期与创建方法

4.2 线程常用方法

4.3 Daemon Thread

4.4 ThreadLocal

第五章 Java中的锁

5.1 Lock

Lock接口提供的synchronized字所不具的主要特性:

5.2 队列同步器(AQS

列同步器AbstractQueuedSynchronizer(以下称同步器),是用来构建或者其他同步组件的基础框架,它使用了一个int员变量表示同步状,通内置的FIFO列来完成获取线程的排工作,并包的作者(Doug Lea)期望它能为实现大部分同步需求的基

同步器的主要使用方式是承,子过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()setState(int newState)compareAndSetState(int expect,int update))来行操作,因为的改是安全的。子推荐被定义为自定同步件的静内部类,同步器自身没有实现任何同步接口,它仅仅是定了若干同步状态获取和放的方法来供自定义同步件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态这样就可以方便实现不同型的同步件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

同步器是实现锁(也可以是任意同步件)的关在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作和同步器很好地隔离了使用者和实现者所需关注的域。

5.2.1 AQS的API

同步器的设计基于模板方法模式

重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状

1) getState()取当前同步状

2) setState(int newState)置当前同步状

3) compareAndSetState(int expect,int update):使用CAS置当前状方法能态设置的原子性。

可重写方法:

AQS提供的模板方法:

同步器提供的模板方法基本上分3:独占式取与放同步状、共享式取与放同步状态查询同步列中的等待线程情况。独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。

5.2.2 队列同步器的实现分析

同步器依内部的同步列(一个FIFO双向队列)来完成同步状的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

点是构成同步列(等待列,在5.6中将会介)的基同步器拥有首节点(head)和尾节点(tail),没有成功取同步状线程将会成为节点加入该队列的尾部,同步列的基本结构如5-1所示。

为了保证线程安全,AQS提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线认为的尾点和当前点,只有置成功后,当前点才正式与之前的尾节点建立关

置首点是通过获取同步状成功的线程来完成的,由于只有一个线程能成功取到同步状,因此头节点的方法并不需要使用CAS来保证。

独占式同步状态获取与释放

取同步状态时,同步器维护一个同步列,取状线程都会被加入到列中并在列中行自旋;移出列 (或停止自旋)的条件是前驱节为头节点且成功取了同步状。在放同步状态时,同步器调tryRelease(int arg)方法放同步状,然后头节点的后继节点。

独占式超时获取同步状态

共享式同步状态获取与释放

获取条件:tryAcquireShared(int arg)方法返回值大于等于0

释放:循环CAS唤醒后续等待节点

5.3 重入锁

ReentrantLock支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平(先请求先响应,FIFO)和非公平性选择。

Mutex(独占式锁)不支持冲入,synchronized关键字隐式支持冲入。

5.3.1 重入锁的获取与释放

获取:识别获线程是否当前占据线程。

释放:线程重复n取了,随后在第n该锁后,其他线程能够获取到该锁。的最终释放要求锁对数自增,数表示当前被重复取的次数,而锁被释数自减,当数等于0表示成功放。

5.3.2 公平锁与非公平锁

公平锁与非公平锁的唯一不同的位置为判断条件多了 hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释 放锁之后才能继续获取锁。

5.4 读写锁

Mutex 和 ReentrantLock 都是排他锁,而读写锁允许多个读线程同时访问。

5.4.1 读写锁的实现

在ReentrantLock中同步状态表示锁被一个线程重复获取的次数,而读写锁将 变量切分成了两个部分,高16位表示读,低16位表示写。


5.4.2 读写锁的获取与释放

写锁是一个可重入的排他锁,而读锁是一个可重入的共享锁。读锁的获取不具有安全性,只有在增加读状态时依靠CAS保证线程安全(读状态时所有线程获取读锁次数的综合,而每个线程的读锁获取次数只保存在ThreadLocal中

5.4.3 锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读 锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到 读锁,随后释放(先前拥有的)写锁的过程。

示例:

    public void processData() {
        readLock.lock();
        if (!update) {
            // 必须先释放读锁
            readLock.unlock();
            // 锁降级从写锁获取到开始
            writeLock.lock();
            try {
                if (!update) {
                    // 准备数据的流程(略)
                    update = true;
                }
                readLock.lock();
            } finally {
                writeLock.unlock();
            }
            // 锁降级完成,写锁降级为读锁 }
        }
        try {
            // 使用数据的流程(略) 
        } finally {
            readLock.unlock();
        }
    }

上述示例中,当数据发生变更后,update变量(布尔类型且volatile修饰)被设置为false,此 时所有访问processData()方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,其 他线程会被阻塞在读锁和写锁的lock()方法上。当前线程获取写锁完成数据准备之后,再获取 读锁,随后释放写锁,完成锁降级。

锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修 改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级 的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进 行数据更新。

5.5 LockSupport工具

当阻塞或者唤醒一个线程的时候都会使用LockSupport工具类来完成。

5.6 Condition接口

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、 wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以 实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等 待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

5.6.1 Condition 的实现分析

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要 获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队 列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。

等待队列

等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是 在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会 释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点 的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类 AbstractQueuedSynchronizer.Node。

在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的 Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,其对应关系如图5-10所示。

如图所示,Condition的实现是同步器的内部类,因此每个Condition实例都能够访问同步器 提供的方法,相当于每个Condition都拥有所属同步器的引用。

第六章 Java并发容器和框架

6.1 concurrentHashMap

6.1.1 为什么使用concurrentHashMap

1)HashMap线程不安全。多线程会导致HashMap的Entry链表形成环形数据结构,从而产生死循环

2)HashTable使用synchronized,保证了线程安全但是效率低下

3)ConcurrentHashMap的锁分段技术可有效提升并发访问率

6.1.2 ConcurrentHashMap 的结构

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重 入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数 据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种 数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元 素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时, 必须首先获得与它对应的Segment锁

6.2 ConcurrentLinkedQueue

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值