文章目录
- 多线程基础
- 1. 进程和线程
- 概念
- 2. 线程创建(三种)
- a.Thread 类-继承
- 文档
- 使用总结
- 代码测试
- 案例:下载图片
- b.Runnable 接口-实现()
- 文档
- 区别:
- 一个对象被多个线程操作的情况
- 案例:龟兔赛跑
- c.实现Callable接口(了解即可)
- 区别
- 代码
- Callable好处
- 并发问题
- 3. 剖析Thread类
- a.静态代理(StaticProxy)
- 定义
- 这个接口的使用
- 好处
- b.Lamda表达式
- 目的
- 函数式接口
- 简化方法
- 推导
- 书写结构
- c.回归多线程
- 使用
- 4. 线程五大状态
- 线程方法
- 线程停止
- 进程休眠
- 进程礼让
- 概念
- 进程强制执行
- 概念
- 代码
- 观测线程状态
- 文档
- 用代码观察
- 线程优先级
- 概念
- 文档
- 守护线程
- 概念
- 测试守护线程代码
- 5. 线程并发
- 线程同步
- 概念
- 代码:三大不安全的案例
- 买票
- 不安全的取钱
- 线程不安全的集合ArrayList
- 总结
- 如何保证线程安全
- synchronized关键字使用
- 同步方法
- 同步块
- CopyOnWriteArrayList
- JUC
- 测试JUC安全
- 死锁
- 概念
- 代码
- 解决
- Lock锁
- 概念
- 代码
- 总结
- 6. 线程通信
- 生产者消费者问题
- 问题背景
- 解决思路
- 解决方法
- 管程法代码-利用缓冲区解决
- 信号灯法-标志位
- 线程池
- 概念
- 接口
- 代码
- 多线程进阶 JUC
- 准备工作
- 环境
- 1.基础回顾
- 进程状态wait和sleep的区别
- Lock锁
- Lock锁和Synchronized区别
- 生产者和消费者问题
- 虚假唤醒
- Lock版的生产者消费者问题
- Condition优势
- 2. 锁是什么,如何判断锁的是谁。8个锁的问题
- 锁的对象
- 暂停
- Synchronized锁时
- 1.标准情况下,两个线程都加了Synchronized关键字
- 2.其中一个线程,延迟4秒执行
- 3.增加了一个普通线程。另外两个线程用了一把锁
- 4.new了两个对象时
- 5.两个对象,两个线程加了static静态方法
- 6.一个对象,调用一个static锁线程和一个没加static锁的线程
- 7.两个对象,每个都调用一个static锁线程和一个没加static锁的线程
- 小结
- 3. 集合类不安全
- List不安全
- CopyOnWriteArrayList
- CopyOnWriteArrayList比Vector好在哪
- Set不安全
- HashSet底层是什么
- Map不安全
- ConcurrentHashMap
- 3. JUC下剩下的几个类
- Callable
- 为什么需要
- 使用介绍
- CountDownLatch
- 文档
- 代码
- CyclicBarrier
- 代码
- Semaphore
- 代码
- 2. 剩下的一个锁
- 读写锁
- 概念
- 代码
- 4. 阻塞队列
- 阻塞队列 BlockingQueue
- 概念
- 文档
- 何时使用
- 其他队列
- 四组API(四个方法)
- 使用
- 同步队列 SynchronousQueue
- 代码
- 5. 线程池
- 概念
- 池化技术
- 线程池的好处
- 代码来了解
- 七大参数
- 文档
- 详细介绍
- 举例
- 概念
- 代码,使用七大参数
- CPU密集型和IO密集型
- 6. Java新特性
- 函数式接口
- 函数型接口 Function
- 文档
- 总结
- 断定型接口 Predicate
- 消费型接口 consumer
- 供给型接口 apply
- Stream流计算
- 概念
- 文档
- 链式编程
- 7. 分支合并
- ForkJoin
- 概念
- 特点
- 求累加和
- 思路
- 实现
- 文档
- 代码
- 使用并行流,把计算交给Stream流
- 异步回调
- Future
- 异步调用
- Volatile
- 概念
- 特性
- 保证可见性,通过JMM
- 什么是JMM
- 哪些具体的约定
- 主存和工作内存之间的八种操作
- 存在的问题
- 代码演示
- 解决办法
- 验证volatile的特性
- 可见性
- 不保证原子性
- 指令重排
- 如何避免指令重排的
- 8. 单例模式
- 饿汉式
- 概念
- 存在的问题
- 懒汉式
- 概念
- 在单线程是没问题,但多线程不安全
- 解决方法
- 静态内部类
- 反射
- 解决这种破坏
- 再尝试解决
- 通过枚举类型防止单例被破坏
- 枚举类型也有一点问题
- 9. CAS
- 概念
- ++的底层
- unsafe类
- 概念
- 方法和对象
- 总结++
- 总结CAS
- ABA问题
- 概念
- 代码
- SQL中
- 解决办法:原子引用
- 代码
- 10. 各种锁的理解
- 公平锁,非公平锁
- 概念
- 可重入锁
- 概念
- 代码
- 自旋锁
- 概念
- 代码
- 死锁排查
- 概念
- 代码
- 四要素
- 如何排查死锁
- 其他锁
多线程基础
1. 进程和线程
概念
- 进程:在内存中运行了
- 程序:磁盘中
- 线程:真正执行的单位,独立执行
- 不需要创建线程时,也有默认线程存在
- 多线程需要开辟
- 需要管理
- 有可能数据不一致
- 使用时
- 线程就是一个资源类,线程就是一个类
2. 线程创建(三种)
a.Thread 类-继承
文档
- 继承于谁
- 实现了什么接口
- 文字介绍
- 优先权:执行的先后
- 守护线程
- 用户线程
- 创建方法
- 声明为Thread的子类
- 重写run方法
- 开启
- new 一个对象
- 对象.start();就可以开启
使用总结
- 自定义一个线程类
- 继承Thread类
- 重写run()方法,作为线程执行体
- 创建线程对象
- 调用start()方法
代码测试
- run方法
- 写个for循环中打印内容
- main方法
- 创建一个线程对象,调用start()
- 主线程中也打印,另一个内容
- start方法来启动
- 两个线程交替执行
- run方法
- 按顺序执行
- 线程开启不会立即执行
案例:下载图片
- 用多线程同时下载多个图片
- 用commons-io jar包
- 拷贝
- 然后添加
- 构造器?就是构造函数?
- 并不是按照代码顺序执行的
b.Runnable 接口-实现()
文档
- 类 实现Runnable接口
- 类中 重写run方法
- 启动
- runnable方法的实现类
- 创建线程对象new Thread,丢入实现类的对象作参数
- start()方法
- runnable的实现类
- new出来的只是一个参数
- 还要再放到thread对象中。
- thread才能使用
- 相当于一个代理
- 继承Thread
- new出来的thread对象就是可以使用的
- 接口可以new
区别:
- 相同点:
- 都是重写run方法
- 不同点
- Thread对象的创建需要参数
- 推荐使用runnable接口
- thread继承时,单继承具有局限性
- 实现接口的方法更灵活,一个对象可被多个线程用
一个对象被多个线程操作的情况
- 多个线程操作同一个资源,造成紊乱
- 进程同步问题
案例:龟兔赛跑
- 获取线程名字
- 写一个线程,跑步
- 线程内部都是for循环到100
- 再写一个判断
- 每次进来要判断一下
- 开启一个线程,取名为兔子
- 开启一个线程,取名为乌龟
- 打印出winner,终止循环(线程也被终止)
c.实现Callable接口(了解即可)
区别
- 实现的接口不一样
- 需要返回值
- 重写的方法
- call
- 多了一个开启
- 多了一个关闭
代码
- 返回值
- implements Callable<返回值> //Bollean即可
- 开启
- 相当于线程池
- 创建一个池子
- 执行
- 提交submit三个线程
- 获取结果(返回值,)
- 关闭
- shutdown掉
Callable好处
- 可以定义返回值
- 可以抛出异常
并发问题
3. 剖析Thread类
a.静态代理(StaticProxy)
定义
- 真实对象/代理对象
- 实现同一个接口
这个接口的使用
- 代理对象
- 直接用
- 真实对象
- 代理对象调用真实对象(传入真实对象作为参数)
- 从构造函数传入即可
好处
- 真实对象可以只用作自己的事
- 代理对象帮做其他的事
b.Lamda表达式
目的
- 为了简化代码
- 必须是函数式接口
- 是函数式编程
函数式接口
- 一个只包含一个抽象方法的接口
简化方法
Lamda表达式
推导
- 原始情况
- 存在一个 函数式接口
- 在某个类中要实现这个接口(实现类)
- 调用这个被实现的接口对象(使用类)
- 这里明明只用一次,却分成了三个类
- 改进:静态内部类
- 把这个接口类,放到“使用类”中去实现
- 即实现类在使用类之中
- 要加static
- 再改进:局部内部类
- 在使用类的,方法内部,实现
- 再改进:匿名内部类
- 直接new 接口
- 必须借助一个接口
- 再改进:用Lamda简化
- 省略了new
- 直接
- 接口名 对象名=()-> 实现代码
- 只是省略了写法
书写结构
- ()-> 自己的代码
- 行数
- 如果实现方法中只有一行,会被简化为一行
- 如果有多行,会被变成代码块 {}
- 接口的方法中,存在多个参数
- 括号中放入多个参数
c.回归多线程
使用
- new Thread(new Runnable() {} ).start;
- 真实类
- 真实类和代理类都做事
- 但是真实类做核心的事
- 是线程的底部实现原理
4. 线程五大状态
线程方法
- 改优先级
- sleep()
- join() 线程终止
- 让这个线程的插队,直接执行
- yield 礼让别人
- 暂停这个线程
- 测试线程是否在活动
线程停止
- 不推荐的stop()、destroy()方法
- 被划线的
- JDK不推荐的
- 推荐线程自己停下来
- 在外部写一个flag标识位,在外部更改
- 当flag=false 线程终止运行
- 线程内部执行时,先判断flag
进程休眠
- 存在异常需要抛出
- 每个对象都有个锁,sleep不会释放锁
- 保持在这个进程内
- 模拟网络延迟
- 放大问题的发生概率
- 倒计时
- 一个数:10
- 线程内容为-1
- 一秒钟跑一次这个线程
进程礼让
概念
- 让当前执行的线程暂停,转为就绪状态,不是阻塞
- 不一定成功,
- 可能CPU下一个再调度的还是自己
- 可能直接执行完了,没来得及礼让
进程强制执行
概念
- 阻塞掉其他线程
- 使本线程执行
代码
- 在main线程的for循环中间
- 假如一个thread.join()
观测线程状态
文档
Thread.state
- new
- 新生
- runnable
- 运行
- blocked
- 阻塞
- timed waiting
- 阻塞
- terminated
- 终止
用代码观察
- 线程内部是一个for循环
- new 之后 :get一次
- start() 之后 :get一次
- 用while监听
- 只要不终止
- while(终止)
- 用sleep间隔监听
- 进程终止后不会再启动
线程优先级
概念
- 分为1~10
- 最小为1,最大为10
- 优先级越高,CPU选择时权重越大
- 不是一定每次CPU都能选得准高的
- 调度策略取决于CPU
- 可以用getPriority()
- 来获取
- setPriority();
- 来设置
文档
- 大于10或小于1会抛出异常
- 会默认给一个级别:5
- 主线程默认优先级改不了
守护线程
概念
- 线程分为用户线程和守护线程
- JVM必须确保用户线程执行完毕
- JVM不用等待守护线程执行完毕
- 比如垃圾回收、监控内存、记录日志
测试守护线程代码
- 定义一个线程
- 设置成守护线程 .setDaemon();
- 启动线程
- 定义一个线程
- 启动
- 程序结束之后,守护线程也得跑一会
5. 线程并发
线程同步
概念
- 同步
- 多个线程操作同一个资源
- 排队的问题(队列)
- 等待池
- 并发
- 一个对象被多个线程操作
- 不安全
- 同步就是为了解决并发问题
- 使用一个队列
- 让他排队
- 队列
- 锁
- 光有队列不够,包装安全还需要锁
- 每个对象都有一把锁,可以解开可以锁起
- synchronized
- 追求安全会损失性能
- 优先级高的不能等待优先级低的问题
- 性能倒置
代码:三大不安全的案例
买票
- 定义一个int对象票数
- 用外部flag来让线程停止
- 如果票数为0
- 三个线程,来买票,操作同一个对象
- 对于每个线程来说,当时看到的可能都为1
- 都拿
- 然后就变成-1了
不安全的取钱
- 两个线程去取钱
- 一个账户类型
- 一个int类型作为账户余额
- 一个String作为卡号
- 取钱对象继承Thread
- 信息
- 一个账户类型
- 取多少钱
- 余额
- run函数
- 判断是否有钱
- 算出卡内余额
- 算出现在手里的钱
- 另一个对象,main函数
- main函数
- 用sleep来放大问题
- 可能取成负数
- 当时看到的都是正的
线程不安全的集合ArrayList
- 为什么是不安全的
- List<String> list= new ArrayList<string>();
- 用1000个线程向list中加数据
- 然后看一下list.size();
- 发现没有1000个内容
- 一个顺序,两个线程加数据加在了同一个位置
- 被覆盖了
总结
- 内存是各自的(工作内存)
- 当时看到的都是各自读取下来的
- 取钱
- 买票
- 加数组
如何保证线程安全
- 对于对象
- get/set方法
- 对于方法
- synchronized
synchronized关键字使用
同步方法
- 每个对象对应一把锁
- synchronized的方法,必须获得该对象的锁
- 才能执行
- 否则会阻塞
- 一旦执行
- 独占该锁
- 直到释放
- 后面的进程才能使用
- 缺陷
- 大大影响效率
- 修改时候才需要同步
- 代码,直接加在方法前
同步块
原因
- synchronized方法锁的范围有限
- synchronized方法默认锁住本身的this对象
- 想要锁别的类的对象:同步块
- synchronized( Obj)
- Obj可以是任何对象
- 但是推荐为共享资源(需要修改的)
- 代码 :synchronized(){}
- 需要锁的对象放入()
- 相关代码丢入{}中
CopyOnWriteArrayList
JUC
- Java.Util.concurrent包
- Java的并发包
- 有一个关于List安全的记号
测试JUC安全
- CopyOnWriteArrayList是线程安全的
- 集合都加范型:<>
- 不需要加锁了,本身就是安全的
死锁
概念
相互拥有对方的资源,僵持住了
代码
- static,保证只有一个
- 第一个,synchronized锁住其中一个资源
- 用一个时间差 sleep
- 锁住另一个资源
- 第二个,synchronized锁住其中一个资源
- 用一个时间差 sleep
- 锁住另一个资源
解决
- 不要去锁对面的资源
- 破坏必要条件
Lock锁
概念
- 显式的锁,区别于synchronized
- 相当于Synchronized
- 是可重入锁的实现类
代码
- 定义一个可重入锁 lock
- 加锁 lock.lock
- 解锁 lock.unlock
总结
- 一般写在try catch中
- lock只能锁代码块
6. 线程通信
生产者消费者问题
问题背景
解决思路
- 仅有Synchronized不够
- p操作方法(等待),v操作方法(唤醒)
解决方法
- 管程法
- 通过池子
- 池子,缓冲区
- 信号灯法
- 有个标志位
- true 等待,false 唤醒
管程法代码-利用缓冲区解决
- 生产者
- 消费者
- 容器
- push方法
- if满了
- 等待 (p操作)
- push完,在末尾处
- v操作
- pop方法
- if空
- 等待 (p操作)
- pop完,在末尾处
- v操作
信号灯法-标志位
- 生产者
- 消费者
- 中间的处理函数
- if flag为真
- 生产者操作
- 然后唤醒消费者
- if flag为假
- 消费者操作
- 然后唤醒生产者
- 只能交替执行
线程池
概念
经常创建和销毁,开销大。
使用完放入池子,使用时放入池子。
只创建一次,销毁一次
接口
- ExecutorService
- 真正的线程池接口
- 执行
- 关闭
- Executor
- 工厂类
- 用于创建线程池
代码
- 只要记住这两个类
- ExecutorService es=Executor.newFixedThreadPool
- 工厂类,用打点创建
- 丢入池子
- execute,无返回值
- submit,有返回值
- 关闭
多线程进阶 JUC
准备工作
官方文档,(学习)
环境
1.基础回顾
进程状态wait和sleep的区别
- 来自于不同的类
- wait会释放锁,sleep不会释放锁
- wait必须在同步代码块才能用,sleep随处可用
- wait不需要捕获异常,sleep需要
Lock锁
可重入锁:可以多次获取同一个锁,也必须多次释放
- Lock是一个接口
- 有三个实现类
- 可重入锁
- 读锁
- 写锁
- 公平锁/非公平锁
- 可重入锁
- 无参构造:非公平锁
- 有参构造,可以传入公平/非公平
- 公平锁
- 必须先来后到
- 非公平锁
- 可以插队
- 根据CPU来
- 默认是非公平锁,交给CPU调度,速度更快
- 使用步骤
- new 一个锁
- 加锁
- 写锁 放在finally里
- 而Synchronized是自动的
Lock锁和Synchronized区别
- Synchronized是内置的关键字,Lock是个类
- Synchronized不能判断是否获取到锁,Lock可以
- Synchronized会自动释放锁,Lock必须手动解锁
- 不释放会出现死锁
- Synchronized 线程1占有锁,线程2会一直等
- Lock可以尝试获取锁
- 不会傻傻等
- 等不到就结束
- Synchronized是关键字,很多东西不能改
- 是可重入锁
- 不可以中断
- 非公平
- Lock自由度更高
- 以上三个都可以自己设置
- 适用范围
- Synchronized适合少量的代码
- Lock适合大量的代码
生产者和消费者问题
- 两个线程操作同一个变量,临界资源,AB线程合作,产生通信
虚假唤醒
- 多个线程同时被唤醒时,
- 其中一个线程可能改变了唤醒条件
- 使另一个线程唤醒条件不满足了
- 但是另一个却已经被唤醒了
- 使用if()时,进去就不会停
- 换成while(),另一个就会等待
Lock版的生产者消费者问题
- 在Synchronized版时
- P操作时wait
- V操作是notify
- Lock版
- new一个Condition
- Condition condition=lock.newCondition
- await
- condition.await
- signal
- await.signal
Condition优势
- 线程是一个随机状态,没办法做到
- 线程A->B->C->D
- 如何精准控制被唤醒线程
- condition2.signal();
- 唤醒condition2的线程
- signal是通知自己
- Synchronized版中的notify是通知他人
2. 锁是什么,如何判断锁的是谁。8个锁的问题
锁的对象
- 锁只能锁
- 整个Class出来的全部东西
- 锁单个对象
暂停
- 使用TimeUnit.SECONDS.sleep();
Synchronized锁时
1.标准情况下,两个线程都加了Synchronized关键字
- 两个线程,为什么执行顺序是固定的了?
- 不是因为先调用的原因
- 是因为锁的存在
- 两个线程用了同一把锁
- 谁先拿到谁执行
2.其中一个线程,延迟4秒执行
- 执行顺序还是固定的
- 不是因为先调用的原因
- 是因为锁的存在
- 两个线程用了同一把锁
- 谁先拿到谁执行
3.增加了一个普通线程。另外两个线程用了一把锁
- 普通方法没有用到锁
4.new了两个对象时
- 按时间来
- 两个对象
- 不是同一把锁
5.两个对象,两个线程加了static静态方法
- static造成在类刚加在的时候就有了锁
- 锁住的是整个class
- 锁住的是整个模版
- 两个对象用来一把锁
6.一个对象,调用一个static锁线程和一个没加static锁的线程
- static锁线程
- 锁的是类
- 没加static锁的线程
- 锁的是调用者
- 不是同一个目标
- 所以不是同一把锁
- 所以看CPU调度谁先谁后
7.两个对象,每个都调用一个static锁线程和一个没加static锁的线程
- 共4个,全不是同一把锁
小结
- 锁的对象
- new出来的
- 具体的一个对象
- static
- 锁住的一个模版
3. 集合类不安全
List不安全
ArrayList
- 单线程下的确安全
- 多线程下不安全
- 代码
- 在for循环中
- 每次循环,new一个线程放入一个值
- 出错了,并发修改异常
- 解决方法,加锁
- 直接不用ArrayList,换成Vector
- 用工具类 Collections.synchronizedList(new )
- 用CopyOnWriteArrayList
CopyOnWriteArrayList
- 写入时复制技术COW
- 读取时是没问题
- 写入时防止覆盖
- 写入时复制一份
- 复制完给调用者
- 调用者在复制的数组中写
- 用复制出来的替换原来那个
- 写入时不发生覆盖
CopyOnWriteArrayList比Vector好在哪
- Vector有Synchronized,是通过锁实现的
- 效率低
- 从源码看到的
- CopyOnWriteArrayList是复制
- 从源码看到的
Set不安全
本质和List是一样的,都是从Collection而来
- 同样多线程下不安全
- 代码
- 在for循环中
- 每次循环,new一个线程放入一个值
- 解决办法
- 工具类
- 写入时复制,CopyOnWriteArraySet
HashSet底层是什么
- 底层是HashMap
- key部分不重复
- 一个if语句
- 如果那个key!==null
- 就插入失败
Map不安全
- HashMap是怎么用的
- new的时候要填加载因子和初始化容量
- HashMap默认等价于什么
- 加载因子:默认0.75
- 初始化容量:默认16
- 原因
- 16: 位运算,2的4次方
- 不安全的代码测试
- 每次put进去的时候都new一个线程来put
- 解决方法
- Collections工具类
- 没有CopyOnWrite了
- 使用的是ConcurrentHashMap
ConcurrentHashMap
- 和写时复制有区别
- 1
3. JUC下剩下的几个类
Callable
为什么需要
- 多线程的创建方式
- 重写的方法不同
- 有返回值
- 可以抛出异常
- Runnable太简单
使用介绍
- 泛型的参数 = 方法的返回值类型
- 线程的启动方式有且只有一个 .start()
- Thread只能启动runnable
- 如何启动Callable
- 通过相互调用的关系
- 适配类
- runnable
- FutureTask可以相当于runnable
- FutureTask的参数是callable的实现类
- 返回值
- 返回结果在FutureTask中
- FutureTask对象的get的方法
- 可能会阻塞
- 异步通信
- 有缓存
- 执行两次相同的
- 只输出了一次结果
- 可以提高效率
CountDownLatch
减法计数器
文档
- 辅助工具类
- 允许一个或者多个线程等待
- 用来计数
代码
- new一个对象
- 倒计时一个int对象
- 使用方法,可以-1
- 每次一个操作完成之后可以-1
- 等待归零之后再向下执行
- 打点.await();
- 归零之后才会从此处执行,向下走
CyclicBarrier
加法计数器
代码
- new出来一个对象
- 参数为最终结果
- 每次执行完一个线程,就会+1
- 不需要使用方法,自动+1
- await方法,达到指定值才会向下执行
Semaphore
相当于一个通行证,先来后到,用来限制线程数量,限流
代码
- new一个对象
- 参数为信号量数量
- 用来限制线程数量
- 信号量-1
- .acquire()
- 信号量+1
- .release()
2. 剩下的一个锁
读写锁
概念
- 读可以被多个线程拿到(共享锁)
- 读的时候不能被写打扰
- 写只能给一个线程(独占锁)
代码
- 自定义缓存
- 使用Map来存储
- put方法加一个数据
- 加前打印:写入
- 加后打印:写入完成
- get方法减一个数据
- 读前打印:读取
- 读后打印:读取入完成
- 没有加锁的情况下
- 分别使用5个线程写入
- 再使用5个线程读取
- 发现读和写是无序的
- 加读写锁
- 声明
- 读写锁的接口 读写锁名=new 实现类();
- 加锁
- 加写锁
- .writelock().lock();
- 加读锁
- .readlock().lock();
- 解锁
- 假如是纯锁lock
- 限制太大
4. 阻塞队列
阻塞队列 BlockingQueue
概念
- 队列
- 先进先出
- 阻塞
- 写入
- 队列满了时
- 读取
- 队列为空时
文档
- 父类Collection
- 实现类
- 同步队列
- 链表队列
何时使用
- 多线程
- 线程池
其他队列
BlockingDeQeue 双端队列
AbstraQueue 非阻塞队列
四组API(四个方法)
- 抛出异常
- 不会抛出异常
- 阻塞 等待
- 超时 等待
使用
- new时要填参数
- 队列大小
- 抛出异常的API
- 出错了,会报异常
- 满了时,还add
- 空了之后,还move
- 查看队首的元素,.element()
- 不会抛出异常的API
- offer来存,poll返回
- 成功返回true
- 满了之后还加,会返回false
- 空了之后还poll,返回null
- 查看队首的元素,peek()
- 阻塞 等待的API
- 阻塞时一直等待
- 使用
- 加 .put()
- 取 .take()
- 超时 等待的API
- 阻塞时等待超过一定时间就不等了
- 使用
- 使用不抛异常的方法,再加参数
- offer(“D”,等待时间,等待时间的单位)
- poll(等待时间,等待时间的单位)
同步队列 SynchronousQueue
没有容量。进去一个元素之后,必须取出来才能再放下一个
代码
- new 是阻塞队列的实现类
- 一个线程
- 放入 put()
- 另一个线程
- 拿取 take()
- 很稳定
- 和其他的BlockingQueue 不同
5. 线程池
概念
池化技术
- 程序运行的本质
- 占用系统资源
- 优化资源的使用
- 池化技术
- 概括池化技术
- 事先准备好一些资源,有人要用就来拿
- 避免返回拿取和归还
线程池的好处
- 降低资源的消耗
- 从而提高响应效率
- 方便管理
- 总结
- 线程复用
- 控制最大并发数
- 管理线程
- 考点
- 三大方法
- 七大参数
- 四种拒绝策略
代码来了解
- Executors.
- 单线程池
- Fixed,固定线程池
- Cached,可伸缩线程池
- 使用线程池创建
- 新建:用ExecutorService es=
- 使用:es.execute(线程对象)
- 关闭:shutdown,放到finally里
- 单线程池
- Fixed
- Cached
七大参数
文档
- 开启线程的本质是 ThreadPoolExecutor
- 有7个参数
- 核心线程池大小(银行营业窗口)
- 最大线程池大小(银行总窗口,营业窗口+关闭窗口)
- 超时时间,会自动释放
- 超时单位
- 阻塞队列(银行等待区)
- 线程工厂,创建线程,不需要修改
- 拒绝策略
- 最好通过本质去创建
详细介绍
举例
- 单线程池
- 核心线程池大小:1
- 最大线程池大小:1
- Fixed
- 核心线程池大小:固定值
- 最大线程池大小:同
- Cached
- 核心线程池大小:0
- 最大线程池大小:21亿个
概念
- 核心线程池
- 永远是开着的线程
- 最大线程池
- 线程太满了,超过一定值
- 超时等待
- 时间+时间单位
- 超过时间之后会关闭线程池
- 阻塞队列
- 设置触发最大线程池的等待线程数
- 线程工厂
- 拒绝策略
- 4种
- 报出异常
- 哪来的去哪里,让别的线程执行它,比如main线程
- 不抛出异常,丢掉任务
- 不抛出异常,尝试和最早的竞争
代码,使用七大参数
- ExecutorService es=ThreadPoolExecutor(,)
CPU密集型和IO密集型
- 核
- 可以同时执行的线程数
- 最大最大线程池数该如何定义
- CPU密集型
- 几核,就是几
- IO密集型
- 要大于核数
- 判断IO密集型线程数目
- 一般为IO密集型线程的两倍
6. Java新特性
- Java新特性中要掌握的
- Lamda表达式
- 函数式编程/链式编程
- 函数式接口
- Stream流式计算
- 函数式接口
- 只有一个方法的接口
函数式接口
函数型接口 Function
文档
- 泛型<T,R>
- 传入一个参数T,返回一个类型R
- new 一个匿名内部类
- 方法中,
- 传入的参数是T
- 返回的参数是R
总结
- new一个对象
- 有一个传入参数,有一个返回参数
- 用Lamda表达式简化
- (str)->{ return str;
断定型接口 Predicate
- new一个对象
- 只有一个输入参数
- 返回值是bool类型
- 配合if()使用, return true/false;
消费型接口 consumer
- 只有输入参数,没有返回值
- 用Lamda表达式简化
供给型接口 apply
- 没有输入,只有返回
- 用Lamda表达式简化
Stream流计算
概念
- 所有的东西无非两种
- 存储
- 计算
- 存储
- 集合 Collection
- MySQL
- 计算
- 交给Stream流来操作
文档
- Stream下,有很多流的方法
- 将list转换成流
- list.Stream()
- 就变成stream对象了
- filter
- 断定型接口
- 传入一个对象返回true/false
- ()->{}中,可以省略掉()
- 如果参数中
- 断定型接口返回是true
- 就返回原对象
- forEach
- 消费型接口
- 不需要参数
- ()->{}中,可以省略掉:()->
- map
- 可以转换
- 是一个映射
- 函数型接口
- 有一个传入
- 有一个返回
- 转换为大写:toUpperCase()
- 排序 sorted
- 参数:comparator
- 是函数型接口
- 两个参数
- 使用排序方法:compareTo
链式编程
- 连续打点调用,一大行
- 为了方便阅读,换行
7. 分支合并
ForkJoin
概念
- 并行执行时,提高效率
- 一个线程并发成多个
- 把大任务拆分成小任务
- 合并汇总结果
特点
- 工作窃取
- 概念
- 自己线程的任务执行完了
- 别的线程的任务还没做完
- 把别的线程的任务拿过来做
- 能够加快速度
- 如何实现
- 双端队列
求累加和
思路
1. 设定一个值
1. 如果小于该值,就正常计算 +=
1. 如果大于该值,就使用分支合并
1. 感觉类似于递归
实现
文档
- 使用ForkJoinPool来执行
- 构造方法里
- 打点,执行:.execute(参数)
- 参数是一个ForkJoinTask
- 是一个任务
- 需要定义一个任务
- 两个子类
- 递归事件
- 没有返回值
- 递归任务
- 有结果,有返回值
- 本题是有返回值
代码
- 新建任务
- 新建一个类,继承递归任务类,
- 泛型是返回值
- 实现接口中的方法
- 如果小于该值
- 直接+=
- 如果大于该值
- 计算出中间值(二分法)
- 再new一个本类的对象(递归)
- ForkJoinTask对象
- 该对象
- fork方法:把任务压入线程队列,拆分
- join方法:合并结果
- 相当于叶子与叶子的相加
- 使用总结
- new 一个 ForkJoinPool对象
- new 一个 ForkJoinTask对象
- 用ForkJoinPool对象
- .execute(task) 无结果
- submit(task) 有结果
使用并行流,把计算交给Stream流
- 使用LongStream
- rangeClosed
- ( ]
- range
- ( )
- parallel()
- 并行计算
- reduce()
- 从流中生成一个值
- 按照参数中的模型
异步回调
Future
- 为了对将来的某个结果进行建模
异步调用
- 服务器和客户端:Ajax
- 发起一个请求
- Future类的增强类CompletableFuture
- 泛型,返回值,可以为void
- 异步执行
- 成功会回调
- 失败也会回调
- new出对象
- 打点,runAsync( runnable )执行
- 也可以Executor执行
- 获取结果
- 打点,get()
- 任务处理
- 打点
- 任务成功 :wenComplete
- Consumer型接口,两个参数
- 任务成功执行
- 第一个是正常的返回值
- 第二个是null
- 任务失败
- 第一个是null
- 第二个是错误信息
- 跳转到任务失败的函数来执行
- 任务失败:exceptional
- function型接口,有输入有输出
- 参数是 Execeptio e
- 输出和自己定 return 233
Volatile
概念
- 是一个关键字
- 是一个轻量级的同步机制,没有Synchronized重
特性
- 保证可见性
- 不保证原子性
- 禁止指令重排
保证可见性,通过JMM
什么是JMM
- 是一个概念,一个约定
- Java内存模型
- 为了保证内存安全
哪些具体的约定
- 线程解锁前
- 把临界变量立刻从工作内存刷回主存
- 线程加锁前
- 读取主存中的内存,拷贝到工作内存
- 加锁和解锁必须是同一把锁(不是8锁问题)
- JMM就是工作内存和主内存操作的一组规定
- 线程中包括工作内存
- 和执行引擎
主存和工作内存之间的八种操作
主存 中间变量 工作内存 执行引擎
- read 主存向工作内存读取过程中,先到中间变量处
- load 中间变量,再向工作内存运送数据
- use 工作内存向执行引擎送数据
- assign 执行引擎向工作内存送数据
- write 工作内存向主存送数据过程中,经过中间变量
- store 中间变量向主存送数据
- lock 整个过程中的加锁操作
- unlock 整个过程中的解锁操作
- 以上8个,4组 - 必须成对使用
存在的问题
- 线程A读取了主内存中的变量flag=true
- 随后线程B将主内存中flag改为false
- 线程A没有获取到最新的flag,怎么办
代码演示
- 设定num=0
- 一个线程A
- 当num=0就不停下来
- 另一个线程B
- 把num设为1
- 及时当num=1了,第一个线程仍然没停下来
解决办法
- 主内存中num值发生变化之后要通知线程A
- 加上volatile
验证volatile的特性
可见性
- 加上volatile之后,主存的变化工作内存能看到
不保证原子性
- 原子性:执行过程中不能被打扰
- ACID原则
- 用20000个线程向里面加值
- 判断是否执行完
- 当前线程数大于2
- main线程和gc线程一直在执行
- synchronized可以保证值最后是20000
- volatile不能保证
- 为什么会被覆盖
- num++不是一个原子操作
- 别的线程可能会参与进来
- 如何能保证原子性(不使用synchronize和lock)
- 使用原子类
- 把int类型改成原子的int
- AtomicInteger
- 不使用++,
- 把++换成 .getndIncrement()
- 使用了cpu底层的CAS
- unsafe类
- 和操作系统底层相关
指令重排
- 禁止指令重排,测不出来
- 什么是指令重排
- 计算机并不是按照你写的代码的顺序执行的
- 编译器会对代码优化
- 指令并行也可能会重排
- 内存系统也可能重排
- 指令重排可能会对代码造成影响
- 多线程影响赋值操作的顺序
如何避免指令重排的
- 内存屏障、就是CPU执行的指令
- 保证操作的执行顺序
- 保证某些变量的内存可见性(内存可见性是通过此来实现)
- 如何保证的屏障
- 正常情况下
- 一个线程来读
- 一个线程来写
- 加了volatile
- 就会在工作内存上面和下面加了一个内存屏障
- 使得每次工作内存读要从主存读最新的
- 写也是主存最新的
- 在单例模式中用的最多
8. 单例模式
- 只要是单例模式
- 构造函数必须私有
饿汉式
概念
- 在类加载时就会实例化一个对象
- private保证对外不可见,然后getInstance
- static方法
存在的问题
- 可能会浪费内存
- 一上来就会加载进内存
- 而此时可能还没使用
懒汉式
概念
- 在使用时才会加载一个对象
在单线程是没问题,但多线程不安全
- 启动十个线程
- 都getInstance
- 导致启动了多个实例
- 每个线程执行到if判断时
- 拷贝到自己的工作空间
- 当时的instance都为null
解决方法
- 当==null时,
- 加一个Synchronized锁
- 锁住这个类
- 保证只有一个
- 在锁中再判断一次
- 双重检测锁模式
- 为什么不在getInstance方法上加锁
- Synchronized锁的准备时间太长
- 每次到了都要准备一下
- 把它放在if里面,避免Synchronized锁的准备
- new的操作不是原子性
- 分配内存空间
- 执行构造方法,初始化对象
- 将对象指向内存空间
- 启动一个线程时候有可能执行了132
- 线程A还没完成2时
- 导致线程B直接以为对象非空了
- 直接return了
- B帮A return了
静态内部类
- 在一个类里面再写一个内部类
- 内部类是static的
- 在内部类里面new对象
- 再弄一个getInstance
- return那个内部类
- 是不安全的
反射
- 只要有反射,任何单例都不安全
- 先获得一个单例
- 通过反射破坏单例
- Constructor<类型> cs=.class.getDeclaredConstrator(null);
- 获得空参构造器
- 通过反射来创另一个对象,又破坏了
解决这种破坏
- 在构造函数里加一个Synchronized锁
- 升级成三重检测
- 两个对象都是用反射创建的
- 又被破坏了
再尝试解决
- 再定义一个变量作为标志位
- 判断条件变成了标志位+防止破坏
- 标志位也可能被破坏
- 通过枚举类型
通过枚举类型防止单例被破坏
- 枚举自带单例模式
- 枚举本身也是个类
- 枚举一个对象,就是单例类
- 用getInstance来获取
枚举类型也有一点问题
- 提示枚举当中没有无参单构造方法
- javap
- jad反编译
- 发现枚举过程中只有一个有参的构造器
9. CAS
概念
- 原子类的底层用了CAS
- 原子类对象打点: .compareAdnSet()
- CAS即compareAdnSet 即 比较并交换
- 两个参数
- expect
- update
- 如果值是expect,就把它改为update
- 和期望(expect)相同,就改为update
++的底层
- ++即是getAndIncrement
- 是getAndInt的返回值,getAndInt是unsafe类打点调用的
- getAndInt的参数
- this:当前对象
- valueOffSet
unsafe类
概念
- Java无法直接操作内存
- C++可以操作内存
- Java调用C++可以通过native
- unsafe是java的后门,可以帮助操作内存
方法和对象
- valueOffSet=unsafe.objectFieldOffset
- unsafe.objectFieldOffset
- 是内存地址的偏移值
- value通过volatile实现
- 避免指令重排
- 可以被隔离
总结++
- ++方法是getAndIncrement中的getAndInt
- getAndInt参数
- ths:本对象
- 当前对象内存的值
- 1
- getAndInt方法内部
- 定义了var5
- 获取内存地址的值
- 先获取内存地址偏移量
- 两个相加就是当前对象的值,赋值给var5
- 然后调用“比较并交换方法”
- 如果当前对象的内存地址偏移值就是还是当前对象的值
- 相当于复核查一下
- 就把它内存地址的值 + 1
- 1就是传进来的第三个参数
- 比较并交换方法 ,是while内的
- 无限循环下去
- 自旋锁
总结CAS
- CAS是比较,并交换工作内存中的值和主存中的值
- 如果是期望的,就执行操作
- 不是,就一直循环,阻塞住了
- 有三个操作数
- 好处:自带原子性,CPU层面的,所以快
- 缺点:
- 会循环,会耗时
- 一次性只能保证一个变量的原子性(够了)
- 存在ABA问题
ABA问题
概念
- 两个线程去操作同一个变量
- A期望值是1
- B速度快,过来把1改成2,然后再把2改成1
- 对于A来说,A不知情,以为全部没变
- 我们希望谁动过值,要告诉我们
代码
- 捣乱线程B,改值,再改回来
- A执行期望的值,如果是,就update
SQL中
- 上一把乐观锁
- 只要判断锁没动过,再改值
解决办法:原子引用
- 带版本号的原子操作
- 有个类来帮助使用原子引用
- 不使用原子类,而使用原子引用的类
- 两个参数
- 初始值
- 初始的版本号时间戳
- 为1
- 每次动过就+1
代码
- B线程操作,A要知道
- 线程每次进入先获取版本号
- B每次修改时,用CAS
- CAS方法操作,加了两个新的参数
- 期望的版本号,
- 新的版本号
- A再改时,版本号不一致,无法CAS
- 修改失败
- 引入 原子引用,可以解决ABA机制
- 同乐观锁的机制
10. 各种锁的理解
公平锁,非公平锁
概念
- 是否可以插队
- 默认非公平锁
- 修改成公平/非公平
- 参数
可重入锁
概念
- 又叫递归锁
- 拿到了外面的锁就自动拿到里面的锁
代码
- 一个加锁的方法,调用另一个加锁的方法
- 在外面的锁进入之后,
- 不需要再获得内部的锁
- Lock锁必须配对,不然会锁死在里面
自旋锁
概念
- 不断循环,等待
- 直到成功
代码
- 自己写一个锁,使用原子类型和CAS
- 线程进入之后,打印线程名
- 使用原子类型对象
- 加锁
- while循环 ,内部是CAS,期望值为null
- 解锁
- 如果是想要的值
- update为null
- t1加锁
- t2也进入,自旋
- t1结束自旋,解锁后
- t2才能解锁
死锁排查
概念
- 两个人互相抢夺资源
- 互相占用了对方的资源
- 试图获取对方的资源
代码
- 第一个线程
- Synchronized(lockA)中
- 嵌套
- Synchronized(lockB)
- 第二个线程
- Synchronized(lockB)中
- 嵌套
- Synchronized(lockA)
四要素
如何排查死锁
- 看日志
- Jdk/bin下的很多工具
- jps定位进程号
- jps -l : 定位进程号
- jstack + 进程号:查看堆栈信息
- 堆栈信息
- Found 1 deadlock
- 详细信息
其他锁
- 读写锁(独占锁、共享锁)
- 乐观锁悲观锁(MySQL)