1 线程的创建和运行
1.1 进程与线程
- 进程:系统资源调度与分配的基本单位
- 线程:CPU资源调度的最小单位
- JDK1.8的 JVM 内存模型
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区
- 程序计数器
- 进程与线程的资源分配:多个线程共享进程的堆和方法区,每个线程都有自己独立的程序计数器、虚拟机栈、本地方法栈。
1.2 线程创建的三种方法
- 继承 Thread 类,重写 run(),创建对象后并没有启动多线程,调用 start() 方法后才启动
/*
继承Thread类
重写run()方法-无参无返回值
创建线程 new MyThread()
启动线程 start()
*/
----为什么是 start() 而不是 run() 方法启动线程?
start() 调用 native方法 start0() 启动多线程, run() 只是一个普通的方法
- 实现 Runnable 接口的 run() 方法:把线程和任务分开
/*
实现Runnable接口
重写run()方法-无参无返回值
创建子类对象 new RunnableTask()
创建线程 new Thread(futureTask)
启动线程 start()
*/
----为什么子类对象的 run() 方法会运行在线程对象中?
线程对象中声明了一个 run() 方法,子类对象的方法赋给了线程对象!
if run() == null, do nothing and return.
因为在Thread中成员变量接收了传入的Runnable子类对象,所以线程能运行子类对象的run()方法。
- 使用 Callable 方式
/*
实现Callable接口
重写call()方法-无参有返回值
创建子类对象 new CallerTask()
创建异步计算结果对象 new FutureTask(callerTask)
创建线程 new Thread(futureTask)
启动线程 start()
*/
----提交Callable任务获取返回结果
futureTask.get()
1.3 三种方法的特点
- 继承Thread:可以通过构造方法或者set方法,方便传参,单继承局限性
- 实现Runnable:只能使用主线程里final修饰的变量,接口可以实现多继承,更便于数据共享
- 使用FutureTask:前两种方法都没办法拿到返回结果,FutureTask可以
1.4 线程调度方式
给线程分配CPU处理权的过程
- 协同式线程调度
- 线程执行时间由线程本身决定,线程完成工作后通知系统切换线程
- 实现简单,执行时间不可控
- 抢占式线程调度(Java线程执行方式)
- 线程执行时间由系统决定,谁抢到谁执行(线程优先级)
1.5 线程的优先级
- 操作系统的优先级
- 动态优先级:正在执行中的线程,随着执行时间的延长而降低
- 静态优先级:处于等待中的线程,随着等待时间的延长而升高
- Java的优先级
- 最高优先级:10
- 最低优先级:1
- 默认优先级:5
- 这里的优先级只是对操作系统的建议,具有统计意义,高优先级的占用CPU时间多,低优先级的占用CPU时间少
2 线程的状态与切换
2.1 线程控制 API
- 线程休眠 sleep ( long millions ) 与 TimeUnit.SECONDS.sleep ( long second ); 等效
- 线程合并 join() 插队:在哪个线程执行,哪个线程就等待;哪个线程调用了 join API ,等待的就是哪个线程
- 线程礼让 yield() 放弃当前CPU的执行权,再次加入下一次抢夺CPU执行权的队伍(抢占式线程调度)
- 设置守护线程 setDaemon( boolean on ) 该方法需要在启动线程之前调用
- user thread:用户线程,main线程是一个用户线程,GC线程是守护线程
- daemon thread:守护线程,当正在运行的所有线程都是守护线程时,JVM退出
- 线程中断 interrupt() 通过异常处理打断线程执行,实际上只抛出异常(标记),没有中断
- 线程终止 stop() 不安全,已废弃
- 设置一个 boolean flag 如果(if) flag 为 true 正常执行程序,否则(else) false 此时线程发生了中断,记录中断信息在 log.txt
2.2 线程状态
- 新建状态(New),刚 new 出来,还没有 start
- 就绪状态(Ready),执行了 start 方法,还没有获取CPU执行权
- 执行状态(Running),有了执行权,正在执行
- 阻塞状态(Blocked),除了没有执行权,还缺少一些必要资源
- 死亡状态(Dead)
- 线程的上下文切换
- 时间片轮转算法(Round-Robin,RR)保证每个线程都能轮流执行。
- 每次切换的时候都要保存现场与恢复现场
3 线程的数据安全
3.1 多线程产数据安全的原因
- 多线程环境
- 数据共享
- 非原子操作(事务的ACID)
3.2 异步与同步
指的是被调用者,多线程天生异步
- 同步:你走我不走,等你满足我的条件了我再走
- 异步:你走你的我走我的
3.3 synchronized(关键字)
- 同步代码块:可以是任意对象,但是要保证是同一个对象
- 同步方法:锁对象隐含为this
- 静态方法:锁对象的字节码文件对象
3.4 Lock(interface)
try{
lock.lock();
//具体实现代码块
}finally{
lock.unlock();
}
3.5 死锁
两个或者以上的线程争抢资源,互相等待的现象
- 一般出现在 synchronized 嵌套代码块情况下
- 解决死锁问题
- 更改加锁顺序
- 再加一把锁,把一个操作变成原子操作
3.6 并发与并行
- 串行:按顺序先后执行
- 并行:同一个时间点,同时执行
- 并发:同一个时间段,同时执行(微观上先后,宏观上同时)
4 线程的通信
4.1 线程的等待与唤醒
- wait():
- 当某个线程调用共享变量的 wait() 方法时,该调用线程会被阻塞挂起,放弃对锁对象的持有
- 直到发生下面之一才会返回:①其他线程调用了该共享对象的 notify() 或者 notifyAll() 方法,②其他线程调用了该线程的 interrupt() 方法(抛出异常)。
synchronized(obj){
//代码块
obj.wait();
obj.notifyAll();
}
- notify():唤醒当前监视器等待的单个对象,选择是随机的。
- notifyAll():唤醒当前监视器等待的所有对象
4.2 生产者与消费者模型
- 参考练习
5 线程池
5.1 线程池 Executors
- 带缓存的线程池 ExecutorService newCachedThreadPool()
- 线程数量不固定
- 超过60s线程无任务自动死亡
- 适用于很多短期异步小程序或者负载较轻的任务
- 固定数量的线程池 ExecutorService newFixedThreadPool(int nThreads)
- 线程数量固定
- 维护一个无队列,任务发现当前无线程空闲,入队等待线程有线程空闲
- 适用于长期任务
- 单个线程的线程池 ExecutorService newSingleThreadPool(int nThreads)
- 只有一个线程
- 维护一个无队列,任务发现当前无线程空闲,入队等待线程有线程空闲
- 适用于一个接一个执行的任务
- 线程池的提交
- Future<?> submit(Runnable task)
- Future<?> submit(Callable task)
- 关闭线程池
- shoutdown()
- shoutdownNow()
5.2 线程池的使用
// 创建线程池
// ExecutorService newFixedThreadPool(int nThreads)
ExecutorService pool = Executors.newFixedThreadPool(2);
// 提交任务
// Future<?> submit(Runnable task)
// Future<?> submit(Callable task)
pool.submit(new RunnableTask);
pool.submit(new RunnableTask);
// 关闭线程池
// shoutdown 保证现在运行的任务执行完毕,并且将在队列中等待的任务执行完
pool.shoutdown();
// shoutdownNow() 保证现在运行的任务执行完毕,不执行其他任务
pool.shoutdownNow();
6 定时器
6.1 定时任务方法
return | 方法 | 描述 |
---|---|---|
void | cancel() | 终止此计时器,丢弃任何当前计划的任务。 |
int | purge() | 从该计时器的任务队列中删除所有取消的任务。 |
void | schedule(TimerTask task, long delay) | 在指定的延迟之后安排指定的任务执行。 |
void | schedule(TimerTask task, long delay, long period) | 在指定的延迟之后开始,周期性period执行指定任务。 |
void | schedule(TimerTask task, Date time) | 在指定的时间安排指定的任务执行。 |
void | schedule(TimerTask task, Date firstTime, long period) | 从指定的时间开始,周期性period执行指定任务。 |
void | scheduleAtFixedRate(TimerTask task, long delay, long period) | 在指定的延迟之后开始,重新执行固定速率的指定任务。 |
void | scheduleAtFixedRate(TimerTask task, Date firstTime, long period) | 从指定的时间开始,对指定的任务执行重复的固定速率执行 。 |
6.2 定时器的使用
// MyTimerTask()继承了TimerTask,重写了run()方法
Timer timer = new Timer();
// 创建时间
SimpleDateFormat sf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = "2021-09-16 09:50:50";
Date time = sf.parse(date);
// 用定时器方法
timer.schedule(new MyTimerTask(),time,2000);