前言
在学习多线程之前,我们必须了解什么是线程?作用是什么?而线程的知识又与进程有关系,因此我们需要先了解进程再去了解线程,这样才能更好的学习到多线程的知识。本文只是多线程的一部分,多线程涉及的知识点很多很多,锁啊、线程安全啊、CAS等知识,需要耐心学习。
一.Java的多线程
1.1多线程的认识
多线程,从字面上理解,就是从多个单线程一起执行多个任务。在Java 编程中,已经给多线程编程提供了内置的支持。多线程是多任务的一种特别的形式,但多线程使用了更小的cpu资源开销。 多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
线程本身就是操作系统提供的概念,因此操作系统提供了一些API供程序员使用,而在Java中,也存在一些API供人们使用和编译。在Java标准库中Thread类就是用于多线程的创建。
注:创建多线程的方式不仅仅只有Thread类
1.2Java多线程的创建方式
Java语言中,目前可以创建多线程的方式有四种方式:
- 继承Thread类
- 实现Runnable接口
- 使用lambda表达式(基于Runnable接口搭配内部类的优化)
- 使用线程池
- 使用FutureTask类和Callable接口
以上的1、2、5方法可以搭配匿名内部类使用,而目前最为常用的方式有:实现Runnable接口、调用多线程池。
1.3Java多线程的生命周期
新建状态、可执行状态、执行状态以及死亡状态是每一个线程都会发生,而阻塞状态则是选择性发生,当需要阻塞时,使用sleep()、join()方法。
1.4Java多线程的执行机制
当Java程序运行时,先创建出一个进程,该进程里至少包含一个线程,主线程,就是负责执行main方法的线程。然后在mian()方法里创建出其他线程。我们主要学习的就是创建和使用线程。
注:一般情况下,主线程与子线程相互不影响,即子线程结束,主线程不一定结束;主线程结束,子线程不一定结束;主线程异常,子线程不一定异常;子线程异常,主线程不一定异常。但当设置守护线程等特殊操作时,主线程与子线程会发生相互影响。
二.创建多线程的四种方式
2.1继承Thread类
⭐创建线程
使用Thread类创建线程有2种方式,最基本的实现多线程方式,就是创建一个类继承Thread类,然后再实例化该类。可实例化多个线程。
1.继承Thread类创建一个线程
class MyThread extends Thread { @Override public void run() { System.out.println("这里是线程运行的代码"); } } public class Text{ public static void main(String[] args) { //创建MyThread实例 MyThread t1=new MyThread(); //调用start方法启动线程 t1.start(); } }
注:继承Thread类需要重写run()方法,而调用的是start()方法,而不是run()方法,start()是启动线程,而run()则是执行方法。
2.匿名内部类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象 Thread t1 = new Thread() { @Override public void run() { System.out.println("使用匿名类创建 Thread 子类对象"); } }; //启动线程 t1.start();
注:该方法不需要继承其他类,而是直接实例化Thread类和使用匿名内部类。
⭐Thread的构造方法和常见属性
Thread构造方法:
构造方法 | 解释 |
Thread() | 创建线程 |
Thread(Runnable 对象名) | 使用Runnable对象创建线程 |
Thread(String 线程名) | 创建线程对象,并命名 |
Thread(Runnable 对象名,String 线程名) | 使用Runnable对象创建线程对象,并命名 |
常见属性:
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否有后台线程 | isDaemon() |
是否存活 | isAlive() |
是否中断 | isInterrupted() |
ID 是线程的唯一标识,不同线程不会重复
名称是各种调试工具用到
其中重要的是前台和后台线程:
- 前台线程,会影响到进程结束,如果前台进程没有执行完毕,进程不会结束
- 后台线程,也称守护线程,当主线程结束时,进程结束,后台线程无论是否还在执行也结束
Java多线程当中默认为前台线程,也可以通过setDaemon方法设置,false
存活是指线程是否执行结束。中断是指让正在执行的线程强行结束。类似循环的break;
2.2.实现Runnable接口
⭐创建线程
Runnable接口,将需要执行的线程放入其中,再通过Runnable和Thread配合,就可以进行线程的实现。而方法有2种。
第一种:创建Thread类实例,调用构造方法时,将Runnable对象传参
class MyRunnable implements Runnable { @Override public void run() { System.out.println("这里是线程运行的代码"); } } //创建Thread类实例 Thread t = new Thread(new MyRunnable()); //调用start()方法 t.start();
实现一个接口Runnable,然后重写run方法,再将Runnable实例化出的对象,放入Thread的构造函数Thread(Runnable 对象名),就可以实现线程的执行。
第二种:创建Thread类实例,搭配使用匿名内部类创建Runnable子类对象
// 使用匿名类创建 Runnable 子类对象 Thread t2 = new Thread(new Runnable() { @Override public void run() { System.out.println("使用匿名类创建 Runnable 子类对象"); } });
这种方法更为的简便,直接在匿名内部类当中重写run()方法,在run()方法内部写上需要执行的操作。
⭐使用lambda表达式创建
lambda表达式用于匿名内部类和接口的情况,而创建Thread类时,搭配匿名内部类创建了Runnable子类对象,因此可以使用lambda表达式的方法简化操作。
lambda表达式的学习:http://t.csdn.cn/VxRLy
代码示例:
// 使用 lambda 表达式创建 Runnable 子类对象 Thread t4 = new Thread(() -> { System.out.println("使用匿名类创建 Thread 子类对象"); });
如果看不懂lambda表达式,你就记住这是一种创建线程的方法,不去理解。
注:相比前几种创建方式,使用lambda表达式创建更为方便简单。
2.3实现Callable接口创建多线程
⭐线程的创建
Callable 是一个 interface . 使用时需要创建utureTask类的对象,相当于把线程封装了一个 "返回值". 方便程序猿借助多线程的方式计算结果.该方法是基于jdk5.0之后新增的方法,
方法步骤:
- 创建一个实现Callable接口的类。
- 在这个实现类中实现Callable接口的call()方法,并创建这个类的对象。
- 将这个Callable接口实现类的对象作为参数传递到FutureTask类的构造器中,创建FutureTask类的对象。
- 将这个FutureTask类的对象作为参数传递到Thread类的构造器中,创建Thread类的对象,并调用这个对象的start()方法。
- 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结
果。
代码示例:
Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() throws Exception { int sum = 0; for (int i = 1; i <= 1000; i++) { sum += i; } return sum; } }; FutureTask<Integer> futureTask = new FutureTask<>(callable); Thread t = new Thread(futureTask); t.start(); int result = futureTask.get(); System.out.println(result);
⭐Callable接口的特点
- Callable 和 Runnable 相对, 都是描述一个 "任务". Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务.
- Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为
- Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
- FutureTask 就可以负责这个等待结果出来的工作.
2.4通过线程池创建多线程
虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效.
线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到一个 "池子"中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了.
⭐创建线程
1、线程池的使用,需要使用以下类:
- Executors 是一个工厂类, 能够创建出几种不同风格的线程池.
- ExecutorService 表示一个线程池实例.
- ExecutorService 的 submit 方法能够向线程池中提交若干个任务.
2、Executors 创建的几种风格线程池:
实例化一个线程池格式:ExecutorService pool = Executors.方法();
创建线程池种类 | 解释 |
Executors.newFixedThreadPool(int Num threads) | 创建固定线程数的线程池 |
Executors.newSingleThreadExecutor() | 创建只包含单个线程的线程池. |
Executors.newCachedThreadPool() | 创建线程数目动态增长的线程池. |
Executors.newScheduledThreadPool(int corePoolSize) | 设定延迟时间后执行命令,或者定期执行命令. 是 进阶版的 Timer. |
注:Executors 本质上是 ThreadPoolExecutor 类的封装
代码示例:
ExecutorService pool = Executors.newFixedThreadPool(10); pool.submit(new Runnable() { @Override public void run() { System.out.println("hello"); } }); //线程池使用结束之后需要使用shutdown()关闭 pool.shutdown();
三.线程的状态
线程的状态是一个枚举类型 Thread.State,而线程的状态一共有6种,6种状态定义在Thread类的State枚举中。
- 初始状态(NEW):线程对象被创建但尚未调用start()方法。
- 就绪状态(RUNNABLE):线程处于可运行线程池中,等待被线程调度选中获取CPU的使用权。就绪状态分为两种情况,一是线程对象创建后,其他线程调用了该对象的start()方法,此时线程处于某个线程拿到对象锁、当前线程时间片用完调用yield()方法、锁池里的线程拿到对象锁后,这些线程也将进入就绪状态。
- 运行状态(RUNNABLE之RUNNING):线程正在被执行,具体来说就是线程获得了CPU的使用权。
- 阻塞状态(BLOCKED):线程阻塞于锁,即线程在等待获得对象锁。
- 等待状态(WAITING):线程需要等待其他线程做出一些特定动作,比如等待其他线程的通知或中断。
- 超时等待状态(TIMED_WAITING):与等待状态不同的是,超过指定时间后会自动返回。
- 终止状态(TERMINATED):线程的run()方法执行完成,或者主线程的main()方法执行完成,线程终止。终止状态的线程不能再被复生。如果在终止状态的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
注:之前的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着
的
3.1状态的抽象说明
状态图:
- 初始状态(NEW):刚刚拿到银行卡
- 就绪状态(RUNNABLE):排队等待中
- 运行状态(RUNNABLE之RUNNING)):开始与柜台人员进行交流取钱
- 阻塞状态(BLOCKED):阻塞了,相当于有人提前预约了取钱,你只能等待他结束
- 等待状态(WAITING):使用了wait()等待,例如柜台人员有一点事情,让你等到他回来
- 超时等待状态(TIMED_WAITING):使用了sleep(),柜台人员告诉了你等他多久;使用join()方法,就是你和你朋友同时在取钱,取钱到一半你说先停下来,我朋友那边取完了你再取。
多线程的概念、创建、状态的讲解本章已经结束了,而后期还有很多线程操作、线程安全、锁等知识,因此多线程是一个极为复杂并且重要的知识。