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

目录

  • 第1章 并发编程的挑战
    • 1.1 上下文切换
    • 1.2 死锁
  • 第2章 并发机制的底层实现原理
    • 2.1 volatile关键字
    • 2.2 synchronized
    • 2.3 原子操作的实现原理
  • 第3章 Java内存模型
    • 3.1 java内存模型基础
    • 3.2 重排序
    • 3.3顺序一致性
    • 3.4volatile的内存语义
    • 3.5锁的内存语义
    • 3.6 final域的内存语义
    • 3.7 happens-before
    • 3.7 双重锁检查
  • 第4章 java并发编程基础
    • 4.1 线程状态
    • 4.2 几个线程函数
    • 4.3 线程间通信
  • 第5章 Java中的锁
    • 5.1 lock
    • 5.2队列同步器
    • 5.3 读写锁
    • 5.4 LockSupport
    • 5.5 Condition接口
  • 第6章 java并发容器和框架
    • 6.1 ConcurrentHashMap
    • 6.2 ConcurrentLinkedQueue
    • 6.3 Java中的阻塞队列
  • 第七章 13个原子操作类
    • 7.1 原子类
    • 7.2 原子更新数组
    • 7.3 原子引用类型
    • 7.4 原子更新字段
  • 第八章 并发工具类
    • 8.1 CountDownLatch
    • 8.2 CyclicBarrier
    • 8.3 Semaphore
    • 8.4 Exchanger
  • 第九章 线程池
    • 9.1 原理

第1章 并发编程的挑战

1.1 上下文切换

因为上下文的创建和切换存在系统开销,所以使用了多线程不一定就快。
多线程竞争条件时会引起上下文切换,所以可以用一些方法来变使用锁。

处理多线程数据的无锁方法有:

  • 将数据ID按照Hash算法取模,不同线程处理不同段的数据。
  • CAS
  • 使用最少线程。避免创建不需要的线程。
  • 协程。在单线程调度多任务。

1.2 死锁

避免死锁的方法:

  • 避免同一个线程获得多个锁
  • 尽量保证每个锁只占用一个资源
  • 尝试使用定时锁
  • 数据库锁,加锁和解锁必须在一个数据库链接里。

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

java代码编译后会变成java字节码,字节码被加载器加载到JVM里,JVM找到main入口执行代码,最终需要转换成汇编指令在cpu上执行。
java中所使用的汇编机制依赖于JVM的实现和cpu的指令。

2.1 volatile关键字

volatile是轻量级的synchronized,能保证可见性。可见性是说当一个线程修改变量后另一个线程能看到。
volatile比synchronized执行成本更低,因为他不会引起上下文切换和调度。
为了提高处理速度,处理器不直接和内存通信,而是通过缓冲区,但是操作完缓冲区不会立即写内存。但是修改使用了volatile修饰的变量,会立即将缓存回写主内存,并将其他缓存中的旧数据置为无效。

《Java并发编程的艺术》- 读书笔记_第1张图片

2.2 synchronized

2.2.1 synchronized实现同步的基础:java中每一个对象都可以作为锁

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是synchronized括号中配置的对象

2.2.2 java对象头信息
hotspot虚拟机的对象头主要包括两部分数据:Mark Word、Kclass Pointer。Kclass Pointer指向类元数据。Mark Word存储自身运行时数据,其存储结构如下:

2.2.3 原理

synchronized在JVM的实现原理:同一时刻只有一个线程可以获得对象监视。
monitorenter和monitorexit指令在编译后分别插入到同步代码块的开始位置和结束位置。

下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:
《Java并发编程的艺术》- 读书笔记_第2张图片

2.3 原子操作的实现原理

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

处理器可以保证基本的内存操作的原子性。当一个处理器读取一个字节时,其他处理器不能访问这个内存地址。
但是复杂的内存操作处理器不能保证原子性,复杂操作比如:跨总线宽度,跨多个缓存行,跨页表的访问。
总线锁定和缓存锁定用来解决复杂操作的原子性。

  • 总线锁定:处理器提供一个LOCK#信号,当一个处理器在总线上输出次信号时,其他处理器的请求将被阻塞,那么该处理器可以独占共享内存。
  • 缓存锁定

应该优先使用缓存锁定,因为总线锁定会阻塞其他处理器的所有内存操作,但是有两种情况不能使用缓存锁定:操作数据不缓存在处理器内部或者操作的数据跨多个缓存行、有些处理器不支持缓存锁定。

2.3.2 java是如何实现原子操作的

有两个实现方法:cas和锁。
cas的三个问题:

  • ABA。用乐观锁来解决。比如JDK的Atomic包提供了一个类AtomicStampedRefrence。
  • 循环时间长开销大。
  • 只能保证一个共享变量的原子操作。办法是:把多个变量合并成一个变量再cas。JDK提供了AtomicRefrence,可以把多个变量放在一个对象里。

第3章 Java内存模型

并发编程中的两个问题:线程通信和同步。通信是指线程之间交换信息。同步是指线程间发生操作的相对顺序。
java用共享内存的方式解决通信问题,在共享内存并发模型里,同步是显式进行的,程序员需要显式指定某个逻辑需要线程之间互斥执行。

3.1 java内存模型基础

3.1.1 java内存模型
JMM模型如下图。JMM决定一个共享变量的变化何时对另一个线程可见。线程共享的区域才存在内存可见性问题。
《Java并发编程的艺术》- 读书笔记_第3张图片
3.1.2 JMM视角:java线程通信过程
每个线程都有一个本地内存,本地内存中存储了该线程以读写共享变量的副本。线程A和B通信必须经过主存。
《Java并发编程的艺术》- 读书笔记_第4张图片
《Java并发编程的艺术》- 读书笔记_第5张图片

3.1.3 重排序
编译期和处理器会做重排序。

这些重排序可能导致内存可见性问题。解决办法:
编译期重排序:JMM编译器重排序规则会禁止特定类型的编译器重排序。
处理器重排序:JMM编译器重排序规则会要求Java编译期生成指令序列时插入特定类型的内存屏障,通过屏障禁止特定类型的处理器重排序。

内存屏障可以被分为以下几种类型
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

《Java并发编程的艺术》- 读书笔记_第6张图片

3.1.4 happens-before
JSR-133内存模型引入happens-before概念。
在JMM中,如果一个操作的结果需要对另一个操作可见,那么这两个操作之间存在happens-before关系。这里的两个操作不一定在一个线程。

两个操作有happens-before关系,并不是说一个操作要在另一个操作之前执行。
happens-before仅仅要求前一个操作对后一个操作可见,切前一个操作按顺序排在第二个操作之前。

happens-before与JMM关系:
《Java并发编程的艺术》- 读书笔记_第7张图片
程序员遵循的happens-before规则影响这JMM是否要禁止某些重排序。程序员遵循的happens-before规则有:

  • 程序顺序规则:一个线程中的操作happens-before与其后续操作
  • 监视器锁规则:解锁happens-before加锁
  • volatile变量规则:volatile的写happens-before后续的读
  • 传递性

3.2 重排序

3.2.1 数据依赖性
单个线程中的两个操作,且其中一个是写操作,则这两个操作存在数据依赖。这两个操作不会被重排序。
3.2.2 as-if-serial语义
含义:单线程环境下,重排序后,执行结果不能变。
as-if-serial语义完美诠释了happens-before的定义。
举个例子,计算圆的面积,示例代码如下:

按照程序顺序规则,A happens-before B,但是A和B可能会重排序,B先于A执行,但是计算圆面积的结果不变,遵循了as-if-serial语义。
3.2.3 重排序对多线程的影响
举个例子。
《Java并发编程的艺术》- 读书笔记_第8张图片
这里1和2不存在依赖关系,当这两个操作被重排序,多线程的语义被破坏。
《Java并发编程的艺术》- 读书笔记_第9张图片
3,4被重排序也会破坏多线程语义。
《Java并发编程的艺术》- 读书笔记_第10张图片

对flag加volatile修饰,或者将读写方法加synchronized可以实现正确同步。

3.3顺序一致性

3.4volatile的内存语义

3.4.1 volatile特性:

  • 可见性
  • 原子性
    3.4.2 volatile可见性的实现:
    JMM为了实现volatile可见性,通过内存屏障对重排序做了限制。
    《Java并发编程的艺术》- 读书笔记_第11张图片
  • 第二个操作是volatile是写,不能重排序
  • 第一个操作是volatile读,不能重排序
  • 第一个操作是volatile写,第二个是volatile读不能重排序。

3.5锁的内存语义

3.5.1 以ReentrantLock为例,加锁和解锁的实现是基于volatile state;
CAS也依赖于volatile的读写内存语义。
3.5.2 current包
current包的通用实现模式:

  • 声明volatile变量
  • 使用CAS的原子条件实现线程同步
  • 用volatile读/写和CAS所具有的volatile读和写的内存语义实现线程通信。
    《Java并发编程的艺术》- 读书笔记_第12张图片

3.6 final域的内存语义

对final域,编译器和处理器遵循两个重排序规则:

  • 在构造函数内对final域的写入,与随后把这个构造对象赋值给另一个引用,这两个操作不能重排序。
  • 初次读一个包含final域的对象,与随后初次读这个对象中的final域,不能重排序。
    如果final域为引用类型增加如下约束:
  • 在构造函数内对final域的写入,与随后在构造函数外把这个构造对象赋值给另一个引用,这两个操作不能重排序。

3.7 happens-before

JMM对程序员承诺:

  • 如果一个操作happens-before于另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
    JMM对编译器和处理器重排序的约束原则:
  • 如果一个操作happens-before于另一个操作,不一定要第一个操作的执行顺序排在第二个操作之前。如果不影响执行结果可以重排序。

3.7 双重锁检查

3.7.1 双重锁检查的由来
懒汉模式的单例初始化为例
《Java并发编程的艺术》- 读书笔记_第13张图片
因为synchronized存在性能开销,因此出现了“双重锁检查”。代码如下。如果第一次检查instance不为null就不需要加锁直接返回。
《Java并发编程的艺术》- 读书笔记_第14张图片
上面代码的问题在于第7行在实际执行时其实是三个动作
在这里插入图片描述
如果2,3发生重排序,将会出问题。线程B可能读到一个还没有初始化的对象。
《Java并发编程的艺术》- 读书笔记_第15张图片
知道了问题的原因,就有了解决方案:

  • 不允许2,3重排序:基于volatile
  • 允许2,3重排序,单不允许其他线程看到:基于类初始化
    《Java并发编程的艺术》- 读书笔记_第16张图片

JVM在类的初始化阶段(class被加载后,被线程使用前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
《Java并发编程的艺术》- 读书笔记_第17张图片
《Java并发编程的艺术》- 读书笔记_第18张图片

第4章 java并发编程基础

4.1 线程状态

《Java并发编程的艺术》- 读书笔记_第19张图片

4.2 几个线程函数

4.2.1 中断
4.2.2 终止
4.2.3 Thread.join()
4.2.4 ThreadLocal

4.3 线程间通信

4.3.1 volatile/synchronized
4.3.2 wait/notify
《Java并发编程的艺术》- 读书笔记_第20张图片
4.3.3 等待/通知经典范式
等待方:
《Java并发编程的艺术》- 读书笔记_第21张图片
通知方:

示例:
《Java并发编程的艺术》- 读书笔记_第22张图片

第5章 Java中的锁

5.1 lock

lock比synchronized优势在于:

  • 尝试非阻塞获取锁
  • 可超时获取锁
  • 可相应中断

5.2队列同步器

读懂这段灵魂代码就够了

public final void acquire(int arg) {
       if (!tryAcquire(arg) &&
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();
   }

5.3 读写锁

  • 一个32位int表示锁状态,高16位表示读状态,低16位表示写状态
  • 获取写锁的时候必须没有线程持有读写锁
  • 锁降级:持有写锁,持有读锁,释放写锁
  • 问题:读锁长时间占有,写锁获取不到,导致线程“饥饿”,解决办法:JDK8的stampLock

5.4 LockSupport

  • 阻塞/唤醒线程。park()/unpark()
  • 每个线程都有一个permit,取值0或者1,park()/unpark()通过修改permit实现阻塞/唤醒

5.5 Condition接口

配合lock使用,提供await()、signal()两个方法。功能可替代synchronized和wait()、nitify().
优势:一个锁可以有多个condition().

第6章 java并发容器和框架

6.1 ConcurrentHashMap

为什么用ConcurrentHashMap:

  • HashMap在多线程环境下可能会出现死循环、丢数据。发生在扩容场景。
  • HashTabe 使用synchronized低效
  • ConcurrentHashMap分段锁提高并发

remove(key)
加段锁
要将删除的节点之前的节点clone一遍,因为next指针域是final;

V remove(Object key, int hash, Object value) {  
     lock();  
     try {  
         int c = count - 1;  
         HashEntry<K,V>[] tab = table; //table是volatile,读写代价大,所以这里先赋值给tab 
         int index = hash & (tab.length - 1);  
         HashEntry<K,V> first = tab[index];  
         HashEntry<K,V> e = first;  
         while (e != null && (e.hash != hash || !key.equals(e.key)))  
             e = e.next;  
         V oldValue = null;  
         if (e != null) {  
             V v = e.value;  
             if (value == null || value.equals(v)) {  
                 oldValue = v;  

                 // All entries following removed node can stay 
                 // in list, but all preceding ones need to be 
                 // cloned. 
                 ++modCount;  
                 HashEntry<K,V> newFirst = e.next;  
                 *for (HashEntry<K,V> p = first; p != e; p = p.next)  //要将删除的节点之前的节点clone一遍
                     *newFirst = new HashEntry<K,V>(p.key, p.hash,  
                                                   newFirst, p.value);  
                 tab[index] = newFirst;  
                 count = c; // write-volatile 
             }  
         }  
         return oldValue;  
     } finally {  
         unlock();  
     }  
 }

《Java并发编程的艺术》- 读书笔记_第23张图片
get(key)

V get(Object key, int hash) {  
     if (count != 0) { // count是volatile 当前桶的数据个数是否为0 
         HashEntry<K,V> e = getFirst(hash);  得到头节点
         while (e != null) {  
             if (e.hash == hash && key.equals(e.key)) {  
                 V v = e.value;  
                 if (v != null)  
                     return v;  //不加锁
                 return readValueUnderLock(e); // 读到空值加锁在get一次
             }  
             e = e.next;  
         }  
     }  
     returnnull;  
 }
  • get不加锁,因为get里用到的变量都是volatile。
  • 结构更新对应count,非结构更新对应value,这俩都是volatile。
  • 根据hash和key遍历查找元素,指针的next是final,所以不加锁。但是头指针不是final,getFirst(hash)可能返回过时的头结点。可能会出现读取的过程中被其他线程修改的现象。

put

V put(K key, int hash, V value, boolean onlyIfAbsent) {  
     lock();  
     try {  
         int c = count;  
         if (c++ > threshold) // 扩容判断
             rehash();  
         HashEntry<K,V>[] tab = table;  
         int index = hash & (tab.length - 1);  
         HashEntry<K,V> first = tab[index];  
         HashEntry<K,V> e = first;  
         while (e != null && (e.hash != hash || !key.equals(e.key)))  
             e = e.next;  
         V oldValue;  
         if (e != null) {  
             oldValue = e.value;  
             if (!onlyIfAbsent)  
                 e.value = value; //覆盖旧值 
         }  
         else {  //插入头结点
             oldValue = null;  
             ++modCount;  
             tab[index] = new HashEntry<K,V>(key, hash, first, value);  
             count = c; // write-volatile 
         }  
         return oldValue;  
     } finally {  
         unlock();  
     }  
 }

size()
把每个segement的count相加得到size是不安全的。怎样才能安全准确高效呢?
安全的做法是在统计size时锁住put、remove、clean但是这非常低效。
所以做法是:尝试不加锁统计size,如果统计完发现在统计过程中count有修改再加锁统计。
如何能统计完发现在统计过程中count有修改?
使用modCount变量,put、remove、clean操作前都会modCount++;

6.2 ConcurrentLinkedQueue

无界非阻塞线程安全队列
6.1.1 入队列

  • 入队就是将元素添加到队列尾部
  • tail节点并不一定是尾节点,因为CAS设置尾节点开销大,所以不是每次入队都更新尾节点。
    6.1.2 出队列
  • headl节点并不一定是头节点,因为CAS设置头节点开销大,所以不是每次出队都更新头节点。
public E poll() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;

                if (item != null && p.casItem(item, null)) {
                    // Successful CAS is the linearization point
                    // for item to be removed from this queue.
                    if (p != h) // hop two nodes at a time
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                else if ((q = p.next) == null) {//队列已空,更新头结点
                    updateHead(h, p);
                    return null;
                }
                else if (p == q)
                    continue restartFromHead;
                else
                    p = q;
            }
        }
    }

6.3 Java中的阻塞队列

6.3.1 七个阻塞队列

  • ArrayBlockingQueue.基于数组的有界队列。
public ArrayBlockingQueue(int capacity, boolean fair) {
       if (capacity <= 0)
           throw new IllegalArgumentException();
       this.items = new Object[capacity];
       lock = new ReentrantLock(fair);
       notEmpty = lock.newCondition();
       notFull =  lock.newCondition();
   }
  • LinkedBlockingQueue。基于链表的有界队列,有两把锁分别用于入队出队。
/** Lock held by take, poll, etc */
   private final ReentrantLock takeLock = new ReentrantLock();

   /** Wait queue for waiting takes */
   private final Condition notEmpty = takeLock.newCondition();

   /** Lock held by put, offer, etc */
   private final ReentrantLock putLock = new ReentrantLock();

   /** Wait queue for waiting puts */
   private final Condition notFull = putLock.newCondition();
  • PriorityBlockingQueue。基于优先级的无界队列
public PriorityBlockingQueue(int initialCapacity,
                                Comparator<? super E> comparator) {
       if (initialCapacity < 1)
           throw new IllegalArgumentException();
       this.lock = new ReentrantLock();
       this.notEmpty = lock.newCondition();
       this.comparator = comparator;
       this.queue = new Object[initialCapacity];
   }
  • DelayQueue
    可用于设计缓存系统、定时任务调度。
  • SynchronousQueue。不存储元素
  • LinkedTransferQueue。链表无界阻塞。tryTransfer()试图将入队的元素直接给等待的消费者。
  • LinkedBlockingDeque。链表双向。只有一把锁,性能低于LinkedBlockingQueue.使用场景:工作窃取模式(每个消费者消费自己的队列,消费完了可以消费其他队列,从其他队列尾开始消费)
    6.3.12 阻塞队列的实现
    通知模式实现。

第七章 13个原子操作类

7.1 原子类

Atomic包提供三个类:

  • AtomicInteger
  • AtomicBoolean
  • AtomicLong
    Java并发包只实现了三个基本类型的原子类,int,boolean,long,那么其他的呢,char,duble,float怎么办?——借鉴boolean的实现,先转为int在计算。

7.2 原子更新数组

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

7.3 原子引用类型

  • AtomicReference

7.4 原子更新字段

  • AtomicIntegerFieldUpdater

第八章 并发工具类

8.1 CountDownLatch

使用场景:等待所有子线程执行完再继续执行主线程
功能类似join()。

8.2 CyclicBarrier

使用场景:等所有子线程执行到一个节点后再开一起继续执行。

8.3 Semaphore

流量控制。与lock的区别:非owner线程可以释放Semaphore。lock只有owner才能释放锁。

8.4 Exchanger

数据交换,支持timeout

第九章 线程池

9.1 原理

9.1.1 处理流程
《Java并发编程的艺术》- 读书笔记_第24张图片
1>当前运行的线程少于corePoolSize,创建新线程执行任务(加全局锁)
2>任务数>=corePoolSize,则加入BlockingQueue
3>BlockingQueue满,创建新线程执行任务(加全局锁)
4>运行的线程数>maximumPoolSize,调用拒绝策略拒绝任务
《Java并发编程的艺术》- 读书笔记_第25张图片
线程池创建线程会被封装成工作线程,工作线程处理完现在任务还会继续在队列中取任务。
《Java并发编程的艺术》- 读书笔记_第26张图片
9.1.2 线程池的创建

    public ThreadPoolExecutor(int corePoolSize,//核心线程数
                              int maximumPoolSize,//最大线程数
                              long keepAliveTime,//线程存活时间
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,//阻塞队列
                              ThreadFactory threadFactory,//线程创建工厂
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  • keepAliveTime
    线程最大空闲时间。
    只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用
    如果调用了allowCoreThreadTimeOut(true)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用
  • RejectedExecutionHandler
    线程池满 并且 缓存队列满 拒绝策略会起作用
    AbortPolicy 丢弃并抛异常 RejectExecutionException
    DiscardPolicy 丢弃不抛异常
    DiscardOldestPolicy 丢弃队列最前的任务 并执行当前任务
    callerRunnsPolicy 由调用线程处理该任务
    9.1.3 线程池的几个方法
  • execute()
  • submit()。
    它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果
  • shutdown()。线程处于SHUTDOWN状态,此时线程池不在接收新的任务,等待现有的线程执行完毕
  • shutdownNow()。线程处于STOP状态,尝试终止正在执行的任务。
    9.1.4 线程池状态
volatile int runState;
static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;//调用shutdown()
static final int STOP       = 2;//调用shutdownNow()
static final int TERMINATED = 3;//线程处于SHUTDOWN或者STOP状态,并且所有工作线程已销毁,任务队列被清空或者已经执行完。

9.1.5线程池大小
cpu密集型任务:N+1
io密集型任务:2*N

你可能感兴趣的:(java,web)