线程是什么?
从操作系统的角度,可以简单认为,线程是系统调度的最小单元,一个进程可以包含多个线程,作为任务的真正运行者,有自己的栈(Stack)、寄存器(Register)、本地存储(Thread Local)等,但是会和进程内其他线程共享文件描述符、虚拟地址空间等。
在具体实现中,线程还分为内核线程、用户线程,Java 的线程实现其实是与虚拟机相关的。
如果我们来看 Thread 的源码,你会发现其基本操作逻辑大都是以 JNI 形式调用的本地代码。
private native void start0();
private native void setPriority0(int newPriority);
private native void interrupt0();
什么是线程安全?
线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下共享的、可修改的状态的正确性,这里的状态反应在程序中其实可以看做是数据。即可变资源(内存)线程间共享
换个角度来看,如果状态不是共享的,或者不是可修改的,也就不存在线程安全问题,进而可以推理出保证线程安全的两个办法:
- 封装:通过封装,可以将对象内部状态隐藏、保护起来
- 不可变:如final 和 immutable
线程安全需要保证几个基本特性:
- 原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现.
- 可见性,是一个线程修改了某个共享变量,其状态能够立即被其它线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。
- 有序性,是保证线程内串行语义,避免指令重排等。
保证可见性的方法?
- 使用 final 关键字
- 使用 volatile 关键字
- 加锁,锁释放时会强制将缓存刷新到主内存
保证原子性的方法?
- 加锁,保证操作的互斥性
- 使用 CAS 指令(如 Unsafe.compareAndSwapInt)
- 使用原子数值类型(如 AtomicInteger)
- 使用原子属性更新器(AtomicReferenceFieldUpdate)
如何实现线程安全?
- 不共享资源
- 共享不可变资源
- 共享可变资源
- 可见性
- 操作原子性
- 禁止重排序
1、进程和线程的区别
- 线程不能看做独立应用,而进程可看做独立应用
- 进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径
- 线程没有独立的地址空间,多进程的程序比多线程健壮
- 进程的切换比线程的切换开销大
- 一个进程里面可以有多条线程,至少有一条线程。一条线程一定会在一个进程里面
- 进程是资源分配的最小单位,线程是CPU调度的最小单位。
- 所有与进程相关的资源,都被记录在PCB(进程控制块)中
- 进程是抢占处理机的调度单位;线程属于某个进程,共享其资源
- 线程只由堆栈寄存器、程序计数器和TCB(线程控制块)组成
2、Thread 中的 start 和 run 方法的区别
- 调用 start() 方法会创建一个新的子线程并启动
- run() 方法只是 Thread 的一个普通方法的调用
Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread() 其实只是 new 出一个 Thread 的实例,还没有操作系统中真正的线程挂起钩来。 只有执行了 start()方法后,才实现了真正意义上的启动线程。
start()方法让一个线程进入就绪队列等待分配 cpu,分到 cpu 后才调用实现 的 run()方法,start()方法不能重复调用,如果重复调用会抛出异常。
而 run 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方 法并没有任何区别,可以重复执行,也可以被单独调用。
3、一个线程两次调用start()方法会出现什么情况?
Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误。
在第二次调用 start() 方法的时候,线程可能处于终止或者其他(非 NEW)状态,但是不论如何,都是不可以再次启动的。
4、Thread 和 Runnable 是什么关系及区别
关系:
- Thread 是实现了 Runnable 接口的类,使的 run 支持多线程
- 因类的单一继承原则,推荐多使用 Runnable 接口
区别:
Thread 才是 Java 里对线程的唯一抽象,Runnable 只是对任务(业务逻辑) 的抽象。Thread 可以接受任意一个 Runnable 的实例并执行。
5、有几种新启线程的方式?
有两种
- 类 Thread
- 接口 Runnable
启动线程的方式有:
- 1、X extends Thread;,然后 X.start
- 2、X implements Runnable;然后交给 Thread 运行
从 Thread.java 源码中可知,有两种启动线程的方式。
6、如何给 run() 方法传参
- 构造函数传参
- 成员变量传参(通过变量或者方法传递参数)
- 回调函数传参
具体详解可参考:Java多线程:向线程传递参数的三种方法
7、如何实现处理线程的返回值
- 主线程等待法
- 使用 Thread 类的 join() 阻塞当前线程以等待子线程处理完毕
- 通过 Callable 接口实现:通过FutureTask or 线程池获取
主线程等待法
缺点:需要自己实现循环等待的逻辑,当需要等待的变量一多,代码变得臃肿;也没法精准的控制
代码示例如下:
public class CycleWait implements Runnable {
private String value;
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = "have data";
}
public static void main(String[] args) {
CycleWait cycleWait = new CycleWait();
Thread thread = new Thread(cycleWait);
thread.start();
while (cycleWait.value == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("value = " + cycleWait.value);
}
}
使用 Thread 类的 join() 阻塞当前线程以等待子线程处理完毕
代码示例如下:
public class CycleWait implements Runnable {
private String value;
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
value = "have data";
}
public static void main(String[] args) {
CycleWait cycleWait = new CycleWait();
Thread thread = new Thread(cycleWait);
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("value = " + cycleWait.value);
}
}
通过 Callable 接口实现:通过FutureTask or 线程池获取
Callable 接口的实现类MyCallable示例如下:
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
String value = "test";
System.out.println("Ready to work");
Thread.sleep(5000);
System.out.println("task done");
return value;
}
}
FutureTask 示例如下
public class FutureTaskDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> task = new FutureTask<>(new MyCallable());
new Thread(task).start();
if (!task.isDone()) {
System.out.println("task has not finish, please wait");
}
System.out.println("task return:" + task.get());
}
}
线程池 示例如下
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> future = executor.submit(new MyCallable());
if (!future.isDone()) {
System.out.println("task has not finish, please wait");
}
try {
System.out.println("task return:" + future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
//关闭线程池
executor.shutdown();
}
}
}
FutureTask 和 线程池 示例运行结果
8、线程的状态
线程状态被明确定义在其公共内部枚举类型 java.lang.Thread.State 中,分别是:
- 新建(NEW),表示线程被创建出来还没真正启动的状态,可以认为它是个 Java 内部状态。
- 就绪(RUNNABLE),表示该线程已经在 JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它 CPU 片段,在就绪队列里排队。
- 在其他一些分析中,会额外区分一种状态 RUNNING,但是从Java API 的角度,并不能表示出来。
- 阻塞(BLOCKED),阻塞表示线程在等待 Monitor lock。比如,线程试图通过 Synchronized 去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。
- 等待(WAITING),表示正在等待其他线程采取某些操作。一个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似 notify 等动作,通知消费线程可以继续工作了。Thread.join() 也会令线程进入等待状态。
- 计时等待(TIMED_WAITING),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如 wait 或 join 等方法的指定超时版本,在一定时间后会由系统自动唤醒,如下面示例:
public final native void wait(long timeout) throws InterruptedException; - 终止(TERMINATED),不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死忙。
状态和方法之间的对应图:
9、sleep 和 wait 的区别
- sleep 是 Thread 类的方法,wait 是 Object 类中定义的方法
- sleep 方法可以在任何地方使用
- wait 方法只能在 Synchronized 方法或 Synchronized 块中使用(原因如下:为什么wait()和notify()需要搭配synchonized关键字使用
) - sleep 只会让出 CPU,不会导致锁行为的改变
- wait 不仅让出 CPU,还会释放已经占有的同步资源锁
10、notify 和 notifyAll 的区别
- notifyAll 会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会
- notify 只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会
锁池
假设线程 A 已经拥有了某个对象(不是类)的锁,而其它线程B、C想要调用这个对象的某个 Synchronized 方法(或者块),由于B、C线程在进入对象的 Synchronized 方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧该对象的锁目前正被线程 A 所占用,此时 B、C线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池
11、yield
当调用Thread.yield()函数时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示。
12、 如何中断线程
- 线程自然终止
要么是 run 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
- stop
暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend()、resume() 和 stop()。但是这些 API 是过期的,也就是不建议使用的。不建议使用的原因主要有:以 suspend() 方法为例,在调用后,线程不会释放已经占有的资源(比如 锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop() 方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、 resume()和 stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方 法。
- 中断
安全的中止则是其他线程通过调用某个线程 A 的 interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表 线程 A 会立即停止自己的工作,同样的 A 线程完全可以不理会这种中断请求。 因为 java 里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为 true 来进行响应。
线程通过方法 isInterrupted()来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()来进行判断当前线程是否被中断,不过 Thread.interrupted()会同时将中断标识位改写为 false。
如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、 thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在 这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即 将线程的中断标示位清除,即重新设置为 false。
不建议自定义一个取消标志位来中止线程的运行。因为 run 方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为,
- 一、一般的阻塞方法,如 sleep 等本身就支持中断的检查,
- 二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断
被抛弃的方法
- 通过调用 stop()方法停止线程
- 通过suspend() 和 resume() 方法
目前使用的方法
- 调用 interrupt() 方法,通知线程应该中断了
1.如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出一个 InterruptedException异常
2.如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将继续正常运行,不受影响 - interrupt()需要被调用的线程配合中断
在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志位就自行停止线程
13、线程状态
线程状态
yield()方法
使当前线程让出 CPU 占有权,但让出的时间是不可设定的。也不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行 yield( )的线程不一定就会持有锁,我们完全可以在释放锁后再调用 yield 方法。 所有执行 yield() 的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。
join()方法
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续 执行线程 B。
示例:
/**
* 演示Join()方法的使用
*/
public class UseJoin {
static class Goddess implements Runnable {
private Thread thread;
public Goddess(Thread thread) {
this.thread = thread;
}
public Goddess() {
}
public void run() {
System.out.println("Goddess开始排队打饭.....");
try {
if(thread!=null) thread.join();
} catch (InterruptedException e) {
}
SleepTools.second(2);//休眠2秒
System.out.println(Thread.currentThread().getName()
+ " Goddess打饭完成.");
}
}
static class GoddessBoyfriend implements Runnable {
public void run() {
SleepTools.second(2);//休眠2秒
System.out.println("GoddessBoyfriend开始排队打饭.....");
System.out.println(Thread.currentThread().getName()
+ " GoddessBoyfriend打饭完成.");
}
}
public static void main(String[] args) throws Exception {
Thread lison = Thread.currentThread();
GoddessBoyfriend goddessBoyfriend = new GoddessBoyfriend();
Thread gbf = new Thread(goddessBoyfriend);
Goddess goddess = new Goddess(gbf);
//Goddess goddess = new Goddess();
Thread g = new Thread(goddess);
g.start();
gbf.start();
System.out.println("lison开始排队打饭.....");
g.join();
SleepTools.second(2);//让主线程休眠2秒
System.out.println(Thread.currentThread().getName() + " lison打饭完成.");
}
}
运行结果:
线程的优先级
在 Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范围从 1~10,在线程构建的时候可以通过 setPriority(int) 方法来修改优先级,默认优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。 设置线程优先级时,针对频繁阻塞(休眠或者 I/O 操作)的线程需要设置较 高优先级,而偏重计算(需要较多 CPU 时间或者偏运算)的线程则设置较低的 优先级,确保处理器不会被独占。在不同的 JVM 以及操作系统上,线程规划会 存在差异,有些操作系统甚至会忽略对线程优先级的设定。
守护线程
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调 度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非 Daemon 线程的 时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true) 将线程设置 为 Daemon 线程。我们一般用不上,比如垃圾回收线程就是 Daemon 线程。
Daemon 线程被用作完成支持性工作,但是在 Java 虚拟机退出时 Daemon 线程中的 finally 块并不一定会执行。在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑。
示例:
/**
* 守护线程的使用
*/
public class DaemonThread {
private static class UseThread extends Thread {
@Override
public void run() {
try {
while (!isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ " I am extends Thread.");
}
System.out.println(Thread.currentThread().getName()
+ " interrupt flag is " + isInterrupted());
} finally {
//守护线程中finally不一定起作用
System.out.println(" .............finally");
}
}
}
public static void main(String[] args)
throws InterruptedException, ExecutionException {
UseThread useThread = new UseThread();
useThread.setDaemon(true);
useThread.start();
Thread.sleep(5);
useThread.interrupt();
}
}










网友评论