一. 线程和进程
1. 什么是进程
进程是代码在数据集合上的一次运行活动,是系统除CPU外进行资源分配和调度的基本单位(CPU资源是直接分配到线程的,线程是CPU分配的基本单位),进程包括线程。
2. 什么是线程
线程是进程的一个实体,一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源
3. 在Java中,启动main函数就相当于启动了一个JVM进程,main函数所在的线程称为主线程
4. 一个进程中的线程共享该进程的堆和方法区的资源,堆是一个进程中最大的一块内存,主要存放使用new操作创建的对象实例。方法区用来存放JVM加载的类、常量、及静态变量等信息。
5. 每个线程有自己的程序计数器和栈区域,程序计数器用于记录当前线程要执行的指令地址,栈用来存储该线程的局部变量,该局部变量无法被其他线程访问。
二. 线程创建与运行
创建线程一共有三种方式
方式一:继承Thread类
public class ThreadTest { public static class MyThread extends Thread{ @Override public void run() { System.out.println("I am a child thread"); } } public static void main(String[] args) { //1. 创建线程 MyThread myThread = new MyThread(); //2. 启动线程 myThread.start(); } }
需要注意的是,当第一步创建了Thread对象后,线程并没有被执行,直到调用了start方法,该线程才被启动执行,调用start方法后,该线程处于就绪状态(已经获得了除CPU外其他资源),当该线程获取了CPU资源后才真正处于运行状态,执行完run方法,线程处于终止状态。
方式二:实现Runnable接口
public class ThreadTest { public static void main(String[] args) { //1. 创建线程 RunnableTask task = new RunnableTask(); //2. 启动线程 new Thread(task).start(); new Thread(task).start(); } public static class RunnableTask implements Runnable { @Override public void run() { System.out.println("I am a child thread"); } } }
前两种方式任务都没有返回值,可以FutureTask的方式来获取任务的返回值
方式三:FutureTask
public class ThreadTest { public static void main(String[] args) { //1. 创建异步任务:使用FutrueTask对象作为任务,创建了一个进程 FutureTask<String> futureTask = new FutureTask<>(new CallerTask()); //2. 启动线程 new Thread(futureTask).start(); try { //3. 等待任务执行完毕,并返回结果 String result = futureTask.get(); System.out.println(result);//输出:zhangjia } catch (Exception e) { e.printStackTrace(); } } //创建任务类,类似Runnable public static class CallerTask implements Callable<String> { @Override public String call() throws Exception { return "ZhangJia"; } } }
三种方式的优缺点:
-
继承Thread类
-
优点:在run()方法中可以直接使用this获取当前线程,方便传参,可以在子类里添加成员变量,通过set方法设置参数或者通过构造方法传递
-
缺点:java不支持多继承,任务和代码没有分离,当多个线程执行一样的任务时需要多份任务代码
-
实现Runnable接口
-
优点:可以继承其他类,多个线程可以共用一份代码
-
缺点:没有返回值,只能使用主线程里面被声明为final的变量
-
FutureTask:
-
优点:可以拿到任务的返回值
三. 线程的通知与等待
在讨论线程的通知与等待之前,想起之前面试的时候,面试官一直在问各种多线程相关的问题,其中有一个问题印象深刻:你用过Object类中的哪些方法?菜鸡的我只说出了equals,toString,getClass,hashCode之类常见的方法,其实面试官想要的答案是wait、notify、notifyAll,接下来我们就从这几个函数入手,来讲解一下线程的通知与等待
1. wait()函数
一个线程可以通过调用一个共享变量的wait()方法将自己挂起,直到发生以下事情之一才返回:
-
其他线程调用了该共享对象的notify() 方法或者notifyAll() 方法
-
其他线程(或者该线程自己调用interrupt)调用了该线程的interrupt()方法导致该线程抛出InterruptedException异常返回
另外如果调用wait方法的线程事先没有获取该独享的监视器锁,那么会抛出IllegalMonitorStateException异常,可以通过下面两种方法来获取该对象的监视器锁来避免该异常
//方法一 synchronized (共享变量){ //防止虚假唤醒 while(条件不满足) { 共享变量.wait(); } } //方法二 synchronized void add(int a, int b){ //doSomething }
需要注意的是,如果一个线程持有多个共享变量的锁,那么调用wait方法后只会释放当前共享变量的锁,举个例子来理解一下这句话:
package io.zhangjia.threads.test.Test; import org.apache.ibatis.annotations.Param; /** * @Author : ZhangJia * @Date : 2019/12/4 21:45 * @Description : */ public class Test2 { private static volatile Object a = new Object(); //volatile关键字能够保证代码的有序性 private static volatile Object b = new Object(); public static void main(String[] args) { new Thread(new ThreadA()).start(); new Thread(new ThreadB()).start(); } public static class ThreadA implements Runnable { @Override public void run() { try { synchronized (a) { System.out.println("线程A获得了共享变量a的监视器锁"); synchronized (b) { System.out.println("线程A获得了共享变量b的监视器锁"); System.out.println("线程A阻塞,并释放获取到的共享变量a的锁"); a.wait(); } } } catch (InterruptedException e) { e.printStackTrace(); } } } public static class ThreadB implements Runnable { @Override public void run() { try { Thread.sleep(1000); synchronized (a) { System.out.println("线程B获取到共享变量a的锁啦"); System.out.println("我是线程B,我一直在尝试获取共享变量b的锁,就是没获取到"); synchronized (b) { System.out.println("线程B获取到共享变量b的锁啦"); System.out.println("线程B阻塞,并释放获取到的共享变量a的锁"); a.wait(); } } } catch (InterruptedException e) { e.printStackTrace(); } } } } 输出: 线程A获得了共享变量a的监视器锁 线程A获得了共享变量b的监视器锁 线程A阻塞,并释放获取到的共享变量a的锁 线程B获取到共享变量a的锁啦 我是线程B,我一直在尝试获取共享变量b的锁,就是没获取到
上例中,线程A一开始拥有变量a和变量b的锁,通过调用a的wait方法后,只有a的锁被释放了,b的锁并没有被释放掉,所以线程B无法获取到b变量的锁。
2. wait(long tiemout)函数
timeout意为超时参数,单位是毫秒(ms),比如wait(1000)意为在1秒内如果没被其他线程调用notify()或者notifyAll()方法唤醒,还是会因为超时返回。
3. wait(long timeout,int nanos)函数
timeout单位是毫秒,nanos单位是纳秒,1毫秒 = 1000 微秒 = 1000 000 纳秒 ,该函数的主要作用是为了能够更加精确的控制等待时间
4. notify()函数
该方法用户唤醒被挂起的线程。注意阻塞和挂起的区别,阻塞是被动的,挂起是主动的。
另外如果一个共享变量上有多个线程被挂起了,那么具体唤醒哪个等待线程是随机的,被唤醒的线程依旧需要先获取共享对象的锁才可以返回。
notify函数和wait函数类似,如果当前线程没有获取到共享变量的监视器锁就调用notify方法,也会抛出IllegalMonitorStateException异常。
5. notifyAll()函数
当一个线程通过共享变量调用notifyAll()函数时,该方法会唤醒在此之前所有通过该共享变量调用了wait系列函数的线程。
举一个例子来理解:
package io.zhangjia.threads.test.Test; /** * @Author : ZhangJia * @Date : 2019/12/15 19:30 * @Description : */ public class NotifyTest { private static volatile Object a = new Object(); public static void main(String[] args) throws InterruptedException { Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (a) { System.out.println("线程A获得了共享变量a的监视器锁"); try { System.out.println("线程A开始等待"); a.wait(); System.out.println("线程A结束等待"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (a) { System.out.println("线程B获得了共享变量a的监视器锁"); try { System.out.println("线程B开始等待"); a.wait(); System.out.println("线程B结束等待"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); Thread threadC = new Thread(new Runnable() { @Override public void run() { synchronized (a) { System.out.println("线程c开始唤醒"); a.notify(); } } }); threadA.start(); threadB.start(); Thread.sleep(1000); //主线程先休眠1s,为了让线程A和线程B都执行完wait方法后再调线程c的notify方法 threadC.start(); //等待线程结束 threadA.join(); threadB.join(); threadC.join(); System.out.println("主线程结束"); } } //输出: 线程A获得了共享变量a的监视器锁 线程A开始等待 线程B获得了共享变量a的监视器锁 线程B开始等待 线程c开始唤醒 线程A结束等待
通过上例可以看到,当线程A和线程B分别通过a变量调用wait方法将自身挂起后,线程c调用notify方法,随机唤醒了一个线程,如果将线程c中的 a.notify();修改为 a.notifyAll();则结果为:
线程A获得了共享变量a的监视器锁 线程A开始等待 线程B获得了共享变量a的监视器锁 线程B开始等待 线程c开始唤醒 线程B结束等待 线程A结束等待 主线程结束
四. 等待线程执行终止的Join方法
未完待续…
最近一次更新:19.12.15
请登录之后再进行评论