java从诞生之日起,就明智的选择了内置对多线程的支持。
几个概念
在开始写并发之前,先介绍几个简单的概念:
并发和并行: 并发指多个任务交替的执行,并行指多个任务同时执行
临界区:表示一种公共资源或者共享数据,一次只能有一个线程访问它
JMM的特性: 原子性,可见性,有序性
程序、进程、线程
程序:具有某些功能的代码。
进程:操作系统进行资源分配和资源调度的基本单位。进程是程序执行的实体。
线程:轻量级的进程,程序执行的最小单位,线程中含有独立的计数器,堆栈和局部变量等属性,必须拥有一个父进程,并且共享进程中所拥有的全部资源。
进程之间不能共享内存,线程共享内存非常容易。
线程的状态
线程是有生命周期的,生命周期的状态如下:
NEW :新建
READY:就绪
RUNNABLE : 运行
BLOCKED :阻塞
WAITING : 等待
TIME_WAITING:超时等待
TERMINATED : 终止
Java程序中的线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换。转换图如下:
线程生命周期状态图.png
新建状态
使用new关键字创建线程之后,线程就处于新建状态,此时JVM为其分配了内存,并初始化了成员变量。此时并没有表现出线程的任何动态特征,程序也不会执行线程执行体。
就绪状态
对象调用了start()方法后,该线程就处于就绪状态,JVM会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有运行,只是表示可以运行了,何时运行,取决于系统调度。
运行状态
就绪状态的线程,获得CPU之后,就处于运行状态,当当前的时间片用完,或者运行yield()/sleep()方法时,线程就必须放弃CPU,结束运行状态。
阻塞状态/等待/超时等待
线程调用sleep()/join()/wait()方法,放弃CPU资源,线程被阻塞/等待
线程调用了一个IO方法,在该方法返回前,被阻塞
线程试图获取一个同步监视器,但是失败了,被阻塞
线程在等待某个通知
线程结束
run()/call()方法执行体执行完成,线程正常结束
抛出Exception/Error
调用线程结束控制
上面提到了线程创建的三种方式,在代码层级来看看线程的具体实现:
线程的创建
继承Thread类
public class ThreadTest extends Thread { @Override public void run() { super.run(); System.out.println("我是线程执行体 !"); } public static void main(String[] args) { ThreadTest threadTest = new ThreadTest(); threadTest.start(); } }
实现Runnable接口
class RunnableTest implements Runnable{ @Override public void run() { System.out.println("runnable 执行体"); } public static void main(String[] args) { Thread thread = new Thread(new RunnableTest()); thread.start(); } }
实现Callable接口和FutureTask
class callableTest implements Callable{ @Override public Object call() throws Exception { System.out.println(Thread.currentThread().getName()); return 3; } public static void main(String[] args) throws ExecutionException, InterruptedException { callableTest callableTest = new callableTest(); FutureTask<Integer> futureTask = new FutureTask<Integer>(callableTest); Thread thread = new Thread(futureTask , "有返回值的线程"); thread.start(); System.out.println(Thread.currentThread().getName()); System.out.println(futureTask.get()); } }
上面展示了三种创建线程的三种方式,下面做一下简要的分析:
实现接口的方式,还可以继承其他的类
多个线程可以共享同一个target对象,适合多个线程来处理同一份资源。但是编程稍微复杂
继承Thread类的方式不能在继承其它的类,但是编写简单。
一般推荐使用实现接口的方式来创建多线程。
线程执行的任务定义在了run()方法中,只有通过start()方法才能创建线程,这是为什么呢,通过源代码来分析一下:
@FunctionalInterfacepublic interface Runnable { public abstract void run(); }
Runable是一个函数式接口,定义也比较简单,只是定义了一个抽象的方法。
@FunctionalInterfacepublic interface Callable<V> { V call() throws Exception; }
Callable也是一个函数式接口,并且支持泛型,并返回泛型的类型。
看一下Thread类的实现,Thread类的代码比较多,这里只阐述一些重要的点:
先来看Thread类的定义
public class Thread implements Runnable
Thead类实现了Runable接口
构造方法: Thread类提供了9个构造方法,只阐述2个构造方法,这两个也是最常用的方法,如下。
public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); }public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }//init 方法重载private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null); }private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc) { this.name = name.toCharArray(); //删除了一些代码,这些代码会获取一些父线程的属性,并设置到当前线程 .... this.target = target; //敲黑板,画重点 }
上面的代码是一个线程的创建过程,包括获取一些父进程的属性,如设置线程的名字,设置是否是守护进程,优先级等。如果在创建线程的时候没有指定线程的名字,那么线程的名字为:Thread-num的形式,就是通过上面的代码来创建的,nextThreadNum()方法会返回一个数字,nextThreadNum()是一个被synchronized关键字修饰的方法,会将一个被static int类型修饰的整数进行+1操作,并返回,这样就保证了线程的名字不会重复,当然也可以自己指定线程的名字,在创建的时候传入即可。
上面代码target是一个私有的Runable变量,定义如下:
private Runnable target;
看一下Thread类的start()方法:
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); //敲黑板,画重点,调用start0()方法 started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } }private native void start0();
start()方法是一个同步方法,并且会调用start0()来运行,start0()方法是一个被native修饰的本地方法,将委托给操作系统来运行,并调用run()方法。
定义如下:
private native void start0();
调用start()方法后,线程就处于了就绪状态,系统调度后,变为运行状态,并调用run()方法, 看一下run()方法的定义:
@Override public void run() { if (target != null) { target.run(); } }
Thread类实现了Runbale接口所以必须要实现run(),run()方法的实现比较简单,首先判断target对象是否为null,如果为null就什么也不做,如果不为null,则调用target对象的run()方法。
分析到这,应该就能明白,有很多书上都说,无论是继承Thread类,还是实现了Runable接口都必须要重写run()方法,原因就在这。
继承Thread类的对象,调用Thread类的无参构造方法,此时target对象为空,但是它重写了run()方法,它会调用自己的run方法,实现了Runnable接口的对象,需要借助Thread(Runable target ) 构造方法运行 , 此时target对象不为空,会调用target的run()方法。
关于Callable和FutureTask的实现方式,这里简单说一下, FutureTask实现了 RunnableFuture接口, 而RunnableFuture接口继承了Runable接口, 所以它能通过new Thread(Runnable target)构造函数来运行。会调用FutureTask的run()方法,在run()方法中调用了Callable对象的call()方法,获得了其返回值,换句话说 FutureTask对象封装了该Callable对象的返回值。通过FutureTask的get()方法来获得子线程执行结束后的返回值。
线程控制
join线程:让一个线程等待另一个线程执行完成的方法 join() ,主线程创建了一个子线程,并且调用了join()方法,那么主线程只有等待子线程执行完成后,才能向下运行。如果调用了join(long millis)主线程会在等到millis时间后向下执行。
守护线程(daemon):一个线程设置成为守护线程后,会随着前台线程的结束而结束。GC(垃圾回收)就是一个非常典型的守护进程。
线程睡眠sleep():使线程进入阻塞状态,不在运行
线程让步yield():yield和sleep有点类似,不同是它进入的不是阻塞状态,而是就绪状态。
作者:起个名忒难
链接:https://www.jianshu.com/p/1f5ad4a518ee
共同学习,写下你的评论
评论加载中...
作者其他优质文章