java并发编程小结
参考资料:
深入浅出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.线程间通信
线程同步:线程之间按照一定的顺序执行。
- 锁与同步(对象锁
synchronized
) - 等待/通知机制(在
synchronized
基础上+ wait()和notify() 通知唤醒线程) - 信号量 (
Semaphore
对象或者volatile
关键字) - 管道(管道是基于“管道流”的通信方式。JDK提供了许多)
- 其他信道相关
join()
线程礼让,排队等待其他线程完成。sleep()
线程睡眠,不释放锁,(wait释放)ThreadLocal
线程本地变量,内部是一个弱引用的Map
InheritableThreadLocal
添加子线程继承父线程本地变量
- JDK通讯工具类
二、并发原理
1.java内存模型基础
存在一个问题,各线程之间变量的一致性问题
2.重排序与happens-before
- 指令重排
指令重排对于提高CPU处理性能十分必要。虽然由此带来了乱序的问题,但是这点牺牲是值得的。
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。
- 顺序一致性模型
Java内存模型(JMM)对于正确同步多线程程序的内存一致性做了以下保证:
如果程序是正确同步的,程序的执行将具有顺序一致性。 即程序的执行结果和该程序在顺序一致性模型中执行的结果相同。
这里的同步包括了使用volatile
、final
、synchronized
等关键字来实现多线程下的同步。
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.偏向锁 :
偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能。
1 | 锁记录偏向的线程ID |
适用于竞争少的情况。
2.轻量级锁:
1 | 尝试用CAS将锁的Mark Word替换为指向锁记录的指针 |
3.重量级锁:
依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对比较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。
当调用一个锁对象的wait
或notify
方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。
4.各种锁对比:
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 | 追求响应时间。同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。同步块执行时间较长。 |
5.CAS与原子操作
悲观锁:
悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。
乐观锁:
乐观锁又称为“无锁”,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性。
由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说乐观锁天生免疫死锁。
乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。
- CAS概念
CAS的全称是:比较并交换(Compare And Swap)。在CAS中,有这样三个值:
- V:要更新的变量(var)
- E:预期值(expected)
- N:新值(new)
比较并交换的过程如下:
判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。
Java实现CAS的原理 - Unsafe类
原子操作-AtomicInteger类
- CAS实现原子操作的三大问题
1 | 1.ABA问题 |
6.AQS抽象队列同步器
AQS是AbstractQueuedSynchronizer
的简称,即抽象队列同步器
,从字面意思上理解:
- 抽象:抽象类,只实现一些主要逻辑,有些方法由子类实现;
- 队列:使用先进先出(FIFO)队列存储数据;
- 同步:实现了同步的功能。
那AQS有什么用呢?AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的同步器,比如我们提到的ReentrantLock
,Semaphore
,ReentrantReadWriteLock
,SynchronousQueue
,FutureTask
等等皆是基于AQS的。
…..
…..
三、JDK并发工具
1.线程池 ThreadPoolExecutor
使用线程池主要有以下三个原因:
- 创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程。
- 控制并发的数量。并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。(主要原因)
- 可以对线程做统一管理。
四种拒绝策略:(线程数量大于最大线程数就会采用拒绝处理策略)
ThreadPoolExecutor.AbortPolicy
:默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException
异常。ThreadPoolExecutor.DiscardPolicy
:丢弃新来的任务,但是不抛出异常。ThreadPoolExecutor.DiscardOldestPolicy
:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。ThreadPoolExecutor.CallerRunsPolicy
:由调用线程处理该任务。
四种常见线程池:
CacheThreadPool
核心线程数=0,全部是临时线程。newFixedThreadPool
核心线程数=最大线程数newSingleThreadExecutor
核心线程数=最大线程数=1newScheduledThreadPool
一个定长线程池
2.阻塞队列
源自生产者-消费者模式。
只管往里面存、取就行,而不用担心多线程环境下存、取共享变量的线程安全问题。
BlockingQueue
的操作方法| 方法\处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
| :—————-: | :———-: | :————: | :————: | :————————: |
| 插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
| 移除方法 | remove() | poll() | take() | poll(time,unit) |
| 检查方法 | element() | peek() | - | - |BlockingQueue
的实现类ArrayBlockingQueue
由数组结构组成的有界阻塞队列。LinkedBlockingQueue
由链表结构组成的有界阻塞队列。DelayQueue
没有大小限制,只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。PriorityBlockingQueue
优先队列,内部采用非公平锁SynchronousQueue
没有任何内部容量,并且每个 put 必须等待一个 take,反之亦然。
对于无界队列:生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。
阻塞队列的原理
- 阻塞队列的原理很简单,利用了Lock锁的多条件(Condition)阻塞控制。
3.锁和接口类
Java原生的锁——基于对象的锁,它一般是配合synchronized关键字来使用的。
Java在
java.util.concurrent.locks
包下,还为我们提供了几个关于锁的类和接口。它们有更强大的功能或更高的性能。
synchronized的不足:
- 无论读写,同一时间只能有一个线程执行。
- 无法知道线程有没有成功获取到锁
- 如果临界区因为IO或者sleep方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待。
锁的几种分类:
- 可重入锁(
synchronized
、ReentrantLock
)和 非可重入锁 - 公平锁与非公平锁 (
ReentrantLock
支持非公平锁和公平锁两种。) - 读写锁和排它锁(
synchronized
、ReentrantLock
)
- 可重入锁(
JDK中有关锁的一些接口和类
JDK中关于并发的类大多都在
java.util.concurrent
(以下简称juc
)包下。
- 抽象类AQS/AQLS/AOS
- 接口
Condition
/Lock
/ReadWriteLock
(读写锁) ReentrantLock
可重入锁ReentrantReadWriteLock
可重入读写锁StampedLock
基于无锁,性能之王
4.并发容器集合
整体架构(列举常用的容器类)
大致分为:
- 阻塞队列
CopyOnWrite
容器- 并发Map、Set
5.CopyOnWrite
CopyOnWrite
容器即写时复制的容器 ,适合读多写少,读不加锁
CopyOnWrite
容器有数据一致性的问题,它只能保证最终数据一致性。
6.通讯工具类
它们都在
java.util.concurrent
包下。
类 | 作用 |
---|---|
Semaphore | 限制线程的数量 |
Exchanger | 两个线程交换数据 |
CountDownLatch | 线程等待直到计数器减为0时开始工作 |
CyclicBarrier | 作用跟CountDownLatch类似,但是可以重复使用 |
Phaser | 增强的CyclicBarrier |
7.Fork/Join框架
分而治之
1 | solve(任务): |
8.Java8 Stream并行计算原理
从Java 8 开始,我们可以使用
Stream
接口以及lambda表达式进行“流式计算”。它可以让我们对集合的操作更加简洁、更加可读、更加高效。
Stream接口有非常多用于集合计算的方法,比如判空操作empty、过滤操作filter、求最max值、查找操作findFirst和findAny等等。
9.计划任务
自JDK 1.5 开始,JDK提供了
ScheduledThreadPoolExecutor
类用于计划任务(又称定时任务)内部使用优化的
DelayQueue
来实现
这个类有两个用途:
- 在给定的延迟之后运行任务
- 周期性重复执行任务
本文只是简要概括,详情查看:
《Java 并发编程的艺术》
《实战Java高并发程序设计》