多线程

1. 上天篇

1.1. 基本理论

1.1.1. 线程和进程

进程:是运行中的程序,可以看做是程序的实例,是资源分配的最小单位。
线程:线程存在于进程中,进程中的执行路径,是最小的调度单位。
进程和线程的区别:
进程:之间是相互独立的,进程拥有共享的资源,进程内的通信比较复杂,同一台机器之间进程通信成为IPC,不同计算机通讯需要通过网络http。
线程:线程通讯简单,因为他们共享内存的资源,线程更轻量。

1.1.2. 并行与并发

并行:同时执行多个任务。
并发:轮流处理多个任务。

2. 入地篇

2.1. 创建线程的方式

方法一:直接使用Thread类

多线程 - 图1

方法二:是用Runnable配合Thread类

多线程 - 图2
把任务和线程分开,各司其职。
Jdk8之后可以通过lamdba:
多线程 - 图3

Thread与Runnable的关系

通过实现Runnable接口创建的线程任务,实际上使用的还是Thread的run方法
多线程 - 图4
直接使用Thread实际上 是创建子类对象覆盖run方法。

方法三:FutureTask配合Thread

FutureTask配合Callable结合使用
多线程 - 图5
多线程 - 图6

2.2. 线程运行的原理

栈与栈帧

Java Virtual Machine Stacks >java虚拟机栈
Jvm是由栈、堆、方法区组成(更细致的还有本地方法栈、程序计数器),其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
l 每个栈由多个栈帧组成,对应着每次方法调用时所占的内存。
l 每个线程只能有一个活动栈帧,对应着当前正在执行的方法。

线程上下文切换

当因为以下的原因导致cpu不再执行当前的线程,会执行另一个线程:
n 线程的cpu时间片用完了
n 垃圾回收
n 有更高优先级的线程需要执行
n 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法。
当上下文切换触发时,需要由操作系统保存当前线程的执行状态,并恢复另一个线程的执行状态。在java中这样的概念是 程序计数器,它的作用就是记住下一条jvm指令的执行地址,是线程私有的。
人话就是:每个线程都有自己的程序计数器 保存着下一条执行的命令地址。
l 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
l Context Switch 频繁发生会影响性能

常用方法

多线程 - 图7
多线程 - 图8
多线程 - 图9

1. Start与run

直接调用run方法
多线程 - 图10
程序仍在main线程执行,并没有开启线程,
需要通过start启动线程。

2. Sleep与yield

Sleep:
1.调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
2.其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
3.睡眠结束后的线程未必会立刻得到执行
4.建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
多线程 - 图11
Yield:
1.yield()方法是一个和sleep()方法有点相似的方法,它也是Thread类提供的一个静态方法。可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是将该线程转入就绪状态。
2.yeild()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()线程暂停之后,线程调度器又将其调度出来重新执行。
当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行机会。
3.具体实现要依赖操作系统的任务调度器。

3. Join

多线程 - 图12
人话:等待我干完事儿,你再干活。Join实现用的wait
多线程 - 图13

4. Interrupt

打断sleep、wait、join的线程。
这几个方法都会让线程进入阻塞状态
其中 打断sleep线程会清空打断状态,:
多线程 - 图14
输出:
多线程 - 图15
可以看到 线程状态 false 主要是为了让线程继续运行 但通常有异常我们需要手动true。
而打断正在正常运行的线程 线程状态 true:
多线程 - 图16
通过interrupt可以提醒该线程你可以挂了,具体如何处理看该线程如何操作。
输出:
多线程 - 图17
· 理解这个中断方法:中断是去中断线程的阻塞状态,而不是中断线程的执行
线程被 wait() 通知后进入等待池,可以由本线程的 interrupt() 方法解救,使本线程可以去重新竞争锁等等。是如何实现的呢?
实际上,中断仅仅是在线程对象做一个标记而已,称为中断标志。中断标志默认为false,在线程 t 调用自己的 t.interrupt() 方法后,此线程中断标志就变成true。但是,中断标志为true实际上不会对正常运行的线程产生影响,因为正常运行的线程不会自己去检查自己的中断标志。
只有那些被阻塞的线程才会不停的检查自己的中断标志,这个阻塞包括因 wait、join、yield、而未处于运行状态的线程,这些被阻塞的线程如果检查到自己的中断标志为true,就会抛出InterruptException异常。
在线程正常运行时,也可以通过中断标志做一些事情,比如利用它做分支条件、循环退出条件等。对于线程 t ,可以用 t.isInterrupted() **获取 t 的中断标志为** true or false。
Tip:isInterrupted()和interrupted()区别在于后者会清楚打断标记。

5. Wait、notify、notifyAll

1、wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
2、wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
3、 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁
4、wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。
5、notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。
6、notify 和 notifyAll的区别
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。

6. 不推荐的方法

多线程 - 图18

7. 主线程和守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守
护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
多线程 - 图19
注意
垃圾回收器线程就是一种守护线程
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等
待它们处理完当前请求

8. 线程五种状态(操作系统)

多线程 - 图20
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
【运行状态】指获取了 CPU 时间片运行中的状态
当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
【阻塞状态】
如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入
【阻塞状态】
等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑
调度它们
【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

9. 线程六种状态(Java Api)

在Thread类的State枚举中:
多线程 - 图21
多线程 - 图22
多线程 - 图23
NEW 线程刚被创建,但是还没有调用 start() 方法
RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的
【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为
是可运行)
BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节
详述
TERMINATED 当线程代码运行结束

(1) 代码实现六种状态

多线程 - 图24
多线程 - 图25
多线程 - 图26
多线程 - 图27

10. 生产者与消费者

资源类:
多线程 - 图28
生产者:
多线程 - 图29
消费者:
多线程 - 图30
Main 启动创建两个消费者 两个生产者
多线程 - 图31
多线程 - 图32

11. 锁

死锁、活锁、饥饿

死锁:死锁是多线程中最差的情况,发生在并发情况下,当两个或多个线程线程相互持有对方所需要的资源,又不主动释放,导致所有线程都无法继续前进,导致程序陷入无尽的阻塞。
多线程 - 图33
活锁:线程并没有阻塞 可以运行但是线程得不到进展,总是做同样的事情。
活锁Demo:
筷子:
多线程 - 图34
人:
多线程 - 图35
多线程 - 图36
饥饿:当前线程需要资源,却始终得不到。

公平锁和非公平锁

公平锁:按照顺序 先来的线程先执行。

非公平锁:多个线程具备执行资格之后争夺执行权

可重入锁

同一个线程外层方法获得锁之后,内层方法扔可获得该锁的代码。

自旋锁

尝试获取锁的线程不会立即被阻塞,而是采用循环的方式去尝试获取锁,好处减少线程上下文切换的消耗,坏处 循环会消耗cpu。

独占锁/互斥锁

同步块一个时刻 只能被一个线程进入 比如synchronized和ReentranLock。

共享锁

锁可以被多个线程持有,比如 ReentranReadWriteLock 的读锁就是共享锁。

乐观锁和悲观锁

乐观锁:自己在使用这个数据的时候不会有其他线程修改这个数据,在更新数据的时候去判断之前有没有别的线程更新这个数据。比如 CAS算法。
悲观锁:与乐观锁相反,自己使用数据时,一定会有别的线程来修改数据,加锁 确保不会有别的线程来打扰。比如 synchronized、lock实现类。

锁的四种状态

无锁:适用于单线程,不锁资源
偏向锁:适用于只有一个线程访问同步块的时候,因为多个线程访问同步块,给其中某一个线程特权不合理的。
轻量级锁:竞争的线程不多,循环等待消耗cpu资源的线程数量在可接受范围之内,如:自旋CAS算法。
重量级锁:多个线程同时竞争资源,只会让一个线程执行,其他线程阻塞。

2.3. JUC

2.3.1. JUC是什么?

Java.util.concurrent在并发编程中使用的工具类。
多线程 - 图37
在jdk1.5之后新增的并发包,包括线程池、异步IO、轻量级任务框架以及用于多线程上下文中的Collection实现等。
多线程 - 图38

2.4. JUC基本理论概念

Java内存模型

Java Memory Model 简称JMM,主要是从来屏蔽掉java程序在不同的硬件和操作系统中对内存的访问存在的差异。
人话:JMM的存在主要为了线程之间的通信以及屏蔽不同系统对于内存的操作规则。
多线程 - 图39
线程之间 通过共享内存 进行变量数据的读写操作;每个线程都有自己的私有内存,流程是这样的:
线程A 读取共享内存的name变量是张三,此时A要对name改值操作既 name=”李四”;他会把修改之后的值存到自己的私有内存(工作内存)中,然后在写到主内存中去。
线程B 同理。
但是伴随着 私有内存和共享内存的存在 操作数据变得复杂化:可见性、原子性、有序性是JMM内存模型中经常处理的问题。

可见性

可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。

原子性

原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
  在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。

有序性

Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步快只能穿行执行。
Tip:重排序:是指CPU采用了允许将多条指令不按程序规则规定的顺序 分开发送给各相应的电路单元处理。(真拗口)。
人话:可能程序执行顺序和代码顺序不同 代码可能先改后读 实际可能先读后改。

Volatile

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当一个变量被定义为volatile时 就具备两个特性:
1保证了变量对所有线程的可见性,立即刷新到主内存中,
2 禁止重排序:volatile修饰的变量赋值后 会多执行一个load addl $0x0, (%esp),既内存屏障
内存屏障:重排序时不能把volaile修饰的变量后面的指令排序到前面去。
多线程 - 图40
代码实现:
多线程 - 图41多线程 - 图42
对flag添加volatile关键词 就可以强制到主内存读写 不再缓存。加锁也可以解决 但是效率低。

CAS算法

Compare-And-Swap 算法是硬件对于并发的支持,针对多处理器操作而设计的,处理器中一种特殊指令,用于管理对共享数据的并发访问。
用处:可以在无锁并发的情况下 对数据进行原子操作。
原理:当一个线程要修改值得时候,会先把值拷贝自己线程内部中然后进行运算,运算完之后 写之前会先读取主内存的值 如果和之前拷贝的相同就写入否则就覆盖旧的值 重新比较,直到相同为止。
ABA问题:可能值相同 但是已经被别的线程改变了 恰好改成和之前的值一样了。
解决:对值加版本号。

AQS算法

AbstractQueuedSynchronizer,在并发包的locks包下,一个用于构建锁和同步器的框架,
同步队列,比如 ReentranLock、ReentrantReadWriteLock、FutureTask等都是基于AQS的;
如果请求的资源是空闲的 也就是没有线程用的,就让请求的线程设置有效的工作线程,将这个共享资源设置锁定的状态可以通过volatile表示的State变量获取资源状态。
如果请求的资源是占用的,就放在CLH公平自旋锁实现的队列中。

可重入

什么是可重入锁,不可重入锁呢?”重入”字面意思已经很明显了,就是可以重新进入。可重入锁,就是说一个线程在获取某个锁后,还可以继续获取该锁,即允许一个线程多次获取同一个锁。比如synchronized内置锁就是可重入的,如果A类有2个synchornized方法method1和method2,那么method1调用method2是允许的。显然重入锁给编程带来了极大的方便。假如内置锁不是可重入的,那么导致的问题是:1个类的synchornized方法不能调用本类其他synchornized方法,也不能调用父类中的synchornized方法。与内置锁对应,JDK提供的显示锁ReentrantLock也是可以重入的。

2.5. JUC详解

Lock

主要提供了显示锁,如重入锁(ReentrantLock)和读写锁(ReadWriteLock)。核心是AQS这个抽象队列同步器框架。J.U.C包中很多工具类都是基于AQS
多线程 - 图43

ReentrantLock

ReadWriteLock

读写锁:
多线程 - 图44
ReadWriteLock管理者一组锁,一个是读锁,一个是写锁。
ReadWriteLock支持降级锁 不支持升级锁;既 写可以降级读,读不能升级写。
写锁是独占锁,读锁是可以在没有写锁的情况下获取多次的;适合多读少写的情况

Atomic

核心是CAS,主要提供了一系列原子变量更新操作的类,提供非阻塞式算法基础。
多线程 - 图45

Collections

并发容器:此处内容javaSE集合部分具体详解。
多线程 - 图46

ConcurrentHashMap

ConcurrentHashMap 同步容器类是 Java5 增加的一个线程安全的哈希表;介于 HashMap 与 Hashtable 之间;内部采用”锁分段”机制替代Hashtable的独占锁,进而提高性能;
此包还提供了设计用于多线程上下文中的Collection实现: ConcurrentHashMap,ConcurrentSkipListMapConcurrentSkipListSet, CopyOnWriteArrayList 和 CopyOnWriteArraySet;
当期望许多线程访问一个给定collection时,ConcurrentHashMap通常优于同步的HashMap;
ConcurrentSkipListMap通常优于同步的TreeMap;
当期望的读数和遍历远远大于列表的更新数时, CopyOnWriteArrayList优于同步的ArrayList;

Tip:锁分段:将map的数据分成若干段 分别上锁 同步块 粒度变小。

Tools

CountDownLatch

CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
多线程 - 图47
多线程 - 图48
等待某些线程先执行。

CyclicBarrier

从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。
它的作用就是会让所有线程都等待完成后才会继续下一步行动。
举个例子,就像生活中我们会约朋友们到某个餐厅一起吃饭,有些朋友可能会早到,有些朋友可能会晚到,但是这个餐厅规定必须等到所有人到齐之后才会让我们进去。这里的朋友们就是各个线程,餐厅就是 CyclicBarrier。
多线程 - 图49
CyclicBarrier 与CountDownLatch``区别:
CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
· CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。

Semaphore

Semaphore是线程同步的辅助类,可以维护当前访问自身的线程个数,并且其中的代码是同步的 可以看做synchronized的加强版,可以控制同时进入的下才能拿个数。
Semaphore的主要方法摘要:
  void acquire():``从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
  void release():释放一个许可,将其返回给信号量。
  int availablePermits():返回此信号量中当前可用的许可数。
  boolean hasQueuedThreads():查询是否有线程正在等待获取。
多线程 - 图50

executor

多线程 - 图51

线程池

线程池的优势:
线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
主要特点:线程复用,控制最大并发,管理线程。
第一:降低资源消耗,通过复用已创建的线程降低线程创建和销毁的消耗
第二:提高响应速度,当任务达到时,任务不需要等待线程创建就可以立即执行
第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,会影响系统的稳定向,使用线程池可以统一管理线程的分配和调优监控。
Java中的线程池 是通过Executor框架实现的,该框架中用到了Executor、Executors、ExecutorService、ThreadPoolExecutor这几个类
多线程 - 图52

常用线程池

ThreadPoolExecutor是Executors类的实现,Executors类里面提供了一些静态工厂,生成一些常用的线程池,主要有以下几个:1. newSingleThreadExecutor:
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2. newFixedThreadPool:
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。(我用的就是这个,同上所述,相当于创建了相同corePoolSize、maximumPoolSize的线程池)
3. newCachedThreadPool:
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

连接池核心参数:

多线程 - 图53

CorePoolSize:核心线程数
MaximumPoolSize:线程池中最大的线程数量上限
KeepAliveTime:池中线程数大于核心线程数时 设置线程池闲置线程中大于指定时间的会被回收 保持核心线程数的数量。
TimeUnit:keepAliveTime的单位
WorkQueue:线程池中 核心线程都在执行任务中,新来的放到队列中。

连接池流程:

多线程 - 图54
多线程 - 图55
首先进入线程池的任务都会被创建创建worker线程去执行,当线程数达到设置的corePoolSize时会把新来的任务 放到队列中去,直到队列的任务也满了,就会在线程池中再次创建新的线程去执行,直到线程数量达到设置的MaxinumPoolSize时,就会对新来的任务进行拒绝策略。
拒绝策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

问题来了生产中如何设置?

通常 maxinumPoolSize 设置和cpu核数+1
int processors = Runtime.getRuntime().availableProcessors();