目录
- 一、 多线程基础
- 1.1 进程和线程的区别
- 1.2 并发与并行的区别
- 二、多线程的创建和使用
- 2.1 线程的创建方式
- 2.1.1 继承Thread类创建线程
- 2.1.2 实现Runnable接口创建线程
- 2.1.3 使用Calable和Future创建线程
- 2.1.4 使用线程池创建线程
- 线程池种类:
- 三、线程的生命周期
- 1. 生命周期状态定义
- 2. 线程状态详解和转换
- 四、线程的同步
- 1. 背景
- 2. 线程同步的方法
- 2.1 Synchronized同步代码块
- 2.2 Synchronized同步方法
- 2.3 Lock锁
- 3.synchronized 与Lock 的对比
- 五、线程的通信
- 1. 线程通信方法
- 2. 案例
- 六、线程池
- 线程池种类:
- 七、面试题
- 1.java中有几种方法可以实现一个线程(jdk5.0之前)?用什么关键字修饰同步方法? stop()和suspend()方法为何不推荐使用?
- 2. suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,
- 3. sleep() 和 wait() 有什么区别?
- 4. 同步和异步有何异同,在什么情况下分别使用他们?举例说明。
- 5. 启动一个线程是用run()还是start()?
- 6. 当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?
- 7. 请说出你所知道的线程同步的方法。
- 8. 多线程有几种实现方法,都是什么?同步有几种实现方法,都是什么?
- 9. 线程的基本概念、线程的基本状态以及状态之间的关系
- 10. 简述synchronized和java.util.concurrent.locks.Lock的异同 ?
- 八、应用案例
一、 多线程基础
1.1 进程和线程的区别
进程:是指一个程序的执行过程,简而言之,每个应用就是一个进程
线程:进程的细化,进程内可以开启多个线程,一个进程统一时间开启多个线程并行执行,则是多线程执行
1.2 并发与并行的区别
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
二、多线程的创建和使用
2.1 线程的创建方式
2.1.1 继承Thread类创建线程
创建步骤:
- 定义类并继承Thread,重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
- 创建Thread子类的实例,也就是创建了线程对象
- 启动线程,即调用线程的start()方法
代码:
/**
* @title: ExtendThread
* @Author Liuyang Tian
* @Date: 2023/3/2 15:10
* @Version 1.0
*/
public class ExtendThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println("主线程:" + Thread.currentThread().getName());
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("创建子线程:" + currentThread().getName());
}
}
2.1.2 实现Runnable接口创建线程
创建步骤:
- 定义子类,实现Runnable接口
- 子类中重写Runnable接口中的run方法
- 通过Thread类含参构造器创建线程对象
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法
代码:
/**
* @title: ImpRunnable
* @Author Liuyang Tian
* @Date: 2023/3/2 15:20
* @Version 1.0
*/
public class ImpRunnable {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
Thread thread = new Thread(myThread1);
thread.start();
System.out.println("主线程:" + Thread.currentThread().getName());
}
}
class MyThread1 implements Runnable{
@Override
public void run() {
System.out.println("创建子线程:" + Thread.currentThread().getName());
}
}
2.1.3 使用Calable和Future创建线程
创建步骤:
- 创建Callable接口的实现类,并实现call()方法,然后创建该类的实例
- 使用Future Task类来包装Callable对象。该FutureTask对象封装了Callable对象的call()方法的返回值
- 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口并在重写的run方法中执行call方法)
- 调用FutureTask对象的get方法来获取线程执行结束后的返回值
代码:
/**
* @title: ImpCallable
* @Author Liuyang Tian
* @Date: 2023/3/2 15:34
* @Version 1.0
*/
public class ImpCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Mythread2 mythread2 = new Mythread2();
FutureTask<String> task = new FutureTask<>(mythread2);
new Thread(task).start();
System.out.println(task.get());
System.out.println("主线程:" + Thread.currentThread().getName());
}
}
class Mythread2 implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("创建子线程:" + Thread.currentThread().getName());
return "我是子线程返回值";
}
}
2.1.4 使用线程池创建线程
线程池种类:
线程池这里只简单列举下,详细知识点请往下看第六部分线程池部分;
- FixedThreadPool:定长线程池
- CachedThreadPool:可缓冲线程池
- ScheduledThreadPool:调度线程池
- SingleThreadExecutor:单线程化线程池
创建步骤:
- 创建定长线程池
- 创建好Runnable类线程对象并重新run方法
- 向线程池提交任务
代码
/**
* @title: UseThreadPool
* @Author Liuyang Tian
* @Date: 2023/3/2 15:49
* @Version 1.0
*/
public class UseThreadPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
Runnable task =new Runnable(){
public void run() {
System.out.println("子线程:"+ Thread.currentThread().getName());
}
};
// 使用线程池创建5个子线程
for (int i = 0; i < 5; i++) {
executorService.execute(task);
}
System.out.println("主线程:" + Thread.currentThread().getName());
}
}
三、线程的生命周期
1. 生命周期状态定义
Thread定义了线程的几种状态,如下图所示:
2. 线程状态详解和转换
线程状态的解释:
- NEW:新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- RUNNABLE:运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
- BLOCKED:阻塞:线程在获取synchronized排他锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
- WAITING:等待:处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态。
- TIMED_WAITING:超时等待:处于这种状态的线程也不会被分配CPU执行时间,不过无须等待其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态
- TERMINATED:死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程生命周期图:
线程状态转换图:
四、线程的同步
1. 背景
当多个线程操作共享变量时会发生线程不安全的问题,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
示例卖票场景:
/**
* @title: MyThread3
* @Author Liuyang Tian
* @Date: 2023/3/2 16:21
* @Version 1.0
*/
public class MyThread3 {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable{
private int ticketNum = 10;
@SneakyThrows
@Override
public void run() {
while (true){
if (ticketNum > 0){
//模拟出票
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() +" 售出车票,ticket号为:"+ticketNum--);
}else {
break;
}
}
}
}
2. 线程同步的方法
2.1 Synchronized同步代码块
2.2 Synchronized同步方法
2.3 Lock锁
3.synchronized 与Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
五、线程的通信
1. 线程通信方法
- wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
- notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
- notifyAll():唤醒正在排队等待资源的所有线程结束等待.
2. 案例
使用两个线程打印1-100。线程1, 线程2 交替打印
/**
* @title: MyThread3
* @Author Liuyang Tian
* @Date: 2023/3/2 16:21
* @Version 1.0
*/
public class MyThread3 {
public static void main(String[] args) {
Ticket t = new Ticket();
for (int i = 0; i < 2; i++) {
Thread t1 = new Thread(t);
t1.start();
}
// Thread t2 = new Thread(t);
// t2.start();
}
}
//使用两个线程打印1-100。线程1, 线程2 交替打印
class Ticket implements Runnable {
private final ReentrantLock lock = new ReentrantLock();
private int num = 1;
@SneakyThrows
@Override
public void run() {
while (true){
synchronized (this){
notify();
if (num <= 100){
System.out.println(Thread.currentThread().getName() + " " + (num++));
}else {
break;
}
try {
wait();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}
六、线程池
线程池种类:
- FixedThreadPool:定长线程池
- CachedThreadPool:可缓冲线程池
- ScheduledThreadPool:调度线程池
- SingleThreadExecutor:单线程化线程池
下次补充
七、面试题
1.java中有几种方法可以实现一个线程(jdk5.0之前)?用什么关键字修饰同步方法? stop()和suspend()方法为何不推荐使用?
答:有两种实现方法,分别是继承Thread类与实现Runnable接口。
用synchronized关键字修饰同步方法,反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。
2. suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,
指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。
3. sleep() 和 wait() 有什么区别?
答:sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
4. 同步和异步有何异同,在什么情况下分别使用他们?举例说明。
答:如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
5. 启动一个线程是用run()还是start()?
答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。run()方法就是正常的对象调用方法的执行,并不是使用分线程来执行的。
6. 当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?
答:不能,一个对象的一个synchronized方法只能由一个线程访问。
7. 请说出你所知道的线程同步的方法。
答:wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
8. 多线程有几种实现方法,都是什么?同步有几种实现方法,都是什么?
答:多线程有两种(具体可以说4种)实现方法,分别是继承Thread类与实现Runnable接口
同步的实现方面有两种,分别是synchronized,wait与notify
9. 线程的基本概念、线程的基本状态以及状态之间的关系
答:线程指在程序执行过程中,能够执行程序代码的一个执行单位,每个程序至少都有一个线程,也就是程序本身。
Java中的线程有四种状态分别是:创建、就绪、运行、阻塞、结束
10. 简述synchronized和java.util.concurrent.locks.Lock的异同 ?
答:主要相同点:Lock能完成synchronized所实现的所有功能
主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。
八、应用案例
下次补充