多线程
一 多线程基础
1.1 关于并发与并行
并行:在同一时刻,有多个指令在多个CPU上同时执行
并发:在同一时刻,有多个指令在单个CPU上交替执行。
1.2 进程与线程
进程:是正在运行的程序(软件)
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
动态性:进程的实质是程序(代码)的一次执行过程,进程是动态产生,动态消亡的
并发性:任何进程都可以同其他进程一起并发执行
线程(进程中的各个部分):是进程中的单个顺序控制流,是一条执行路径
单线程:一个进程如果只有一条执行路径,则称为单线程程序
多线程:一个进程如果有多条执行路径,则称为多线程程序
1.3 关于线程的优先级
1.3.1 线程调度
线程调度是获取CPU的使用权的方式;
1.3.2 线程调度的两种方式
分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
Java使用的是抢占式调度模型,该模型拥有一种特性叫做随机性;这种随机性是指假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
1.3.3 优先级相关方法
| final int getPriority() | 返回此线程的优先级
| final void setPriority(int newPriority) | 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10
1.4 关于守护线程
作用:为了守护普通线程存在
相关方法:
void setDaemon(boolean on):将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
1.5 关于线程同步以及处理同步代码的问题
线程同步带来的问题
安全问题出现的条件
- 是多线程环境
- 有共享数据
- 有多条语句操作共享数据
如何处理这个问题
- 同步代码块
synchronized(任意对象:锁关闭) {
多条语句操作共享数据的代码
}
- 同步方法
- 普通同步方法:同步方法的锁对象是this
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
- 静态同步方法:同步静态方法的锁对象是类名.class
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}
同步代码块与同步方法的区别:
同步代码可以锁住制定代码,同步方法是锁住方法中的所有方法
同步代码块可以制定锁对象,同步方法不能制定锁对象(同步方法中的锁对象是this/类名.class)
1.6 关于死锁
1.6.1 死锁产生的必要条件
1、互斥条件
某资源只能被一个进程使用,其他进程请求该资源时,只能等待,直到资源使用完毕后释放资源。
2、请求和保持条件
程序已经保持了至少一个资源,但是又提出了新要求,而这个资源被其它进程占用,自己占用资源却保持不放。
3、不剥夺条件
任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
4、循环等待条件
当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
1.6.2 处理死锁思路
1. 预防死锁(破坏死锁的四个条件中的一个或多个来预防死锁。但不能破坏互斥条件,其他三个都可。)
破坏请求和保持的条件
破坏不可剥夺调价
破坏循环等待条件
2. 避免死锁(在资源动态分配过程中,用某种方式阻止系统进入不安全状态。比如银行家算法。)
银行家算法
3. 检测死锁
允许系统在运行过程中发生死锁,但可已设置检测机构及时检测死锁的发生,并采取适当措施加以清除
4. 解除死锁(发生死锁后,采取适当措施将进程从死锁状态中解脱出来。解除死锁主要方法:资源剥夺法,撤销进程法,进程回退法。)
1. 资源剥夺
挂起某些死锁进程,并抢占它的资源,讲这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到进程,而处于资源匮乏状态。
2. 终止进程
强制将一个或多个死锁进程终止(撤销)并剥夺这些进程的资源,直至打破循环环路,使系统从死锁状态中解脱出来。撤销的原则可以按照进程的优先级和撤销进程代价的高低进行。
3. 进程回退
让一个或多个进程回退到足以避免回避死锁的地步,进程回退时资源释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。
1.7 关于阻塞队列
1.7.1 阻塞队列继承结构
1.7.2 常见BlockingQueue
ArrayBlockingQueue: 底层是数组,有界
LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值
1.7.3 BlockingQueue的核心方法
put(anObject): 将参数放入队列,如果放不进去会阻塞
take(): 取出第一个数据,取不到会阻塞
二 多线程高级
2.1 线程的几种状态
/* 新建 */
NEW ,
/* 可运行状态(就绪) */start
RUNNABLE ,
/* 阻塞状态 */
BLOCKED ,
/* 无限等待状态 */ wait
WAITING ,
/* 计时等待(睡眠状态) */ sleep
TIMED_WAITING ,
/* 终止 */
TERMINATED;
2.1 各个状态的转换
2.3 线程池的设计思路
- 准备一个任务容器
- 一次性启动多个(2个)消费者线程
- 刚开始任务容器是空的,所以线程都在wait
- 直到一个外部线程向这个任务容器中扔了一个"任务",就会有一个消费者线程被唤醒
- 这个消费者线程取出"任务",并且执行这个任务,执行完毕后,继续等待下一次任务的到来
2.4 创建线程池对象的其中一种方法
ThreadPoolExecutor 创建线程池对象 :
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
// 参数一 corePoolSize:核心线程数量 ,不能小于0
// 参数二 maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
// 参数三 keepAliveTime:空闲线程最大存活时间,不能小于0
// 参数四 unit:时间单位
// 参数五 workQueue:任务队列 不能为null:任务等待队列(new ArrayBlockingQueue<>(10), )
// 参数六:创建线程工厂 ,,不能为null (Executors.defaultThreadFactory(),)
// 参数七:任务的拒绝策略,不能为null
1、生么时候拒绝?当提交的任务 > 池子中最大线程数量 + 队列容量
2、如何拒绝?(abortPolicy:默认)
2.4.1 非默认任务拒绝策略
- ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
- ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常 这是不推荐的做法。
- ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
- ThreadPoolExecutor.CallerRunsPolicy: 调用任务的run()方法绕过线程池直接执行。
2.5 关于volatile关键字
==作用 ==:强制线程每次在使用的时候,都会看一下共享区域最新的值,但是volatile关键字不能保证原子性
2.6 Atomic包
2.6.1概述
Atomic包里一共提供了13个类,属于4种类型的原子更新方式
1. 原子更新基本类型
AtomicBoolean: 原子更新布尔类型
AtomicInteger: 原子更新整型
AtomicLong: 原子更新长整型
2. 原子更新数组
3. 原子更新引用和
4.原子更新属性(字段)
2.6.2 内存解析
AtomicInteger原理 :自旋锁 + CAS 算法
2.6.2.1 CAS算法:
有3个操作数(内存值V, 旧的预期值A,要修改的值B),当旧的预期值A == 内存值 此时修改成功,将V改为B ,当旧的预期值A!=内存值 此时修改失败,不做任何操作 ,并重新获取现在的最新值(这个重新获取的动作就是自旋)
2.6.3 源码解析:
weakCompareAndSetInt(o, offset, v, v + delta)重点
2.6.4 悲观锁和乐观锁
synchronized和CAS的区别
相同点:在多线程情况下,都可以保证共享数据的安全性。
不同点:synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作 共享数据之前,都会上锁。(悲观锁)
悲观锁
synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作 共享数据之前,都会上锁。(悲观锁)
乐观锁
cas是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会 检查一下,别人有没有修改过这个数据。如果别人修改过,那么我再次获取现在最新的值。 如果别人没有修改过,那么我现在直接修改共享数据的值.(乐观锁)
2.7 并发工具类
2.7.1 并发工具类-Hashtable
Hashtable出现的原因 : 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题:出现null值)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。
Hashtable 如何保证安全(采取悲观锁)?为啥效率低(只要有线程访问,锁整张表)?
Hashtable底层也是哈希表(数组(数组长度是:16 加载因子是0.75 到12就要扩容)加链表):
2.7.2 并发工具类-ConcurrentHashMap
出现的原因: 在集合类中HashMap是比较常用的集合对象,但是HashMap是线程不安全的(多线程环境下可能会存在问题)。为了保证数据的安全性我们可以使用Hashtable,但是Hashtable的效率低下。
体系结构:
三个map总结 :
1 ,HashMap是线程不安全的。多线程环境下会有数据安全问题
2 ,Hashtable是线程安全的,但是会将整张表锁起来,效率低下
3,ConcurrentHashMap也是线程安全的,效率较高。 在JDK7和JDK8中,底层原理不一样。
2.7.2.1 ConcurrentHashMap的底层原理
jdk1.7:
分析: 底层也是哈希表结构 也有数组(Sement)16,加载因子0.75,HashEntey[]:小数组(长度为2)给大数组索引为0,作为模版使用,二次哈希(也要扩容 扩容两倍,加载到)
线程安全如何保证:锁小表,最多允许16个线程同时访问
jdk1.8:
底层哈希表,数据+链表+当链表的长度大于的8的时候转位红黑树
CAS + synchronized同步代码块
线程安全:锁链表,锁对象为头结点
2.7.2.1 ConcurrentHashMap的底层原理的总结
1. 如果使用空参构造创建ConcurrentHashMap对象,则什么事情都不做。 在第一次添加元素的时候创建哈希表
2. 计算当前元素应存入的索引。
3. 如果该索引位置为null,则利用cas算法,将本结点添加到数组中。
4. 如果该索引位置不为null,则利用volatile关键字获得当前位置最新的结点地址,挂在他下面,变成链表。
5. 当链表的长度大于等于8时,自动转换成红黑树6,以链表或者红黑树头结点为锁对象,配合悲观锁保证多线程操作集合时数据的安全性
2.7.1 并发工具类-CountDownLatch
- public CountDownLatch(int count) |:参数传递线程数,表示等待线程数量
- public void await() : 让线程等待 :当计数器为0时,会唤醒等待的线程
- public void countDown():当前线程执行完毕,会将计数器-1
2.7.1 并发工具类-Semaphore
//获得了通行证
semaphore.acquire();
//归还通行证
semaphore.release();