《深入浅出Java多线程》笔记
基础篇
一、进程与线程的基本概念
批处理操作系统内存里只能有一个程序,指令串行执行、效率低。于是引入进程(内存里分配了空间、正在运行的程序)的概念,加上CPU时间片轮转,实现了并发。后来人们不满于进程只能执行一个任务,引入线程的概念,于是QQ上可以一边聊天一边下载文件了。总结一下,就是进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。
相比多进程,多线程实现并发有一些好处:
- 通常需要使用共享资源,线程间的通信比较简单
- 线程是轻量级的,系统开销更小
进程和线程的区别:
- 本质区别是是否单独占有内存地址空间及其它系统资源(比如I/O)
- 进程单独占有一定的内存地址空间,数据共享复杂但是同步简单
- 一个进程出现问题不会影响其他进程,
可靠性高 - 进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大
- 进程是OS进行资源分配的基本单位,而线程是OS进行调度(CPU分配时间)的基本单位
上下文切换。上下文是指某一时间点CPU寄存器和程序计数器的内容。上下文记录着任务运行状态,切换会有时间消耗,进程间、线程间切换也有所不同,这部分操作系统里再详细讨论。
二、Java多线程入门类和接口
实现线程类:
- 继承Thread类,并重写run()方法
- 实现Runnable接口的run()方法
然后调用start()方法后,虚拟机会先为我们创建一个线程,然后等这个线程第一次得到时间片时再调用run()方法。两种实现方式相比较,实现接口更灵活、降低了耦合性、没有Thread类的诸多方法更轻量。
run()方法没有返回值,Callable接口与Future接口解决了这个问题,这是个“异步”模型。
Callable与Runnable类似,但提供的call()方法支持泛型的返回值。线程池submit实现了Callable接口的任务,返回Future,之后get。
Future接口对于具体的Runnable或者Callable任务的执行进行尝试取消、查询是否完成、获取结果。Future实现很复杂,JDK里提供了FutureTask类。FutureTask类实现了继承Runnable接口和Future接口的RunnableFuture接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。而且,FutureTask能够在高并发环境下确保任务只执行一次(运行状态为NEW才会执行)。
三、线程组和线程优先级
线程组是一个树状的结构,每个线程组下面可以有多个线程或者线程组。线程组可以起到统一控制线程的优先级和检查线程的权限的作用。每个Thread必然存在于一个ThreadGroup中,默认是父线程线程组。
Java中线程优先级只是给操作系统的参考值,最终还是由操作系统决定。
四、Java线程的状态及主要转化方法
在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态是一致的。
Java里:
- NEW:刚创建,还没调用start()
- RUNNABLE;ready+running
- BLOCKED:正在等待锁的释放以进入同步区
- WAITING:等待,需要被唤醒
- TIMED_WAITING:等待,定时自动唤醒
- TERMINATED:执行完毕,终止状态
Object.wait()释放锁,进入等待状态(WAITING),直到被唤醒;thread.join(),让此线程不释放锁、直到执行完毕(TERMINATED)。
PS: sleep方法是不会释放当前的锁的,而wait方法会。
五、Java线程间的通信
锁与同步:让线程按一定顺序执行
等待/通知机制:通过wait()、notify()、notifyAll()方法实现
信号量:Semaphore,volatile 变量可充当
管道:通过管道读写
原理篇
六、Java内存模型基础知识
并发编程的两个关键问题:线程间如何同步?如何通信?Java中使用的是共享内存并发模型,通过读写内存中的公共状态进行隐式通信,必须显示指定某段代码在线程间互斥执行。
Java线程之间的通信由Java内存模型(简称JMM)控制。共享变量存在主内存中,每个线程有本地内存,存储着共享变量的副本。线程间通信需要 本地内存a -> 主内存 -> 本地内存b 。JMM规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读取。
七、重排序与happens-before
指令重排提高性能,可以保证串行语义一致,但不能保证多线程间的语义也一致。
JMM的具体实现方针是:在不改变(单线程、正确同步的多线程)程序执行结果的前提下,尽量为编译期和处理器的优化打开方便之门(重排序)。
如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的,不管它们在不在一个线程。
八、volatile
内存可见性,指的是线程之间的可见性,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。
volatile:
- 保证变量的内存可见性:写时,立即把该线程对应的本地内存中的共享变量的值刷新到主内存;读时,立即把xxx置为无效,从主内存中读取共享变量的值。
- 禁止volatile变量与普通变量重排序:生成字节码时,插入内存屏障。
九、synchronized与锁
Java多线程的锁都是基于对象的。一个对象有四种锁状态,它们级别由低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁升级容易降级难。锁的信息存放在对象头里。有一个进程访问,就升级为偏向锁;另一个进程访问发现访问需要竞争,升级为轻量级锁,CAS竞争,失败者自旋;自旋多次、失败,升级为重量级锁,失败者阻塞。
十、乐观锁和悲观锁
乐观锁,“无锁”,所以不会导致死锁,适合读多写少。通常由CAS实现,Unsafe类里的native方法,atomic包里用于原子操作的类。
CAS实现原子操作的三大问题:
- ABA问题:先检查引用和时间戳。
- 长时间自旋浪费资源:让JVM支持处理器提供的pause指令,自旋失败睡眠一段时间。
- 只能保证一个共享变量的原子操作:多个变量存入对象,进行对象的原子操作;使用锁。
十一、AQS
AQS,AbstractQueuedSynchronizer,抽象队列同步器,一个用来构建锁和同步器的框架。模板方法模式!
两种资源共享(同步)模式:独占模式(ReentrantLock),共享模式(Semaphore/CountDownLatch)。ReadWriteLock同时属于两种。
AQS的设计基于模板方法模式,有一些方法必须要子类去实现:是否独占、尝试获取/释放资源的方法。
JDK工具篇
十二、线程池原理
为什么要使用线程池?
- 创建/销毁线程消耗资源,可以复用已创建的线程
- 控制并发数量(主),数量过多会造成服务器崩溃
- 统一管理线程
ThreadPoolExecutor构造方法,5必须+2非必须:
- int corePoolSize:核心线程数最大值
- int maximumPoolSize:线程总数最大值(=核心+非核心线程)
- long keepAliveTime:非核心线程闲置超时时长(超时则销毁)
- TimeUnit unit:keepAliveTime的单位(从天到微毫秒)
- BlockingQueue workQueue:阻塞队列,维护着等待执行的Runnable任务对象
- ThreadFactory threadFactory:批量创建线程,统一设置参数
- RejectedExecutionHandler handler:拒绝处理策略,线程数量大于最大线程数时
创建新线程时需要线程池全局锁ReentrantLock。

十三、阻塞队列
阻塞队列是线程安全的队列访问方式,一般用于生产者-消费者模式,由Lock+Condition实现。
put和take的流程:自旋获取锁——>判断队列是否可用(非空、非满)——>可用(不可用,阻塞、释放锁,直到被唤醒,然后拿到锁),执行任务,唤醒一个线程。
十四、锁接口和类
synchronized的不足之处,这些都是java.util.concurrent.locks包下的锁可以解决的。:
- 如果临界区是只读操作,其实可以多线程一起执行,但使用synchronized的话,同一时间只能有一个线程执行。
- 无法知道线程有没有成功获取到锁
- 使用synchronized,如果临界区因为IO或者sleep方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待。
锁的分类:可重入锁与不可重入锁、公平锁与非公平锁、读写锁和排它锁。synchronized是可重入锁。
抽象类:AQS等。接口:Condition/Lock/ReadWriteLock。实现类:ReentrantLock,可重入锁,支持公平锁和非公平锁;ReentrantReadWriteLock等。
十五、并发容器集合
Vector和HashTable对方法加锁,是线程安全的容器类,但使用时还需要客户端加锁。
包括阻塞队列、并发容器、CopyOnWrite容器。
ConcurrentHashMap
- JDK1.7中,粒度更细的加锁机制——分段锁,Segment(一种可重入锁)+ HashEntry(存储键值对数据),Segment的结构和HashMap类似(数组+链表);
- JDK1.8中两点优化:链表在长度达到8的时候转化为红黑树,提升查询效率;在头结点加synchronized锁,配合CAS自旋避免不必要的锁开销,进一步提升并发性能。
十六、CopyOnWrite容器
写时复制
- 优点:在并发的场景下对容器进行"读操作"而不需要"加锁",从而达到读写分离的目的。
- 缺点:写操作复制一份,内存压力大;读取的是老数据。
比如CopyOnWriteArrayList,实现方式是:写时,加锁,复制容器,进行写操作,指向新容器,释放锁。及时性不高,只能保证最终数据一致性。
十七、通信工具类

十八、Fork/Join框架
一种多线并发处理框架,不同之处是使用了工作窃取算法(线程执行完自己队列的任务从其他线程队列尾部窃取任务执行)。适合执行能被递归地拆解成子任务的工作,将大任务分解成若干个小任务,最后将各个小任务的结果结合起来得到大任务的结果,分而治之,充分利用多核CPU。
由于这方面内容接触少,更多信息略。
十九、Java 8 Stream并行计算原理
从Java 8 开始,我们可以使用Stream接口以及lambda表达式进行“流式计算”。Stream的并行计算底层其实是使用的Fork/Join框架。
二十、计划任务(定时任务)
ScheduledThreadPoolExecutor:在给定的延迟之后运行任务;周期性重复执行任务。略。

浙公网安备 33010602011771号