复习day6
1. 线程生命周期/状态
线程的生命周期.
- New:新建状态/瞬态,当线程对象创立后,即进入了新建状态,如:Thread t = new MyThread()
- Runnable:就绪状态,当调用线程对象的start()方法(t.start()),线程就进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待cpu调度执行,并不是说执行了t.start()此线程立即就会执行
- Running:运行状态,当cpu开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
- Blocked:阻塞状态,处于运行状态中的线程由于某种原因,暂时放弃对cpu的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被cpu调用以进入到运行状态
- Dead:死亡状态(结束状态),线程执行完了或者因异常退出了run()方法,该线程结束生命周期
(1)就绪状态是进入到运行状态的唯一入口
(2)线程想要进入到运行状态执行,首先必须处于就绪状态中
(3)根据阻塞产生的原因,阻塞状态又可以分为三种:
【1】等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态
【2】同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态
【3】其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态.之前获取键盘输入.
线程安全和线程不安全
线程不安全是指多线程操作同一个对象可能会出现问题。而线程安全则是多线程操作同一个对象不会有问题。
2. start()和run()
start方法是线程内的方法,run方法是接口Runnable的的方法,这个接口内只有这一个方法
start方法它的作用是启动一个新线程。通过start()方法来启动的新线程,处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行相应线程的run()方法(start方法只能被调用一次,因为一个线程只能被初始化一次)
run()就和普通的成员方法一样,可以被重复调用。
如果直接调用run方法,并不会启动新线程!程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码
3. wait和sleep
wait:Object类的方法,必须放在循环体和同步代码块中,执行该方法的线程会释放锁,进入线程等待池中等待被再次唤醒(notify随机唤醒,notifyAll全部唤醒,线程结束自动唤醒)即放入锁池中竞争同步锁
sleep:Thread类的方法,必须带一个时间参数。会让当前线程休眠进入阻塞状态并释放CPU(阿里面试题 Sleep释放CPU,wait 也会释放cpu,因为cpu资源太宝贵了,只有在线程running的时候,才会获取cpu片段),提供其他线程运行的机会且不考虑优先级,但如果有同步锁则sleep不会释放锁即其他线程无法获得同步锁 可通过调用interrupt()方法来唤醒休眠线程。
区别:
a. wait方法一定是出现在synchronized同步代码内部的.[sleep是可以放在同步/不同步的代码内部的]
b. wait方法,执行这个方法的线程就会释放锁,释放cpu.但是sleep是不会释放锁的.只是释放了cpu - 核心的区别
c. sleep - 线程会自动进入到阻塞状态[其他阻塞状态],一旦睡眠时间结束.会自动从阻塞状态恢复到可就绪态 - 等到cpu到来 - 运行态.
wait - 线程会自动进入到等待阻塞状态,自己不会主动”醒过来”,必须是其他线程调用notify或者notifyAll,进入到锁定状态[再次尝试 去获取锁]
4. yield
yield:让出CPU调度,Thread类的方法,类似sleep只是不能由用户指定暂停多长时间 ,并且yield()方法只能让同优先级的线程有执行的机会。 yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。调用yield方法只是一个建议,告诉线程调度器我的工作已经做的差不多了,可以让别的相同优先级的线程使用CPU了,没有任何机制保证采纳
5. join
join:一种特殊的wait,当前运行线程调用另一个线程的join方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。 注意该方法也需要捕捉异常。等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测
6. 死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
死锁产生的条件
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
只要打破四个条件的一个,就可以防止死锁.
死锁是不可避免,但是需要写程序的破坏四个条件中的一个.
静态域容易产生死锁.
/**
* 本类用来演示: 死锁 - 慎用静态域 - 更大概率容易造成死锁
*/
public class DeadLockDemo extends Thread{
//临界资源
//每个对象对应一个监视器锁[monitor对象]
public static Object oo1 = new Object();
public static Object oo2 = new Object();
}
class D1 extends Thread{
private DeadLockDemo dd = new DeadLockDemo();
@Override
public void run() {
synchronized (dd.oo1){//申请到了oo1对象的锁资源
System.out.println("======11======");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (dd.oo2){//申请到了oo1对象的锁资源
System.out.println("=======22======");
}
}
}
}
class D2 extends Thread{
private DeadLockDemo dd = new DeadLockDemo();
@Override
public void run() {
synchronized (dd.oo2){
System.out.println("====33====");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (dd.oo1){
System.out.println("=====44====");
}
}
}
}
class TestDead{
public static void main(String[] args) {
Thread t1 = new D1();
Thread t2 = new D2();
t1.start();
t2.start();
}
}
7.生产者和消费者
单个生产者和单个消费者
生产者线程 - 负责生产产品-box[把产品放入到box]
消费者线程 - 负责消费产品-box[从box中去获取产品]
通信的流程
俩条线程并发执行 - 调用的是不同的方法
假设消费者线程先执行 - 直接从box中去获取产品.但是此时此刻box中尚无产品.
消费者线程就需要调用wait方法 - 自己会释放锁资源,消费者线程进入到线程等待池[意味着当前的线程会进入到等待阻塞的状态]
-getter方法
- 假设生产者线程 - setter方法
先执行了.发现box中没有产品的,顺利放入一个产品到box中的.但是放完之后.生产者还是会继续执行.继续执行的时候又会调用
setter方法.这个时候,不能允许生产者线程连续再放 - 就得调用生产者线程的wait方法,同时还需要唤醒消费者线程过来消费.
- 消费者线程一旦消费成功,它还是会继续执行自己的getter方法.发现box中没有了.回到第2个步骤
package tech.aistar.day15.thread02;
/**
* 本类用来演示: 线程之间的通信 - 生产者和消费者
*
* 效果是:必须是生产者线程先进去执行.
* 放1个
* 取1个
* 放2个
* 取2个
* 放3个
* 取3个
*
* 一次性放,一次性取.
* 不能连续出现俩次放/取.
*/
public class ThreadTongXinDemo {
public static void main(String[] args) {
//典型的不共享代码,但是共享资源.
Box box = new Box();
Thread product = new ProductThread(box);
Thread customer = new CustomerThread(box);
product.setName("生产者");
customer.setName("消费者");
product.start();
customer.start();
}
}
//定义一个临界资源
class Box{
private int content;//产品
//假设的是是要调用getter方法 - 消费了 - box中没有东西了.
//如果box中有东西 - 刚生产完,flag为true
//如果box中没有东西 - 刚消费完 - flag为false
private boolean flag;//标志
//生产者线程去调用的
//线程通信的方法wait,notify,notifyAll方法必须存在于循环体的同步方法中.
public synchronized void setContent(int content) {
if(flag){//box中是有东西的
//一旦调用wait方法之后,线程会释放锁.并且会自动进入到线程等待池中
//进入到一个阻塞的状态.
//它自己是不会主动被唤醒的.必须由其他线程调用notify或者notifyAll才能够
//notify - 随机唤醒线程等待池中的一个线程
//notifyAll - 唤醒所有的等待的线程
try {
wait();//为了防止生产者线程连续执行..
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生产者顺利去执行.
//设置flag
flag = true;
this.content = content;
//通知一下消费者来消费.消费者很有可能已经处于等待阻塞的状态.
notifyAll();//唤醒全部等待阻塞的线程
}
//消费者线程去调用的
public synchronized int getContent() {
if(!flag){//1. 保证消费者第一次执行,肯定会进来
try {
wait();//释放锁,自己是不会主动"醒过来的"
} catch (InterruptedException e) {
e.printStackTrace();
}
}
flag = false;//消费完了...
//唤醒一下生产者
notifyAll();
return content;
}
}
//定义生产者线程
class ProductThread extends Thread{
private Box box;
private int i = 0;//产品
public ProductThread(Box box){
this.box = box;
}
@Override
public void run() {
//不断去生产
while(true){
System.out.println(Thread.currentThread().getName()+"=>放"+(++i)+"个");
box.setContent(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//定义一个消费者线程
class CustomerThread extends Thread{
private Box box;
public CustomerThread(Box box){
this.box = box;
}
@Override
public void run() {
while(true){
System.out.println(Thread.currentThread().getName()+"->取"+box.getContent()+"个");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程的通信
同程公司笔试题 - 循环打印abc ABC 123 abc ABC 123
打印小写字母abc的线程,打印大写字母ABC线程,打印数字123的线程
保证必须按照上面的顺序进行输出 - synchronized - wait和notify/notifyAll不能解决原因是
notify随机唤醒,notifyAll唤醒所有
使用Lock中的同步队列 - 解决方案 - 唤醒指定的等待队列上的某个线程.
Lock接口中提供的构建等待队列的方法 - Condition newCondition();
Contion提供的关于线程通信的俩个方法 - 出现在同步代码内部 - lock()->unlock()内部
- void await();//当前线程会进入到阻塞状态.必须要等其他线程唤醒的
- void signal();//哪个线程的Condition对象去调用signal()方法,哪个线程就会被唤醒
package tech.aistar.day15.lock; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 本类用来演示: * * 同程公司笔试题 - 循环打印abc ABC 123 abc ABC 123 * * 打印小写字母abc的线程,打印大写字母ABC线程,打印数字123的线程 * * 使用Lock中的同步队列 - 解决方案 - **唤醒指定的等待队列上的某个线程.** * * @author: success * @date: 2021/8/11 2:29 下午 */ public class LockTongxinDemo { private Lock lock = new ReentrantLock(); //有几个线程,就构建几个队列 //打印小写字母的 private Condition c1 = lock.newCondition(); //打印大写字母的 private Condition c2 = lock.newCondition(); //打印数字的 private Condition c3 = lock.newCondition(); //定义了一个标记 private int count = 0; //count = 0,打印小写字母的执行 //count = 1,打印大写字母的执行 //count = 2,打印数字的执行 //1. void await();//当前线程会进入到阻塞状态.必须要等其他线程唤醒的 //2. void signal();//哪个线程的Conition对象去调用signal()方法,哪个线程就会被唤醒 public void printLower(){ while(true){ try { lock.lock(); if(count!=0){ //等待 //一开始,第一次count=0,说明打印小写字母的线程正常执行 c1.await(); } System.out.print("abc"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count=1; c2.signal();//唤醒打印大写字母的线程 } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } public void printUpper(){ while(true){ try { lock.lock(); if(count!=1){ try { c2.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print("ABC"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count=2; c3.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } public void printNum(){ while(true){ try { lock.lock(); if(count!=2){ c3.await(); } System.out.print(123); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count = 0; c1.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static void main(String[] args) { LockTongxinDemo demo = new LockTongxinDemo(); Thread t1 = new Thread(()->{ demo.printLower(); }); Thread t2 = new Thread(()->{ demo.printUpper(); }); Thread t3 = new Thread(()->{ demo.printNum(); }); t1.start(); t2.start(); t3.start(); } }
8.四个线程池
ExecutorService是Java提供的用于管理线程池的类。该类的两个作用:控制线程数量和重用线程
- Executors.newCacheThreadPool():可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务
- Executors.newFixedThreadPool(int n):创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。
- Executors.newScheduledThreadPool(int n):创建一个定长线程池,支持定时及周期性任务执行
- Executors.newSingleThreadExecutor():创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
好处:1. 降低资源的消耗 2. 提高响应速度 3. 对线程进行统一的管理(以便调优,监控)
线程池原理
任务来执行的时候, 他会先到线程池里面查看线程数是否小于核心线程数, 如果小于核心线程数,即使线程池里面有空闲的线程, 也会创建一个线程去执行这个任务, 如果下一次有任务执行的时候, 线程池的线程等于核心线程数, 并且没有空闲的线程, 他就会把这个任务放入到阻塞队列里面, 等待线程池里面有执行完空闲的线程;如果下一个任务进来的时候, 如果阻塞队列都满了, 就会判断线程池里面的线程数是否小于线程池的最大线程数, 如果小于, 就会再创建一个线程给这个任务执行, 如果再进来一个任务, 以上条件都不符合了, 就会有个饱和策略, 对任务队列里面已经满了并且线程池的线程已经达到最大线程数的情况, 就需要用到饱和策略, 默认的饱和策略是AbortPolicy, 会对这些想要执行的任务进行丢弃, 然后抛出异常.
ThreadPoolExecutor方法几个重要的参数
corePoolSize: 表示线程池内, 核心线程的大小, 就是要将一部分线程保存到线程池留给以后使用. 如果当前线程池的线程数小于核心线程, 即使有空闲线程, 也会再线程池创建一个线程, 如果到了corePoolsize, 不会创建线程了.
maximumPoolSize: 表示线程池内所容纳的最大线程个数, 如果任务队列满了, 就需要判断线程池的线程是否小于maximumPoolSize, 小于就需要创建线程, 去执行任务队列里面的任务.
**keepAliveTime: ** 空闲线程存活时间. 就当线程池内的线程数大于corePoolSize并且存活的时间大于keepAliveTime, 就会将这些线程销毁, 减少资源消耗.
**unit: ** 时间单位, keepAliveTime的时间单位.
workQueue : 阻塞队列, 就是当线程池里的线程达到corePoolSize的时候, 就代表需要把这个任务放入到阻塞队列里面, 等待有空闲的线程去执行里面的任务.
**handler: **: 饱和策略, 就是当阻塞队列的任务满了并且线程池达到最大线程数, 说明线程池处于饱和状态了, 就需要一个策略来缓和这种状态, 以下是几种策略
AbortPolicy(默认):直接拒绝所提交的任务, 并抛出异常
CallerRunsPolicy: 只用调用者所在的线程来执行任务.
DiscardPolicy: 不处理直接丢弃放弃任务.
DiscardOldestPolicy: 丢弃掉阻塞队列中存放最久的任务, 执行当前任务.
9. 项目中多线程应用
如何处理高并发
10. 守护线程
GC - 运行在后台的 - 负责回收垃圾对象的.
核心:线程结束的时候不需要关心后台的守护线程是否也运行结束.线程是不会等后台的守护线程全部运行结束才结束.
当后台只有守护线程在执行的时候,就可以认为线程可以结束了.
11. 多线程优缺点
优点;在多线程程序中,多个线程被并发的执行以提高程序的效率,CPU不会因为某个线程需要等待资源而进入空闲状态。多个线程共享堆内存(heap memory),因此创建多个线程去执行一些任务会比创建多个进程更好。
缺点:每开启一条线程就会占用一定内存空间,降低程序性能;线程越多,CPU调度(多个线程之间切换)开销越大,时间开销、空间开销。