前言
进程和线程是操作系统内两个很基本、很重要的概念,而且是我们参加笔面试必问的知识点之一。今天这篇文章就整理一下有光线程和进程的相关知识点。
01 线程的基本概念
笔面试的时候基本都会问:什么是进程,什么是线程?它们有什么区别和关系?
进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。
而线程是进程的组成部分,是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。
一个正在运行的软件(比如微信、QQ等)就是一个进程,一个进程可以同时运行多个任务(比如我们使用微信和QQ可以同时和别人视频聊天,也可以打字聊天,则它们分别都是一个进程), 所以可以简单的认为进程是线程的集合。
线程是一条可以执行的路径。多线程就是同时有多条执行路径在同时(并行)执行。
系统中的进程线程模型是这样的:
进程从操作系统获得基本的内存空间,所有的线程共享着进程的内存地址空间。当然,每个线程也会拥有自己私有的内存地址范围,其他线程不能访问。
由于所有的线程共享进程的内存地址空间,所以线程间的通信就容易的多,通过共享进程级全局变量即可实现。
在JAVA中用Thread 这个类抽象化描述线程,线程有以下六种状态:
- NEW:线程刚被创建,尚未启动的线程的状态;
- RUNNABLE:可运行线程的线程状态;
- BLOCKED: 受阻塞并且正在等待监视器锁的某一线程的线程状态;
- WAITING:等待线程的线程状态;
- WAITING:等待线程的线程;
- TIMED_WAITING:具有指定等待时间的某一等待线程的线程状态 ;
- TERMINATED:线程执行结束,被终止;
公众号之前有篇文章,专门讲过线程的六种状态:线程的六种状态 ,有需要的同学,可以查看这篇文章。
02 线程的创建和启动
前面说到Java中使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。在Java可以用四种方式来创建线程,如下所示:
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
4)使用线程池,比如exExecutor框架
1、继承 Thread 类
线程启动时会去调用 run 方法,所以只要重写 Thread 类的 run 方法也是可以定义出线程类。
package Demo1; public class MyThread extends Thread{ @Override public void run(){ System.out.println("hello world"); } public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); } } 复制代码
运行结果:
2、实现Runnable接口
定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
package Demo1;public class MyThread1 implements Runnable { @Override public void run() { System.out.println("hello world"); } public static void main(String[] args) { Thread thread = new Thread(new MyThread1()); thread.start(); }} 复制代码
运行结果:
复制代码
其实 Thread 这个类也是继承 Runnable 接口的,并且提供了默认的 run 方法实现:
@Overridepublic void run() { if (target != null) { target.run(); }} 复制代码
target 是一个 Runnable 类型的字段,Thread 构造函数会初始化这个 target 字段。所以当线程启动时,调用的 run 方法就会是我们自己实现的实现类的 run 方法。
3、使用Callable和Future创建线程
和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。
- call()方法可以有返回值
- call()方法可以声明抛出异常
Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。
#视图取消该Future里面关联的Callable任务 boolean cancel(boolean mayInterruptIfRunning): #返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值 V get() #返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException V get(long timeout,TimeUnit unit) #若Callable任务完成,返回True boolean isDone() #如果在Callable任务正常完成前被取消,返回True boolean isCancelled() 复制代码
使用Callable和Future创建并启动有返回值的线程其实是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。具体的步骤如下:
1、创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3、使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
package Demo1; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class MyThread2 { public static void main(String[] args) { Callable<Integer> myCallable = new MyCallable(); // 创建MyCallable对象 FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象 for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Thread thread = new Thread(ft); //FutureTask对象作为Thread对象的target创建新的线程 thread.start(); //线程进入到就绪状态 } } System.out.println("主线程for循环执行完毕.."); try { int sum = ft.get(); //取得新创建的新线程中的call()方法返回的结果 System.out.println("sum = " + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } class MyCallable implements Callable<Integer> { private int i = 0; // 与run()方法不同的是,call()方法具有返回值 @Override public Integer call() { int sum = 0; for (; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); sum += i; } return sum; } } 复制代码
部分运行结果如下:
4、使用线程池,比如exExecutor框架
Java通过Executors提供四种线程池,分别为:
1.newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2.newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3.newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
4.newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
package Demo1; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class MyThread3 { public static void main(String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); for(int i=0;i<5;i++){ executorService.execute(()->{ System.out.println(Thread.currentThread().getName()+"执行中"); }); } System.out.println("线程任务开始执行"); executorService.shutdown(); } } 复制代码
主要使用的还是前面两种方式创建线程。
03 线程相关的其他方法
Thread 类中也提供了一些其他方法进行线程的操作。
1、sleep
public static native void sleep(long millis) 复制代码
Thread.sleep(long millis)静态方法强制当前正在执行的线程休眠(即暂停执行)。当线程睡眠时,它睡在某个地方,在苏醒之前是不会反悔到可运行状态。当睡眠时间到期,则返回到可运行的状态。所以,sleep()方法指定的时间为线程不会运行的最短时间。当线程休眠时间结束后,会返回到可运行状态,注意不是运行状态,如果要到运行状态还需要等待CPU调度执行。
package Demo1; public class Thread4 { public static void main(String[] args) { Runner1 r1 = new Runner1(); Thread t = new Thread(r1); t.start(); for (int i = 0; i < 3; i++) { System.out.println("main thread :"+i); } } } class Runner1 implements Runnable{ @Override public void run() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0; i < 3; i++) { System.out.println("Runner1 : " + i); } } } 复制代码
2、start
public synchronized void start() 复制代码
我以前很疑惑,为什么重写 Runnable 的 run 方法指定了线程的工作,但却是通过 start 方法来启动线程的?
因为启动一个线程不仅仅是给定一个指令开始入口即可,操作系统还需要在进程的共享内存空间中划分一部分作为线程的私有资源,创建程序计数器,栈等资源,最终才会去调用 run 方法。这就好比汽车跑主要是发动机,但是有发动机还不行,必须得用钥匙启动。这里的启动就好比start();而run()就类似于发动机了。
这也是经常问的start和run方法的区别。
3、interrupt
public void interrupt() 复制代码
调用interrupt(),通知线程应该中断了
1.如果线程处于被阻塞状态,那么该线程将立即退出被阻塞状态,并且抛出一个InterrupedException异常.
2.如果线程初一正常活动状态,那么会将该线程的中断标志设置为true.被设置中断标志的线程将继续中场运行,不受影响.
需要被调用的线程配合中断
1.在正常运行任务时,经常检查本线程的中断标志,如果被设置了中断标志就自行停止线程.
注意:调用interrupt()方法并不会使得线程中断,而是使得线程的中断标志置为true
package Demo1; public class MyThread4 { public static void main(String[] args) throws InterruptedException { Runnable interruptTask = new Runnable() { int i = 0; @Override public void run() { try { //在正常运行任务时,经常进行本线程的中断标志,如果被设置了终端标志就自行停止线程 while (!Thread.currentThread().isInterrupted()){ //休眠100ms Thread.sleep(100); i++; System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") loop " + i); } }catch (InterruptedException e){ //在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。) System.out.println(Thread.currentThread().getName() + " (" + Thread.currentThread().getState() + ") catch InterruptedException."); } } }; Thread t1 = new Thread(interruptTask,"t1"); System.out.println(t1.getName() +" ("+t1.getState()+") is new."); // 启动“线程t1” t1.start(); System.out.println(t1.getName() +" ("+t1.getState()+") is started."); // 主线程休眠300ms,然后主线程给t1发“中断”指令。 Thread.sleep(300); t1.interrupt(); System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted."); // 主线程休眠300ms,然后查看t1的状态。 Thread.sleep(300); System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now."); } } 复制代码
运行结果如下:
4、join
public final synchronized void join(long millis) 复制代码
这个方法一般在其他线程中进行调用,指明当前线程需要阻塞在当前位置,等待目标线程所有指令全部执行完毕。例如:thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
package Demo1;public class ThreadDemo5 { public static void main(String[] args) { Thread t = new Thread(new RunnableImpl()); t.start(); try { t.join(1000); System.out.println("joinFinish"); } catch (InterruptedException e) { e.printStackTrace(); } }}class RunnableImpl implements Runnable { public void run() { try { System.out.println("Begin sleep"); Thread.sleep(1000); System.out.println("End sleep"); } catch (InterruptedException e) { e.printStackTrace(); } }} 复制代码
运行结果为:
Begin sleep joinFinish End sleep 复制代码
当main线程调用t.join时,main线程等待t线程,等待时间是1000。
04 总结
进程作为系统分配资源的基本单元,而线程是进程的一部分,共享着进程中的资源,并且线程还是系统调度的最小执行流。在实时系统中,每个线程获得时间片调用 CPU,多线程并发式使用 CPU,每一次上下文切换都对应着「运行现场」的保存与恢复,这也是一个相对耗时的操作。