一、多线程最基础的基本概念
一个程序最少需要一个进程,而一个进程最少需要一个线程。
我们常说的高并发,也并不全是线程级别的并发,在很多开发语言中,比如PHP,很常见的都是进程级别的并发。但是在Java中谈论到的基本都是线程级别的并发。当然了,高并发的来源,与摩尔定律的概念相当,等单个人无法满足任务的需求时,找多个人一起干活。就成为了主流。
这里介绍几个多线程的概念:
1.1、进程和线程:
程序:开发写的代码称之为程序。程序是一组代码,是一组数据和指令集,当然了程序是一个静态的概念。
进程:将程序运行起来,称之为进程。进程存在生命周期,随着程序的终止而销毁。进程之间通过TCP/IP端口实现交互。
线程:线程是CPU调度和执行的最小单位。PS:很多多线程都是模拟出来的,真正的多线程是指多个CPU,也就是多核,如服务器。但是如果CPU只有一个,在同一个时间点内只会执行一个代码,但是由于计算机执行速度极快,导致产生同时执行的错觉。
1.2、并发、并行、串行
并行:多个任务同时进行,但是必须具备多核能力,才能实现。
并发:同一个对象被多个线程同时操作,这一种是假并发,一个CPU可以完成工作。
串行:一个程序处理完当前进程,按照顺序处理下一个进行,一个接着一个进行。
1.3、同步和异步:
同步:当进行方法调用时,必须等到方法调用返回后,才能够执行后续行为。例子:打电话、单线程。
异步:很像是消息传递,一旦开始,调用方法就会立刻返回,调用可以继续后续行为。例子:发短信、多线程。
1.4、临界区、死锁、活锁、饥饿:
临界区:一次被一个线程占用,其他线程必须排队,例子:办公室的打印机。注意:多线程情况下,共享资源必须上锁。
死锁:在唯一性的条件下,完成任务需要A、B,但是线程1拿着A,线程2拿着B,导致双方一直在等待,形成死循环。
活锁:经典例子:孔融让梨,虽然线程们都谦让资源,但是大家都没有办法使用资源,导致任务永远完不成。这种情况下,会耗费CPU的时间。
饥饿:当线程的优先级特别低的情况下,完成线程所需要的资源永远无法获取,会造成饥饿现象。
二、线程的创建以及启动
-
继承Thread类,并重写run()方法。
-
实现Runnable接口,并重写run()方法。
-
通过Callable和Future创建线程
2.1.1、继承Thread类
1、自定义线程类继承Thread类 2、重写run方法,编写线程执行体 3、创建线程对象,调用start()方法 注意:创建不代表执行,需要抢CPU资源
2.1.2、代码
public class A extends Thread { public A(String name){ super(name); } @Override public void run(){ // 线程执行体 for (int i = 0; i < 10; i++) { System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i); } } public static void main(String[] args){ // 创建线程对象,准备抢占资源 A a1 = new A("线程1"); A a2 = new A("线程2"); a1.start(); a2.start(); for (int i = 0; i < 10; i++) { System.out.println("我是主线程--" + i); } } }
2.2.1 实现Runnable接口
1、自定义线程类实现Runnable接口 2、重写run方法,编写线程执行体 3、创建线程对象,调用start()方法 注意:创建不代表执行,需要抢CPU资源
2.2.2 代码
public class A implements Runnable { @Override public void run(){ // 线程执行体 for (int i = 0; i < 10; i++) { System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i); } } public static void main(String[] args){ // 创建线程对象,准备抢占资源 // 创建实现类对象 A myRunnable = new A(); Thread t1 = new Thread(myRunnable,"线程1"); Thread t2 = new Thread(myRunnable,"线程2"); t1.start(); t2.start(); for (int i = 0; i < 10; i++) { System.out.println("我是主线程--" + i); } } }
2.3.1 实现Callable接口(不常用)
1、实现Callable接口,先要返回值类型 2、重写call()方法,需要抛出异常 3、创建目标对象 4、创建执行服务:ExecutorService ser = Executor.newFixedThreadPool(1); 5、提交执行:Future<Boolean> res = ser.submit(t1); 6、获取结果:boolean r1 = res.get(); 7、关闭服务:ser.shutdownNow();
2.3.2 代码
import java.util.concurrent.*; // 自定义线程对象,实现Callable接口,重写call()方法 public class A implements Callable<Boolean> { @Override public Boolean call() throws Exception { // 线程执行体 for (int i = 0; i < 10; i++) { System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i); } return true; } public static void main(String[] args) throws ExecutionException, InterruptedException { // main线程,主线程 // 创建线程实现类对象 A thread = new A(); A thread2 = new A(); // 创建执行服务,参数是线程池线程数量 ExecutorService ser = Executors.newFixedThreadPool(2); // 提交执行 Future<Boolean> res = ser.submit(thread); Future<Boolean> res2 = ser.submit(thread2); // 获取结果 boolean r1 = res.get(); boolean r2 = res2.get(); // 关闭服务 ser.shutdownNow(); } }
三、线程的生命周期
新建(New)状态: 创建后尚未启动(未调用start()方法)的线程处于这种状态。
就绪(Ready)状态: 当调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行。
运行(Running)状态: 当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
限期等待(Timed Waiting)状态: 处于这种状态的线程也不会被分配CPU执行时间,不过无须等待其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
新建无限期等待(Waiting)状态: 处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态。
阻塞(Blocked)状态: 线程在获取synchronized排他锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
死亡(Dead)状态: 线程执行完了或者因异常退出了run()方法,该线程生命周期结束。