第一章 多线程初阶
文章目录
- 第一章 多线程初阶
- 前言
- 一、进程和线程的关系(面试)
- 二、创建线程方式
- 1.继承Thread
- 2.实现Runnable
- 3. 使用匿名内部类--继承Thread
- 4. 使用匿名内部类--Runnable
- 5. lambda表达式
- 其他
- 三、线程常见方法和属性
- 四、后台线程创建
- 提问
- 五、线程终止/中断
- 1. 设置标志位
- 2. 用interrupt设置标志位
- 下划线
- 六、线程等待
- 1.获取线程引用
- 七、线程休眠
- 八、线程状态
- 九、线程安全(面试)
- 1.synchronized
- 2. volatile
- 十、wait & notify
- 十一、多线程编程案例
- 1. 单例模式
- 2. JDBC案例
- 3. 阻塞队列BlockingQueue
- 十二、线程池
- 十三、案例
- 1. 自己实现线程池
- 2. 定时器
- 总结
前言
JavaEE 是Java开发web后端的技术体系
一、进程和线程的关系(面试)
线程是啥:
轻量级进程.线程也是操作系统调度执行的基本单位.和进程的关系:
一个进程包含多个线程和线程区别
每个进程有自己独立的资源,进程和进程之间不能共享;同一个进程的线程之间,共享相同的资源;
进程之间不会相互影响,一个进程挂了,其他进程不会有事;同一个进程线程挂了,可能把其他线程一起带走;
进程之间通常(不绝对)不会有“资源冲突”情况.同一个进程的线程之间,特别容易出现资源访问冲突;
进程是操作系统资源分配的基本单位,线程是操作系统调度执行的基本单位.
Java生态中不鼓励使用多进程编程,非常鼓励多线程的~~
二、创建线程方式
1.继承Thread
线程的相关操作 , 操作系统提供了对应的API , JAVA标准库对操作系统线程的API统一封装 ---- Thread
先继承Thread , 再重写run方法(main是Java入口 , run就是线程入口)
创建实例 , 启动线程
用start调用操作系统API在系统内部创建一个线程
这个线程被创建好之后就会自动执行上面重写的run
这样一类方法 , 你写了由别人给你调用 , 该方法 / 函数成为回调函数(callback)
classMyThreadextendsThread{@Overridepublicvoidrun(){while(true){System.out.println("hello thread");}}}用两个循环来体会多线程
main方法自身也对应一个线程 , start 又创建了一个线程
publicclassdemo1{publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt=newMyThread();t.start();while(true){System.out.println("hello class");}}多线程编辑中 , 没有父/子线程说法 , 只有主线程一般线程这样的说法
转太快了 , 我们可以让他休息一下 , sleep是Thread的静态方法 , sleep起到阻塞作用
try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}为什么sleep在main里面有两个办法 , 在run里面只有一个
方法重写中要求 ,父类子类方法标签名完全一致(包括方法名 , 参数列表 , 声明的抛出异常)
线程的调度是操作系统负责 , 操作系统的调度过程可以认为是随机的 , 因此不一定是交替的
调试的时候也可以看到所包含的线程
2.实现Runnable
Thread就是给线程执行的
Runnable不关心 ,
实现这个接口
创建一个runnable实例 , 创建Thread对象 , 把前面的传进这里 , 再去调用Thread的start
classMyRunnableimplementsRunnable{@Overridepublicvoidrun(){while(true){System.out.println("hello Runnable");try{Thread.sleep(1000);}catch(InterruptedExceptione){}}}}publicclassDemo2{publicstaticvoidmain(String[]args)throwsInterruptedException{MyRunnabler=newMyRunnable();Threadt=newThread(r);t.start();while(true){System.out.println("hello main");Thread.sleep(1000);}}}runnable这样的写法更加解耦合
3. 使用匿名内部类–继承Thread
继承Thread , 使用匿名内部类
方便 , ,没有名字 , 一次性
记住调用start才是真正创建线程
publicstaticvoidmain(String[]args){Threadt=newThread(){publicvoidrun(){while(true){System.out.println("hello thread");try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}};t.start();while(true){System.out.println("hello main");try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}4. 使用匿名内部类–Runnable
此处针对Runnable定义匿名内部类 , 返回结果是一个Runnable的引用 , 把这个传入Thread
publicstaticvoidmain(String[]args){Threadt=newThread(newRunnable(){publicvoidrun(){while(true){System.out.println("hello thread");try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}});t.start();while(true){System.out.println("hello main");try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}5. lambda表达式
线程捕获规则
publicstaticvoidmain(String[]args){Threadt=newThread(()->{while(true){System.out.println("hello thread");try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}});t.start();while(true){System.out.println("hello main");try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}}其他
Java约定 , 一个Thread对象和一个操作系统中的线程, 是一一对应的关系
因此一个Thread对象就只能strat一次
想要复用就重新创建对象
三、线程常见方法和属性
线程起名字
Threadt1=newThread(()->{while(true){System.out.println("hello t1");try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}},"t1");也就是方法括号里 , 逗号后面就是写名字的
四、后台线程创建
main线程包括代码中手动创建的线程默认都是前台线程
后台线程结束不会阻止线程结束
可以设置后台线程
thread.setDaemon(true);
publicstaticvoidmain(String[]args){Threadt1=newThread(()->{for(inti=0;i<5;i++){System.out.println("hello t1");try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}System.out.println("t线程结束");});t1.setDaemon(true);t1.start();try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}System.out.println("主线程结束");}提问
如果成员变量改为局部变量能否达到一样效果?
publicstaticvoidmain(String[]args){Threadt1=newThread(()->{for(inti=0;i<5;i++){System.out.println("hello t1");try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}System.out.println("t线程结束");});t1.setDaemon(true);t1.start();try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}System.out.println("主线程结束");}改为局部变量编译不通过 , 触发语法变量捕获 , 捕获的变量必须是final或事实final , 匿名内部类必须是事实final , 由于后续的地方要改running , running不再是事实final
写成成员变量情况不一样 , 触发的语法是内部类访问外部类 ,外部类的成员可以被内部类随时进行访问
五、线程终止/中断
1. 设置标志位
publicclassDemo8{privatestaticbooleanrunning=true;publicstaticvoidmain(String[]args){Threadt=newThread(()->{while(running){System.out.println("hello thread");try{Thread.sleep(1000);}catch(InterruptedExceptione){thrownewRuntimeException(e);}}System.out.println("t线程结束");});t.start();Scannerscanner=newScanner(System.in);System.out.println("输入0退出");inti=scanner.nextInt();if(i==0){running=false;}}}2. 用interrupt设置标志位
此方法不光可以设置标志位 , 还能唤醒sleep等导致线程阻塞的办法 , 会使sleep抛出异常
cur和t指向同一个对象 ,不能直接使用t
publicstaticvoidmain(String[]args){Threadt1=newThread(()->{Threadcur=Thread.currentThread();while(!cur.isInterrupted()){System.out.println("hello thread");try{Thread.sleep(100_000);}catch(InterruptedExceptione){thrownewRuntimeException(e);break;}}});t1.start();Scannerscan=newScanner(System.in);System.out.println("输入0退出");intn=scan.nextInt();if(n==0){t1.interrupt();}}改进 在实际开发中 , catch中一般不会再次抛出异常 , 那怎么修改呢
让他打印 , 而不是抛出异常
throw new RuntimeException(e);改成
e.printStackTrace();可运行的时候为什么不会中断呢
一旦唤醒了sleep , sleep会清标志位 , 把当前线程中断的位置重新设置回false
那怎么正常退出呢 , 直接break
下划线
可以把整数常量加上下划线_ , 也就是分隔符 , 提高可读性
100000改成100_000 , 不会改变原意
六、线程等待
执行发现得到的结果是t线程执行之前的 , 执行顺序是概率的 , 那加一个sleep , 等待t线程结束 , 但时间不好评估 ,使用join是最优解
join 的等待时间是不确定的 , 取决于t何时结束
1.获取线程引用
我们也可以让t等待main线程 , 获取main指向
那程序一直不推出怎么办
可以加数字 , 文档里面可以看到
一般用第二种 , Windows/Linux很难做到ns级别
join等待 , t线程确实没有执行完 , 如果调用join之前`t已经执行完了 , 在其调用此时不会阻塞
七、线程休眠
sleep(0)线程主动放弃cpu(放权)
八、线程状态
NEW: 安排了工作, 还未开始⾏动
• TERMINATED: 工作完成了.
• RUNNABLE: 可工作的. ⼜可以分成正在⼯作中和即将开始⼯作.
• BLOCKED: 这⼏个都表⽰排队等着其他事情 , 特指由于锁引起的阻塞
• WAITING: 这⼏个都表⽰排队等着其他事情 , 死等进入阻塞状态 , join
• TIMED_WAITING: 这⼏个都表⽰排队等着其他事情 , 带有超时间的阻塞状态
九、线程安全(面试)
一个进程的多个线程共享同一分内存资源 , 如果两个线程都尝试改变某个变量 , 就可能出现冲突
线程安全问题的原因
1.[根本原因]操作系统对于线程的调度是随机的~
(没有办法去应对)
2.两个线程,针对同一个变量进行修改操作
3.修改操作不是原子的~
4.内存可见性
5.指令重排序
1.synchronized
处理从修改操作不是原子的入手
锁的核心原则:互斥 , 独占
对应操作 加锁 , 解锁
synchronized 会起到互斥效果, 某个线程执⾏到某个对象的 synchronized 中时, 其他线程如果也执⾏到同⼀个对象 synchronized 就会阻塞等待
synchronized 几个要点:1.进入{就是加锁,进入}就是解锁2.加锁操作是会防止其他线程"插队"不影响本线程调度出cpu(和临界区做区分)3.锁对象,两个线程针对同一个对象加锁,才会有锁竞争~~不同对象,则不会有
具体操作
synchronized(obj){
代码
}
synchronized 几个要点:1.进入{就是加锁,进入}就是解锁2.加锁操作是会防止其他线程"插队"不影响本线程调度出cpu(和临界区做区分)3.锁对象,两个线程针对同一个对象加锁,才会有锁竞争不同对象,则不会有
synchronized其他写法
synchronized 修饰一个实例方法 , 给this加锁
synchronized 还可以修饰一个静态方法 , 针对类对象加锁
无论哪种写法, synchronized针对啥对象加锁不重要 , 重要的是两个线程是否针对同一个对象加锁
死锁涉及到的情况
- 一个线程一把锁 , 连续多次加锁(可重入锁直接解决了
- 两个线程两把锁 , 钥匙锁在车里 , 车钥匙锁家里了
- n个线程m把锁
死锁的必要条件(背下来 , 面试
4. 锁是互斥的(锁的基本特点
5. 锁不可被抢占(也是基本特点
6. 请求和保持(取决于代码的基本结构 , a线程在获取到lok1的情况下保持lok1的状态 , 再尝试获取lok2
7. 循环等待/环路等待 , 钥匙锁在车里 , 车钥匙锁家里了
解决
- 打破请求和等待 , 避免锁的嵌套
- 打破循环等待 , 约定加锁顺序
2. volatile
处理内存可见性问题
Demo16中出现的问题 , 为什么修改了可是代码没有反应
在此前编译器flag每次读到的都是相同的值 , 编译器无法分析出另一个线程的执行时机 , 因此编译器有一个大胆的决定 , 把load操作优化掉 , 后续的循环只从寄存器中读取flag
我们使用关键字volatile , 用这个关键字修饰某个变量 , 此时编译器知道了这个变量"容易变" , 此后编译器针对这个变量读写操作就不会涉及到优化了
加上sleep并把voilate删掉问题不见了 ,为什么
内存可见性问题本质是编译器优化导致的 , 因为引入sleep , 这个代码中load没有被编译器优化掉 , 因为sleep背后涉及的指令消耗时间比load要多很多 , sleep是性能瓶颈 , 把load干掉于事无补 , sleep背后的调度过程是省略不了的
十、wait & notify
join只影响线程结束的顺序
那如果执行的过程中就能按照一定顺序进行一个影响 , 那怎么办呢
需要用到我们的wait和notify
线程t1执行到wait阻塞 , 在线程t1中遇到notify唤醒
这个机制也可以应对线程饿死问题
wait和notify是object类的对象 , 不是Thread的方法 , 可以使用任意对象调用
wait必须放到synchronized进行使用 , notify也是
加锁的对象必须和调用wait的对象使用一个
wait会等待和释放锁 , 最后再拿回来
notify不会释放
以上是死等版本 , 也有带有超时时间的版本
notify也有一个版本 , notify如果有若干个等待线程 , 随即唤醒其中一个
notifyAll唤醒所有
wait和sleep的区别
都可以让线程阻塞 , 都可以指定线程阻塞时间
- wait的设计是为了被notify , 超时间只是"后手" ; sleep的设计就是为了按照一定时间阻塞
- wait必须搭配锁使用 , sleep不需要
- wait一进来会先释放锁 , 在获取到锁 , sleep防到锁内部 , 休眠时就不会释放锁
- wait虽然也可以通过interrupt唤醒 , 实际上更希望通过notify唤醒(正常情况) , notify唤醒之后还可以随时再wait , 再notify , sleep和interrupt就不是了 , interrupt是可以把线程终止掉
十一、多线程编程案例
1. 单例模式
可以保证一个Java进程中某个类只有唯一一个实例 , 实例在这里指类在这个进程中只能有一个对象 , 不允许创建多个
某个类 , 本身从需求功能上一个实例就够了 , 完全可以使用单例模式
饿汉模式 : 类加载的时候创建对象
懒汉模式 : 首次使用的时候创建实例
通常认为懒汉模式更好 , 避免启动就占用大量内存
正题 : 这两种模式下多线程是否线程安全
饿汉模式天然就是线程安全的 , getlnstance中只是进行读操作 , 多线程对同一个变量进行读取 , 没有问题
懒汉模式线程不安全 , 赋值和读取放在一起不一定是原子 , 可能会插队执行
为解决这一问题 , 我们通过加锁这一方式
加完锁了 , 但是每次执行的时候都要加一下锁 , 会产生阻塞
在if外面再加一个锁
一个这个判断是否要加锁 , 一个判断是否创建实例
双重if , 避免枷锁开销
指令重排序
也是编译器的优化手段
调整指令的顺序 , 从而使代码的效率更高
锁只是在t1执行到锁的部分的时候, 线程还是可以同时调用t2的 , t2不能进入自己锁的部分
在这个例子里 , 还是有可能出现线程安全问题 , 指令重排序
instance = new SinglentontLazy();
这一行, 指令顺序变为分配内存后 , 先引用赋值在初始化 , 引用赋值完后 , 就直接调用t2的线 , t2判断其不为空了跳过括号里面内容直接开始执行下面的代码 , 进一步使用未初始化对象 , 使逻辑出现一些问题
解决方法加volatile
编写这段代码的时候涉及三个要到点
- 加锁
- 双重if判定
- volatile避免指令重排序产生的问题
2. JDBC案例
微信
3. 阻塞队列BlockingQueue
也是先进先出
和其他队列的区别
- 线程安全
- 阻塞功能 a. 队列为空 , 尝试出队列 , 产生阻塞 , 知道队列不为空 b.队列为满 , 尝试入队列 , 产生阻塞 , 直到队列不满
后端开发中,这个东西非常重要,非常常用
有大佬把这样的阻塞队列数据结构,单独提取出来,封装成了一组服务器=>消息队列MessageQueue(MQ)
生产者消费者模型,能够产生的作用~~
1)降低资源的竞争
2)解耦合
3)削峰填谷
弊端 :
- 对于单个请求响应时间的影响
- 服务器结构更复杂
Java标准库提供了阻塞队列 , 后面有容量值
把sleep加到消费者里面后 , 生产者满了之后 , 消费一个才能继续生产一个
现在我们自己造一个阻塞队列
后续使用wait的时候使用while , 防止使用完if条件还是不成立
十二、线程池
线程虽然相较于进程更加轻量 , 但是线程创建销毁开销不能忽略不计 , 为了进一步提高效率削减这里的开销 , 于是引入线程池
线程池就是提前把线程创建好 , 在创建线程 , 就不需要调用操作系统的API来获取(这会进操作系统内核执行逻辑 , 不可控) , 从池子里是用户态
线程池构造函数参数都代表什么(经典面试题)
ThreadPoolExecutor ( int corePoolSize, int maximumPoolSize, , long keepAliveTime, TimeUnit unit, BlockingQueue< Runnable> workQueue, TreadFactory threadFactory. RejectedExecutionHandler )
1)自动扩容缩容
int corePoolSize : 核心线程数 , 线程数最小是这些
int maximumPoolSize : 最大线程数=核心线程数+非核心线程数
2)描述了非核心线程释放时机
long keepAliveTime : 非核心线程允许空闲的最大时间
TimeUnit unit : 相当于枚举类型 , 选择时间单位 , 毫秒什么的
3)工作队列
BlockingQueue< Runnable> workQueue : 阻塞队列 , 放任务的 , 队列为空的时候阻塞 , 满的时候通过拒绝策略处理 , 而不是简单粗暴的阻塞
4)线程工厂
TreadFactory threadFactory : 工厂模式的体现 , 给线程池的线程统一初始化提供了方案 , 就是Thread类的工厂类 , 这个类就会提供一个/N个工厂方法 , 创建出Thread对象
工厂模式
当我们需要用两个构造方法但是参数的类型个数都一样的时候
通过一个工厂类 , 在类里面提供一个工厂方法去创建一个指定对象 , 创建对象的时候可以通过一个普通的构造方法new出来 , 再通过一系列set来设置这里的属性
微信里面有具体代码
5)拒绝策略 (经典面试题)
RejectedExecutionHandler
线程池中有任务队列 , 任务队列长度是有上限的 , 任务达到上限 , , 继续尝试添加任务会触发"拒绝策略"
四种策略
- 任务队列满 , 继续添加的时候 , 抛出异常 , 处理不好 , 程序直接崩溃
- 莫个线程直接调用submit提交任务 , 由于队列满了 , 新任务无法加入到队列中 , 看可以让调用submit的这个线程负责执行这个任务
- 把任务队列中最老的任务给丢了 , 然后空余位置留给新任务
- 吧任务队列最新任务丢掉 , 把空余位置留给最新任务
如果按照阻塞的方法来处理 , 接下来新的请求就会处理不了 , 超时不合适 , 如果采用上面的拒绝策略 , 可以使得新来的任务及时处理
ThreadPoolExecutor太麻烦了 , 标准库提供了一个方法Executors , 把ThreadPoolExecutor进一步封装
executors是工厂类
这两个用法在实际开发中 , 根据项目情况综合考虑
十三、案例
1. 自己实现线程池
这个是创建一定的数目的线程 , 线程里面去while , 都并发的去一直take这个任务
2. 定时器
Java标准库也提供了定时器的实现 , Timer类
Timer这个类内部有专门的线程执行任务 ,内置线程是前台线程 , 因此想要让他退出需要用到cancel方法
现在模拟实现一下
有地方要注意 , 线程安全问题 , 主方法中 , 我们往里放 , 同时线程又要取出, 有可能出现线程安全问题 , 需要加锁
还有地方 , 我们创建新线城的时候 , 有continue` , 则会个循环过程会消耗很多CPU , 并且反复获取锁 , 出发县城饿死问题 , 用wait解决 ,
如果不用优先级对队列 , 而是用优先级阻塞队列 , 有可能会出现思索死锁情况 , 放入和抓取加锁顺序不一致就循环等待了