一、基础概念
多线程的学习从一些概念开始,进程和线程,并发与并行,同步与异步,高并发。
1.1 进程与线程
几乎所有的操作系统都支持同时运行期多个任务,所有运行中的任务通常就是一个进程,进程是处于运行过程中的程序,进程是操作系统进行资源分配和调度的一个独立单位。
进程有三个如下特征:
独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中部是不具备的。
并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
线程是进程的组成部分,一个进程可以拥有多个线程,而线程必须有一个父进程,线程可以有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源。比如使用QQ时,我们可以同事传文件,发送图片,聊天,这就是多个线程在进行。
线程可以完成一定的任务,线程能够独立运行的,它不知道有其他线程的存在,线程的执行是抢占式的,当前线程随时可能被挂起。
总之:一个程序运行后至少有一个进程,一个进程里可以有多个线程,但至少要有一个线程。
1.2 并发和并行
并发和并行是比较容易混淆的概念,他们都表示两个或者多个任务一起执行,但并发侧重多个任务交替执行,同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。而并行确实真正的同时执行,有多条指令在多个处理器上同时执行,并行的前提条件就是多核CPU。
1.3 同步和异步
同步和异步通常用来形容一次方法调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作。
1.4 高并发
高并发一般是指在短时间内遇到大量操作请求,非常具有代表性的场景是秒杀活动与抢票,高并发是互联网分布式系统架构设计中必须考虑的因素之一,高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。
多线程在这里只是在同/异步角度上解决高并发问题的其中的一个方法手段,是在同一时刻利用计算机闲置资源的一种方式
1.5 多线程的好处
线程在程序中是独立的、并发的执行流,拥有独立的内存单元,多个线程共享父进程里的全部资源,线程共享的环境有进程的代码段,进程的公有数据等,利用这些共享数据,线程很容易实现相互之间的通信,可以提高程序的运行效率。
多线程的好处主要有:
进程之间不能共享内存,但线程之间共享内存非常容易。
系统创建进程时需要给进程重新分配系统资源,但创建线程代价小得多,所以使用多线程实现多任务并发比多进程效率高
Java语言内置了多线程功能支持。
二、使用多线程
上面讲了多线程的一些概念,都有些抽象,下面将学习如何使用多线程,创建多线程的方式有三种。
2.1 继承Thread类创建
继承Thread创建并启动多线程有三个步骤:
定义类并继承Thread,重写run()方法,run()方法中为需要多线程执行的任务。
创建该类的实例,即创建了线程对象。
调用实例的start()方法启动线程。
public class FirstThread extends Thread { private int i=0; public void run() { for (; i < 100; i++) { //获取当前线程名称 System.out.println(this.getName() + " " + i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { //Thread的静态方法currentThread,获取当前线程 System.out.println(Thread.currentThread().getName()); if (i == 20) { //创建线程并启动 new FirstThread().start(); new FirstThread().start(); } } } }
运行结果可以看到两个线程的i并不是连续的,说明他们并不共享数据。
2.2 实现Runnable接口
实现Runnable接口创建并启动多线程也有以下步骤:
定义类并继承Runnable接口,重写run()方法,run()方法中为需要多线程执行的任务。
创建该类的实例,并以此实例作为target为参数来创建Thread对象,这个Thread对象才是真正的多线程对象。
public class SecondThread implements Runnable { private int i = 0; @Override public void run() { for (; i < 100; i++) { //此时想要获取到多线程对象,只能使用Thread.currentThread()方法 System.out.println(Thread.currentThread().getName() + " " + i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { //Thread的静态方法currentThread,获取当前线程 System.out.println(Thread.currentThread().getName()); if (i == 20) { //创建线程并启动 SecondThread secondThread=new SecondThread(); new Thread(secondThread,"线程一").start(); new Thread(secondThread,"线程二").start(); } } } }
2.3 使用Callable和Future
Callable是Runnable的增加版,主要是接口中的call()方法可以有返回值,并且可以申明抛出异常,使用Callable创建的步骤如下:
定义类并继承Callable接口,重写call()方法,run()方法中为需要多线程执行的任务。
创建类实例,使用FutureTask来包装对象实例,
使用FutureTask对象作为Thread的target来创建多线程,并启动线程。
调用FutureTask对象的get()方法来获取子线程结束后的返回值。
public class ThirdThread { public static void main(String[] args) { //使用lambda表达式 FutureTask<Integer> task = new FutureTask<>((Callable<Integer>) () -> { int i = 0; for (; i < 100; i++) { System.out.println(Thread.currentThread().getName() + "的循环变量i的值:" + i); } return i; }); for (int i = 0; i < 100; i++) { //Thread的静态方法currentThread,获取当前线程 System.out.println(Thread.currentThread().getName()); if (i == 20) { //创建线程并启动 new Thread(task, "有返回值的线程").start(); } } try { System.out.println("线程的返回值:" + task.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
这里使用了lambda表达式,不使用表达式的方式也很简单,可以去源码中查看。Callable与Runnable方式基本相同,只不过增加了返回值且可允许声明抛出异常。
使用三种方式都可以创建线程,且方式也相对简单,大体分为实现接口和实现Thread类两种,这两种都各有优缺点。
继承接口实现:
- 优点:除了继承接口之外,还可以继承其他类。这种方式多个线程共享一个target对象,可以处理用于共同资源的情况。
- 缺点:编程稍微复杂一些,并且没有直接获取当前线程对象的方式,必须使用Thread.currentThread()方式。
基础Thread类:
优点:编程简单
缺点:不能继承其他类
三、多线程的生命周期
线程状态是线程中非常重要的一个概念,然而我看过很多资料,线程的状态理解有很多种方式,很多人将其分为五个基本状态:新建、就绪、运行、阻塞、死亡,但在状态枚举中并不是这五个状态,我不知道是什么原因(有大神可以解答更好),只能按照枚举中的状态根据自己的理解。
初始(NEW):新创建了一个线程对象,但还没有调用start()方法,而且就算调用了改方法也不代表状态立即改变。
运行(RUNNABLE):在运行的状态肯定就处于RUNNABLE状态。
阻塞(BLOCKED):表示线程阻塞,或者说线程已经被挂起了。
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
终止(TERMINATED):表示该线程已经执行完毕。
状态流程图如下:
理解:初始状态很好理解,这个时候其实还不能被称为一个线程,因为他还没被启动,当调用start()方法后,线程正式启动,但是也不代表立即就改变了状态。
运行状态中其实包含两种状态,运行中(RUNING)和就绪(READY)。
就绪状态表示你有资格运行,只要CPU还未调度到你,就处于就绪状态,有几个状态会是线程状态编程就绪状态
调用线程的start()方法。
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁。
当前线程时间片用完了,调用当前线程的yield()方法。
锁池里的线程拿到对象锁后。
运行中(RUNING)状态比较好理解,线程调度程序选择了当前线程作。
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
等待状态是指线程没有被CPU分配执行时间,需要等待,这种等待是需要被显示的唤醒,否则会无限等待下去。
超时等待状态是这现在没有被CPU分配执行时间,需要等待,不过这种等待不需要被显示的唤醒,会设置一定的时间后zi懂唤醒。
死亡状态也很好理解,说明线程方法被执行完成,或者出错了,线程一旦进入这个状态就代表彻底的结束