参考资料:
深入浅出Java多线程
廖雪峰的官网

一、并发基础

1.线程与进程

进程:单独占有一定的内存地址空间,数据隔离,数据共享复杂,同步复杂。稳定但资源开销大。

线程:共享进程的内存资源,数据共享简单,同步复杂。可靠性低但资源开销小。

线程并非越多越好,线程越多,上下文切换越多,消耗大量的CPU时间。


2.多线程入门和接口

1.Thread类

  • 继承Thread类,并重写run方法;

2.Runnable接口

  • 实现Runnable接口,并重写run方法;

3.Callable接口

  • Callable一般是配合线程池工具ExecutorService来使用的。有返回值

4.Future接口

  • 拥有取消,获取线程状态的功能。有返回值

5.FutureTask类

  • 同时实现Runnable和Future接口。有返回值

3.线程组和线程优先级

1.线程组:

  • 使用线程组对线程进行批量控制。线程组是一个标准的向下引用的树状结构。

2.线程优先级:

  • Java中线程优先级可以指定,范围是1~10
  • Java默认的线程优先级为5,线程的执行顺序由调度程序来决定,线程的优先级会在线程被调用之前设定。
  • 高优先级的线程将会比低优先级的线程有更高的几率得到执行。
  • 使用方法Thread类的setPriority()实例方法来设定线程的优先级。
  • 总结:优先级你设置了,理不理你就是系统调度的事情了。

4.线程状态及转换

1.操作系统线程主要有以下三个状态:

  • 就绪状态(ready):线程正在等待使用CPU,经调度程序调用之后可进入running状态。
  • 执行状态(running):线程正在使用CPU。
  • 等待状态(waiting): 线程经过等待事件的调用或者正在等待其他资源(如I/O)。

2.Java线程的6个状态

  • NEW:未启动状态。还没调用Thread实例的start()方法。

  • RUNNABLE:线程正在运行中。(正在Java虚拟机中运行 | 等待CPU分配资源。)

  • BLOCKED:阻塞状态。处于阻塞状态的线程正等待锁的释放以进入同步区。

  • WAITING:等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。

    1
    2
    3
    4
    5
    6
    7
    8
    调用如下3个方法会使线程进入等待状态:
    * Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
    * Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法;
    * LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。

    调用如下2个方法唤醒线程:
    * notify():唤醒一个等待的线程
    * notifyAll():唤醒所有等待的线程
  • TIMED_WAITING:超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。

    1
    2
    3
    4
    5
    6
    7
    调用如下方法会使线程进入超时等待状态:

    Thread.sleep(long millis):使当前线程睡眠指定时间;
    Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
    Thread.join(long millis):等待当前线程最多执行millis毫秒,如果millis为0,则会一直执行;
    LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
    LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;
  • TERMINATED:终止状态。此时线程已执行完毕。


5.线程间通信

线程同步:线程之间按照一定的顺序执行。

  1. 锁与同步(对象锁synchronized
  2. 等待/通知机制(在synchronized基础上+ wait()和notify() 通知唤醒线程)
  3. 信号量 (Semaphore对象或者volatile关键字)
  4. 管道(管道是基于“管道流”的通信方式。JDK提供了许多)
  5. 其他信道相关
    1. join()线程礼让,排队等待其他线程完成。
    2. sleep()线程睡眠,不释放锁,(wait释放)
    3. ThreadLocal线程本地变量,内部是一个弱引用的Map
    4. InheritableThreadLocal添加子线程继承父线程本地变量
  6. JDK通讯工具类

二、并发原理

1.java内存模型基础

Java内存模型

存在一个问题,各线程之间变量的一致性问题

2.重排序与happens-before

  1. 指令重排

指令重排对于提高CPU处理性能十分必要。虽然由此带来了乱序的问题,但是这点牺牲是值得的。

指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致

  1. 顺序一致性模型

Java内存模型(JMM)对于正确同步多线程程序的内存一致性做了以下保证:

如果程序是正确同步的,程序的执行将具有顺序一致性。 即程序的执行结果和该程序在顺序一致性模型中执行的结果相同。

这里的同步包括了使用volatilefinalsynchronized等关键字来实现多线程下的同步

  1. happens-before规则

    JMM提供了happens-before规则(JSR-133规范),满足了程序员的需求——简单易懂,并且提供了足够强的内存可见性保证。

    总之,如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的,不管它们在不在一个线程。

3.volatile共享变量

volatile关键字有特殊的内存语义。volatile主要有以下两个功能:

  • 保证变量的内存可见性
  • 禁止volatile变量与普通变量重排序

在功能上,锁比volatile更强大;在性能上,volatile更有优势

4.synchronized与锁

Java多线程的锁都是基于对象的。

CAS: Compare and Swap

比较并设置。用于在硬件层面上提供原子性操作。在 Intel 处理器中,比较并交换通过指令cmpxchg实现。 比较是否和给定的数值一致,如果一致则修改,不一致则不修改。

1.java对象头:(java锁存放的地方)

每个Java对象都有对象头。如果是非数组类型,则用2个字宽来存储对象头,如果是数组,则会用3个字宽来存储对象头。

Mark Word : 存储对象的hashCode或锁信息等

2.几种锁

在Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

几种锁会随着竞争情况逐渐升级,锁的升级很容易发生,但是锁降级发生的条件会比较苛刻

1.偏向锁 :

偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能。

1
2
3
锁记录偏向的线程ID
if(下次进入同步块 == 自己的ID) 无需CAS操作加锁和解锁
else 升级为轻量级锁 竞争

适用于竞争少的情况。

2.轻量级锁:
1
2
3
4
5
尝试用CAS将锁的Mark Word替换为指向锁记录的指针
if(成功) 当前线程获得锁
else 当前线程就尝试使用自旋来竞争获取锁。

自旋到一定程度升级成重量级锁。
3.重量级锁:

依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对比较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。

当调用一个锁对象的waitnotify方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁

4.各种锁对比:
优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行时间较长。

5.CAS与原子操作

悲观锁:

悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

乐观锁:

乐观锁又称为“无锁”,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性。

由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说乐观锁天生免疫死锁

乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。

  1. CAS概念

CAS的全称是:比较并交换(Compare And Swap)。在CAS中,有这样三个值:

  • V:要更新的变量(var)
  • E:预期值(expected)
  • N:新值(new)

比较并交换的过程如下:

判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。

  1. Java实现CAS的原理 - Unsafe类

  2. 原子操作-AtomicInteger类

  3. CAS实现原子操作的三大问题
1
2
3
1.ABA问题
2.循环时间长开销大
3.只能保证一个共享变量的原子操作

6.AQS抽象队列同步器

AQSAbstractQueuedSynchronizer的简称,即抽象队列同步器,从字面意思上理解:

  • 抽象:抽象类,只实现一些主要逻辑,有些方法由子类实现;
  • 队列:使用先进先出(FIFO)队列存储数据;
  • 同步:实现了同步的功能。

那AQS有什么用呢?AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的同步器,比如我们提到的ReentrantLockSemaphoreReentrantReadWriteLockSynchronousQueueFutureTask等等皆是基于AQS的。

…..

…..

三、JDK并发工具

1.线程池 ThreadPoolExecutor

使用线程池主要有以下三个原因

  1. 创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程
  2. 控制并发的数量。并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。(主要原因)
  3. 可以对线程做统一管理

四种拒绝策略:(线程数量大于最大线程数就会采用拒绝处理策略)

  1. ThreadPoolExecutor.AbortPolicy默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
  2. ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

四种常见线程池

  1. CacheThreadPool 核心线程数=0,全部是临时线程。
  2. newFixedThreadPool 核心线程数=最大线程数
  3. newSingleThreadExecutor 核心线程数=最大线程数=1
  4. newScheduledThreadPool 一个定长线程池

2.阻塞队列

源自生产者-消费者模式。

只管往里面存、取就行,而不用担心多线程环境下存、取共享变量的线程安全问题。

  1. BlockingQueue的操作方法

    | 方法\处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
    | :—————-: | :———-: | :————: | :————: | :————————: |
    | 插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
    | 移除方法 | remove() | poll() | take() | poll(time,unit) |
    | 检查方法 | element() | peek() | - | - |

  2. BlockingQueue的实现类

    1. ArrayBlockingQueue数组结构组成的有界阻塞队列。
    2. LinkedBlockingQueue链表结构组成的有界阻塞队列。
    3. DelayQueue 没有大小限制,只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。
    4. PriorityBlockingQueue 优先队列,内部采用非公平锁
    5. SynchronousQueue 没有任何内部容量,并且每个 put 必须等待一个 take,反之亦然。

    对于无界队列:生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。

  3. 阻塞队列的原理

  • 阻塞队列的原理很简单,利用了Lock锁的多条件(Condition)阻塞控制。

3.锁和接口类

Java原生的锁——基于对象的锁,它一般是配合synchronized关键字来使用的。

Java在java.util.concurrent.locks包下,还为我们提供了几个关于锁的类和接口。它们有更强大的功能或更高的性能。

  1. synchronized的不足:

    • 无论读写,同一时间只能有一个线程执行
    • 无法知道线程有没有成功获取到锁
    • 如果临界区因为IO或者sleep方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待
  2. 锁的几种分类:

    1. 可重入锁(synchronizedReentrantLock)和 非可重入锁
    2. 公平锁与非公平锁 (ReentrantLock支持非公平锁和公平锁两种。)
    3. 读写锁和排它锁(synchronizedReentrantLock
  3. JDK中有关锁的一些接口和类

JDK中关于并发的类大多都在java.util.concurrent(以下简称juc)包下。

  1. 抽象类AQS/AQLS/AOS
  2. 接口Condition/Lock/ReadWriteLock(读写锁)
  3. ReentrantLock可重入锁
  4. ReentrantReadWriteLock可重入读写锁
  5. StampedLock 基于无锁,性能之王

4.并发容器集合

整体架构(列举常用的容器类)

整体架构

大致分为:

  1. 阻塞队列
  2. CopyOnWrite容器
  3. 并发Map、Set

5.CopyOnWrite

CopyOnWrite容器即写时复制的容器 ,适合读多写少,读不加锁

CopyOnWrite容器有数据一致性的问题,它只能保证最终数据一致性

6.通讯工具类

它们都在java.util.concurrent包下。

作用
Semaphore 限制线程的数量
Exchanger 两个线程交换数据
CountDownLatch 线程等待直到计数器减为0时开始工作
CyclicBarrier 作用跟CountDownLatch类似,但是可以重复使用
Phaser 增强的CyclicBarrier

7.Fork/Join框架

分而治之

1
2
3
4
5
6
7
8
solve(任务):
if(任务已经划分到足够小):
顺序执行任务
else:
for(划分任务得到子任务)
solve(子任务)
结合所有子任务的结果到上一层循环
return 最终结合的结果

8.Java8 Stream并行计算原理

从Java 8 开始,我们可以使用Stream接口以及lambda表达式进行“流式计算”。

它可以让我们对集合的操作更加简洁、更加可读、更加高效。

Stream接口有非常多用于集合计算的方法,比如判空操作empty、过滤操作filter、求最max值、查找操作findFirst和findAny等等。

9.计划任务

自JDK 1.5 开始,JDK提供了ScheduledThreadPoolExecutor类用于计划任务(又称定时任务)

内部使用优化的DelayQueue来实现

这个类有两个用途:

  • 在给定的延迟之后运行任务
  • 周期性重复执行任务

本文只是简要概括,详情查看:

深入浅出Java多线程
廖雪峰的官网

《Java 并发编程的艺术》

《实战Java高并发程序设计》