线程
进程是操作系统分配资源的单元,是运行中的程序。
线程:
cpu调度的最小单位;
一个具体的执行单元(任务)。
多线程
就是一个进程(程序)内,允许多个线程,同时进行;
可以提高cpu的利用率,增强程序的功能;
对硬件(cpu,内存,硬盘)的要求提高;
多线程访问同一个共享资源,线程安全问题。
线程创建方式
继承Thread,
实现Runnable接口 run();
实现Callable接口, call()可以抛出异常,有返回值
线程状态
新建
start()
就绪
yield() cpu加载
运行
sleep()
wait()
join()
IO
等待同步锁
阻塞
stop()
任务结束
死亡
多线程安全问题
卖票,取款,秒杀,抢购;
解决: 加锁,排队
守护线程
线程间的通信
并发编程
并行:同时做多件事
并发:同一时间,如何应对多件事
多线程访问同一个共享资源,产生线程安全问题
现在的cpu使多核的,可以同时执行多个线程
安全问题
1.性能
2.死锁
3.可见性,有序性,原子性
cpu–>内存–>IO(硬盘)
三者之间的读写速度有差别
cpu提供缓存
任务细化到线程,切换执行
cpu对我们指令代码的顺序进行优化(重排)
为了线程使用数据更快速,会把线程中使用到的变量复制到线程的工作内存中。
线程间的数据是不可见的;
JMM(Java Memory Model)内存模式:
线程安全问题
可见性
不同线程中,有一个缓存,缓存要操作的变量;
为了提高效率,会等所有操作完成后,在将数据写入到主内存中;
当数据还未写入到主内存时,其线程对修改数据不可见。
有序性
cpu在执行过程中,可能会对我们代码执行的顺序做出优化,重新排列指令。
原子性
多线程在多核cpu中运行,线程切换会造成打破原子性;
一行代码,本来是应该不被拆分执行的,但是受线程切换导致其他线程执行的影响,最终运行的结果与预期值可能会不一致。
总结:
缓存导致可见性问题,线程切换带来的原子型问题,编译优化带来的有序性问题。
Volotlie
1.修饰后的变量,在一个线程操作后,对其他线程立即可见。
2.修饰后的变量,禁止指令重排序。
3.不能解决原子性问题
保证原子性
保证共享变量的修改是互斥的(从并行—>并发)
加锁
互斥,同一时间只能有这一个线程访问共享变量;
**synchronized:**可以保证在一个线程执行时,其他线程不能操作其共享变量;保证原子性,可见性和有序性。
JUC–原子变量
原子类的原子性是通过volatile + CAS实现原子操作的
加锁是一种阻塞式方式实现;
原子变量是非阻塞式方式实现。
CAS
Compare and Swap:比较并交换
采用的是乐观锁与自旋锁;
每次先将数据加载至工作内存 V;
再次从内存中读取其值 A;
如果A==V,则主存数据没有其他线程修改,此时就把计算后的数据B写入内存中。
是一种无锁实现方式,非阻塞式的;
适合低并发;
缺点:
非阻塞式的,其他线程依就可以执行,还要自旋,导致cpu消耗比较大;
ABA问题:先将A改为B,再将B改为A,导致其他线程出现误判,内存值与预期值一致;
解决ABA问题:可以通过添加版本号,每次修改后更改版本号,可以通过比较版本号来确定是否发生了更改。
ConcurrentHashMap
是线程安全的;
Hashtable也是线程安全的,直接将整个put()方法加锁,锁粒度大,效率低。
JUC包中提供了一些用于高并发且线程安全,效率高的类与方法;
放弃分段锁(将某个段整体加锁)而采用了CAS原则+cynchronized;
在put时,先判断添加元素是否是第一个节点;
如果是,则采用CAS原则(比较判断),加入数据到第一个节点;
如果不是,会用第一个节点作为锁,添加synchronized锁机制,加锁
保证安全。
java中的锁
不是真实的锁,只是锁的设计或特性
乐观锁:
采用CAS原则,更新数据时,进行判断而已,不加锁;
悲观锁:
采用加锁更新数据,加锁synchronized或Lock;
公平锁:
就是等待锁的线程按照顺序排队,一旦锁释放了,那么排在第一位置的线程获得锁,执行;
非公平锁:
不用排队,释放后,那个线程抢到就先执行;
可重入锁(递归锁):
当线程获取到外层方法锁对象时,依然可以获得内部同步方法的锁,可以进入内部同步方法的锁,可以进入内部方法,否则会出现死锁。
读写锁(ReadWriteLock): 实锁
可以多个同时读;
写数据时必须只有一个执行。
分段锁:
例如jdk8以前的ConcurrentHashMap,分段加锁,提高效率。
自旋锁:
不断重试去抢占cpu,不会阻塞线程,但数量过多后就会很消耗cpu。
共享锁:
多个线程可以共享一把锁;
独占锁(互斥锁):
一次只能有一个线程持有该锁;
**AQS(**AbstractQueuedSynchronized):
抽象队列同步器,没有获得锁的其他线程,把他们加入到一个队列中(阻塞状态,cpu不再加载),当所释放后,队列中的第一个线程将会获得锁,按照顺序排队获取。
锁的状态:
偏向锁:
只有一个线程,一直获得锁对象,此时锁对象中的锁状态改为偏向锁,并记录线程ID;当同一个线程再次访问时便会直接获得该锁,效率高。
轻量级锁:
当第二个线程访问时,偏向锁将会升级为轻量级锁,其他的线程将会进行自旋,尝试获取锁,不会阻塞,提高效率(线程数量少)。
重量级锁:
当前为轻量级锁时,并发访问量增多,锁的状态也升级为重量级锁,其他线程进入阻塞状态,不再尝试自旋。
synchronized实现
在synchronized修饰的程序块前后会添加一个监视器(进入,推出),利用对象头来记录锁是否被使用;
获取锁,计数器+1,退出监视器,释放锁,计数器-1.
特点:
使用一个唯一的对象,作为锁状态的标记。
ReentrantLock
在内部有一个锁的状态,默认状态为0,如果有线程获取到了锁,将状态改为1;
其他线程有两种处理方式:
公平锁与非公平锁;
如果使用公平锁,就会将等待线程添加到同步等待队列中。
ThreadLocal
为每个线程复制一份变量副本;
//创建一个ThreadLocal对象,用来为每个线程会复制保存一份变量,实现线程封闭
private static ThreadLocal<Integer> localNum = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {//定于初始值
return 0;
}
};
底层使用的是一个ThreadLocalMap对象存储;
会存在内存泄漏的问题:
使用ThreadLocal对象作为键,是弱引用,可以被回收掉,键为null;但是value值为强引用.
建议: 在使用结束后,主动调用remove()方法删除
线程池
为了减少频繁创建销毁线程开销
可以使用线程池, 于心啊创建一部分线程, 重复使用, 减少创建销毁开销
jdk5自带创建线程池的类:
ThreadPoolExecutor:
有七个参数:
corePoolSize: 核心线程数量(不会被销毁)
1.prestartAllCoreThread()与prestartAllCoreThreads()方法, 可以预先创建核心数量个线程;
2.起初不会创建, 直到有任务到达时创建, 直到达到核心数量个线程.
maximumPoolSize: 空闲多久最大线程数量;
keepAliveTime: 超出核心数量部分的线程,在没有执行任务时, 空闲多久后销毁;
unit:keepAliveTime : 时间的属性
workQueue: 一个阻塞队列,用来存储核心线程之后的线程
threadFactory: 创建线程
handler: 拒绝任务后的处理
执行过程:
例如: 核心线程:5 队列:5 线程池最大线程数量:5
开始有任务到达,创建核心线程;
当任务到达6时,核心线程已满,将创建线程进入队列;
当任务到达11时,队列已满,创建非核心线程;
当任务到达16时,已超过最大线程数量与队列,执行拒绝策略.
关闭线程池:
shutdownNow: 对正在执行的任务进行interrupt()停止执行;未开始的全部取消,返回未开始的列表;
shutdown: 不会强行终止已提交或正在执行的任务,但也不会接受新的任务
orkQueue: 一个阻塞队列,用来存储核心线程之后的线程
threadFactory: 创建线程
handler: 拒绝任务后的处理
执行过程:
[外链图片转存中…(img-hCfQi8CD-1632961659999)]
例如: 核心线程:5 队列:5 线程池最大线程数量:5
开始有任务到达,创建核心线程;
当任务到达6时,核心线程已满,将创建线程进入队列;
当任务到达11时,队列已满,创建非核心线程;
当任务到达16时,已超过最大线程数量与队列,执行拒绝策略.
关闭线程池:
shutdownNow: 对正在执行的任务进行interrupt()停止执行;未开始的全部取消,返回未开始的列表;
shutdown: 不会强行终止已提交或正在执行的任务,但也不会接受新的任务