1. 基本概念
1.1 程序
用某种语言编写的一组指令的集合,一段静态的代码
1.2 进程
程序的一次执行过程,或正在运行的一个程序,是一个动态的过程
- 进程是系统进行资源分配的基本单位
1.3 线程
进程可以进一步细化为线程,是程序内部的一条执行路径
- 若一个进程可同一时间并行执行多个程序,就是支持多线程的
- 线程是CPU调度和执行的最小单位,每个线程拥有独立的运行栈和程序计数器
- 线程切换的开销小
- 一个进程中的多个线程可以共享相同的内存单元(堆和方法区),使进程间的通信变得便捷,但也可能带来安全隐患
1.4 多线程的优点
- 提高计算机CPU的利用率
- 改善程序结构,将长而复杂的进程分为多个线程,独立运行,利于理解和维护
- 增强用户体验,对图形化界面更有意义
2. 线程的创建和使用
2.1 创建方式一:继承Thread类
-
流程:
- 继承Thread类
- 重写
run()方法
- 创建该子类对象,调用
start()方法,此时则启动了子线程,调用它的run()方法
-
举例:
class MyThread extends Thread {
@Override
public void run () {
//实现功能
}
}
public class ThreadTest {
public static void main (String args[]) {
MyThread t1 = new Thread();
t1.start();
}
}
- 使用匿名对象创建:
new Thread (){
@Override
public void run () {
super.run () ;
}
} .start () ;
2.2 创建方式二:实现Runnable接口
-
流程:
- 实现
Runnable接口
- 实现
run()方法
- 创建实现类的对象
- 将此对象作为参数传递到
Thread类的构造器中(赋值到Runnable类型的target),创建Thread类的对象
- 通过
Thread类的对象调用start()方法(调用target的run()方法)
- 实现
-
举例:
class MyThread implements Runnable {
@Override
public void run () {
//实现功能
}
}
public class ThreadTest{
public static void main (String args[]) {
MyThread mythread = new MyThread();
Thread t1 = new Thread(mythread);
t1.start();
]
}
- 使用匿名方式创建
new Thread(new Runnable(){
public void run(){
//实现功能
}
}
).start();
2.3 线程常用方法
start():启动当前线程,调用当前线程的run()方法
run():创建的线程需要重写的方法,将需实现的逻辑声明在此方法中
currentThread():静态方法,返回执行当前代码的线程
getName():获取当前线程的名字
setName():设置当前线程的名字
yield():释放当前CPU的执行权,重新竞争执行权
join():当前线程进入阻塞状态,使调用该方法的线程先执行完毕,再结束当前线程的阻塞
stop():强制结束线程
sleep():使线程休眠,单位为毫秒
isAlive():判断线程是否存活
2.4 线程的调度
-
调度策略
- 时间片
- 抢占式:高优先级的线程抢占CPU
-
Java的调度方法
- 同优先级的线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级的线程使用抢占式策略
-
线程的优先级
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
-
相关方法
getPriority():获取线程优先级setPriority():设置线程优先级
-
说明
- 线程创建时继承父线程的优先级
- 低优先级是获得调度的概率低,并非一定在高优先级线程后被调用
2.5 几种创建方式对比
-
开发中优先选择实现
Runnable接口的方式- 没有类的单继承性的局限性
- 适合处理多个线程有共享数据的情况
Thread类也实现了Runnable接口
- 两种方式都需要覆盖
run()方法
3. 线程的生命周期
JDK中用Thread.State类定义了线程的几种状态:
在一个线程完整的生命周期中通常要经历如下的五种状态:
- 新建:线程被声明或创建
- 就绪:新建的线程被
start()后,进入线程队列等待CPU时间片,此时具备了运行的条件,但是并未分配到CPU资源
- 运行:就绪的线程被调度并获得了CPU资源
- 阻塞:被人为挂起或执行输入输出操作,让出了CPU并临时中止自己的执行
- 死亡:线程完成了它的全部工作或被提前强制性地中止或出现异常导致结束
4. 线程的同步机制
4.1 同步代码块
synchronized ( 同步监视器 ) {
//需要被同步的代码(操作共享数据的代码)
}
- 同步监视器:俗称锁,任何一个类的对象都可以充当锁;多个线程必须共用同一把锁。
- 在实现Runnable接口的方式中,考虑使用
this作为锁 - 在继承Thread的方式中,考虑使用
类名.class作为锁
- 在实现Runnable接口的方式中,考虑使用
4.2 同步方法
- 操作共享数据的代码恰好是一个方法,则将这个方法声明为同步的
-
也需注意多个线程共用同一把锁,只是锁不需要显式声明
- 非静态方法中锁是:this
- 静态方法中锁是:当前类本身
线程安全的单例模式懒汉式:
class singleton{
private singleton(){};
private static singleton instance = null;
public static singleton getInstance(){
//双重检索,防止后续线程仍然进入同步代码块
if(instance == null){
synchronized(singleton.class){
if(instance == null){
try{
Thread.sleep(1000);
}
catch(InterruptedException e){
e.printStackTrace();
}
instance = new singleton();
}
}
}
return instance;
}
}
4.3 Lock锁
- 实例化
ReentrantLock
- 调用锁定方法:
lock()
- 调用解锁方法:
unlock()
public class LockTest implements Runnable () {
ReentrantLock lock = new ReentrantLock ();//实例化ReentrantLock
public void run(){
try{
lock.lock();//调用锁定方法
...
//对共享数据的操作代码
}finally{
lock.unlock(); //调用解锁方法
}
}
}
- 与
synchronized的区别:手动启动同步,手动结束同步 - 需确保多个线程共用同一个
Lock实例 - 使用
try-finally保证unlock()方法能被执行
5. 线程的死锁
- 概念:不同的线程分别占用对方需要的同步锁不放弃,都在等待对方放弃自己需要的同步锁;此时所有的线程都处于阻塞状态,无法继续
-
出现死锁的原因及对应解决:
- 互斥条件:互斥无法被破坏,因为线程需要通过互斥来解决安全问题
- 占用且等待:可以考虑一次性申请所有的资源,这样就不存在等待问题
- 不可抢占:占用部分资源的线程在申请不到其他资源时,就主动释放已经占用的资源
- 循环等待:可以将资源改为线性申请,先申请序号较小的
6.线程间通信
需要多个线程执行同一个任务,并且希望它们有规律地执行,那么两个线程就需要通信,即等待唤醒机制
6.1 等待唤醒机制
这是多线程间的协作机制
在一个线程满足某个条件时,就进入等待状态(wait()/wait(time)),等待其他线程执行完它们指定代码后再将其唤醒(notify());有多个线程进行等待时,可以通过notifyAll()来唤醒所有等待的线程
- wait():方法能使线程进入等待状态,并释放同步监视器
- notify():唤醒被wait()的线程中优先级最高的,如果多个线程优先级相同则随机唤醒一个;被唤醒的线程从当初wait()的位置继续执行
- notifyAll():唤醒所有被wait()的线程
以上三个方法的调用者必须是同步监视器;
以上三个方法声明在Object中;
以上三个方法只能在同步方法或同步代码块中使用,Lock锁需配合Condition实现线程通信
- wait()和sleep()的对比
-
相同: 使用后都能使线程进入阻塞状态
-
区别:
- 声明位置:wait()声明在Object类中;sleep()声明在Thread类中,是静态的
- 使用场景:wait()只能在同步代码块或同步方法中使用;sleep()适用任何场景
- 使用效果:wait()调用后能释放同步监视器;sleep()不能
- 唤醒方式:wait()没有设置时间可以通过notify()唤醒;sleep()只能通过时间参数自动唤醒
-
7.新增的线程的创建方式
7.1 实现Callable接口
//1.创建一个Callable的实现类
class callableThread implements Callable{
//2.实现call()方法,将此线程需要执行的操作声明在call()中
@override
public Object call() throws Exceprion{
...
return ...;
}
}
public class CallableTest{
public static void main(String[] args){
//3.创建Callable接口实现类的对象
callabelThread ct = new callableThread();
//4.创建FutureTask的对象,将Callable接口的实现类的对象作为参数传入FutureTask的构造器中
FutureTask futureTask = new FutureTask(ct);
//5.创建Thread类的对象,将FutureTask的对象作为参数传入Thread的构造器中,调用start()启动线程
Thread thread = new Thread(futureTask);
thread.start();
//6.get()方法返回值即为Callable实现类重写的call()的返回值
Object num = futureTask.get();
}
}
- 与Runnable方式的对比
- 好处
- call()方法有返回值,更灵活,可以使用Callable的泛型来指明返回值类型
- call()可以使用
throw来处理异常
- 缺点:获取call()方法的返回值时,主线程为阻塞状态,需要等待分线程的call()方法执行完毕
- 好处
7.2 线程池
思路
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。
可以避免频繁创建和销毁,实现重复利用。
优点
提高响应速度,减少了创建线程的时间。
提高资源复用率,线程未销毁。
可以使用参数设置线程池的属性。