第一部分,线程知识回顾总结
一。为什么要用多线程?
第一,从CPU角度,现在CPU都是多核的,如果只用单线程那就会浪费CPU
第二,从JVM角度,JVM中内存分为三部分,堆、栈、非堆,而栈空间则是由线程瓜分的,所以如果说我们只用单线程编程,那么就浪费了大量的栈空间内存。
二。线程的生命周期:
新建--准备---运行---挂起(分为等待唤醒挂起和超时挂起)---死亡
三。线程的创建方式:
1.继承Thread,重写run方法,由于java是单继承,所以不推荐
2.实现Runnable接口,实现run方法。任何类都可以实现,但是没有返回值,推荐使用
3.实现Callable接口,实现call方法,需要配合FutureTask类使用,有返回值,推荐使用
4.使用Excutor线程池,配合Runnable接口使用,池化技术,首推使用
四。线程方法介绍
1.yield 线程礼让,这个方式是让当前线程释放CPU,让当前线程进入准备状态,和其他优先级大于等于此线程开始重新争夺CPU资源,但是这个线程不会释放锁
2.sleep 睡眠状态,释放CPU,但是不会释放锁,超时后自动醒过来
3.start 方法,启动线程,线程进入准备状态,开始争夺CPU资源
4.stop 方法,线程停止,已经废弃
5.interrupt 中断线程,线程停止方法,也不推荐
6.isAlive 判断当前线程时候还活着
7.setPriority 设置线程优先级,0-10级
8.getPritory 获取线程优先级
9.setName 设置线程名字 getName 获取线程名字
10.getThreadGroup 获取当前线程所在线程组
11.activeCount 获取当前线程所在线程组中活着线程的数量
12.jon 方法,线程插队,当前线程进入阻塞状态,等待插队线程执行超时或执行完后再继续执行
join方法底层其实就是wait方法使当前线程进入阻塞状态,所以会释放锁
13.setDameon 设置当前线程为守护线程,一个进程中至少要有两个线程,一个是用户线程(除了守护线程外的线程都是用户线程),另一个是守护线程(也就是GC线程)
14.getState 获取当前线程状态,六种状态
15.object 中的两个方法
wait 等待,释放CPU,释放锁,使当前线程(哪个线程正在获取CPU,哪个就是当前线程)进入阻塞状态,必须配合锁一起使用
notify 唤醒,唤醒调用wait方法的对象阻塞的线程,这个方法是随机唤醒,无法做到精准唤醒
notifyAll 唤醒所有,唤醒调用wait方法的对象阻塞的所有线程
第二部分,JUC编程总结
一。并发和并行区别
并发:多个线程共同抢夺一个资源
并行:多个线程共同执行,和并发的核心区别就是是否设计到争夺同一个资源
二。sleep和wait的区别
1.sleep是线程类中方法,wait是object中方法
2.sleep可以超时自动醒过来,wait必须要等待唤醒
3.sleep 是睡觉,不会释放锁,wait是等待,会释放锁
4.sleep可以在任意地方使用,wait方法必须要配合着锁使用
三。并发编程(生产者消费者模式)思想
1.创建共享资源类
2.上锁 等待 干活 唤醒 释放锁
3.解决虚假唤醒问题
四。隐式锁和显示锁
隐式锁:synchronized 方法就是隐式锁,就是我们不需要手动上锁解锁,由操作系统自动完成
显示锁:lock 锁,上锁解锁由我们手动实现,如果忘记解锁则会出现死锁
五。悲观锁和乐观锁
悲观锁:认为资源的所有操作都是由多个 线程同时并发进行,所以需要严格的进行同步,synchronized 锁就是悲观锁,悲观锁可以保证绝对线程安全,但是效率低
乐观锁:认为并发一般情况下不会出现,只有在修改数据的时候才会出现问题,CAS机制就是乐观锁
六。重量级锁和轻量级锁
重量级锁:synchronized 就是重量级锁,所有等待获取锁的线程都会处于等待队列中,并且会进入阻塞状态,释放CPU,因此效率低
轻量级锁:CAS机制锁,又称为自旋锁,Atomic包下的所有类都是轻量级锁,所有等待线程不会释放CPU,而是不停的重新计算,直到回写数据时发现堆中数据和原始数据一致时才完成操作,效率高,但是如果自旋次数太高则会浪费CPU资源
七。公平锁和非公平锁
公平锁:所有线程进入等待队列时,是按照先来后到的顺序进入
有点:先来后到,大家获取到执行的权利均匀分布
缺点:CPU频繁的切换线程、切换上下文,会降低执行效率
非公平锁:等待线程不是按照先来后到的顺序加入等待队列的,而是根据一定的规则插队,所有锁的默认模式都是非公平模式
有点:CPU不用频繁的切换线程,切换上线文,执行效率高
缺点:线程之间获取到执行的权利不均匀,出现饿死线程问题
八。可重入锁
锁可以存在嵌套使用,如果不论嵌套多少次,和只使用一次效果一样,那么这个锁就是可重入锁,例如synchronized 和lock都是可重入锁,相反的,非可重入锁,就是必须要先释放掉手上的锁,才可以获取到下一把锁,但是,所有的锁都必须要成对出现,有上锁就是解锁,否则会出现死锁
九。独享锁和共享锁
独享锁:只能由一个线程获取到的锁,其他线程只能等待当前线程释放锁后才可以获取
共享锁:可以同时由多个线程并行获取到的锁,可以并发执行
十。lock锁介绍
1.ReentrantLock 偏向锁,分为公平锁和非公平锁
2.ReadWriteLock 读写锁,分为ReadLock和WriteLock,读锁和写锁,其中读锁是共享锁,就是可以并发执行读操作,写锁是独享锁,只能由一个线程获取到。这里又涉及到锁降级,就是当写锁再次重入获取读锁时,是可以重入的,此时写锁会降级为读锁,但是仅仅是再可重入方面来看是锁降级了,如果此时其他线程访问读锁时,还是无法获取到的。锁只能降级,不可以升级。
十一。lock和synchronized 的区别
1.synchronized 是关键字,lock是类
2.synchronized 是隐式锁,上锁解锁都由系统负责,lock是显示锁,加锁解锁必须由开发人员自己控制,如果忘记解锁则会出现死锁问题
3.synchronized 是重量级锁非公平锁,lock可以细化为公平锁和非公平锁,读写锁,都是可重入锁
4.synchronized 中等待队列中线程必须阻塞等待,lock则不一定会一直等待下去,可以结束等待
十二。lock和synchronized 实现生产者消费者模式代码
1.synchronized 需要配合wait和notifyAll来实现
Object lock =new Object();
synchronized (lock){
while(条件){
lock.wait();
}
//干活
lock。notifyAll();
}
缺点:可以实现线程间通信,但是无法实现精准线程间通信,比如说四个线程A B C D要实现A--B---C----D--A---B--C--D...依次执行,则无法实现
2.lock 需要配合condition来实现,一个lock可以配合多个condition
lock.lock();
try{
while(条件){
condition.await();
}
//干活
condition.signal();
//或者condition.signalAll();
}finally{
lock.unlock();
}
有点:不仅仅可以实现线程间通信,还可以实现精准的线程间通信
十三。解决ArrayList线程不安全问题
1.使用vector ,原理就是每个方法都是同步方法,效率低
2.使用Collectons.synchronized()方法处理,原理也是使用使用同步代码块,效率低
3.使用JUC包下的CopyOnWriteArrayList,写时复制技术,读取数据时不做任何操作,写的时候枷锁--复制一份---操作---写回--解锁,但是原数据要用volatile关键字修饰,保证可见性
十四。解决HashSet线程不安全问题
1.使用Collectons.synchronized()方法处理,原理就是所有方法改成同步方法
2.使用CopyOnWriteArraySet来解决,原理也是,元数据用volatile来修饰,保证数据的可见性,读取数据不做任何操作,修改数据枷锁--复制--修改---写回--解锁
十五。解决map线程不安全问题
1.使用HashTable,所有方法都是枷锁同步方法,效率低
2.使用Collection.synchronized()方法处理,原理也是使用synchronized 同步代码块,效率低
3.使用JUC包下的ConcurrentHashMap,原理和CopyOnWriteArrayList一样
十六。线程同步的三个辅助类
1.CountDwonLatch 减法计数器,当计数器减小到0时,阻塞队列才会执行,可以实现N个线程等待N个线程执行完再执行效果,但是只能使用一次
2.CycleBarrier 循环栅栏,加法计数器,当N个线程同时 执行完后,才会执行等待线程,并且可以多次使用
3.SemapPhore 信号灯,主要解决的就是线程多、资源少问题,没有抢到的线程等待执行,项目上主要用来解决限流问题
十七。线程池
1.为什么要用线程池?
线程的创建和销毁是非常消耗CPU的,线程是珍贵的资源,因此不可以频繁的创建和销毁线程;没有节制的创建线程,会引发OOM问题,因此线程的使用是需要控制的。
2.线程池 Executor ,具体实现类为ThreadPoolExecutor
七个参数含义:
1)核心线程数量,常驻内存
2)最大线程数量,包括核心线程
3)非核心线程空闲最大存活时长
4)非核心线程存活时长单位
5)阻塞队列
6)线程工厂
7)拒绝策略,四大拒绝策略
线程池工作流程:
当任务来了,先用核心线程处理,如果任务超过核心线程,则放入阻塞队列等待,如果阻塞队列满了,则将后面新任务放入非核心线程执行,如果所有线程和阻塞 队列都满了,此时再来新任务,则会触发拒绝策略
五大拒绝策略:
1)AbortPolicy 拒绝策略,并且跑出异常
2)CallerRunsPolicy 拒绝,哪里来的哪里去,交给发送线程任务 线程处理
3)DiscardPolicy 拒绝,并且不会跑出异常
4)DiscardOldestPolicy 尝试和队列中等待时间最长的任务竞争,如果竞争失败了,则直接丢弃此任务,且不会跑出异常
5)自定义拒绝 策略,上面四种拒绝策略其实在实际开发中用的可能比较少,因为任务来了,不论怎么拒绝都不符合需求,因此基本上都是自定义拒绝策略
线程池最大线程的设置规则:
1)cup密集型任务,就是任务不怎么耗时,理解为短而快的任务,此时应该根据CPU的个数来设置最大线程数,不至于cpu来回切换线程降低工作效率
2)IO密集型,也就是任务都是耗时操作,此时最大线程数应大于任务数,保证耗时任务都可以处理,不至于卡顿
十八。Atomic 原子操作类
Atomic原子操作类,底层是通过unsafe的CAS机制保证原子操作的,因此是轻量级锁。基本数据类型和引用数据类型都可以用,缺点就是只能同步一个共享变量。
十九。死锁问题
死锁条件
1.互斥条件,并发操作,且一次只能有一个线程访问资源
2.请求与保持,一个线程获取到资源,需要保持资源不放
3.不剥夺条件:已获得的资源线程,在未使用完前,不可以强制被剥夺
4.循环等待条件:若干线程之间形成一种环形循环等待资源
死锁问题解析:
1.持有多个锁的情况下,持有锁的顺序不一致造成
2.锁嵌套造成
如何发现死锁:
通过jstack拿到应用的堆转储文件,分析文件
第三部分。锁总结
使用锁的优先顺序如下:
1.如果只有一个共享变量,则优先考虑Atomic类,使用CAS机制实现同步
2.如果是多个共享遍历,则优先考虑lock锁
1)使用读写锁
2)使用非公平锁
3)使用公平锁
3.使用synchronized 关键字同步