《深入浅出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:在给定的延迟之后运行任务;周期性重复执行任务。略。

posted @ 2020-07-25 11:26  y1x4  阅读(227)  评论(0)    收藏  举报