实现多线程的方法
- 根据Oracle官方文档,实现多线程的方法只有两种
一、实现Runnable接口,重写run,运行start()
二、继承Thread类,重写run,运行start()
准确地讲,实现多线程只有一种方式,构建一个Thread类。而实现线程执行单元有种方式,实现Runnable接口和继承Thread类。
两种方式的本质:
实现Runnable接口:底层调用了target.run()
继承Thread类:重写了整个run方法
Runnable和Thread的区别
Java不允许多继承,实现runnable接口可以再继承其他类,而Thread不行
Runnable可以实现多个相同的程序代码的线程去共享一个资源,Thread也可以,但从Thread的源码可以看到,当Thread方式去实现资源共享时,实际上源码内部是将thread向下转型为了runnable,其内部依然是一runnable形式去实现资源的共享的。
线程池,collable创建线程的本质,也是新建了一个thread类
启动线程的正确方式
使用start()方法
start()和run()区别
start():
由主线程创建子线程,告诉JVM,让JVM在合适的时候启动。
方法含义:
启动新线程、顺序由JVM来分配调度
分配资源
运行线程
通过源码可知,启动新线程时,
1.先检查线程的状态,如果不为0,才运行,否则报IllegalThreadStateException
2.加入线程组
3.调用native本地方法 start0
run():
通过阅读源码,run方法并没有真正的启动线程,而是由一个main的主线程去调用run方法,这个方法和普通的方法没有区别,run方法的执行线程是主线程,并没有创建新线程。
runnable其实相对于一个task,并不具有线程的概念,如果你直接去调用runnable的run,其实就是相当于直接在主线程中执行了一个函数而已,并未开启线程去执行
如何正确停止线程
通过interrupt来通知线程停止,而不是强制停止
具体场景
通常情况下,代码执行完,线程就会停止运行。
正确方法:
使用interrupt来请求停止线程 [好处是能保证数据安全,把主动权交给被中断的线程]。
要正确停止线程,还需要请求方、被请求方、子方法被调用方互相配合。具体如何配合?
Q:为什么volatile设置boolean是错的?
A:因为它无法长时间处理线程阻塞的情况
Q:如何处理不可中断的阻塞?
A:没有通用的解决方案,具体情况具体分析,编写代码过程中尽量选择可以中断的方法。
可以为了响应中断而抛出InterruptedException的常见方法列表
- Object. wait()/ wait( long)/ wait( long, int)
- Thread. sleep( long) /sleep( long, int)
- Thread. join()/ join( long)/ join( long, int)
- java. util. concurrent. BlockingQueue. take() /put( E)
- java. util. concurrent. locks. Lock. lockInterruptibly()
- java. util. concurrent. CountDownLatch. await()
- java. util. concurrent. CyclicBarrier. await()
- java. util. concurrent. Exchanger. exchange(V)
- java.nio.channels.InterruptibleChannel相关方法
- java.nio.channels.Selector的相关方法
线程的生命周期
线程的六种状态
New 新建 [start尚未执行]
Runnable 运行 [就绪和运行状态(ready)和(running)]
Blocked 阻塞 [被Synchronized所修饰的代码块/方法]
Waiting 等待 [不带参,有锁会释放]
TimedWaiting 超时等待 [计时等待 带time参数]
Terminated 终止
一般习惯而言,把Blocked(被阻塞)、Waiting(等待)、Timed_waiting(计时等待)都称为阻塞状态。
lock不会主动释放锁,要手动释放
线程sleep的时候不会释放Synchronized的monitor,sleep时间到了,正常结束或抛异常,才会释放锁
wait/notify/notifyAll的特点、性质
用必须先拥有monitor
只能唤醒其中一个
属于Object类
类似功能的Condition
同时持有多个锁的情况
生产者消费者模式
Q:为什么要使用这种模式?
A:使生产者和消费者解耦,生产者只需把生产好的数据往缓冲区放,消费者只管从缓冲区取。两者更好的配合[平衡生产快,消费慢的矛盾]
手写生产者消费者模式
package threadcoreknowledge.threadobjectclasscommonmethods;
import java.util.Date;
import java.util.LinkedList;
/**
* 描述: 用wait/notify来实现生产者消费者模式
*/
public class ProducerConsumerModel {
public static void main(String[] args) {
EventStorage eventStorage = new EventStorage();
Producer producer = new Producer(eventStorage);
Consumer consumer = new Consumer(eventStorage);
new Thread(producer).start();
new Thread(consumer).start();
}
}
class Producer implements Runnable {
private EventStorage storage;
public Producer(
EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.put();
}
}
}
class Consumer implements Runnable {
private EventStorage storage;
public Consumer(
EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.take();
}
}
}
class EventStorage {
private int maxSize;
private LinkedList<Date> storage;
public EventStorage() {
maxSize = 10;
storage = new LinkedList<>();
}
public synchronized void put() {
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("仓库里有了" + storage.size() + "个产品。");
notify();
}
public synchronized void take() {
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了" + storage.poll() + ",现在仓库还剩下" + storage.size());
notify();
}
}
线程的各属性
优先级、各属性[ID,Name,IsDaemon,Priority]
守护线程特性[1.线程继承于父线程
2.被谁启动
3.不影响JVM的退出]
Q:守护线程和普通线程的区别?
A: ◆ 整体上没有区别
◆ 唯一区别在于是否影响JVM退出
守护线程主要用于给用户线程提供服务
线程优先级
默认为5,,10个级别
◆ 程序设计不应该依赖于优先级
◇ 不同操作系统的优先级映射和调度都不同
◇ 优先级会被操作系统改变
线程的未捕获异常怎么处理?
Q:为什么会捕获不到异常?
A:对于主线程来说,可以轻松发现异常,但子线程不行,如果子线程发生异常,无法用传统的方法捕获
解决方案
◆ 在每个run方法加try catch[不推荐]
◆ 利用UncaughtExceptionHandler全局捕获异常[推荐]
UncaughtExceptionHandler
package threadcoreknowledge.uncaughtexception;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @author Jaychan
* @date 2020/5/8
* @description 自己的UncaughtExceptionHandler
*/
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private String name;
public MyUncaughtExceptionHandler(String name) {
this.name = name;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.WARNING,"线程异常,终止了"+t.getName(),e);
System.out.println(name+"捕获了异常"+t.getName()+"异常"+e);
}
}
UseUncaughtExceptionHandler
package threadcoreknowledge.uncaughtexception;
/**
* @author Jaychan
* @date 2020/5/8
* @description 使用自己写的UncaughtExceptionHandler
*/
public class UseOwnUncaughtExceptionHandler implements Runnable{
public static void main(String[] args) throws InterruptedException {
try{
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1 "));
new Thread(new UseOwnUncaughtExceptionHandler(),"My Thread -1").start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler(),"My Thread -2").start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler(),"My Thread -3").start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler(),"My Thread -4").start();
Thread.sleep(300);
}catch (RuntimeException e){
System.out.println("Caught Exception.");
}
}
@Override
public void run() {
throw new RuntimeException();
}
}
线程性能和安全问题
Q:◆ 什么是线程安全?
不管业务中遇到怎样的多个线程访问某对象或者某方法的情况,在编写这个业务逻辑的时候,都不需要额外的做任何处理,线程也可以正常运行(得到正确的结果),就可以成为线程安全。
<当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的>
Q:◆ 什么情况下会出现线程安全问题
◇ 运行结果错误(i++)
◇ 活跃性问题:死锁、活锁、饥饿
◇ 对象发布和初始化的安全问题
Q:◆ 什么是对象发布
指的是“使对象能够在当前作用域之外的代码使用”。主要集中在public方法
◇ return 一个private对象
Q:◆什么是对象溢出
“对象逸出”指的是某不应该发布的对象被发布的情况,对象发布、创建最容易犯的错误就是对象逸出。
◇ 方法返回一个private对象
◇ 还未完成初始化(构造函数还没完全执行完毕),就把对象提供给外界
◇ ↑{
1.构造函数中未初始化完毕就this赋值
2.注册监听事件
3.构造函数中运行线程
}
如何解决?
◇ 返回副本,而不是对象的引用
◇ 用工厂模式生产对象
各种需要考虑线程安全的情况
◆ 访问共享的静态变量或资源,会有并发风险
[如对象的属性、静态变量、共享缓存、数据库]
◆ 所有依赖时序的操作,即使线程是安全的
[read-modify-write,check-then-act]
◆ 不同数据存在捆绑关系
[ip和端口]
◆ 使用其他类时,对方没有声明自己线程是安全时
[hashmap]
线程带来的性能问题
◆ 性能问题的体现
◇ 服务响应慢、吞吐量低、资源消耗(例如内存)过高等
◇ 虽然不是结果错误,但依然危害巨大(据统计)
为什么多线程会带来性能问题
◆ 调度:
频繁的上下文切换
Q:◇ 什么是上下文切换?
上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行以下的活动:
(1)挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处,
(2)在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复,(3)跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程。
Q:◇ 何时导致密集的上下文切换?
频繁的竞争锁、IO的读写操作等
同时还会带来额外的缓存开销
◆ 协调:内存同步
涉及到Java内存模型
Thread和Object类中和和线程相关的重要方法
Q:为什么wait()需要在同步代码块内使用,而sleep()不需要?
[防止wait之后没有notify来唤醒,防止线程安全问题的发生]
这是Java设计者为了避免使用者出现lost wake up问题而搞规定的
Lost Wake Up问题
假设有两个线程,一个消费者一个生产者
生产者任务简化成将Count+1,而后唤醒消费者
消费者任务简化成将Count-1,在减到0的时候陷入睡眠
初始化的时候,count为0,这是消费者判断到Count小于等于0,正准备睡眠
而在这瞬间,生产者已把步骤执行完了,发出了通知(notify),这时候消费者还醒着,正准备睡眠,notify毫无效果,通知就丢掉了。紧接着,消费者进入休眠状态。
那么怎么解决这个问题呢?
现在我们应该就能够看到,问题的根源在于,消费者在检查count到调用wait()之间,count就可能被改掉了。
这就是一种很常见的竞态条件。
很自然的想法是,让消费者和生产者竞争一把锁,竞争到了的,才能够修改count的值。
于是生产者的代码是:
trylock()
count+1
notify()
releaseLock()
消费者
tryLock()
while(count<=0){
wait();
}
count-1;
releaseLock();
Q:3.为什么wait()需要在同步代码块内使用,而sleep()不需要
wait()需要在同步代码块内使用主要让通信变得可靠,防止线程死锁,如果不把wait/notify放在同步代码块中的话,很有可能在执行wait之前,线程很有可能已经切换到了另一个执行notify的线程,这样的话有可能另一个线程先把notify都执行完毕了,那wait永远没有被唤醒了,这就导致了永久等待或者死锁的发生,这就需要把两个方法都放到同步代码块中去。
sleep()只关心自己这个线程,和其他线程关系并不大,所以并不需要同步。
Q:4.为什么线程通信的方法wait(),notify()和notifyAll()被定义在Object类里?而sleep定义在Thread类里?
因为在java中,wait(),notify()和notifyAll()属于锁级别的操作,而锁是属于某个对象的。
Q:wait方法是属于Object对象的,那调用Thread.wait会怎么样?
Thread也是个对象,这样调用也没有问题,但是Thread是个特殊的对象,线程退出的时候会自动执行notify,这样会是我们设计的流程受到干扰,所以我们一般不这么用。
Q:6.如何选择用notify还是notifyAll?
唤醒多个线程和一个线程的区别。
Q:7.notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
没抢到锁的线程处于等待状态,等待锁的释放
Q:8.用suspend和resume来阻塞线程可以吗?为什么
这两个方法被弃用了,推荐使用wait、notify。
Q:9.讲讲sleep方法的特点?
相同点
wait和sleep方法都可以使线程阻塞,对应线程状态是Waiting或Time_Waiting。
wait和sleep方法都可以响应中断Thread.interrupt()。
不同点
wait方法的执行必须在同步方法中进行,而sleep则不需要
在同步方法里执行sleep方法时,不会释放monitor锁,但是wait方法会释放monitor锁。
sleep方法短暂休眠之后会主动退出阻塞,而没有指定时间的wait方法则需要被其他线程中断后才能退出阻塞。
wait()、notify()和notifyAll()是Object类的方法, sleep()和yeild()是Thread类的方法。