1. 线程概述
1. 进程
- 进程是一个正在运行的程序
- 进程是系统进行资源分配和调用的独立单位
- 每一个进程都有它自己的内存空间和系统资源
2. 线程
- 线程是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序。
- 多线程:一个进程如果有多条执行路径,则称为多线程程序。
3. 线程与进程的关系
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见。
2. 线程的创建
在 Java 中,可以通过三种方式实现多线程
- 继承 Thread 类,重写
run()
方法 - 实现 Runnable 接口,重写
run()
方法 - 实现 Callable 接口,重写
call()
方法,并使用 Future 来获取 call() 方法的返回结果
1. Thread 类实现多线程
1. Thread 类介绍
- Thread 类是
java.lang
包下的一个线程类,用来实现 Java 多线程 - 局限性:
Java 支持类的单继承,如果某个类已经继承了其他父类,就无法再继承 Thread 类来实现多线
程。
2. 实现过程
-
创建一个 Thread 线程类的子类(子线程),同时重写 Thread 类的
run()
方法- 创建Thread 线程类的子类
class MyThread1 extends Thread {}
- 重写
run()
方法
public void run() {}
-
创建该子类的实例对象,并通过调用
start()
方法启动多线程- 创建该子类的实例对象
MyThread1 thread1 = new MyThread1("thread1");
- 调用
start()
方法启动多线程
thread1.start();
2. Runnable 接口实现多线程
- 特点:共享信息
1. 实现过程
-
创建一个 Runnable 接口的实现类,同时重写接口中的
run()
方法- 定义 Runnable 接口实现类
class MyThread2 implements Runnable {}
- 重写 Runnable 接口的
run()
方法
public void run(){ }
-
创建 Runnable 接口的实现类对象
- 创建 Runnable 接口实现类的实例对象
MyThread2 myThread2 = new MyThread2();
-
使用 Thread 有参构造方法创建线程实例,并将 Runnable 接口的实现类的实例对象作为参数传入
- 使用
Thread (Runnable target,String name)
构造方法创建线程对象
Thread thread1 = new Thread(myThread2,"thread1");
- 使用
-
调用线程实例的
start()
方法启动线程- 调用线程的
start()
方法启动线程
thread1.start();
- 调用线程的
3. Callable 接口实现多线程
- 特点:既能创建多线程又可以有返回值
1. 实现过程
-
创建一个 Callable 接口的实现类,同时重写 Callable 接口的
call()
方法- 定义一个实现 Callable 接口的实现类
class MyThread3 implements Callable<Object>{ }
- 重写 Callable 接口的
call()
方法
public Object call() throws Exception {}
-
创建 Callable 接口的实现类对象
- 创建 Callable 接口的实现类对象
MyThread3 myThread3 = new MyThread3();
-
通过 FutureTask 线程结果处理类的有参构造方法来封装 Callable 接口实现类
- 使用 FutureTask 封装 Callable 接口
FutureTask<Object> ft1 = new FutureTask<> (myThread3);
-
使用参数为 FutureTask 类对象的 Thread 有参构造方法创建 Thread 线程实例
- 使用
Thread(Runnable target ,String name)
构造方法创建线程对象
Thread thread1 = new Thread(ft1,"thread1");
- 使用
-
调用线程实例的
start()
方法启动线程- 调用线程对象的
start()
方法启动线程
thread1.start();
- 调用线程对象的
-
可以通过 FutureTask 对象的方法管理返回值
- 通过 FutureTask 对象的方法管理返回值
System.out.println("thread1 返回结果:" + ft1.get());
2. FutureTask 类
1. FutureTask类介绍
- Callable 接口防止实现的多线程是通过 FutureTask 类来封装和管理返回结果的,该类的直接父接口是 RunnableFuture
- 从名称上可以看出 RunnableFuture 是由 Runnable 和 Future 组成的结合体
- FutureTask 本质是 Runnable 接口和 Future 接口的实现类,而 Future则是 JDK5 提供的用来管理线程执行返回结果的。
2. FutureTask 类的继承关系图
3. Future 接口方法
方法名 | 说明 |
---|---|
boolean cancel(boolean mayInterruptIFRunning) |
用于取消任务参数 ,mayInterruptIfRunning 表示是否允许取消正在执行却没有执行完毕的任务,如果设置 true,则表示可以取消正在执行的任务。 |
boolean isCancelled() |
判断任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true |
boolean isDone() |
判断任务是否已经完成,若任务完成,则返回 true |
V get() |
用于获取执行结果,这个方法会发生阻塞,一直等到任务执行完毕才返回执行结果。 |
V get(long timeout, TimeUnit unit) |
用于在指定时间内获取执行结果,如果在指定时间内,还没获取到结果,就直接返回 null。 |
4. 三种实现多线程方式的对比分析
在创建多线程时如果没有通过构造方法指定线程名称,则系统默认生成线程名称
- 实现 Runnable 接口(或者 Callable 接口)相对于继承 Thread 类实现多线程的好处:
- 可以避免 Java 单继承带来的局限性。
由于一个类不能同时有两个父类,所以在当前类已经有一个父类的基础上,那么就只能采用实现 Runnable 接口或者 Callable 接口的方式来实现多线程。
5. 后台线程
1. 前台线程和后台线程
前台线程和后台线程是一种相对的概念,新创建的线程默认都是前台线程,如果某个对象在启动之前调用了setDaemon(true)
语句,这个线程就变成一个后台线程。
2. 相关代码:
(1)判断是否为后台线程
System.out.println("main 线程是后台线程吗?" + Thread.currentThread().isDaemon());
(2)将线程 thread 线程对象设置为后台线程
thread.setDameon(true);
3. 注意事项
- 要将某个线程设置为后台线程,必须在该线程启动之前;也就是说
setDaemon()
方法必须在start()
方法之前调用,否则后台线程设置无效。 - 适合多个线程去处理同一个共享资源的情况。把线程同程序代码、数据有效的分离,很好地体现了面向对象的设计思想。
3. 线程的生命周期及转换状态
1. NEW(新建状态)
- 创建一个线程对象后,该线程对象就处于新建状态
- 此时它不能运行,和其他 Java 对象一样,仅仅由 JVM为其分配了内存,没有表现出任何线程
的动态特征
2. RUNNABLE(可运行状态)
- 新建状态下的线程对象调用了
start()
方法,此时就会从新建状态进入可运行状态
1. READY(就绪状态)
- 线程对象调用
start()
方法之后,等待 JVM 的调度,此时线程并没有运行。
2. RUNNING(运行状态)
- 线程对象获得 JVM 调度,如果存在多个 CPU,那么允许多个线程并行使用。
3. BLOCKED(阻塞状态)
- 处于运行状态的线程可能会因为某些原因失去CPU的执行权,暂时停止运行进入阻塞状态。
- 此时,JVM 不会给线程分配 CPU,知道线程重新进入就绪状态,才有机会转换到运行状态。
- 阻塞状态的线程只能先进入就绪状态,不能直接进入运行状态。
- 线程进入阻塞状态的两种情况:
(1)当线程 A 运行过程中,试图获取同步锁时,却被线程 B 获取,此时 JVM 把当前线程 A 存到
对象的线程池中,线程 A 就会进入阻塞状态;
(2)当线程运行过程中,发出 I/O 请求时,此时该线程也会进入阻塞状态。
4. WAITING(等待状态)
- 当处于运行状态的线程调用了无时间参数限制的方法后,如
wait()
、join()
等方法,就
会将当前运行中的线程转换为等待状态。 - 处于等待状态中的线程不能立即争夺 CPU 使用权,必须等待其他线程执行特定的操作后,才
有机会再次争夺 CPU 使用权,将等待状态的线程转化为运行状态。
— 例如,调用wait()
方法而处于等待状态中的线程,必须等待其他线程调用notify()
或者
notifyAll()
方法唤醒当前等待中的线程;调用join()
方法而处于等待状态中的线程,必须等待其他
加入的线程终止。
5. TIMED_WAITING(定时等待状态)
- 将运行中的线程转换为定时等待状态中的线程与转换为等待状态中的线程操作类似,只是运行
线程调用了有时间参数限制的方法,如sleep(long millis)
、wait(long timeout)
、
join(long millis)
等方法。 - 处于定时等待状态中的线程也不能立即争夺 CPU 使用权,必须等待其他相关线程执行完特定
的操作或者有限时间结束后,才有机会再次争夺 CPU 使用权,将定时等待状态的线程转换为运
行状态。
— 例如,调用了wait(long timeout)
方法而处于等待状态中的线程,需要通过其他线程
调用notify()
或者notifyAll()
方法唤醒当前等待中的线程,或者等待限时时间结束后
也可以进行状态转换。
6. TERMINATED(终止状态)
- 线程中的
run()
方法、call()
方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、
错误(Error),线程就进入终止状态。 - 一旦进入终止状态,线程将不再拥有运行的资格,也不能再转换到其他状态,生命周期结束。
4. 线程的调度
1. 线程的调度的介绍
- Java 虚拟机按照特定的机制为程序中的每个线程分配 CPU 的使用权,这种机制被称作线程的调度。
- 线程调度的两种模型:分时调度模型,抢占式调度模型。
(1)分时调度模型:指让所有的线程轮流获得 CPU 的使用权,并且平均分配每个线程占用的 CPU 时
间片。
(2)抢占式调度模型:指让可运行池中所有就绪状态的线程争抢 CPU 的使用权,而优先级高的线程获取 CPU 的执行权的概率大于优先级低的线程。 Java 虚拟机默认采用抢占式调度模型。
2. 线程的优先级
- 对线程进行调度,最直接的方式就是设置线程的优先级。
- 优先级越高的线程获得 CPU 执行的机会越大,而优先级越低的线程获得 CPU 执行的机会越小。
- 线程的优先级用 1~10之间的整数来表示,数字越大优先级越高。
1. Thread类的优先级常量
常量名 | 说明 |
---|---|
static int MAX_PRIORITY |
表示线程的最高优先级,相当于 10 |
static int MIN_PRIORITY |
表示线程的最低优先级,相当于 1 |
static int NORM_PRIORITY |
表示线程的普通优先级,相当于值 5 |
2. 优先级设置示例
-
数字
thread1.setPriority(10);
-
优先级常量
thread2.setPriority(Thread.MIN_PRIORITY);
3. 线程休眠
- 如果要想人为地控制线程执行顺序,使正在执行的线程暂停,将 CPU 使用权让给其他线程,可以使用静态方法
sleep(long millis)
。 sleep()
方法可以让正在执行的线程暂停一段时间,进入休眠等待状态,这样其他的线程就可以
得到执行的机会。sleep(long millis)
方法会声明抛出 InterruptedException 异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。- 使用示例:
Thread.sleep(500);
- 线程类 Thread 提供了两种线程休眠方法:
sleep(long millis)
和sleep(long millis,int nanos)
,这两种方法都带有休眠时间参数,当其他线程都终止后并不代表当前休眠的线程会立即执行,而是必须当休眠时间结束后,线程才会转换到就绪状态。
4. 线程让步
- 线程让步可以通过
yield()
方法来实现; yield()
该方法和sleep(long millis)
方法有点类似,都可以让当前正在运行的线程暂停,区别在于yield()
方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。- 当某个线程调用
yield()
方法之后,与当前线程优先级相同或者更高的线程可以获得执行的机会。 - 使用示例:
Thread.yield()
5. 线程插队
- 当某个线程中调用其他线程的
join()
方法时,调用的线程将被阻塞,直到被join()
方法加入的线程执行完成后它才会继续运行 - 关键代码:
(1)public static void main(String [] args) throws InterruptedException {}
(2)thread1.join()
5. 多线程同步
线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。
1. 线程安全
- 线程安全问题其实就是多个线程同时处理共享资源所导致的。
- 要想解决线程安全问题,必须要保证处理共享资源的代码在任意时刻只能有一个线程访问。
2. 同步代码块
-
为了解决线程安全问题,Java 提供了线程同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个使用
synchronized
关键字来修饰的代码块,这段代码块被称作同步代码块。 -
同步代码块格式:
synchronized(lock){ // 操作共享资源代码块 }
-
同步代码块中的锁对象可以是任意类型的变量,但多个线程共享的锁对象必须是相同的。
🌟“任意”说的是共享锁对象的类型。所以,锁对象的创建代码不能放到run()
方法中,否则每个线程运行到run()
方法都会创建一个新对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,线程之间便不能产生同步的效果。
3. 同步方法
- 当把共享资源的操作放在
synchronized
定义的区域内时,便为这些操作加了同步锁。 - 在方法前面使用
synchronized
关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。
🌟🌟具体语法格式:[修饰符] synchronized 返回值类型 方法名 ([参数1, ···]){ } ;
🌟🌟示例代码:private synchronized void saleTicket () { }
- 被
synchronized
修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。
4. 同步锁
synchronized
同步代码块和同步方法使用一种封闭式的锁机制,使用起来非常简单,也能够解决线程同步过程中出现的线程安全问题;但也有一些限制,例如它无法中断一个正在等候获得锁的线程,也无法通过轮询得到锁,如果不想等下去,也就没法得到锁。- 从JDK5 开始,Java 增加了一个功能更强大的 Lock 锁。Lock 锁与 synchronized 隐式锁在功能上基本相同,其最大的优势在于 Lock 锁可以让某个线程在持续获取同步锁失败后返回,不需继续等待,另外 Lock 锁在使用时也更加灵活。
- Lock 锁使用:
(1)定义一个 Lock 锁 :private final Lock lock = new ReentrantLock() ;
(2)对代码块进行加锁:lock.lock() ;
(3)执行玩代码块后释放锁:lock.unlock() ;
- ReentrantLock 类是 Lock 锁接口的实现类,也是常用的同步锁;在该同步锁中除了
lock()
方法和unlock()
方法外,还提供了一些其他同步锁操作的方法,例如tryLock()
方法可以判断某个线程锁是否可用。 - 另外,在使用 Lock 锁时,可以根据需要在不同代码位置灵活地上锁和解锁,为了保证所有情况下都能正常以确保其他线程可以执行,通常情况下会在
finally {}
代码块中调用unlock()
方法来解锁
5. 死锁问题
两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。
6. 多线程同步
1. 问题引出
在多线程的程序中,上下工序可以看作两个线程,这两个线程之间需要协同完成工作,就需要线程之间进行通信。
2. 问题如何解决
- 想要解决上述问题,就需要控制多个线程按照一定的顺序轮流执行,此时就需要让线程间进行通信,保证线程任务的协调进行。
- Java 在 Object 类中提供了
wait()
、notify()
、notifyAll()
等方法用于解决线程间的通信问题,由于 Java 中所有类都是 Object 类的子类或间接子类,因此任何类的实例对象都可以直接使用这些方法。
(1)void wait()
:使当前线程放弃同步锁并进入等待,直到其他线程进入此同步锁,并调用notify()
或notifyAll()
方法唤醒该线程为止。
(2)void notify()
:唤醒此同步锁上等待的第一个调用wait()
方法的线程。
(3)void notifyAll()
:唤醒此同步锁上调用wait()
方法的所有线程。 wait()
、notify()
、notifyAll()
这三个方法的调用者都应该是同步锁对象,如果这三个方法的调用者不是同步锁对象,Java 虚拟机就会抛出 IllegalMonitorStateException 异常。- Java 为线程等待方法
wait()
提供了多个重载方法,包括无参wait()
方法、有等待时间的wait(long timeout)
方法和wait(long timeout, int nanos)
方法。其中,带有等待时间参数的wait()
方法,除了会在其他线程对象调用notify()
和notifyAll()
方法来唤醒当前处于等待状态的线程,还会在等待时间过后自动唤醒处于等待状态的线程。
7. 线程池
1. Executor 接口实现线程池管理
1. Executor 接口实现线程池管理的主要步骤
-
创建一个实现 Runnable 接口或者 Callable 接口的实现类,同时重写
run()
或者call()
方法;- 定义一个实现 Callable 接口的实现类
class MyThread4 implements Callable<Object> {}
- 重写 Callable 接口的
call()
方法
public Object call() throws Exception {}
-
创建 Runnable 接口或者 Callable 接口的实现类对象;
- 创建 Callable 接口的实现类对象
MyThread4 myThread4 = new MyThread4();
-
使用 Executors 线程执行器类创建线程池;
- 使用 Executors 线程执行器类创建可扩展的线程池
ExecutorService executor = Executors.newCachedThreadPool();
-
使用 ExecutorService 执行器服务类的
submit()
方法将 Runnable 接口或者 Callable 接口的实现类对象提交到线程池进行管理;- 将 Callable 接口实现类对象提交到线程池进行管理
Future<Object> result1 = executor.submit(myThread4);
-
线程任务执行完成后,可以使用
shutdown()
方法关闭线程池。executor.shutdown();
2. Executors 创建线程池的方法
方法名 | 说明 |
---|---|
ExecutorService newCachedThreadPool() |
创建一个可扩展线程池的执行器。这个线程池执行器适用于启动许多短期任务的应用程序。 |
ExecutorService newFixedThreadPool(int nThreads) |
创建一个固定线程数量线程池的执行器。这种线程池执行器可以很好地控制多线程任务,也不会导致由于相应过多导致的程序崩溃。 |
ExecutorService newSingleThreadExecutor() |
在特殊需求下创建一个只执行一个任务的单个线程。 |
ScheduledExecutorService newScheduledThreadPool(int corePoolSize) |
创建一个定长线程池,支持定时及周期性任务执行。 |
2. CompletableFuture 类实现线程池管理
1. CompletableFuture 概述
- 在使用
Callable
接口实现多线程时,会用到FutureTask
类对线程执行结果进行管理和获取,由于该类在获取结果时是通过阻塞或者轮询的方式,违背多线程编程的初衷且耗费过多资源。 - JDK8 中对
FutureTask
存在的不足进行了改进,增加了一个强大的函数式异步编程辅助类CompletableFuture
,该类同时实现了Future 接口
和CompletionStage 接口
,并对 Furure 进行了强大的扩展,简化异步编程的复杂性。
2. CompletableFuture 对象创建的4个静态方法
- CompletableFuture 对象的静态方法中,
runAsync()
和supplyAsync()
方法的本质区别就是获取的 CompletableFuture 对象是否带有计算结果。 - 另外,带有 Executor 参数的方法用于传入指定的线程池执行器来进行多线程管理,而未带有 Executor 参数的方法会使用默认的
ForkJoinPool.commonPool()
作为它的线程池进行多线程管理
方法名 | 说明 |
---|---|
static CompletableFuture |
以 Runnable 函数式接口类型为参数,并使用 ForkJoinPool.commomPool() 作为它的线程池执行异步代码获取 CompletableFuture 计算结果为空的对象。 |
static CompletableFuture |
以 Runnable 函数式接口类型为参数,并传入指定的线程池执行器 executor 来获取CompletableFuture 计算结果为空的对象。 |
static CompletableFuture supplyAsync(Suppliersupplier) |
以 Supplier 函数式接口类型为参数,并使用 ForkJoinPool.commonPool() 作为它的线程池执行异步代码获取 CompletableFuture 计算结果非空的对象 |
static CompletableFuture supplyAsync (Suppliersupplier, Executor executor) |
以 Supplier 函数式接口类型为参数,并传入指定的线程池执行器 executor 来获取CompletableFuture 计算结果非空的对象。 |
暂无评论内容