JUC第一讲:juc并发包深入理解(P6熟练 P7精通)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
云解析DNS,个人版 1个月
简介: JUC第一讲:juc并发包深入理解(P6熟练 P7精通)

先来一道面试题

面试题1:如何使线程按序交替?

  • 实现方式:先做一个标记number=1;定义三个方法,先判断是否number为1,阻塞其他线程,打印当前线程,唤醒2号线程

书籍推荐

1、《Java并发编程实战》作者阵容可谓大师云集,也包括Doug Lea(基础篇)

2、《Java并发编程的艺术》讲解并发包内部实现原理,能读明白,内功大增(高段位)

3、《图解Java多线程设计模式》并发编程设计模式方面的经典书籍

4、《操作系统:精髓与设计原理》经典操作系统教材

5、http://ifeve.com 国内专业并发编程网站

6、http://www.cs.umd.edu/~pugh/java/memoryModel/ 很多并发编程的早期资料都在这里

关于java并发包

JUC就是 java.util .concurrent 工具包的简称。这是一个处理线程的工具包, JDK1.5 开始出现的。它加了一些在并发编程中常用的工具类,用于定义类似线程的自定义子系统,包括线程池,异步io,轻量级任务框架等。

concurrentHashmap

一种线程安全的hash表,对于多线程的操作,性能介于hashmap和hashtable之间

1.1、不同版本的并发hashmap区别 美团
版本 数据结构
jdk1.7 内部采用了锁分段机制来替代 hashtable 的独占锁(也就是每一个 Segment 上同时只有一个线程可以操作),从而提高了性能 segment[] 数组和HashEntry[] 数组, Segment 的个数一但初始化就不能改变, 默认 Segment的个数是 16 个
jdk1.8 进行put操作时:上面的segment采用的是 cas 机制来保证线程安全的使用的 Synchronized 锁加 CAS 的机制。 结构也由Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树)。Node 是类似于一个 HashEntry 的结构。 它的冲突再达到一定大小时会转化成红黑树, 在冲突小于一定数量时又退回链表
1.2、属性
initialCapacity初始容量 16
loadFactor 加载因子 0.75
concurrencyLevel 并发级别 16
table 默认为null,初始化发生在第一次put操作,默认大小为16的数组
nextTable 默认为null,扩容时新生成的数组,其大小为原数组的两倍
sizeCtl 默认为0,用来控制table的初始化和扩容操作(-1代表table正在初始化,-N表示n-1个线程正在扩容操作)
Node 保存key,value及key的hash值的数据结构(Node:保存key,value及key的hash值的数据结构)
Segment的数组大小一定是2的次幂?

主要是便于通过按位与的散列算法来定位Segment的index,保证存储空间的充分利用。

1.3、concurrentHashmap组成

由Segment的数组结构(一种可重入的reentrantLock)和HashEntry数组结构(存储键值对数据)组成,对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁

1.4、put操作

1、假设table已经初始化完成,put操作采用CAS+synchronized实现并发插入或更新操作

2、首先对key.hashcode进行hash操作,得到key的hash值,定位索引位置,index = hash &(length-1)

3、获取table中对应索引的对象f,node类型:Unsafe.getObjectVolatile来获取 tabAt[index],获取指定内存的数据,保证每次拿到数据都是最新

4、如果f为null,说明table中该位置第一次插入元素,利用CAS方法插入Node节点

  1. CAS成功,说明Node节点已经插入,随后检查是否需要进行扩容
  2. CAS失败,说明有其它线程提前插入了节点,自旋重新尝试插入节点
    如果f的hash值为-1,意味有其它线程正在扩容,则一起进行扩容操作

5、f不为null的其余情况把新的Node节点按链表或红黑树的方式插入到合适的位置,这个过程采用同步内置锁实现并发(Synchronized锁住Node,减少了锁粒度)

  1. 在节点f上进行同步,节点插入之前,再次利用tabAt(tab, i) == f判断,防止被其它线程修改
  2. 如果f.hash >= 0,说明f是链表结构的头结点,遍历链表,如果找到对应的node节点,则修改value,否则在链表尾部加入节点
  3. 如果f是TreeBin类型节点if(f instanceof TreeBin),说明f是红黑树根节点,则在树结构上遍历元素,更新或增加节点
  4. 如果链表中节点数binCount >= TREEIFY_THRESHOLD(默认是8),则把链表转化为红黑树结构
1.5、get(key)

无需加锁,涉及的共享变量都使用volatile修饰,volatile保证内存可见性。

获取索引的对象f=tabAt[index],遍历key,找到相等的,cas来保证变量的原子性读取

1.6、扩容时要注意的?

元素数量达到容量阈值sizeCtl(长度*0.75),扩容分为两部分:

1、构建一个nextTable,大小为table的两倍

2、Unsafe.compareAndSwapInt修改sizeCtl值-1,保证只有一个线程初始化,扩容后的数组长度为原来的两倍,但是容量是原来的1.5

3、把table的数据复制到nextTable中:扩容操作支持并发插入,支持节点的并发复制

  • ConcurrentHashMap 在扩容时会计算出一个步长(stride),最小值是16,然后给当前扩容线程分配“一个步长”的节点数,例如16个,让该线程去对这16个节点进行扩容操作(将节点从老表移动到新表)。
  • 如果在扩容结束前又来一个线程,则也会给该线程分配一个步长的节点数让该线程去扩容。依次类推,以达到多线程并发扩容的效果。
1.7、为什么使用ConcurrentHashMap

因为hashmap的线程不安全,在并发环境下,可能会形成环状链表(扩容时造成的,与transfer函数有关),导致get操作时,cpu空转。而hashtable实现线程安全的代价太大了,get/put所有相关操作都是基于synchronized的(全表锁)

1.8、ConcurrentHashMap使用案例

这个案例是来演示session会话续期策略的,倘若是用户再次登录系统,会更新其sessionKey的超期时间。renewal为延续session会话,使用的数据结构为ReentrantReadWriteLock读写锁以及ConcurrentHashMap来保存会话信息。

// redis session读取,会话续期
public class RedisSessionManager {
   /**
     * 会话map   ReentrantReadWriteLock 读锁共享  写锁互斥
     */
    private final ReadWriteLock rwl = new ReentrantReadWriteLock();
    private ConcurrentMap<String, Long> sessionKeyMap = new ConcurrentHashMap<>();
    private void addToReNewMap(String id, long lastAccessAt) {
       rwl.readLock().lock();
       try {
           if (sessionKeyMap.size() < 102400) {
               sessionKeyMap.put(id, lastAccessAt);
           }
       } finally {
           rwl.readLock().unlock();
       }
    }
  public void renewal() {
       Map<String, Long> localSessionKeyMap;
       rwl.writeLock().lock();
       try {
           localSessionKeyMap = sessionKeyMap;
           sessionKeyMap = new ConcurrentHashMap<>();
       } finally {
           rwl.writeLock().unlock();
       }
       localSessionKeyMap = Iters.nullToEmpty(localSessionKeyMap);
       if (localSessionKeyMap.size() > 0) {
           LOGGER.warn("renewal session size = {}", localSessionKeyMap.size());
           localSessionKeyMap.forEach(this.expirationPolicy::onExpirationUpdated);
       }
   }
}

1、JUC基础知识点

1.2 进程与线程

进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。

线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

总结来说:

  • 进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
  • 线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位

1.3 线程的状态

枚举:Thread.State

public enum State {
  /**
  * Thread state for a thread which has not yet started.
  */
  NEW,(新建)
  /**
  * Thread state for a runnable thread. A thread in the runnable
  * state is executing in the Java virtual machine but it may
  * be waiting for other resources from the operating system
  * such as processor.
  */
  RUNNABLE,(准备就绪)
  /**
  * Thread state for a thread blocked waiting for a monitor lock.
  * A thread in the blocked state is waiting for a monitor lock
  * to enter a synchronized block/method or
  * reenter a synchronized block/method after calling
  * {@link Object#wait() Object.wait}.
  */
  BLOCKED,(阻塞)
  /**
  * Thread state for a waiting thread.
  * A thread is in the waiting state due to calling one of the
  * following methods:
  * <ul>
  * <li>{@link Object#wait() Object.wait} with no timeout</li>
  * <li>{@link #join() Thread.join} with no timeout</li>
  * <li>{@link LockSupport#park() LockSupport.park}</li>
  * </ul>
  * *
  <p>A thread in the waiting state is waiting for another thread to
  * perform a particular action.
  *
  * For example, a thread that has called <tt>Object.wait()</tt>
  * on an object is waiting for another thread to call
  * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
  * that object. A thread that has called <tt>Thread.join()</tt>
  * is waiting for a specified thread to terminate.
  */
  WAITING,(不见不散)
  /**
  * Thread state for a waiting thread with a specified waiting time.
  * A thread is in the timed waiting state due to calling one of
  * the following methods with a specified positive waiting time:
  * <ul>
  * <li>{@link #sleep Thread.sleep}</li>
  * <li>{@link Object#wait(long) Object.wait} with timeout</li>
  * <li>{@link #join(long) Thread.join} with timeout</li>
  * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
  * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
  * </ul>
  */
  TIMED_WAITING,(过时不候)
  /**
  * Thread state for a terminated thread.
  * The thread has completed execution.
  */
  TERMINATED;(终结)
}

wait/sleep 的区别?

(1) sleep 是 Thread 的静态方法,wait 是 Object 的方法,任何对象实例都能调用。

(2) sleep 不会释放锁,它也不需要占用锁。 wait 会释放锁,但调用它的前提是当前线程占有锁(即代码要在 synchronized 中)。

(3) 它们都可以被 interrupted 方法中断

并发与并行

串行模式

  • 串行表示所有任务都一一按先后顺序进行。串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。
  • 串行是一次只能取得一个任务,并执行这个任务。

并行模式

  • 并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核 CPU。

并发

并发(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。 并发的重点在于它是一种现象, 并发描述的是多进程同时运行的现象。但实际上,对于单核心 CPU 来说,同一时刻只能运行一个线程。所以,这里的"同时运行"表示的不是真的同一时刻有多个线程运行的现象,这是并行的概念,而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占 CPU 的,而是执行一会停一会。

要解决大并发问题,通常是将大任务分解成多个小任务, 由于操作系统对进程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。这可能会出现一些现象:

  • 可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用队列或类似的数据结构来存放各个小任务的成果
  • 可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用或异步的方式,比如只有准备好产生了事件通知才执行某个任务。
  • 可以多进程/多线程的方式并行执行这些小任务。也可以单进程/单线程执行这些小任务,这时很可能要配合多路复用才能达到较高的效率

并发: 同一时刻多个线程在访问同一个资源,多个线程对一个点

  • 例子:春运抢票 电商秒杀…

并行: 多项工作一起执行,之后再汇总

  • 例子:泡方便面,电水壶烧水,一边撕调料倒入桶中

1.5 管程

管程(monitor)是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现).但是这样并不能保证进程以设计的顺序执行

JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁

执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程

1.6 用户线程和守护线程

  • 用户线程:平时用到的普通线程,自定义线程
  • 守护线程:运行在后台,是一种特殊的线程,比如垃圾回收
  • 当主线程结束后,用户线程还在运行,JVM 存活
  • 如果没有用户线程,都是守护线程,JVM 结束

1.2、创建线程的三个方法是什么? 阿里

常见的Java线程的4种创建方式分别为:继承Thread类、实现Runnable接口、通过ExecutorService和Callable实现有返回值的线程、基于线程池,如下图所示:

  • 通过继承 Thread 类创建线程类,该子类重写Thread类的run方法,Thread类实现了Runnable接口并定义了操作线程的一些方法,我们可以通过继承Thread类的方式创建一个线程
  • 优点:编写简单,如果需要访问当前线程,无需使用 Thread.currentThread()方法,直接使用this,即可获得当前线程;
  • 缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。
  • 实现 Runnable 接口创建线程类,实现该类的run()方法,基于Java编程语言的规范,如果子类已经继承(extends)了一个类,就无法再直接继承Thread类,此时可以通过实现Runnable接口创建线程。具体的实现过程为:通过实现Runnable接口创建ChildrenClassThread 线程,实例化名称为childrenThread的线程实例,创建Thread类的实例并传入childrenThread线程实例,调用线程的start方法启动线程。
  • 优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想;
  • 缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法
  • 通过 Callable 和 Future 接口创建线程。

继承 Thread 类创建线程Demo如下, 创建一个类并继承Thread接口,然后实例化线程对象并调用start方法启动线程,start方法是一个native方法,通过在操作系统上启动一个新线程,并最终执行run方法来启动一个线程。run方法内的代码是线程类的具体实现逻辑

class ThreadDemo extends Thread/implements Runnable {
  public void run() {
    for(int x=0;x<60;x++)
      System.out.println(Thread.currentThread()+"子线程运行");
      //System.out.println(Thread.currentThread().getName()+" : "+ x);
  }
}
class ThreadTest {
  public static void main(String[] args) {
    //1,继承方式:extends
    ThreadDemo threadDemo= new ThreadDemo();
    threadDemo.start();
    for(int x=0;x<60;x++){
      System.out.println("主线程运行");
    }
    /**2,实现接口方式测试:implements
    ThreadDemo threadDemo= new ThreadDemo();
    Thread td1 = new Thread(threadDemo);
    Thread td2 = new Thread(threadDemo);
    Thread td3 = new Thread(threadDemo);
    td1.start();
    td2.start();
    td3.start();
    **/
  }
}

以上代码定义了一个名为ThreadDemo的线程类,该类继承了Thread, run方法内的代码为线程的具体执行逻辑,在使用该线程时只需新建一个该线程的对象并调用其start方法即可。

在JDK源码中,run方法的实现代码如下:

1.3、Java 怎么获取多线程的返回值?

有时,我们需要在主线程中开启多个线程并发执行一个任务,然后收集各个线程执行返回的结果并将最终结果汇总起来,这时就要用到Callable接口。具体的实现方法为:创建一个类并实现Callable接口,在call方法中实现具体的运算逻辑并返回计算结果。具体的调用过程为:创建一个线程池、一个用于接收返回结果的Future List及Callable线程实例,使用线程池提交任务并将线程执行之后的结果保存在Future中,在线程执行结束后遍历Future List中的Future对象,在该对象上调用get方法就可以获取Callable线程任务返回的数据并汇总结果

  • 通过ExecutorService和Callable实现有返回值的线程
  • 使用 Thread 的 join 阻塞当前线程等待。
  • 实现 Callable 接口( 通过 FutureTask 或线程池的 Future)

使用 FutureTask 来获取多线程返回值 Demo

通过 Future 接口创建线程 Demo

FutureTask<Response<Map<Long, ItemDTO>>> task = this.buildItemTaskCache(paramDTO.getDistrictIds(), itemIds, itemReadContext.getIncludeRewriteSaleQuantity(), itemReadContext.getIncludeRewriteStockQuantity(), consumerAppName);
            taskMap.put(ITEM_TASK, task);
            microReadThreadPool.submit(task);
 for (Map.Entry<String, FutureTask> taskEntry : taskMap.entrySet()) {
      String key = taskEntry.getKey();
      FutureTask value = taskEntry.getValue();
      Response<Map<Long, ItemDTO>> itemResp = (Response<Map<Long, ItemDTO>>) value.get(batchFindFullDetailTaskTimeout, TimeUnit.MILLISECONDS);
}

通过 CompletableFuture 创建线程 Demo

CompletableFuture<Response<Map<Long, ItemDTO>>> itemFuture = CompletableFuture.supplyAsync(() -> {
            Response<Map<Long, ItemDTO>> mapResponse = itemMap(itemIds);
            return mapResponse;
        }, microReadThreadPool);
Response<Map<Long, ItemDTO>> itemResp = itemFuture.get(15, TimeUnit.SECONDS);

2、线程池 Executor/ThreadPoolExecute

基于线程池

  • 线程是非常宝贵的计算资源,在每次需要时创建并在运行结束后销毁是非常浪费资源的。我们可以使用缓存策略并使用线程池来创建线程,具体过程为创建一个线程池并用该线程池提交线程任务,实现代码如下:

提供了一个线程队列,队列中保存着所有等待状态的线程,避免了创建和销毁的额外开销,提高了程序响应的速度

下一篇文章:java中的线程池会详细讲到

callble和runnable的区别

创建线程的4种方式

3、阻塞队列 blockingqueue

当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空

具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查

组别 插入 移除 检查 处理
第一组 插入 add(e) 移除 remove() 检查 element() 处理方式:抛出异常
第二组 插入 offer(e) 移除 poll() 检查 peek() 处理方式:返回特殊值
第三组 插入 put(e) 移除take() 检查 不可用 处理方式: 一直阻塞
第四组 插入 offer(e,time,unit) 移除 poll(time,unit) 检查 不可用 处理方式:超时退出
对四组不同的行为方式解释:
抛出异常 如果试图的操作无法立即执行,抛一个异常
特定值 如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
阻塞 如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
超时 如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是true / false)
注意:无法向一个BlockingQueue中插入 null。如果你试图插入null,BlockingQueue将会抛出一个NullPointerException
3.1、实现类:(共6种)

1、ArrayBlockingQueue 有界的阻塞队列 其内部实现是将对象放到一个数组里

细节:

1 内部有个数组 items 用来存放队列元素;
2 putindex 下标标示入队元素下标
3 takeIndex是出队下标,count统计队列元素个数
4 独占锁lock用来对出入队操作加锁
5 notEmpty,notFull条件变量用来进行出入队的同步

方法:

1 offer方法 在队尾插入元素,如果队列满则返回false,否者入队返回true
2 Put操作 在队列尾部添加元素,如果队列满则等待队列有空位置插入后返回
3 Poll操作 从队头获取并移除元素,队列为空,则返回null
4 Take操作 从队头获取元素,如果队列为空则阻塞直到队列有元素。 (当前线程会被挂起放到 notEmpty 的条件队列里面,直到入队操作执行调用notEmpty.signal后当前线程才会被激活,)
5 Peek操作 返回队列头元素但不移除该元素,队列为空,返回null
6 Size操作 获取队列元素个数,非常精确因为计算size时候加了独占锁,其他线程不能入队或者出队或者删除元素
总结 锁的粒度比较大 类似在方法上添加synchronized

2、DelayQueue 延迟无界阻塞队列

只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的Delayed 元素

运用:缓存系统的设计,缓存中的对象,超过了空闲时间,需要从缓存中移出

1、take()和offer()都是lock了重入锁,按照synchronized的公平锁,两个方法是互斥

2、take()方法需要等待1个小时才能返回,offer()需要马上提交一个10秒后运行的任务,此时offer()可以插队获取锁

3、原理:A执行时,B lock()锁,并休眠;当锁被A释放处于可用状态时,B线程却还处于被唤醒的过程中,此时C线程请求锁,可以优先C得到锁

3、LinkedBlockingQueue 有界/无界链表阻塞队列(线程池默认使用)

内部以一个链式结构(链接节点)对其元素进行存储,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限

细节:

1 两个 Node 分别用来存放首尾节点
2 初始值为 0 的原子变量 count用来记录队列元素个数
3 两个ReentrantLock的独占锁,分别用来控制元素入队和出队加锁(takeLock取元素,putLock添加元素)
4 notEmpty和notFull用来实现入队和出队的同步
5 可以同时又一个线程入队和一个线程出队
方法:
1 带时间的Offer操作-生产者
:–
2 带时间的poll操作-消费者 (获取并移除队首元素,在指定的时间内去轮询队列看有没有首元素有则返回,否者超时后返回null)
3 put操作-生产者 (与带超时时间的poll类似不同在于put时候如果当前队列满了它会一直等待其他线程调用notFull.signal才会被唤醒)
4 take操作-消费者 (与带超时时间的poll类似不同在于take时候如果当前队列空了它会一直等待其他线程调用notEmpty.signal()才会被唤醒)
5 size操作-消费者 (当前队列元素个数,如代码直接使用原子变量count获取)
6 peek操作 (获取但是不移除当前队列的头元素,没有则返回null。 )
7 remove操作 删除队列里面的一个元素,有则删除返回true,没有则返回false

4、PriorityBlockingQueue 无界的并发队列

可以实现comparable接口中的方法来排序队列中的元素 //是二叉树最小堆的实现

5、synchronizedQueue 不存储元素的BlockingQueue

每一个put操作必须要等待一个take操作,否则不能继续添加元素;适合做交换工作

面试题2:

在多线程操作下,一个数组中最多只能存入 3 个元素。多放入不可以存入数组,或等待某线程对数组中某个元素取走才能放入,要求使用java的多线程来实现。

使用阻塞队列中的ArrayBlockingQueue队列来实现。

存数据使用put(e)方法,取数据使用take()方法。处理方式: 一直阻塞

面试题3:

如果提交任务时,线程池队列已满,这时会发生什么?

1、如果使用的是LinkedBlockingQueue,也就是无界队列,可以继续添加任务到阻塞队列中等待执行,可以无限存放任务;

2、如果使用的是有界队列,如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,满了后,会使用拒绝策略rejectedExecutionHandler处理满了的任务,

Abort(直接抛出rejectedExecutionException)、 默认

Discard(按照LIFO丢弃)、

DiscardOldest(按照LRU丢弃)、
CallsRun(主线程执行)

4、locks 同步锁(多线程的锁机制) ****

多线程安全的解决方案 同步代码块,同步方法,同步锁lock(是一个显示锁,必须通过unlock方法进行释放锁,放在finally操作中)

Synchronized关键字

synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  • 虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此, synchronized 关键字不能被继承。如果在父类中的某个方法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上 synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
  1. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  2. 修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象。

Demo1 售票系统

class Ticket {
  //票数
  private int number = 30;
  //操作方法: 卖票
  public synchronized void sale() {
    //判断:是否有票
    if(number > 0) {
      System.out.println(Thread.currentThread().getName()+" : "+(number--)+" "+number);
    }
  }
}

如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

①获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

②线程执行发生异常,此时 JVM 会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用 sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过 Lock 就可以办到。

2.2 什么是 Lock

  • Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。 Lock 提供了比 synchronized 更多的功能。

Lock 与的 Synchronized 区别

  • Lock 不是 Java 语言内置的, synchronized 是 Java 语言的关键字,因此是内置特性。 Lock 是一个类,通过这个类可以实现同步访问;
  • Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象

Lock 接口定义如 Demo 所示

public interface Lock {
  void lock();
  void lockInterruptibly() throws InterruptedException;
  boolean tryLock();
  boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  void unlock();
  Condition newCondition();
}

ReentrantLock 可重入锁

  • 举例来说明锁的可重入性Demo
public class UnReentrant{
  Lock lock = new Lock();
  public void outer(){
    lock.lock();
    inner();
    lock.unlock();
  } 
  public void inner(){
    lock.lock();
    //do something
    lock.unlock();
  }
}

outer 中调用了 inner, outer 先锁住了 lock, 这样 inner 就不能再获取 lock。 其实调用outer 的线程已经获取了 lock 锁, 但是不能在 inner 中重复利用已经获取的锁资源, 这种锁即称之为不可重入。

可重入就意味着: 线程可以进入任何一个它已经拥有的锁所同步着的代码块

  • synchronized、 ReentrantLock 都是可重入的锁, 可重入锁相对来说简化了并发编程的开
    发。

重点:消费者/生产者问题

解决方案:为了避免虚假唤醒问题,应该把代码放在循环中。

可以使用synchronized和wait,notifyall机制,也可以使用lock锁加上condition(控制线程通信)机制。

condition接口:

描述了可能会与锁有关的条件变量,这些变量在用法上与使用Object描述了可能会与锁有关的条件变量,这些变量在用法上与使用Object.wait访问的隐式监视器类似,但提供了更强大的功能。await,signal,signalall


5、CopyOnWriteArrayList

写入并复制,不适合添加操作多的场景,每次添加都会进行复制,开销大。适合并发迭代操作多的场景

特点:

1、线程安全版本的ArrayList,每次增加的时候,需要新创建一个比原来容量+1大小的数组

2、拷贝原来的元素到新的数组中,同时将新插入的元素放在最末端

3、然后切换引用

4、迭代时生成快照数组;适合读多写少

6、copyonwriteArrayset

  1. 基于CopyOnWriteArrayList实现
  2. 不能插入重复数据,每次add的时候都要遍历数据,性能略低于CopyOnWriteArrayList

CopyOnWrite特点

  1. 添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器
  2. 如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据
优点 1、对CopyOnWrite容器进行并发的读,而不需要加锁,是一种读写分离的思想 2、适合读多写少的并发场景。比如白名单,黑名单
缺点 1、内存占用问题 写操作的时候,内存里会同时驻扎两个对象的内存 2、数据一致性问题 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性

CopyOnWrite使用Demo:

public Response<Map<Long, Long>> generateXxx(XxxDTO... details) {
  List<Object> updates = Lists.newCopyOnWriteArrayList();
  Arrays.stream(details).forEach(detail -> updates.add(toXxx(detail)));
  return saveSnapshots(updates.toArray(new GoodsSnapshot[updates.size()]));
}

7、Atomic类 (多线程的原子性操作提供的工具类)

背景:我们知道,在多线程程序中,诸如++i或i++等运算不具有原子性,因此不是安全的线程操作。可以通过synchronized或ReentrantLock将该操作变成一个原子操作,但是synchronized和ReentrantLock均属于重量级锁。因此JVM为此类原子操作提供了一些原子操作同步类,使得同步操作(线程安全操作)更加方便、高效,它便是AtomicInteger。

AtomicInteger为提供原子操作的Integer的类,常见的原子操作类还有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference等,它们的实现原理相同,区别在于运算对象的类型不同。还可以通过AtomicReference将一个对象的所有操作都转化成原子操作。AtomicInteger的性能通常是synchronized和ReentrantLock的好几倍。

Atomic 是指一个操作是不可中断的。并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic下,如下图所示

基本类型

使用原子的方式更新基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean :布尔型原子类

如AtomicInteger,AtomicBoolean i++变成原子操作,底层是cas。

数组类型

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray :引用类型数组原子类

引用类型

  • AtomicReference:引用类型原子类
  • AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题
  • issue#626:不能解决 ABA 问题
  • AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题

AtomicReference 类使用示例:首先创建了一个 Person 对象,然后把 Person 对象设置进 AtomicReference 对象中,然后调用 compareAndSet 方法,该方法就是通过 CAS 操作设置 ar。如果 ar 的值为 person 的话,则将其设置为 updatePerson

public class AtomicReferenceTest {
  public static void main(String[] args) {
    AtomicReference<Person> ar = new AtomicReference<Person>();
    Person person = new Person("SnailClimb", 22);
    ar.set(person);
    Person updatePerson = new Person("Daisy", 20);
    ar.compareAndSet(person, updatePerson);
    System.out.println(ar.get().getName());// Daisy
    System.out.println(ar.get().getAge());// 20
  }
}

使用场景:需要原子性

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段

8、volatile关键字 详解可以看多线程部分

作用:保证内存的可见性,线程每次都从主存中读取数据。

缺点:不具备“互斥性”:多个线程能同时读写主存,不能保证变量的“原子性”:

(i++不能作为一个整体,分为3个步骤读-改-写),可以使用cas算法保证数据可原子性。


9、cas乐观锁:(非阻塞算法)

是一种硬件对并发的支持,用于管理对共享数据的访问。相当于是无锁的非阻塞实现。

包含三个操作数,内存值V,预估值A,更新值B,当且仅当V==A,V=B;否则,不做任何操作。

悲观锁:

  • 假定并发环境是悲观的,如果发生并发冲突,就会破坏一致性,所以要通过独占锁彻底禁止冲突发生

乐观锁:(锁的粒度小)

  • 假定并发环境是乐观的,即,虽然会有并发冲突,但冲突可发现且不会造成损害,所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。

CAS算法 :

乐观锁的实现往往需要硬件的支持,多数处理器都都实现了一个CAS指令,实现“Compare And Swap”的语义

CAS包含3个操作数:

1 需要读写的内存位置V

2 进行比较的值A

3 拟写入的新值B

当且仅当位置 V 的值等于 A 时,CAS 才会通过原子方式用新值 B 来更新位置 V 的值;否则不会执行任何操作

CAS乐观锁的缺点

ABA问题:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化

代码示例:

public class AtomicIntegerDefectDemo {
    public static void main(String[] args) {
        defectOfABA();
    }
    // ABA缺陷
    static void defectOfABA() {
        final AtomicInteger atomicInteger = new AtomicInteger(1);
        Thread coreThread = new Thread(
                () -> {
                    final int currentValue = atomicInteger.get();
                    System.out.println(Thread.currentThread().getName() + " ------ currentValue=" + currentValue);
                    // 这段目的:模拟处理其他业务花费的时间
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    boolean casResult = atomicInteger.compareAndSet(1, 2);
                    System.out.println(Thread.currentThread().getName()
                            + " ------ currentValue=" + currentValue
                            + ", finalValue=" + atomicInteger.get()
                            + ", compareAndSet Result=" + casResult);
                }
        );
        coreThread.start();
        // 这段目的:为了让 coreThread 线程先跑起来
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread amateurThread = new Thread(
                () -> {
                    int currentValue = atomicInteger.get();
                    boolean casResult = atomicInteger.compareAndSet(1, 2);
                    System.out.println(Thread.currentThread().getName()
                            + " ------ currentValue=" + currentValue
                            + ", finalValue=" + atomicInteger.get()
                            + ", compareAndSet Result=" + casResult);
                    currentValue = atomicInteger.get();
                    casResult = atomicInteger.compareAndSet(2, 1);
                    System.out.println(Thread.currentThread().getName()
                            + " ------ currentValue=" + currentValue
                            + ", finalValue=" + atomicInteger.get()
                            + ", compareAndSet Result=" + casResult);
                }
        );
        amateurThread.start();
    }
}

Thread-0 ------ currentValue=1

Thread-1 ------ currentValue=1, finalValue=2, compareAndSet Result=true

Thread-1 ------ currentValue=2, finalValue=1, compareAndSet Result=true

Thread-0 ------ currentValue=1, finalValue=2, compareAndSet Result=true

如何解决ABA问题:用另一个标识判断某值是否有改变过。

乐观锁的业务场景及实现方式 20181222 以后再补

java中的并发工具类 10/11/12/13

10、闭锁countdownlatch (等待多线程完成)实现了join的功能

10.1、概念:

在完成某些运算时,只有其他所有线程的运算全部完成,当前运算才继续执行,

可以用于统计多线程执行的时间。

可被多个线程并发的实现减1操作,并在计数器为0后调用await方法的线程被唤醒,从而实现多线程间的协作

10.2、可以实现的需求

现需要解析一个excel里的多个sheet数据,使用多线程,每个线程解析其中一个sheet的数据,等到所有sheet解析完,程序提示解析完成构造函数传入int型参数做改为计数器,countDown被调用,计数器减1,await会一直阻塞程序,直至计数为0.

如果某个sheet解析较慢,可以使用带时间参数的await方法,到时间后,不再阻塞当前线程

10.3、分析CountDownLatch的实现原理

1、在AQS队列中,将线程包装为Node.SHARED节点,即标志位共享锁

2、当头节点获得共享锁后,唤醒下一个共享类型结点的操作

  • 1、头节点node1,调用unparkSuccessor()方法唤醒了Node2,并且调用tryAcquireShared方法,检查下一个节点是共享节点
  • 2、如果是,更改头结点,重复以上步骤,以实现节点自身获取共享锁成功后,唤醒下一个共享类型结点的操作
10.5、什么是AQS?******

1、提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架

使用方式是继承:

子类通过继承同步器并需要实现它的方法来管理其状态,管理方式是通过acquire和release方式操纵状态

在多线程环境中对状态的操作必须保证原子性,需要使用这个同步器提供的以下三个方法对状态进行操作

1、AbstractQueuedSynchronizer.getState()

2、AbstractQueuedSynchronizer.setState(int)

3、AbstractQueuedSynchronizer.compareAndSetState(int, int)

同步器是实现锁的关键

同步器面向的是线程访问和资源控制,他定义了线程对资源是否能够获取以及线程的排队等操作。

依赖于FIFO队列,队列中的node就是保存着线程引用和线程状态的容器。

对于一个排它锁的获取和释放

//获取:

while(获取锁){

if(获取到)

退出while循环

else{

if(当前线程没有入队)

入队

阻塞当前线程

}

}

//释放:

if(释放成功){

删除头结点

激活原头结点的后继结点

};

10.6、AQS与锁(如LOCK)的对比

1、锁是面向使用者的,定义了用户调用的接口,隐藏了实现细节;

2、AQS是锁的实现者,屏蔽了同步状态管理,线程的排队,等待唤醒的底层操作;

3、锁是面向使用者,AQS是锁的具体实现者

10.7、CountDownLatch中的方法?

1、countDownLatch.await()发生什么?

直接调用了AQS的acquireSharedInterruptibly

当前线程就会进入了一个死循环当中,在这个死循环里面,会不断的进行判断,通过调用tryAcquireShared方法,如果值为0(说明共享锁没有了),会跳出循环

2、释放操作 //countDown操作实际就是释放锁的操作,每调用一次,计数值减少1

3、限定时间的await方法 await(long timeout, TimeUnit unit)

spinForTimeoutThreshold写死了1000ns,这就是所谓的自旋操作,让线程在循环中自旋,否则阻塞线程

代码:参考并发编程的艺术第8章 8.1/8.2

11、cyclicBarrier(同步屏障)

11.1、cyclicBarrier是什么?

让一组线程到达一个屏障(也称同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续工作

  • 线程进入屏障通过cyclicBarrier的await()方法
11.2、cyclicBarrier底层原理?

由ReentrantLock可重入锁和Condition共同实现的

注意:

1、前面四个线程等待最后一个线程超时了,这个时候这四个线程不会再等待最后一个线程写入完毕,而是直接抛出BrokenBarrierException异常,继续执行后续的动作。最后一个线程完成写入数据操作后也继续了后续的动作

2、构造函数的参数表示屏障拦截的线程数量,还有一个高级的构造函数CyclicBarrier(int parties,Runnable barrierAction):在线程达到屏障后,优先执行barrierAction

11.3、cyclicBarrier的应用场景

用于多线程计算数据,最后合并计算结果的场景

例如:excel保存了用户所有的银行流水,每一个sheet保存一个账户近一年的每笔流水,现需要统计用户的日均银行流水,先用多线程处理每个sheet的银行流水,再用barrierAction计算最后结果,计算整个日均流水

11.4、cyclicBarrier和countDownLatch的区别

CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同

  • 1、CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行
  • 2、CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的

代码:参考并发编程8.2 P191

12、Semaphore详解(控制并发线程数)

Semaphore可以控同时访问的线程个数,通过acquire()获取一个许可,如果没有就等待,而release()可以释放一个许可

Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限 //操作系统中讲过

应用场景:

semaphore可以用作流量控制,特别是公用资源有限的应用场景,如数据库连接。现在要读取几万个文件的数据,IO密集型任务,可以启动几十个线程去并发读取,读到内存后,还需要保存到数据库中,而数据库的连接数只有10个,这时必须并发控制10个线程同时获取数据库连接,来保存数据,否则报错无法获取数据库连接

方法:

  • 1、semaphore(int permits) //许可证数量
  • 2、acquire()/tryAcquire() //获取许可证
  • 3、release() //归还许可证
  • 4、intavailablePermits() //返回此信号量中当前可用的许可证数
  • 5、intgetQueueLength() //返回正在等待获取许可证的线程数

代码:参考并发编程P196

13、Exchanger详解(线程间交换数据)

提供了在线程间交换数据的一种手段,它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,他会一直等待第二个线程也执行此方法,当两个线程都到达同步点时,这两个线程就交换数据。

应用场景:

1、用于遗传算法:选两个人作为交配对象,需要交换两人的数据,并使用交叉规则得出2个交配结果

2、用于校对工作:我们需要将纸质银行流水通过人功能的方式录入成电子银行流水,为避免错误,采用AB岗录入,对两个excel数据进行校对,看是否录入一致

可以使用exchange(V x,long timeout,TimeUnit unit)  //设置最大等待时长

代码:参考并发编程P198

14、公平锁和非公平锁

公平锁:

就是在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁

否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己

非公平锁:

比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式

15、并发队列-非阻塞队列

非阻塞队列使用的是CAS(compare and swap)来实现线程执行的非阻塞

入队:

  • 1、add():底层调用offer();
  • 2、offer():Queue接口继承下来的方法,实现队列的入队操作,不会阻碍线程的执行,插入成功返回true

出队:

  • 1、poll():移动头结点指针,返回头结点元素,并将头结点元素出队;队列为空,则返回null
  • 2、peek():移动头结点指针,返回头结点元素,并不会将头结点元素出队;队列为空,则返回null

Action1:获取单例对象需要保证线程安全,其中的方法也要保证线程安全。

  • 说明:资源驱动类、工具类、单例工厂类都需要注意。

Action2:创建线程或线程池时请指定有意义的线程名称,方便出错时排查

正例: 自定义线程工厂, 并且根据外部特征进行分组, 比如, 来自同一机房的调用, 把机房编号赋值给whatFeatureOfGroup:

public class UserThreadFactory implements ThreadFactory {
  private final String namePrefix;
  private final AtomicInteger nextId = new AtomicInteger(1);
  // 定义线程组名称, 在利用 jstack 来排查问题时, 非常有帮助
  UserThreadFactory(String whatFeatureOfGroup) {
    namePrefix = "FromUserThreadFactory's" + whatFeatureOfGroup + "-Worker-";
  }
  @Override
  public Thread newThread(Runnable task) {
    String name = namePrefix + nextId.getAndIncrement();
    Thread thread = new Thread(null, task, name, 0, false);
    System.out.println(thread.getName());
    return thread;
  }
}

Action3、线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

  • 说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

Action4、线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险。

  • 说明:Executors 返回的线程池对象的弊端如下:
  • 1、FixedThreadPoolSingleThreadPool
  • 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;
  • 2、CachedThreadPool
  • 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM;
  • 3、ScheduledThreadPool
  • 允许的请求队列长度为 Integer.MAX_VALUE, 可能会堆积大量的请求, 从而导致 OOM

Action5、SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils工具类。

  • 正例:注意线程安全,使用DateUtils。或者如下处理
private static final ThreadLocal<DateFormat> df = new  ThreadLocal<DateFormat>(){
  @Override
  protected DateFormat initialValue () {
    return new SimpleDateFormat("yyyy-MM-dd");
  }
};
  • 说明:如果是JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe.
String createAt = DateUtils.format(logRecordPushMq.getCreatedAt(), DateUtils.FORMAT_YMD);
 public static String format(Date dateDate, String format) {
        SimpleDateFormat formatter = new SimpleDateFormat(format);
        String dateString = formatter.format(dateDate);
        return dateString;
}

Action6、高并发时,同步调用应该去考量锁的性能损耗,能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

  • 说明:尽可能使锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。

Action7、对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。

  • 说明:线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。

Action8、并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version作为更新依据

  • 说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁,乐观锁的重试次数不得小于3次。

Action9、多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其他任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题。

Action10、使用CountDownLatch 进行异步转同步操作,每个线程退出前必须调用countDown方法,线程执行代码注意catch异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果。

  • 说明:注意:子线程抛出异常堆栈,不能在主线程try-catch到。

Action11、避免Random实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed 导致性能下降

  • 说明:Random 实例包括java.util.Random 的实例或者 Math.random() 方式
  • 正例:在JDK7之后,可以直接使用API ThreadLocalRandom,而在JDK7 之前,需要编码保证每个线程持有一个实例。

Action12、在并发场景下,通过双重检查锁(double-checked locking) 实现延迟初始化的优化问题隐患,推荐解决方案中较为简单的一种,将目标属性声明为volatile型 (比如修改 helper 的属性声明为 private volatile Helper helper = null;

正例:

public class LazyInitDemo {
  private volatile Helper helper = null;
  public Helper getHelper() {
    if (helper == null) {
      synchronized(this) {
        if (helper == null) {
          helper = new Helper();
        }
      }
    }
    return helper;
  }
  // other methods and fields...
}

Action13、volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。

如果是 count++ 操作,使用如下类实现:

AtomicInteger count = new AtomicInteger();
count.addAndGet(1);
  • 如果是 JDK8,推荐使用 LongAdder 对象, 比 AtomicLong 性能更好(减少乐观锁的重试次数)。
private static LongAdder BATCH_FIND_BRAND_BY_ID_HIT = new LongAdder();
BATCH_FIND_BRAND_BY_ID_HIT.increment();

Action14、HashMap在容量不够进行 resize 时,由于高并发可能出现死链,导致 CPU 飙升,在开发过程中可以使用其他数据结构或加锁来规避此风险

  • JDK1.7 版本会有这个问题吧,JDK1.8已经修复了

Action15、ThreadLocal 无法解决共享对象的更新问题,ThreadLocal对象建议使用 static 修饰。

  • 这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操纵这个变量。

Action16、必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收

正例:

objectThreadLocal.set(userInfo);
try {
  // ...
} finally {
  objectThreadLocal.remove();
}

Action17、在使用阻塞等待获取锁的方式中, 必须在 try 代码块之外, 并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用, 避免加锁成功后, 在 finally 中无法解锁。

说明一: 在 lock 方法与 try 代码块之间的方法调用抛出异常, 无法解锁, 造成其它线程无法成功获取锁。

说明二: 如果 lock 方法在 try 代码块之内, 可能由于其它方法抛出异常, 导致在 finally 代码块中, unlock 对未加锁的对象解锁, 它会调用 AQS 的 tryRelease 方法(取决于具体实现类) , 抛出 IllegalMonitorStateException 异常

说明三: 在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常, 产生的后果与说明二相同。

正例:

Lock lock = new XxxLock();
// ...
lock.lock();
try {
  doSomething();
  doOthers();
} finally {
  lock.unlock();
}

反例:

Lock lock = new XxxLock();
// ...
try {
  // 如果此处抛出异常, 则直接执行 finally 代码块
  doSomething();
  // 无论加锁是否成功, finally 代码块都会执行
  lock.lock();
  doOthers();
} finally {
  lock.unlock();
}

Action18、资金相关的金融敏感信息, 使用悲观锁策略。

说明: 乐观锁在获得锁的同时已经完成了更新操作, 校验逻辑容易出现漏洞, 另外, 乐观锁对冲突的解决策略有较复杂的要求, 处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。

  • 正例: 悲观锁遵循一锁二判三更新四释放的原则。
相关文章
|
7天前
|
Java 调度 开发者
揭秘Java并发包(JUC)的基石:AQS原理和应用
揭秘Java并发包(JUC)的基石:AQS原理和应用
|
2月前
|
安全 Java
多线程(进阶三:JUC)
多线程(进阶三:JUC)
52 0
|
11月前
|
SQL 安全 Java
Java并发编程面试题——JUC专题
Java并发编程面试题——JUC专题
358 0
|
12月前
|
Web App开发 安全 Java
JUC高并发编程(一)——JUC基础知识
JUC高并发编程(一)——JUC基础知识
110 0
|
Java 关系型数据库 MySQL
【Java并发编程 十】JUC并发包下的工具类
【Java并发编程 十】JUC并发包下的工具类
125 0
|
Java
【Java并发编程 七】JUC并发包概述
【Java并发编程 七】JUC并发包概述
145 0
|
存储 缓存 Java
【Java并发编程 十二】JUC并发包下线程池(下)
【Java并发编程 十二】JUC并发包下线程池(下)
56 0
|
存储 缓存 监控
【Java并发编程 十二】JUC并发包下线程池(上)
【Java并发编程 十二】JUC并发包下线程池(上)
52 0
|
存储 安全 算法
【Java并发编程 十一】JUC并发包下并发容器类(上)
【Java并发编程 十一】JUC并发包下并发容器类(上)
59 0
|
存储 安全 算法
【Java并发编程 十一】JUC并发包下并发容器类(下)
【Java并发编程 十一】JUC并发包下并发容器类(下)
83 0