概念
1. 线程的数据结构
一个线程就是一个栈的数据结构
2. 进程和线程的比较
进程: 是后台运行的一个程序, 一个进程包含多个线程【进程属于多个线程的整体】
线程: 线程是进程的基本组成单位, 是由cpu 进行执行和调度的
3. 并发和并行
并发: cpu 交替的不断切换的执行线程中的 代码
并行: 多个线程同时运行, 是有多个核心CPU实现的
4. 线程调度的方式
抢占式调度【java中】
分时调度,使用时间片的概念
5. java 线程中的线程的主线程
main()
6. 为什么使用多线程:
提高cpu的利用率
提高程序运行的实时性
创建多线程的方式
【4种】Thread类==> Runnable接口 ==> Callable 接口 /Future ==> 线程池
注意:一个线程对象的 start() 方法不能多次调用
【1】通过Thread 类来创建线程:
- 创建类继承Thread 类
- 重写 run方法, 编写线程执行的代码
- 创建线程对象
- 调用start() 方法
【2】通过Runnable 接口创建线程:
1.创建类实现Runnable接口
2. **重写run方法 ,编写执行线程的代码**
3. **创建线程对象**
4. **将这个对象传给 Thread 中的构造方法**
5. **调用Thread 中的 start() 方法**
【3】通过Callable 接口来创建线程:
- 创建类实现Callable 接口
- 重写call 方法 ,编写线程执行的代码
- 创建FutureTask 对象 , 把新创建的对象作为参数传给, FutureTask 构造方法
- 在将FutureTask 对象传给Thread 类的构造方法
- 执行Thread 类中的start() 方法
- 【接收线程的返回值】: 通过 futureTask.get() 获取线程执行的返回值
【4】通过线程池 Excutors 对象
1. **创建线程池对象**
2. **从线程池中获取线程对象 ExcutorService**
3. **传递线程对象**
【5】比较: Thread 和 Runnable 和 Callable 比较
Thread 和 Runnable 线程代码 都没返回值
Callable 线程代码有返回值
【6】Thread类中常用的Api
-
获取线程的名字
- Thread.currentThread.getName()
- thread.getName();
-
设置线程的名字
- thread.setName();
- Thead(Runnable runnable ,String name);
-
睡眠线程
- Thread.sleep()
- Thread.sleep(long time)
生命周期: 【6中状态】
生命周期的原因是有cup的调度产生的结果
创建 ==> 运行 ==>阻塞 ==> 死亡 ==>等待 ==> 超时等待
大概表述
图片1 .1 :
图片1.2 :
【1】新建状态
new Thread(); 但是线程还没有运行
【2】运行状态
运行状态分为两种:
-
就绪状态
- 执行状态
通过调用start() 方法进行运行 , start() 方法运行后线程不一定启动。 因为cpu 还没分给资源 要等到cpu 调度
【3】终结状态
线程代码执行完毕, 回收
【4】等待状态
线程在执行时间, 执行了 wait() 方法 或者 sleep() 方法, 也就是失去锁对象。 同时也失去了 cpu 资源
处于这个状态的线程, 会被送进等待队列中,等待获取锁,通过执行 notify() 获取锁对象之后,进入阻塞/同步对列中 等待获取cpu
sleep() 方法不会让线程失去锁
【5】阻塞状态
线程阻塞,主要原因是 已经获取锁, 但是没有获取CPU的执行权力
进入这个状态后会 会进入阻塞队列 等待CPU调用
【6】超时等待状态
超时等待是等待状态的一种, 这个状态是有时间记时的,到时间后会 自动进入阻塞对列 , 不用执行notify()
7 . 等待对列 和 同步对列
失去锁对象的线程 等待对列 wait()
被通知获取锁的对象的线程 阻塞对列 notify()
线程安全 / 锁的使用
0. 什么是线程安全
- 原子性
- 一致性
- 有序性
- 可见性
1. JUC 并发工具
Lock 锁的使用
可以重入锁 Lock lock = new ReentrantLock();
读写锁 Lock lock = new ReentryWriteReadLock();
/*
ReentrantLock 可重入锁优点:
锁使更加的灵活
解决死锁问题
可以支持从新进入,锁只需要记录重入的次数就可以,和sychronized类似
ReentryWriteReadLock 读写锁优点:
读 和 读之间是不排斥的
读和写 写和写 是排斥的
*/
锁使用的API
/**
使用锁
lock.lock();
释放锁
lock.unlock();
*/
公平锁和非公平锁
/**
公平锁
非公平锁【Lock中默认的是非公平锁】
区别:
非公平锁: 一上来就插队获取锁 CAS自选获取锁
公平锁: 老实排队 按照顺序获取锁对象
*/
Lock 锁的实现
// 锁的实现是 用JUC 里面的AQS 同步对列 来记录被阻塞线程队列的
AQS = AbstractQueuedSynchronizer 抽象类
AQS 一个同步队列的工具 排队的机制【重点】
AQS 先进先出的同步工具
AQS 记录阻塞线程的状态: 获取锁成功-->执行线程代码
获取锁失败-->进入同步列等待
// 当多个线程竞争锁的时间 , Lock 锁 是如何阻塞的???
state 是 AQS中的锁状态 -->
0 代表无锁状态
1 代表有锁状态
setExclusiveOwnerThread(Thread.currentThread()) 设置已经获取锁的线程
线程获取锁的过程
第一个线程获取锁 成功
第二个线程获取锁,判断阻塞队列里面有没有 线程对象
没有--->通过 CAS 自旋转,尝试获取锁,如果无法锁-->进入阻塞对列
有-->进入阻塞队列
第三个线程获取锁, 进入阻塞队列
/*线程进入阻塞对列--> 被中断
线程获取准备获取锁--> 被唤醒*/
线程的通信 Condition类的使用
使用场景
线程通信-->实现生产者 和 消费者
线程通信的前提
多个线程 使用的必须是通一把锁 Lock lock = new ReentryLock();
Condition 对象也必须是同一个。
获取condition 对象
获取Condition对象的代码: Condition condition = lock.newCondition(); 方法
使用condition对象
- 阻塞 condition.await() 阻塞当前线程
- 唤醒 condition.signal() 唤醒别的阻塞线程
通信演示
CountDownLatch 类的使用
概念
同步工具类/计数器
作用
允许一个或者多个线程一直等待 , 直到 别的线程执行完毕
API 使用
-
CountDownLatch countDownLatch =new CountDownLatch(3);
-
countDownLatch.countDown();
-
countDownLatch.await();
public static void main(String[] args) throws InterruptedException {
//创建对象
CountDownLatch countDownLatch =new CountDownLatch(3);
new Thread(()->{
System.out.println("Thread1");
//削减
countDownLatch.countDown(); //3-1=2
}).start();
new Thread(()->{
System.out.println("Thread2");
countDownLatch.countDown();//2-1=1
}).start();
new Thread(()->{
System.out.println("Thread3");
countDownLatch.countDown();//1-1=0
}).start();
//等待
countDownLatch.await();
}
Semaphore 限流类
作用
限制资源的数量,限制线程同时访问数量
概念
基于AQS 同步对列 , 基于令牌的概念 限制同时访问资源的线程数量
使用过程
- 创建信号量对象
- 请求信号量
- 使用资源
- 释放资源
CyclicBarrier 线程屏障
作用
让一组线程达到同步点之前 进行阻塞
等待一些线程执行完之后才会执行后面线程的代码
和CountDownLatch 类 相似
API
-
CyclicBarrier cyclicBarrier=new CyclicBarrier (3,/*最后执行的线程*/new CycliBarrierDemo());
-
cyclicBarrier.await(); //阻塞 condition.await()
public class DataImportThread extends Thread{
//屏障
private CyclicBarrier cyclicBarrier;
private String path;
public DataImportThread(CyclicBarrier cyclicBarrier, String path) {
this.cyclicBarrier = cyclicBarrier;
this.path = path;
}
@Override
public void run() {
System.out.println("开始导入:"+path+" 数据");
//TODO
try {
cyclicBarrier.await(); //阻塞 condition.await()
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public class CycliBarrierDemo extends Thread{
@Override
public void run() {
System.out.println("开始进行数据分析");
}
//循环屏障
//可以使得一组线程达到一个同步点之前阻塞.
public static void main(String[] args) {
CyclicBarrier cyclicBarrier=new CyclicBarrier
(3,/*最后执行的线程*/new CycliBarrierDemo());
new Thread(new DataImportThread(cyclicBarrier,"file1")).start();
new Thread(new DataImportThread(cyclicBarrier,"file2")).start();
new Thread(new DataImportThread(cyclicBarrier,"file3")).start();
}
}
3.并发集合ConcurrentHashMap 阻塞队列 ArryBlockingQueue
1. ConcurrentHashMap
tomcat 中的session 存储 基于ConcurrentHashMap
2. ArrayBlockingQueue / LinkedBlockingQueue ......[7中 在JUC 中]
应用场景
阻塞队列 可以实现生产者 / 消费者的应用场景
类比
相当于 RabbitMQ 实现顺序处理 消息
特点
特点: 如果阻塞队列为空的情况下--> 获取数据的方法将会阻塞在那--> 直到队列里面添加了数据
3. 原子类的操作
实现原理: 通过CAS = CompareAndSwap 实现
- AtomicInteger 类
- AtomicBoolean类
- AtomicLong类...............
volatile 无法保证原子性 可以通过原子类配合使用
4. ThreadLocal
概念
- 提供线程类的局部变量 --不同线程不会进行干扰【保证同一个线程中 数据的一致性】
- 线程中 的调度是抢占式的调度
存在的问题是:
一个线程会获取到了另一个线程的数据
解决办法 使用ThreadLocal 类【set 和 get】
作用
将变量绑定到当前的线程中
与synchronized 代码快 对比
- ThreadLocal 没有加锁 【属于并行执行】
- synchronized 加锁 【属于串行执行】
- ThreadLocal 比 synchronized 效率更高
多线程连接数据库中的操作【ThreadLocal 的使用】
将 连接数据库的 connection 连接交给 ThreadLocal管理
底层代码
01设计的好处
02设计思想
一个Thread 【线程】 中维护者一个ThreadLocalMap 【集合】 集合中存的key 是 我们new 出来了的 ThreadLocal 对象 vule 是我们 从 ThreadLocal 对象中设计的值 【太秒了......】
03自己总结
每一个线程中 有一个ThreadLocalMap 集合【内部类】 一个线程对应一个Map 集合 集合存的key 是 ThreadLocal 对象 value 是我们通过 ThreadLocal 对象 中set 方法传进去的
ThreadLocalMap 的底层分析
? 描述 : Thread 里面的【静态内部类】内部类 , 没有实现 map 接口 独立实现的 map 功能
?弱引用:
? 弱引用 和 内存泄漏问题
- 弱引用相关该概念 *
- 弱引用会发生GC
- 内存泄漏
- 已经分配的堆内存 无法释放内存
- 原因 *
- 解决办法 : 手动执行remove() 方法 或者关闭线程 *
- 解决办法 : 手动执行remove() 方法 或者关闭线程 *
?ThreadLocal hash 冲突的解决办法
5. synchronized 关键字
性质
-
可见性
-
排他性/ 互斥性 线程的等待状态/ 与阻塞状态
- 原子性
- 独占性 只能有一个线程进行操作资源类
8锁的使用
普通 的synchonized 锁的是当前对象的所有synchonized方法 普通方法与同步锁无关可以正常访问
static synchonized 锁的是这个类【所有的类对象里面的静态方法】
特点
- 同步锁/ 同步堵塞
- 是可重入的
原理
-
每一个对象都与一个 monitor 关联【这个对象其实就是java对象的锁,而且不能为空!!!! 】,当一个 monitor 被某个线程持有后, 它便处于锁定状态。
-
锁的 信息都记录在 对象头上面
01monitor对象的使用过程
- 当一个线程占用这个对象的时间会判段与这个对象关联的monitor 计数器是不是 0
- 如果是 0 说明没有线程占用它 , 线程占用它然后 对monitor计数器加一
- 如果不为0 ,表示已经被其他线程占用 , 这个线程处于等待状态
- 当 另一线程释放资格的时间 monitor 减一【为什么是减一 而不是变为0 ==因为锁是可以重新进入的】
02monitor 指令 [ 作用在代码块]
互斥的入口 -->monitor 计数器 ++ 的操作
互斥的出口 -->monitor 计数器 - - 的操作
出现两个的 出口
03monitor 指令 [ 关键字作用在方法上]
互斥的入口 -->monitor 计数器 ++ 的操作 【采用信号量】
互斥的出口 -->monitor 计数器 - - 的操作
方法执行完毕
04对象头的内容/ 结构
Mark Word 【存储 hashCode 锁信息 分代年龄 或者GC 标志位的信息】
当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线线程
- Contention List:所有请求锁的线程将被首先放置到该竞争队列
- Entry List:Contention List 中那些有资格成为候选人的线程被移到 Entry List
- Wait Set:那些调用 wait 方法被阻塞的线程被放置到 Wait Set
- OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为 OnDeck
- Owner:获得锁的线程称为 Owner
- !Owner:释放锁的线程
05Class Metadata Address 【指明对象是那个类的实列】
作用在代码块
作用在方法上面
-
方法 被static 修饰的【类级别的方法】 --->获取的是当前对象的锁, 方法区中字节码对象锁
-
没有被static 修饰的对象---->采用的锁是 使用调用方法的当前对象
虚拟机对sychronized 关键字的优化
对象头
1.6 之前是 重量级锁
之后进行了优化
- 当只有一个线程进行访问的时间使用的是偏向锁,锁记录着线程的名字
- 当有两个线程交替访问的时间,使用的是轻量级的锁, 通过自选不断的获取锁, 而不是进入阻塞状态
- 当多个线程访问资源的时间, 使用的是重量级锁, 没有获取锁的线程就要进入阻塞队列
优化
-
轻量级锁 向 重量级锁转换的时间---> 用户线程向 核心线程之间的转换【耗时间】
-
自旋锁 【竞争失败的时间不是马上转换级别 而是执行几次空循环---> while(){ }】解决以上的情况
-
锁的级别转换是 从小到大【不能相反】
- 锁的消除---JIT在编译的时间 把不必要的锁去掉
- 目的是: 防止线程状态的转化
获取锁的过程
- 单个线程获取锁: 是偏向锁
- 两个线程交替执行是轻量级锁,通过自旋获取锁
- 多个线程同时获取锁的时间 是重量级别, 线程进入阻塞状态 线程从用户状态 变为核心状态, 进入同步队列 等待 cpu 操作 消耗系统资源
6. volatile 关键字
java中内存模型
01初级了解jmm【内存模型】
- java内存模型与cpu【三级】缓存模型类似【mesi 协议】
-
概述: 线程中保存的是主内存变量的副本。 每个线程的变量相互独立 互不影响
-
使用 【volatile】 关键字 达到线程之间共享数据变量的目的 -- 可以说是线程通信的目的
02jmm数据的原子指令操作
- read 读取 从主内存读取数据
- load 加载 将读取的数据写入到 线程的工作内存
- use 使用 从工作内存读取数据来计算
- assign 赋值 将计算好的值赋值到工作内存中
- store 存储 将工作内存中的数据写入到主内存中
- write 写入 将store过去的变量赋值给主内存的变量
- lock 锁定 将主内存的变量加锁 标示为线程独占状态
- unlock 解锁 将主内存的变量解锁 解锁后的变量其他的线程也可以进行加锁了
原理
【轻量级锁-->lock 指令前缀锁,原子操作级别的锁 】
- volatile关键字的实现原理
- 总线【连接主内存和cpu的线,cpu通过总线读取 内存数据】MES 缓存一致性协议
- 修改数据只有通过总线,cpu 通过嗅探机制【监听】 cpu 让 另外一个线程的变量值失效【硬件基础 cache line 】 然后另外一个线程重新去主内存 读取修改后的数据。
- 当线程中的数据一旦发生改变 立即同步到主内存 同步之后才会执行后面的代码
- 多个线程同时修改的情况
- 在 store【保存数据到主内存中】原子操作之前加一把 锁 保证修改的先后顺序
- 这个关键字没法保证非原子操作【i++】的一致性【不能保证原子性】
- 如果保证一致行动话需要对 线程中的代码方法 添加 synchronized 关键字
作用
-
保证 变量的可见性 【volatile修饰的成员变量可见性】
- 【volatile关键字的作用是】让其他线程感知到某一个线程对某一个变量的修改, 从而让这个线程去【主内存中----------->缓存】从新读取变化后的新值【但是也不一定是最新的值 因为 对非原子操作 i = i + 1 / i ++【分为三步】 不能保证完全数据的一致性--->存在命中率的问题】
-
保证有序性 【volatile顾可以防止指令重排序】
- 指令从排序
- 指令重排序【在编译阶段 指令优化阶段】
- 输入程序的代码顺序并不是实际执行的顺序
- 指令重新排序对单线程没有影响,对多线程有影响
- volatite 可以保证程序的有序性
- 对于volatile 修饰的变量 之前的代码 不能调整到后面
- 对于volatile 修饰的变量 之后的代码 不能调整到前面
- 指令从排序
-
设置内存屏障 防止指令重排序
volatile 关键字 使用的场景
01作为一个事件的开关【状态标志开关】
02双重检查锁定【单例模式应用】
03需要利用顺序性【避免指令重排序】
volatile 与 sychronized 的区别
修饰对象上面的区别
- volatile 只能修饰变量
- sychronized 只能修饰 方法 和代码块
对原子性的保证
- sychronized 可以保证原子性
- volatile 不能保证原子性
对于可见性
- sychronized 和 volatile 都可以保证可见性
- 但是实现原理不同
- volatile 对于变量加了一个 lock 指令前缀
- sychronized 使用的是 monitor 监视器 中的【moniterEnter 和 monitorexit】
对有序性的保证
- volatile 能保证有序性
- sychronized 能保证有序性 【 但是代价太大由并发退化到并行】
7. 造成死锁的条件
产生死锁的四个条件
- 互斥条件
- 不可剥夺条件
- 请求与保持条件
- 循环等待条件
条件
- 死锁是指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局
- 参与死锁的进程数至少为两个
- 参与死锁的所有进程均等待资源
- 参与死锁的进程至少有两个已经占有资源
- 死锁进程是系统中当前进程集合的一个子集
- 死锁会浪费大量系统资源,甚至导致系统崩溃。
- 重复交叉加锁【一个锁里面 包含另一个锁】
- 一个线程拥有锁的过程中 执行代码中 又含有别的锁对象的获取
- 进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞。
代码演示
/**
* 一个简单的死锁类
* 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
* 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
* td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
* td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
* td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
*/
public class DeadLock implements Runnable {
public int flag = 1;
//静态对象是类的所有对象共享的
private static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
System.out.println("flag=" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
//td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
//td2的run()可能在td1的run()之前运行
new Thread(td1).start();
new Thread(td2).start();
}
}
线程池的使用
线程池的工厂类来创建线程池
描述
使用步骤
线程池 Excutor 对象
概念 原理
线程池==容器==集合LIst-->队列实现 Thread td = list.remove(0);
jdk 1.5之后有一个提供的线程池的工厂类来创建线程池
代码实现步骤
创建线程池的规则
创建固定大小的线程池
创建可变大小的线程池
创建只有一个线程的线程池
创建可调度的线程池
核心参数
全局
最初的线程数量
最大的线程数
线程保持活动时间
线程保持活动时间单位
任务阻塞队列
拒绝策略
线程池的状态
关系图
状态的表示形式
运行状态
一旦常见就处于运行状态
关闭状态
执行shutdown 方法进行关闭, 不在接收新的任务,队列中的任务还是要执行
停止状态
执行shutdownNow 方法,停止状态, 不接受新的任务,也不会执行队列中的任务
无线程活动状态
当所有的任务已终止,任务数量”为0,线程池会变为TIDYING状态
终止状态
线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED