线程做为操做系统中最少调度单位,在当前系统的运行环境中,通常都拥有多核处理器,为了更好的充分利用 CPU,掌握其正确使用方式,能更高效的使程序运行。同时,在 Java 面试中,也是极其重要的一个模块。
一个独立运行的程序是一个进程,一个进程中能够包含一个或多个线程,每一个线程都有属于本身的一些属性,如堆栈,计数器等等。同时,一个线程在一个时间点上只能运行在一个 CPU 处理器核心上,不一样线程之间也能够访问共享变量。线程在运行时,系统给每一个线程分配一些 CPU 时间片,CPU 在时间片这段时间运行某个线程,当这个时间片运行完又跳转至下一段时间片线程,CPU 在这些线程中进行高速切换,使得程序像是在同时进行多个线程操做。java
实现线程经常使用的两种方式:继承 java.lang.Thread 类、实现 java.lang.Runnable 接口。面试
经过实例化 java.lang.Thread 类得到线程。建立 Thread 对象,通常使用继承 Thread 类的方式,而后经过方法重写覆盖 Thread 的某些方法。segmentfault
首先建立一个继承 Thread 的子类。网络
public class DemoThread extends Thread{ // 重写 Thread 类中的 run 方法 @Override public void run() { // currentThread().getName() 获取当前线程名称 System.out.println("java.lang.Thread 建立的"+ currentThread().getName() +"线程"); } }
上面代码 DemoThread 实例化的对象就表明一个线程,经过重写 run 方法,在 run 方法中实现该线程的逻辑实现。多线程
public class Main { public static void main(String[] args) { // 实例化 DemoThread 获得新建立的线程实例 DemoThread thread = new DemoThread(); // 给建立的子线程命名 thread.setName("DemoThread 子线程"); // 启动线程 thread.start(); // 经过主线程打印信息 System.out.println("main 线程"); } }
在程序执行的主线程中建立子线程,而且命名为DemoThread 子线程
,在程序的最后打印主线程打印的信息。调用线程必须调用start()
方法,在调用此方法以前,子线程是不存在的,只有start()
方法调用后,才是真正的建立了线程。异步
执行结果:ide
从结果能够看到,因为在主线程中建立了一个子线程,子线程相对于主线程就至关因而一个异步操做,因此打印结果就有可能main线程先于子线程执行打印操做。测试
因为 Java 是单继承的特性,因此当建立线程的子类继承了其余的类,就没法实现继承操做。这时就能够经过实现 Runnable 接口,来实现线程建立的逻辑。this
首先建立一个实现 Runnable 的类。url
public class DemoRunnable implements Runnable { // 实现 Runnable 中的 run 方法 @Override public void run() { System.out.println("java.lang.Runnable 建立的 "+ Thread.currentThread().getName() +"线程"); } }
Runnable 接口中定义有一个 run 方法,因此实现 Runnable 接口,就必须实现 run 方法。实际上 java.lang.Thread 类也实现了 Runnable 接口。
建立线程:
public class Main { public static void main(String[] args) { // 建立 Thread 实例,并给将要建立的线程给命名 Thread thread = new Thread(new DemoRunnable(), "DemoRunnable 子线程"); // 建立一个线程 thread.start(); System.out.println("main 线程"); } }
执行结果
一样也实现了与继承 Thread 方式同样的结果。
建立 Thread 实例时,向新建立的 Thread 实例中传入了一个实现 Runnable 接口的对象的参数。
Thread 中初始化 Thread#init 的具体实现:
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } // 给当前建立的 thread 实例中赋值线程名 this.name = name; // 将要建立的线程的父线程即当前线程 Thread parent = currentThread(); // 添加到线程组操做 SecurityManager security = System.getSecurityManager(); if (g == null) { if (security != null) { g = security.getThreadGroup(); } if (g == null) { g = parent.getThreadGroup(); } } g.checkAccess(); if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } // 线程组中添加为启动的线程数 g.addUnstarted(); this.group = g; // 设置父线程的一些属性到当前将要建立的线程 this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext(); // 将当前传入 target 的参数,赋值给当前 Thread 对象,使其持有 已实现 Runnable 接口的实例 this.target = target; setPriority(priority); if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); // 设置线程的堆栈大小 this.stackSize = stackSize; // 给建立的线程一个 id tid = nextThreadID(); }
上面代码建立 thread 对象时的 init 方法,经过传入 Runnable 的实例对象,thread 对象中就持有该对象。
建立 thread 对象后,调用 start() 方法,该线程就运行持有 Runnable 实现类对象的 run() 方法。
例如本文中案例,就会执行 DemoRunnable#run 方法的逻辑。
这两种方法建立线程的方式,具体使用哪一种,根据自身需求选择。若是须要继承其余非 Thread 类,就须要使用 Runnable 接口。
Java 线程每一个时间点都存在于6种状态中一种。
状态 | 描述 |
---|---|
NEW | 初始状态,thread 对象调用 start() 方法前 |
RUNNABLE | 运行状态,线程 start() 后的就绪或运行中 |
BLOCKED | 阻塞状态,线程得到锁后的锁定状态 |
WAITING | 等待状态,线程进入等待状态,不会被分配时间片,须要等待其余线程来唤醒 |
TIME_WAITING | 超时等待状态,一样不分配时间片,当时间达到设定的等待时间后自动唤醒 |
TERMINATED | 终止状态,表示当前线程执行完成 |
其中 NEW、RUNNABLE、TERMINATED 比较好理解,如今主要针对 BLOCKED、WAITING 和 TIME_WAITING 进行案例讲解。
阻塞状态 是将两个线程之间处于竞争关系,同时在调用 run 时进行加锁。
首先仍是使用上面 Runnable 实现的方式进行改造。
public class DemoRunnable implements Runnable { @Override public void run() { // 经过对DemoRunnable加同步锁,进行无限循环不退出 synchronized (DemoRunnable.class){ while (true){ System.out.println("java.lang.Runnable 建立的 "+ Thread.currentThread().getName() +"线程"); } } } }
先竞争到 DemoRunnable 类的线程进入 run 会一直执行下去,未竞争到的线程则会一直处于阻塞状态。
建立两个线程
public class Main { public static void main(String[] args) { // 建立两个线程测试 new Thread(new DemoRunnable(), "test-blocked-1") .start(); new Thread(new DemoRunnable(), "test-blocked-2") .start(); } }
经过分析执行后的线程如图:
能够得知线程test-blocked-1
竞争到 DemoRunnable 类,一直都在运行 while 循环,因此状态为 RUNNABLE。因为 DemoRunnable#run 中加了同步锁锁住 DemoRunnable 类,因此test-blocked-2
一直处于 BLOCKED 阻塞状态。
等待状态 线程是不被分配 CPU 时间片,线程若是要从新被唤醒,必须显示被其它线程唤醒,不然会一直等待下去。
实现等待状态例子
public class DemoRunnable implements Runnable { @Override public void run() { while (true){ // 调用 wait 方法,使线程在当前实例上处于等待状态 synchronized (this){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("java.lang.Runnable 建立的 "+ Thread.currentThread().getName() +"线程"); } } } } // 建立线程 public class Main { public static void main(String[] args) { new Thread(new DemoRunnable(), "test-waiting") .start(); } }
建立该实例线程后,分析 test-waiting 线程,该线程处于 WAITING 状态。
超时等待状态 线程也是不被分配 CPU 时间片,可是它经过设置的间隔时间后,能够自动唤醒当前线程。也就是说,将等待状态的线程加个时间限制就是超时等待状态。
只需对上面 WAITING 状态案例增长 wait 时间限制。
public class DemoRunnable implements Runnable { @Override public void run() { while (true){ synchronized (this){ try { // 增长等待时长 this.wait(1000000, 999999); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("java.lang.Runnable 建立的 "+ Thread.currentThread().getName() +"线程"); } } } }
分析线程结果,能够看到 test-time_waiting 线程处于超时等待状态,使用 sleep 睡眠时,线程也是属于超时等待状态。
线程状态之间的转换,如图(来源网络):
currentThread 是获取当前线程实例,返回 Thread 对象,这是一个静态方法,使用以下
Thread.currentThread();
start 方法是启动线程的入口方法,这个就是上面实现建立线程例子中的 start 方法。
run 方法是线程建立后,线程会主动调用 run 方法执行里面的逻辑。
join 方法即线程同步,好比上继承 Thread 方法实现建立线程的例子中,若是在 thread.start() 后调用 thread.join() 方法,则 main 线程打印的信息必定在子线程打印的信息以后。这里的 main 线程会等待子线程执行完后,再继续执行。
getName 返回线程名称。
获取线程 Id,这是返回一个 long 类型的 Id 值。
setDaemon(boolean on) 方法是设置线程类型,setDaemon 接受一个 boolean 类型参数。设置为 true 时,线程类型为守护线程,设置为 false 时,线程类型为用户线程。
yield 方法是线程让步,让当前线程进入就绪状态,去执行其它相同优先级的线程,但不必定会执行其余线程,有可能让步后的线程再次被执行。
setPriority(int newPriority) 是设置线程执行的优先级,数值为1~10,默认值为5,数值越大线程越先执行。
interrupt 方法的做用是中断线程,可是它仍是会继续运行。它只是表示其余线程给打了个中断标志。
interrupted 方法是检查当前线程是否被中断。调用此方法时会清除该线程的中断标志。
isInterrupted 方法检测当前线程是否被中断,若是被中断了,也不会清除中断标志。
本文对线程的经常使用功能及概念进行了分析,主要是讲解单线程的一些操做,线程操做的使用在生产中是极容易出现问题的,因此在掌握概念和使用后,须要多研究,多思考应用的设计及实现。在掌握多线程操做时,必须对这些的基本使用和概念进行掌握,从此会出进一步对多线程分析的文章。
推荐阅读