先抛出几个常见的面试题
- 线程的状态
- 线程的几种实现方式
- 三个线程轮流打印ABC十次
- 判断线程是否销毁
- yield功能
- 给定三个线程t1,t2,t3,如何保证他们依次执行
1. 基本概念
进程是程序执行的一个实例
。进程的特点,每一个进程都有自己的独立的一块内存空间、一组资源系统,其内部数据和状态都是完全独立的
。
进程的优点是提高CPU运行效率,在同一时间内执行多个程序,即并发执行。但是从严格上讲,也不是绝对的同一时刻执行多个程序,只不过CPU在执行时通过时间片等调度算法不同进程高速切换。
线程是进程的一个实体,是CPU调度和分派的基本单位
。它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
同类的多个线程共享一块内存空间和一组系统资源
,线程本身的数据通常只有CPU的寄存器数据,以及一个供程序执行时的堆栈。线程在切换时负荷小,因此,线程也被称为轻负荷进程。一个进程中可以包含多个线程
。
有以下对比与场景:
- 进程由操作系统调度,简单而且稳定
- 进程之间的隔离性好,一个进程崩溃不会影响其它进程,任何一个线程崩溃会导致整个进程崩溃。
- 在多核情况下可以把进程和CPU进行绑定,充分利用CPU
- 进程切换代价很高,进程切换也像线程一样需要保持上一个进程的上下文环境
- 进程间通信比线程要慢,因为线程见通信就是读写同一个变量,速度很快
- 程序中出现需要
等待的操作
,比如网络操作、文件 IO 等,可以利用多线程充分使用处理器资源,而不会阻塞程序中其他任务的执行 - 程序中出现
可分解的大任务
,比如耗时较长的计算任务,可以利用多线程来共同完成任务,缩短运算时间 - 程序中出现需要
后台运行的任务
,比如一些监测任务、定时任务,可以利用多线程来完成
并发与并行
并发:多个事件在同一时间段内一起执行
并行:多个事件在同一时刻同时执行
多线程能不能并行,取决于是否有多cpu分配同时运行。所以系统中很多都是多任务一起执行,但是不一定是并行。
多线程的调度
在Java程序中,JVM负责线程的调度。线程调度是值按照特定的机制为多个线程分配CPU的使用权。
调度的模式有两种:分时调度和抢占式调度。分时调度是所有线程轮流获得CPU使用权,并平均分配每个线程占用CPU的时间;抢占式调度是根据线程的优先级别来获取CPU的使用权。JVM的线程调度模式采用了抢占式模式。
多线程编程的问题
- 更复杂的设计 : 多线程在访问共享数据时需要进行同步(在java中需要使用synchronized关键字),某些情况下需要考虑线程的执行顺序和相互配合
- 上下文切换: 上CPU需要从一个线程切换到另一个线程时,它需要先保存当前线程的本地数据和程序指针,然后再加载要切换线程的本地数据和程序指针
- 更多的系统资源:处理需要CPU时间以外,每个线程还需要额外的内存空间来保存它的本地数据栈,更需要操作系统资源来管理多个线程,所以应用程序的线程数量一定要根据实际情况合理安排
2. 线程的启动
2.1 继承Thread类
- 自定义一个类MyThread,继承Thread类,重写run方法
- 在main方法中new一个自定义类,然后直接调用start方法
优点:简单、实例化即可使用
缺点:扩展性差,java 中是单继承
// 通过继承Thread类实现自定义线程类
public class MyThread extends Thread {
// 线程体
@Override
public void run() {
System.out.println("Hello, I am the defined thread created by extends Thread");
}
public static void main(String[] args){
// 实例化自定义线程类实例
Thread thread = new MyThread();
// 调用start()实例方法启动线程
thread.start();
}
}
2.2 实现Runnable接口
- 自定义一个线程,实现Runnable接口的run方法
run方法就是要执行的内容,会在另一个分支上进行
Thread类本身也实现了Runnable接口 - 主方法中new一个自定义线程对象,然后new一个Thread类对象,其构造方法的参数是自定义线程对象
- 执行Thread类的start方法,线程开始执行
自此产生了分支,一个分支会执行run方法,在主方法中不会等待run方法调用完毕返回才继续执行,而是直接继续执行,是第二个分支。这两个分支并行运行
这里运用了静态代理模式:
Thread类和自定义线程类都实现了Runnable接口
Thread类是代理Proxy,自定义线程类是被代理类
通过调用Thread的start方法,实际上调用了自定义线程类的start方法(当然除此之外还有其他的代码)
- 优点:
扩展性好
、可以继承实现其他功能 - 缺点:
相对繁琐一点
public class MyRunnable implements Runnable {
// 线程体
@Override
public void run() {
System.out.println("Hello, I am the defined thread created by implements Runnable");
}
public static void main(String[] args){
// 线程的执行目标对象
MyRunnable myRunnable = new MyRunnable();
// 实际的线程对象
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();
}
}
2.3 实现Callable接口
package com.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Hello, I am the defined thread created by implements Callable";
}
public static void main(String[] args){
// 线程执行目标
MyCallable myCallable = new MyCallable();
// 包装线程执行目标,因为Thread的构造函数只能接受Runnable接口的实现类,而FutureTask类实现了Runnable接口
FutureTask<String> futureTask = new FutureTask<>(myCallable);
// 传入线程执行目标,实例化线程对象
Thread thread = new Thread(futureTask);
// 启动线程
thread.start();
String result = null;
try {
// 获取线程执行结果
result = futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(result);
}
}
- 优点:扩展性与runnable 一致、可以
提供返回值
- 缺点:相对复杂
3. 线程的状态
初始态:NEW
创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。
运行态:RUNNABLE
在Java中,运行态包括就绪态 和 运行态。
就绪态 READY
该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行。
所有就绪态的线程存放在就绪队列中。
运行态 RUNNING
获得CPU执行权,正在执行的线程。
由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程。
阻塞态 BLOCKED
阻塞态专指请求排它锁失败时进入的状态。塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
等待态 WAITING
当前线程中调用wait、join、park函数时,当前线程就会进入等待态。
进入等待态的线程会释放CPU执行权,并释放资源(如:锁),它们要等待被其他线程显式地唤醒。
超时等待态 TIME_WAITING
当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态;
进入该状态后释放CPU执行权 和 占有的资源。
与等待态的区别:无需等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。
终止态
线程执行结束后的状态。
4. 线程的方法
1.start():启动线程并执行相应的run()方法
2.run():子线程要执行的代码放入run()方法中
3.currentThread():静态的,调取当前的线程
4.getName():获取此线程的名字
5.setName():设置此线程的名字
6.yield():调用此方法的线程释放当前CPU的执行权
暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 若队列中没有同优先级的线程,忽略此方法
7.join():在A线程中调用B线程的join()方法,表示:当执行到此方法,A线程停止执行,直至B线程执行完毕,
当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
低优先级的线程也可以获得执行
8.isAlive():判断当前线程是否还存活
9.sleep(long l):显式的让当前线程睡眠l毫秒
令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
抛出InterruptedException异常
10.线程通信:wait() notify() notifyAll()
getPriority() :返回线程优先值
setPriority(int newPriority) :改变线程的优先级
构造函数
这三个使用较多:
- 无参构造
- 带线程名
- runnable + 线程名
但是同样底层都是完全参数,这里简单介绍一下:
不存在都是穿空、不限制穿0
- 线程组
- 可执行runnable
- 线程名
- 需要栈大小
- 线程控制的上下文
- 是否可继承的线程
/**
* Initializes a Thread.
*
* @param g the Thread group
* @param target the object whose run() method gets called
* @param name the name of the new Thread
* @param stackSize the desired stack size for the new thread, or
* zero to indicate that this parameter is to be ignored.
* @param acc the AccessControlContext to inherit, or
* AccessController.getContext() if null
* @param inheritThreadLocals if {@code true}, inherit initial values for
* inheritable thread-locals from the constructing thread
*/
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");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
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();
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
getName
- new 一个子类对象的同时也new了其父类的对象,只是如果不显式调用父类的构造方法super(),那么会自动调用无参数的父类的构造方法。
可以在自定义类MyThread中(继承自Thread类)中写一个构造方法,显式调用父类的构造方法,其参数为一个字符串,表示创建一个以该字符串为名字的Thread对象。 - 效果是创建了一个MyThread对象,并且其父类Thread对象的名字是给定的字符串。
- 如果不显式调用父类的构造方法super(参数),那么默认父类Thread是没有名字的。
isAlive
isAlive活着的定义是就绪、运行、阻塞状态
线程是有优先级的,优先级高的获得Cpu执行时间长,并不代表优先级低的就得不到执行
sleep(当前线程.sleep)
sleep时持有的锁不会自动释放,sleep时可能会抛出InterruptedException。
Thread.sleep(long millis)
一定是当前线程调用此方法,当前线程进入TIME_WAIT状态,但不释放对象锁,millis后线程自动苏醒进入READY状态。作用:给其它线程执行机会的最佳方式。
join(其他线程.join)
t.join()/t.join(long millis)
当前线程里调用线程1的join方法,当前线程进入WAIT状态,但不释放对象锁,直到线程1执行完毕或者millis时间到,当前线程进入可运行状态。
join方法的作用是将分出来的线程合并回去,等待分出来的线程执行完毕后继续执行原有线程。类似于方法调用。(相当于调用thead.run())
yield(当前线程.yield)
Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。
interrupt(其他线程.interrupt)
- 调用Interrupt方法时,线程的中断状态将被置位。这是每一个线程都具有的boolean标志;
中断可以理解为线程的一个标志位属性,表示一个运行中的线程是否被其他线程进行了中断操作。这里提到了其他线程,所以可以认为中断是线程之间进行通信的一种方式,简单来说就是由其他线程通过执行interrupt方法对该线程打个招呼,让起中断标志位为true,从而实现中断线程执行的目的。 - 其他线程调用了interrupt方法后,该线程通过检查自身是否被中断进行响应,具体就是该线程需要调用Thread.currentThread().isInterrupted方法进行判断是否被中断或者调用Thread类的静态方法interrupted对当前线程的中断标志位进行复位(变为false)。需要注意的是,如果该线程已经处于终结状态,即使该线程被中断过,那么调用isInterrupted方法返回仍然是false,表示没有被中断。
- 那么是不是线程调用了interrupt方法对该线程进行中断,该线程就会被中断呢?答案是否定的。因为Java虚拟机对会抛出InterruptedException异常的方法进行了特别处理:Java虚拟机会将该线程的中断标志位清除,然后抛出InterruptedException,这个时候调用isInterrupted方法返回的也是false。
interrupt一个其他线程t时
- 1)如果线程t中调用了可以抛出InterruptedException的方法,那么会在t中抛出InterruptedException并清除中断标志位。
- 2)如果t没有调用此类方法,那么会正常地将设置中断标志位。
如何停止线程?
- 在catch InterruptedException异常时可以关闭当前线程;
- 循环调用isInterrupted方法检测是否被中断,如果被中断,要么调用interrupted方法清除中断标志位,要么就关闭当前线程
- 无论1 还是2,都可以通过一个volatile的自定义标志位来控制循环是否继续执行
但是注意!
如果线程中有阻塞操作,在阻塞时是无法去检测中断标志位或自定义标志位的,只能使用1)的interrupt方法才能中断线程,并且在线程停止前关闭引起阻塞的资源(比如Socket)。
wait(对象.wait)
- 调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内。
- obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。
- 调用wait()方法的线程,如果其他线程调用该线程的interrupt()方法,则会重新尝试获取对象锁。只有当获取到对象锁,才开始抛出相应的InterruptedException异常,从wait中返回。
notify(对象.notify)
obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
wait¬ify 最佳实践
等待方(消费者)和通知方(生产者)
等待方:
synchronized(obj){
while(条件不满足){
obj.wait();
}
消费;
}
通知方:
synchonized(obj){
改变条件;
obj.notifyAll();
}
-
条件谓词:
- 将与条件队列相关联的条件谓词以及在这些条件谓词上等待的操作都写入文档。
- 在条件等待中存在一种重要的三元关系,包括加锁、wait方法和一个条件谓词。在条件谓词中包含多个状态变量,而状态变量由一个锁来保护,因此在测试条件谓词之前必须先持有这个锁。锁对象和条件队列对象(即调用wait和notify等方法所在的对象)必须是同一个对象。
- 当线程从wait方法中被唤醒时,它在重新请求锁时不具有任何特殊的优先性,而要去其他尝试进入同步代码块的线程一起正常地在锁上进行竞争。
- 每一次wait调用都会隐式地与特定的条件谓词关联起来。当调用某个特定条件谓词的wait时,调用者必须已经持有与条件队列相关的锁,并且这个锁必须保护着构成条件谓词的状态变量。
-
过早唤醒:
虽然在锁、条件谓词和条件队列之间的三元关系并不复杂,但wait方法的返回并不一定意味着线程正在等待的条件谓词已经变成真了。
当执行控制重新进入调用wait的代码时,它已经重新获取了与条件队列相关联的锁。现在条件谓词是不是已经变为真了?或许。在发出通知的线程调用notifyAll时,条件谓词可能已经变成真,但在重新获取锁时将再次变成假。在线程被唤醒到wait重新获取锁的这段时间里,可能有其他线程已经获取了这个锁,并修改了对象的状态。或者,条件谓词从调用wait起根本就没有变成真。你并不知道另一个线程为什么调用notify或notifyAll,也许是因为与同一条件队列相关的另一个条件谓词变成了真。一个条件队列与多个条件谓词相关是一种很常见的情况。
基于所有这些原因,每当线程从wait中唤醒时,都必须再次测试条件谓词。 -
notify与notifyAll:
-
由于多个线程可以基于不同的条件谓词在同一个条件队列上等待,因此如果使用notify而不是notifyAll,那么将是一种危险的操作,因为单一的通知很容易导致类似于信号地址(线程必须等待一个已经为真的条件,但在开始等待之前没有检查条件谓词)的问题。
-
只有同时满足以下两个条件时,才能用单一的notify而不是notifyAll:
-
所有等待线程的类型都相同。只有一个条件谓词与条件队列相关,并且每个线程在从wait返回后将执行相同的操作。
-
单进单出:在对象状态上的每次改变,最多只能唤醒一个线程来执行。
-
suspend resume stop destroy(废弃方法)
- 线程的暂停、恢复、停止对应的就是suspend、resume和stop/destroy。
- suspend会使当前线程进入阻塞状态并不释放占有的资源,容易引起死锁;
- stop在结束一个线程时不会去释放占用的资源。它会直接终止run方法的调用,并且会抛出一个ThreadDeath错误。
- destroy只是抛出一个NoSuchMethodError。
- suspend和resume已被wait、notify取代。
线程的优先级
判断当前线程是否正在执行,注意优先级是概率而非先后顺序(优先级高可能会执行时间长,但也不一定)
线程优先级特性:
- 继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。
- 规则性,高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。
- 随机性,优先级较高的线程不一定每一次都先执行完。
注意,在不同的JVM以及OS上,线程规划会存在差异,有些OS会忽略对线程优先级的设定。
守护线程
- 将线程转换为守护线程
- 守护线程的唯一用途是为其他线程提供服务。比如计时线程,它定时发送信号给其他线程;
- 当只剩下守护线程时,JVM就退出了。
- 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
- 注意:Java虚拟机退出时Daemon线程中的finally块并不一定会被执行。
未捕获异常处理器
在Runnable的run方法中不能抛出异常,如果某个异常没有被捕获,则会导致线程终止。
要求异常处理器实现Thread.UncaughtExceptionHandler接口。
可以使用setUncaughtExceptionHandler方法为任何一个线程安装一个处理器,
也可以使用Thread.setDefaultUncaughtExceptionHandler方法为所有线程安装一个默认的处理器;
如果不安装默认的处理器,那么默认的处理器为空。如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象
ThreadGroup类实现了Thread.UncaughtExceptionHandler接口,它的uncaughtException方法做如下操作:
- 如果该线程组有父线程组,那么父线程组的uncaughtException方法被调用。
- 否则,如果Thread.getDefaultExceptionHandler方法返回一个非空的处理器,则调用该处理器。
- 否则,如果Throwable是ThreadDeath的一个实例(ThreadDeath对象由stop方法产生,而该方法已过时),什么都不做。
- 否则,线程的名字以及Throwable的栈踪迹被输出到System.error上。
如果是由线程池ThreadPoolExecutor执行任务,只有通过execute提交的任务,才能将它抛出的异常交给UncaughtExceptionHandler,而通过submit提交的任务,无论是抛出的未检测异常还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由submit提交的任务由于抛出了异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。