JDK提供了 Thread 类和 Runnalble 接⼝来让我们 实现⾃⼰的“线程”类。
- 继承 Thread 类,并重写 run ⽅法;
- 实现 Runnable 接⼝的 run ⽅法;
⾸先是继承 Thread 类:
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
注意要调⽤ start() ⽅法后,该线程才算启动!
- 我们在程序⾥⾯调⽤了start()⽅法后,虚拟机会先为我们创建⼀个线程,然 后等到这个线程第⼀次得到时间⽚时再调⽤run()⽅法。
- 注意不可多次调⽤start()⽅法。在第⼀次调⽤start()⽅法后,再次调⽤start() ⽅法会抛出异常。
接着我们来看⼀下 Runnable 接⼝(JDK 1.8 +):
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
可以看到 Runnable 是⼀个函数式接⼝,这意味着我们可以使⽤Java 8的函数式编 程来简化代码。
public static class MyThread implements Runnable{
public void run() {
System.out.println("mythread");
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread).start();
}
1.1.3Thread类构造⽅法
Thread 类是⼀个 Runnable 接⼝的实现类,我们来看看 Thread 类的源码。
查看 Thread 类的构造⽅法,发现其实是简单调⽤⼀个私有的 init ⽅法来实现初 始化。
// Thread类源码
// ⽚段1 - init⽅法
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals)
// ⽚段2 - 构造函数调⽤init⽅法
public Thread(Runnable target) {
init(null, target, "Thread-" +nextThreadNum(), 0);
}
// ⽚段3 - 使⽤在init⽅法⾥初始化AccessControlContext类型的私有属性
this.inheritedAccessControlContext =acc != null ? acc : AccessController.getContext();
// ⽚段4 - 两个对⽤于⽀持ThreadLocal的私有属性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
- g:线程组,指定这个线程是在哪个线程组下;
- target:指定要执⾏的任务;
- name:线程的名字,多个线程的名字是可以重复的。如果不指定名字,⻅⽚ 段2;
- acc:⻅⽚段3,⽤于初始化私有变量 inheritedAccessControlContext 。
- inheritThreadLocals:可继承的 ThreadLocal ,⻅⽚段4, Thread 类⾥⾯有两 个私有属性来⽀持 ThreadLocal 。
- currentThread():静态⽅法,返回对当前正在执⾏的线程对象的引⽤;
- start():开始执⾏线程的⽅法,java虚拟机会调⽤线程内的run()⽅法;
- yield():yield在英语⾥有放弃的意思,同样,这⾥的yield()指的是当前线程愿
意让出对当前处理器的占⽤。这⾥需要注意的是,就算当前线程调⽤了yield()
⽅法,程序在调度的时候,也还有可能继续运⾏这个线程的; - sleep():静态⽅法,使当前线程睡眠⼀段时间;
- join():使当前线程等待另⼀个线程执⾏完毕之后再继续执⾏,内部调⽤的是 Object类的wait⽅法实现的;
- 由于Java“单继承,多实现”的特性,Runnable接⼝使⽤起来⽐Thread更灵活。
- Runnable接⼝出现更符合⾯向对象,将线程单独进⾏对象的封装。
- Runnable接⼝出现,降低了线程对象和线程任务的耦合性。
- 如果使⽤线程时不需要使⽤Thread类的诸多⽅法,显然使⽤Runnable接⼝更 为轻量。
所以,我们通常优先使⽤“实现 Runnable 接⼝”这种⽅式来⾃定义线程类。
1.2 Callable、Future与FutureTask我们使⽤ Runnable 和 Thread 来创建⼀个新的线程。但是它们有⼀个弊 端,就是 run ⽅法是没有返回值的。⽽有时候我们希望开启⼀个线程去执⾏⼀个任 务,并且这个任务执⾏完成后有⼀个返回值。
JDK提供了 Callable 接⼝与 Future 类为我们解决这个问题,这也是所谓的“异步” 模型。
1.2.1 Callable接⼝Callable 与 Runnable 类似,同样是只有⼀个抽象⽅法的函数式接⼝。不同的 是, Callable 提供的⽅法是有返回值的,⽽且⽀持泛型。
@FunctionalInterface public interface Callable{ V call() throws Exception; }
Callable ⼀般是配合线程池⼯ 具 ExecutorService 来使⽤的。ExecutorService 可以使⽤ submit ⽅法来让⼀个 Callable 接⼝执⾏。它会返回 ⼀个 Future ,我们后续的程序可以通过这个 Future 的 get ⽅法得到结果。
// ⾃定义Callable class Task implements Callable{ @Override public Integer call() throws Exception { // 模拟计算需要⼀秒 Thread.sleep(1000); return 2; } public static void main(String args[]){ // 使⽤ ExecutorService executor = Executors.newCachedThreadPool(); Task task = new Task(); Future result = executor.submit(task); // 注意调⽤get⽅法会阻塞当前线程,直到得到结果。 // 所以实际编码中建议使⽤可以设置超时时间的重载get⽅法。 System.out.println(result.get()); } }
输出结果:21.2.2 Future接⼝
Future 接⼝只有⼏个⽐较简单的⽅法:
public interface Future{ boolean cancel(boolean var1); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long var1, TimeUnit var3) throws InterruptedException, ExecutionException, TimeoutException; }
cancel ⽅法是试图取消⼀个线程的执⾏。
注意是试图取消,并不⼀定能取消成功。因为任务可能已完成、已取消、或者⼀些 其它因素不能取消,存在取消失败的可能。 boolean 类型的返回值是“是否取消成 功”的意思。参数 paramBoolean 表示是否采⽤中断的⽅式取消线程执⾏。
所以有时候,为了让任务有能够取消的功能,就使⽤ Callable 来代替 Runnable 。 如果为了可取消性⽽使⽤ Future 但⼜不提供可⽤的结果,则可以声明 Future 形式类型、并返回 null 作为底层任务的结果。
1.2.3 FutureTask类上⾯介绍了 Future 接⼝。这个接⼝有⼀个实现类叫 FutureTask 。 FutureTask 是 实现的 RunnableFuture 接⼝的,⽽ RunnableFuture 接⼝同时继承了 Runnable 接⼝ 和 Future 接⼝:
public class FutureTaskimplements RunnableFuture { ...... }
public interface RunnableFutureextends Runnable, Future { void run(); }
那 FutureTask 类有什么⽤?为什么要有⼀个 FutureTask 类?前⾯说到 了 Future 只是⼀个接⼝,⽽它⾥⾯的 cancel , get , isDone 等⽅法要⾃⼰实现 起来都是⾮常复杂的。所以JDK提供了⼀个 FutureTask 类来供我们使⽤。
public static class Task implements Callable{ public Integer call() throws Exception { Thread.sleep(1000); return 2; } } public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = Executors.newCachedThreadPool(); FutureTask futureTask = new FutureTask (new Task()); executorService.submit(futureTask); System.out.println(futureTask.get()); }
使⽤上与第⼀个Demo有⼀点⼩的区别 。⾸先,调⽤ submit ⽅法是没有返回值的。 这⾥实际上是调⽤的 submit(Runnable task) ⽅法,⽽上⾯的Demo,调⽤的 是 submit(Callable task) ⽅法。
然后,这⾥是使⽤ FutureTask 直接取 get 取值,⽽上⾯的Demo是通过 submit ⽅ 法返回的 Future 去取值。
在很多⾼并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能 够在⾼并发环境下确保任务只执⾏⼀次。
1.2.4 FutureTask的⼏个状态
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
state表示任务的运⾏状态,初始状态为NEW。运⾏状态只会在set、 setException、cancel⽅法中终⽌。COMPLETING、INTERRUPTING是任 务完成后的瞬时状态。
2、 线程组和线程优先级 2.1 线程组(ThreadGroup)Java中⽤ThreadGroup来表示线程组,我们可以使⽤线程组对线程进⾏批量控制。
ThreadGroup和Thread的关系就如同他们的字⾯意思⼀样简单粗暴,每个Thread必 然存在于⼀个ThreadGroup中,Thread不能独⽴于ThreadGroup存在。执⾏main() ⽅法线程的名字是main,如果在new Thread时没有显式指定,那么默认将⽗线程 (当前执⾏new Thread的线程)线程组设置为⾃⼰的线程组。
public class ThreadGroupTest {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("testThread当前线程组的名字" + Thread.currentThread().getThreadGroup().getName());
System.out.println("testThread线程名字" + Thread.currentThread().getName());
});
thread.start();
System.out.println("执行main方法线程名字:"+Thread.currentThread().getName());
}
}
执⾏main⽅法线程名字:main testThread当前线程组名字:main testThread线程名字:Thread-0
ThreadGroup管理着它下⾯的Thread,ThreadGroup是⼀个标准的向下引⽤的树状 结构,这样设计的原因是防⽌"上级"线程被"下级"线程引⽤⽽⽆法有效地被GC回 收。
2.2 线程的优先级Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都⽀持10 级优先级的划分(⽐如有些操作系统只⽀持3级划分:低,中,⾼),Java只是给 操作系统⼀个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系 统决定。
Java默认的线程优先级为5,线程的执⾏顺序由调度程序来决定,线程的优先级会 在线程被调⽤之前设定。
通常情况下,⾼优先级的线程将会⽐低优先级的线程有更⾼的⼏率得到执⾏。我们 使⽤⽅法 Thread 类的 setPriority() 实例⽅法来设定线程的优先级。
public static void main(String[] args) {
Thread thread = new Thread();
System.out.println("我是默认线程优先级:"+thread.getPriority());
Thread thread1 = new Thread();
thread1.setPriority(10);
System.out.println(thread1.getPriority());
}
我是默认线程优先级:5 我是设置过的线程优先级:10
既然有1-10的级别来设定了线程的优先级,这时候可能有些读者会问,那么我是不 是可以在业务实现的时候,采⽤这种⽅法来指定⼀些线程执⾏的先后顺序?
NO,大肉特肉。
Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给 操作系统⼀个建议,操作系统不⼀定会采纳。⽽真正的调⽤顺序,是由操作系统的线程调度算法决定的。
public static class T1 extends Thread{
@Override
public void run() {
super.run();
System.out.println(String.format("当前执⾏的线程是:%s,优先级:%d",
Thread.currentThread().getName(),
Thread.currentThread().getPriority()));
}
}
public static void main(String[] args) {
IntStream.range(1,10).forEach(i->{
Thread thread = new Thread(new T1());
thread.setPriority(i);
thread.start();
});
}
某次输出:
当前执⾏的线程是:Thread-9,优先级:5 当前执⾏的线程是:Thread-1,优先级:1 当前执⾏的线程是:Thread-13,优先级:7 当前执⾏的线程是:Thread-7,优先级:4 当前执⾏的线程是:Thread-15,优先级:8 当前执⾏的线程是:Thread-17,优先级:9 当前执⾏的线程是:Thread-3,优先级:2 当前执⾏的线程是:Thread-11,优先级:6 当前执⾏的线程是:Thread-5,优先级:3
Java提供⼀个线程调度器来监视和控制处于RUNNABLE状态的线程。**线程的调度策略采⽤抢占式,优先级⾼的线程⽐优先级低的线程会有更⼤的⼏率优先执⾏。**在 优先级相同的情况下,按照“先到先得”的原则。每个Java程序都有⼀个默认的主线程,就是通过JVM启动的第⼀个线程main线程。
还有⼀种线程称为守护线程(Daemon),守护线程默认的优先级⽐较低。 (jvm中的GC回收就是属于守护线程)
- 如果某线程是守护线程,那如果所有的⾮守护线程结束,这个守护线程也会 ⾃动结束。
- 应⽤场景是:当所有⾮守护线程结束时,结束其余的⼦线程(守护线程)⾃ 动关闭,就免去了还要继续关闭⼦线程的麻烦。
- ⼀个线程默认是⾮守护线程,可以通过Thread类的setDaemon(boolean on) 来设置。
在之前,我们有谈到⼀个线程必然存在于⼀个线程组中,那么当线程和线程组的优 先级不⼀致的时候将会怎样呢?我们⽤下⾯的案例来验证⼀下:
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("t1");
threadGroup.setMaxPriority(6);
Thread thread = new Thread(threadGroup, "thread");
thread.setPriority(9);
System.out.println(threadGroup.getMaxPriority());
System.out.println(thread.getPriority());
}
我是线程组的优先级6 我是线程的优先级6
所以,如果某个线程优先级⼤于线程所在线程组的最⼤优先级,那么该线程的优先级将会失效,取⽽代之的是线程组的最⼤优先级。
2.3 线程组的常⽤⽅法及数据结构线程组还可以包含其他的线程组,不仅仅是线程。
⾸先看看 ThreadGroup 源码中的成员变量
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
private final ThreadGroup parent; // ⽗亲ThreadGroup
String name; // ThreadGroupr 的名称
int maxPriority; // 线程最⼤优先级
boolean destroyed; // 是否被销毁
boolean daemon; // 是否守护线程
boolean vmAllowSuspension; // 是否可以中断
int nUnstartedThreads = 0; // 还未启动的线程
int nthreads; // ThreadGroup中线程数⽬
Thread threads[]; // ThreadGroup中的线程
int ngroups; // 线程组数⽬
ThreadGroup groups[]; // 线程组数组
}
然后看看构造函数:
// 私有构造函数
private ThreadGroup() {
this.name = "system";
this.maxPriority = Thread.MAX_PRIORITY;
this.parent = null;
}
// 默认是以当前ThreadGroup传⼊作为parent ThreadGroup,新线程组的⽗线程组是⽬前正在运⾏线
public ThreadGroup(String name) {
this(Thread.currentThread().getThreadGroup(), name);
}
// 构造函数
public ThreadGroup(ThreadGroup parent, String name) {
this(checkParentAccess(parent), parent, name);
}
// 私有构造函数,主要的构造函数
private ThreadGroup(Void unused, ThreadGroup parent, String name) {
this.name = name;
this.maxPriority = parent.maxPriority;
this.daemon = parent.daemon;
this.vmAllowSuspension = parent.vmAllowSuspension;
this.parent = parent;
parent.add(this);
}
第三个构造函数⾥调⽤了 checkParentAccess ⽅法,这⾥看看这个⽅法的源码:
// 检查parent ThreadGroup
private static Void checkParentAccess(ThreadGroup parent) {
parent.checkAccess();
return null;
}
// 判断当前运⾏的线程是否具有修改线程组的权限
public final void checkAccess() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkAccess(this);
}
}
总结来说,线程组是⼀个树状的结构,每个线程组下⾯可以有多个线程或者线程 组。线程组可以起到统⼀控制线程的优先级和检查线程的权限的作用。
3、 Java线程的状态及主要转化⽅法 3.1操作系统中的线程状态转换⾸先我们来看看操作系统中的线程状态转换。
在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状 态其实和操作系统进程的状态是⼀致的。
操作系统线程主要有以下三个状态:
- 就绪状态(ready):线程正在等待使⽤CPU,经调度程序调⽤之后可进⼊ running状态。
- 执⾏状态(running):线程正在使⽤CPU。
- 等待状态(waiting): 线程经过等待事件的调⽤或者正在等待其他资源(如 I/O)。
NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;3.2.1 New
处于NEW状态的线程此时尚未启动。这⾥的尚未启动指的是还没调⽤Thread实例 的start()⽅法。
private void testStateNew() {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // 输出 NEW
}
从上⾯可以看出,只是创建了线程⽽并没有调⽤start()⽅法,此时线程处于NEW状 态。
- 反复调⽤同⼀个线程的start()⽅法是否可⾏?
- 假如⼀个线程执⾏完毕(此时处于TERMINATED状态),再次调⽤这个线程 的start()⽅法是否可⾏?
要分析这两个问题,我们先来看看start()的源码:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
我们可以看到,在start()内部,这⾥有⼀个threadStatus的变量。如果它不等于0, 调⽤start()是会直接抛出异常的。
private native void start0();
我们接着往下看,有⼀个native的 start0() ⽅法。这个⽅法⾥并没有对 threadStatus的处理。到了这⾥我们仿佛就拿这个threadStatus没辙了,我们通过 debug的⽅式再看⼀下:
public void testStartMethod() {
Thread thread = new Thread(() -> {});
thread.start(); // 第⼀次调⽤
thread.start(); // 第⼆次调⽤
}
- 第⼀次调⽤时threadStatus的值是0。
- 第⼆次调⽤时threadStatus的值不为0。
查看当前线程状态的源码:
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}
所以得出答案:
两个问题的答案都是不可⾏,在调⽤⼀次start()之后,threadStatus的值会改变(threadStatus !=0),此时再次调⽤start()⽅法会抛出 IllegalThreadStateException异常。
3.2.2 RUNNABLE表示当前线程正在运⾏中。处于RUNNABLE状态的线程在Java虚拟机中运⾏,也 有可能在等待其他系统资源(⽐如I/O)。
Java中线程的RUNNABLE状态
/**
*可运行线程的线程状态。可运行线程中的一个线程
*状态在Java虚拟机中执行,但它可以
*等待来自操作系统的其他资源
*如处理器。
*/
Java线程的RUNNABLE状态其实是包括了传统操作系统线程的ready和 running两个状态的。
3.2.3 BLOCKED阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进⼊同步区。
举个栗子:
假如今天你下班后准备去⻝堂吃饭。 你来到⻝堂仅有的⼀个窗⼝,发现前⾯已经有个⼈在窗⼝前了, 此时你必须得等前⾯的⼈从窗⼝离开才⾏。 假设你是线程t2,你前⾯的那个⼈是线程t1。 此时t1占有了锁(⻝堂唯⼀的窗⼝), t2正在等待锁的释放,所以此时t2就处于BLOCKED状态。3.2.4 WAITING
等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。
调⽤如下3个⽅法会使线程进⼊等待状态:
- Object.wait():使当前线程处于等待状态直到另⼀个线程唤醒它;
- Thread.join():等待线程执⾏完毕,底层调⽤的是Object实例的wait⽅法;
- LockSupport.park():除⾮获得调⽤许可,否则禁⽤当前线程进⾏线程调度。
我们延续上⾯的例⼦继续解释⼀下WAITING状态:
你等了好⼏分钟现在终于轮到你了,突然你们有⼀个“不懂事”的经理突然来了。 你看到他你就有⼀种不祥的预感,果然,他是来找你的。 他把你拉到⼀旁叫你待会⼉再吃饭,说他下午要去作报告,赶紧来找你了解⼀下项⽬的情况。 你⼼⾥虽然有⼀万个不愿意但是你还是从⻝堂窗⼝⾛开了。 此时,假设你还是线程t2,你的经理是线程t1。 虽然你此时都占有锁(窗⼝)了,“不速之客”来了你还是得释放掉锁。 此时你t2的状态就是WAITING。然后经理t1获得锁,进⼊RUNNABLE状态。 要是经理t1不主动唤醒你t2(notify、notifyAll..),可以说你t2只能⼀直等待了。3.2.5 TIMED_WAITING
超时等待状态。线程等待⼀个具体的时间,时间到后会被⾃动唤醒。
调⽤如下⽅法会使线程进⼊超时等待状态:
- Thread.sleep(long millis):使当前线程睡眠指定时间;
- Object.wait(long timeout):线程休眠指定时间,等待期间可以通过 notify()/notifyAll()唤醒;
- Thread.join(long millis):等待当前线程最多执⾏millis毫秒,如果millis为0,则 会⼀直执⾏;
- LockSupport.parkNanos(long nanos): 除⾮获得调⽤许可,否则禁⽤当前线 程进⾏线程调度指定时间;
- LockSupport.parkUntil(long deadline):同上,也是禁⽌线程进⾏调度指定时 间;
我们继续延续上⾯的例⼦来解释⼀下TIMED_WAITING状态:
到了第⼆天中午,⼜到了饭点,你还是到了窗⼝前。 突然间想起你的同事叫你等他⼀起,他说让你等他⼗分钟他改个bug。 好吧,你说那你就等等吧,你就离开了窗⼝。 很快⼗分钟过去了,你⻅他还没来,你想都等了这么久了还不来, 那你还是先去吃饭好了。 这时你还是线程t1,你改bug的同事是线程t2。 t2让t1等待了指定时间,t1先主动释放了锁。 此时t1等待期间就属于TIMED_WATING状态。 t1等待10分钟后,就⾃动唤醒,拥有了去争夺锁的资格。
等待超时就是等待设置了等待时间。
3.2.6 TERMINATED终⽌状态。此时线程已执⾏完毕。
3.3 线程状态的转换线程状态转换图:
3.3.1 BLOCKED与RUNNABLE状态的转换处于BLOCKED状态的线程是因为在等待锁的释放。假如这⾥有 两个线程a和b,a线程提前获得了锁并且暂未释放锁,此时b就处于BLOCKED状 态。我们先来看⼀个例⼦:
@Test
public void blocked () throws InterruptedException {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "a");
Thread b = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "b");
a.start();
b.start();
System.out.println(a.getName()+":"+a.getState());
System.out.println(b.getName()+":"+b.getState());
}
private synchronized void testMethod(){
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
初看之下,⼤家可能会觉得线程a会先调⽤同步⽅法,同步⽅法内⼜调⽤了 Thread.sleep()⽅法,必然会输出TIMED_WAITING,⽽线程b因为等待线程a释放 锁所以必然会输出BLOCKED。
其实不然,有两点需要值得⼤家注意,⼀是在测试⽅法blockedTest()内还有⼀个 main线程,⼆是启动线程后执⾏run⽅法还是需要消耗⼀定时间的。不打断点的情 况下,上⾯代码中都应该输出RUNNABLE。
测试⽅法的main线程只保证了a,b两个线程调⽤start()⽅法(转化为 RUNNABLE状态), 还没等两个线程真正开始争夺锁,就已经打印此时两个线程的状态(RUNNABLE)了。
这时你可能⼜会问了,要是我想要打印出BLOCKED状态我该怎么处理呢?其实就 处理下测试⽅法⾥的main线程就可以了,你让它“休息⼀会⼉”,调⽤ Thread.sleep⽅法就⾏。
public void blockedTest() throws InterruptedException {
······
a.start();
Thread.sleep(1000L); // 需要注意这⾥main线程休眠了1000毫秒,⽽testMethod()⾥休眠了
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出?
System.out.println(b.getName() + ":" + b.getState()); // 输出?
}
在这个例⼦中,由于main线程休眠,所以线程a的run()⽅法跟着执⾏,线程b再接 着执⾏。
在线程a执⾏run()调⽤testMethod()之后,线程a休眠了2000ms(注意这⾥是没有 释放锁的),main线程休眠完毕,接着b线程执⾏的时候是争夺不到锁的,所以这 ⾥输出:
a:TIMED_WAITING b:BLOCKED3.3.2 WAITING状态与RUNNABLE状态的转换
根据转换图我们知道有3个⽅法可以使线程从RUNNABLE状态转为WAITING状态。 我们主要介绍下**Object.wait()和Thread.join()。 **
Object.wait()
- 调⽤wait()⽅法前线程必须持有对象的锁。
- 线程调⽤wait()⽅法时,会释放当前的锁,直到有其他线程调⽤ notify()/notifyAll()⽅法唤醒等待锁的线程。
- 需要注意的是,其他线程调⽤notify()⽅法只会唤醒单个等待锁的线程,如有多个线程都在等待这个锁的话不⼀定会唤醒到之前调⽤wait()⽅法的线程。
- 同样,调⽤notifyAll()⽅法唤醒所有等待锁的线程之后,也不⼀定会⻢上把时 间⽚分给刚才放弃锁的那个线程,具体要看系统的调度。
Thread.join()
- 调⽤join()⽅法不会释放锁,会⼀直等待当前线程执⾏完毕(转换为 TERMINATED状态)。
public void blockedTest() {
······
a.start();
a.join();
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出 TERMINATED
System.out.println(b.getName() + ":" + b.getState());
}
a:TERMINATED b:TIMED_WAITING
要是没有调⽤join⽅法,main线程不管a线程是否执⾏完毕都会继续往下⾛。
a线程启动之后⻢上调⽤了join⽅法,这⾥main线程就会等到a线程执⾏完毕,所以 这⾥a线程打印的状态固定是TERMIATED。
⾄于b线程的状态,有可能打印RUNNABLE(尚未进⼊同步⽅法),也有可能打印 TIMED_WAITING(进⼊了同步⽅法)。
3.3.3 TIMED_WAITING与RUNNABLE状态转换TIMED_WAITING与WAITING状态类似,只是TIMED_WAITING状态等待的时间是 指定的。
Thread.sleep(long)
使当前线程睡眠指定时间。需要注意这⾥的“睡眠”只是暂时使线程停⽌执 ⾏,并不会释放锁。 时间到后,线程会重新进⼊RUNNABLE状态。
Object.wait(long)
wait(long)⽅法使线程进⼊TIMED_WAITING状态。这⾥的wait(long)⽅法与⽆参⽅法wait()相同的地⽅是,都可以通过其他线程调⽤notify()或notifyAll()⽅法来唤醒。 不同的地⽅是,有参⽅法wait(long)就算其他线程不来唤醒它,经过指定时间long之后它会⾃动唤醒,拥有去争夺锁的资格。
Thread.join(long)
join(long)使当前线程执⾏指定时间,并且使线程进⼊TIMED_WAITING状 态。
我们再来改⼀改刚才的示例:
public void blockedTest() {
······
a.start();
a.join(1000L);
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出 TIMED_WAITING
System.out.println(b.getName() + ":" + b.getState());
}
//这⾥调⽤a.join(1000L),因为是指定了具体a线程执⾏的时间的,并且执⾏时间是⼩于a线程sleep的时间,所以a线程状态输出TIMED_WAITING。
b线程状态仍然不固定(RUNNABLE或BLOCKED)。
3.3.4 线程中断在某些情况下,我们在线程启动后发现并不需要它继续执⾏下去时,需要中断线程。 ⽬前在Java⾥还没有安全直接的⽅法来停⽌线程, 但是Java提供了线程中断机制来处理需要中断线程的情况。 线程中断机制是⼀种协作机制。 需要注意,通过中断操作并不能直接终⽌⼀个线程,⽽是通知需要被中断的线程⾃⾏处理。
- Thread.interrupt():中断线程。这⾥的中断线程并不会⽴即停⽌线程,⽽是设 置线程的中断状态为true(默认是flase);
- Thread.interrupted():测试当前线程是否被中断。线程的中断状态受这个⽅法 的影响,意思是调⽤⼀次使线程中断状态设置为true,连续调⽤两次会使得这 个线程的中断状态重新转为false;
- Thread.isInterrupted():测试当前线程是否被中断。与上⾯⽅法不同的是调⽤ 这个⽅法并不会影响线程的中断状态。
在线程中断机制⾥,当其他线程通知需要被中断的线程后,线程中断的状态 被设置为true,但是具体被要求中断的线程要怎么处理,完全由被中断线程 ⾃⼰⽽定,可以在合适的实际处理中断请求,也可以完全不处理继续执⾏下 去。
4、Java线程间的通信合理的使⽤Java多线程可以更好地利⽤服务器资源。⼀般来讲,线程内部有⾃⼰私 有的线程上下⽂,互不⼲扰。但是当我们需要多个线程之间相互协作的时候,就需 要我们掌握Java线程的通信⽅式。本⽂将介绍Java线程之间的⼏种通信原理。
4.1 锁与同步在Java中,锁的概念都是基于对象的,所以我们⼜经常称它为对象锁。线程和锁的 关系,⼀个锁同⼀时间只能被⼀个线程持有。 其他线程如果需要得到这个锁,就得等这个线程释放该锁。
什么是同步呢?
即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。
举例:
假如我们现在有2位正在 抄暑假作业答案的同学:线程A和线程B。 当他们正在抄的时候,⽼师突然来修改 了⼀些答案,可能A和B最后写出的暑假作业就不⼀样。 我们为了A,B能写出2本相 同的暑假作业,我们就需要让⽼师先修改答案,然后A,B同学再抄。 或者A,B同学先抄完,⽼师再修改答案。这就是线程A,线程B的线程同步。
可以以解释为:线程同步是线程之间按照⼀定的顺序执⾏。
为了达到线程同步,我们可以使⽤锁来实现它。
我们先来看看⼀个⽆锁的程序:
public class NoneLock {
public static void main(String[] args) {
new Thread(new ThreadA()).start();
new Thread(new ThreadB()).start();
}
static class ThreadA implements Runnable{
@Override
public void run() {
for(int i=0;i<50;i++){
System.out.println("ThreadA"+i);
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
for(int i=0;i<50;i++){
System.out.println("ThreadB"+i);
}
}
}
}
执⾏这个程序,你会在控制台看到,线程A和线程B各⾃独⽴⼯作,输出⾃⼰的打 印值。如下是我的电脑上某⼀次运⾏的结果。每⼀次运⾏结果都会不⼀样。
ThreadB14 ThreadA41 ThreadB15 ThreadB16 ThreadB17 ThreadB18 ThreadA42 ThreadB19 ThreadB20 ThreadB21 ThreadB22
那我现在有⼀个需求,我想等A先执⾏完之后,再由B去执⾏,怎么办呢?最简单 的⽅式就是使⽤⼀个“对象锁”:
public class ObjectLock {
private static Object lock =new Object();
static class ThreadA implements Runnable{
@Override
public void run() {
synchronized (lock){
for (int i=0;i<50;i++){
System.out.println("Thread A"+i);
}
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
synchronized (lock){
for (int i=0;i<50;i++){
System.out.println("Thread B"+i);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(10);
new Thread(new ThreadB()).start();
}
}
这⾥声明了⼀个名字为 lock 的对象锁。我们在 ThreadA 和 ThreadB 内需要同步的 代码块⾥,都是⽤ synchronized 关键字加上了同⼀个对象锁 lock 。
上⽂我们说到了,根据线程和锁的关系,同⼀时间只有⼀个线程持有⼀个锁,那么 线程B就会等线程A执⾏完成后释放 lock ,线程B才能获得锁 lock 。
这⾥在主线程⾥使⽤sleep⽅法睡眠了10毫秒,是为了防⽌线程B先得到锁。 因为如果同时start,线程A和线程B都是出于就绪状态,操作系统可能会先让B运⾏。 这样就会先输出B的内容,然后B执⾏完成之后⾃动释放锁,线程A再执⾏。4.2 等待/通知机制
上⾯⼀种基于“锁”的⽅式,线程需要不断地去尝试获得锁,如果失败了,再继续尝 试。这可能会耗费服务器资源。
⽽等待/通知机制是另⼀种⽅式。
Java多线程的等待/通知机制是基于 Object 类的 wait() ⽅法和 notify() , notifyAll() ⽅法来实现的。
notify()⽅法会随机叫醒⼀个正在等待的线程,⽽notifyAll()会叫醒所有正在等 待的线程。
前⾯我们讲到,⼀个锁同⼀时刻只能被⼀个线程持有。⽽假如线程A现在持有了⼀ 个锁 lock 并开始执⾏,它可以使⽤ lock.wait() 让⾃⼰进⼊等待状态。这个时 候, lock 这个锁是被释放了的。
这时,线程B获得了 lock 这个锁并开始执⾏,它可以在某⼀时刻,使 ⽤ lock.notify() ,通知之前持有 lock 锁并进⼊等待状态的线程A,说“线程A你不 ⽤等了,可以往下执⾏了”。
需要注意的是,这个时候线程B并没有释放锁 lock , 除⾮线程B这个时候使⽤ lock.wait() 释放锁, 或者线程B执⾏结束⾃⾏释放锁,线程A才能得 到 lock 锁。
我们⽤代码来实现⼀下:
public class WaitAndNotify {
private static Object lock=new Object();
static class ThreadA implements Runnable{
@Override
public void run() {
synchronized (lock){
for (int i=0;i<5;i++){
try {
System.out.println("ThreadA:"+i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
synchronized (lock){
for (int i=0;i<5;i++){
try {
System.out.println("ThreadB:"+i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
输出:
ThreadA: 0
ThreadB: 0
ThreadA: 1
ThreadB: 1
ThreadA: 2
ThreadB: 2
ThreadA: 3
ThreadB: 3
ThreadA: 4
ThreadB: 4
线程A和线程B⾸先打印出⾃⼰需要的东⻄,然后使 ⽤ notify() ⽅法叫醒另⼀个正在等待的线程,然后⾃⼰使⽤ wait() ⽅法陷⼊等待 并释放 lock 锁。
需要注意的是等待/通知机制使⽤的是使⽤同⼀个对象锁,如果你两个线程使⽤的是不同的对象锁,那它们之间是不能⽤等待/通知机制通信的。
4.3 信号量JDK提供了⼀个类似于“信号量”功能的类 Semaphore 。但本⽂不是要介绍这个类, ⽽是介绍⼀种基于 volatile 关键字的⾃⼰实现的信号量通信。
后⾯会有专⻔的章节介绍 volatile 关键字,这⾥只是做⼀个简单的介绍。
volitile关键字能够保证内存的可⻅性,如果⽤volitile关键字声明了⼀个变量, 在⼀个线程⾥⾯改变了这个变量的值,那其它线程是⽴⻢可⻅更改后的值的。
⽐如我现在有⼀个需求,我想让线程A输出0,然后线程B输出1,再然后线程A输出 2…以此类推。我应该怎样实现呢?
public class Signal {
private static volatile int signal=0;
static class ThreadA implements Runnable{
@Override
public void run() {
while(signal<5){
if (signal%2==0){
System.out.println("ThreadA:"+signal);
synchronized (this){
signal++;
}
}
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
while(signal<5){
if (signal%2==1){
System.out.println("ThreadB:"+signal);
synchronized (this){
signal++;
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
// 输出:
threadA: 0
threadB: 1
threadA: 2
threadB: 3
threadA: 4
我们可以看到,使⽤了⼀个 volatile 变量 signal 来实现了“信号量”的模型。这⾥ 需要注意的是, volatile 变量需要进⾏原⼦操作。 signal++ 并不是⼀个原⼦操 作,所以我们需要使⽤ synchronized 给它“上锁”。
信号量的应⽤场景:
假如在⼀个停⻋场中,⻋位是我们的公共资源,线程就如同⻋辆,⽽看⻔的管理员 就是起的“信号量”的作⽤。
因为在这种场景下,多个线程(超过2个)需要相互合作,我们⽤简单的“锁”和“等 待通知机制”就不那么⽅便了。这个时候就可以⽤到信号量。
其实JDK中提供的很多多线程通信⼯具类都是基于信号量模型的。后面会介绍⼀些常⽤的通信⼯具类。 敬请期待
4.4 管道管道是基于“管道流”的通信⽅式。JDK提供了 PipedWriter 、 PipedReader 、 PipedOutputStream 、 PipedInputStream 。其中,前⾯两个是基于字符的,后⾯两个是基于字节流的。
这⾥的示例代码使⽤的是基于字符的:
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
public class Pipe {
static class ReaderThread implements Runnable{
private PipedReader reader;
public ReaderThread(PipedReader reader){
this.reader=reader;
}
@Override
public void run() {
System.out.println("this is reader");
int receive=0;
try {
while ((receive=reader.read())!=-1) {
System.out.println((char)receive);
// System.out.println(receive);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
static class WriterThread implements Runnable{
private PipedWriter writer;
public WriterThread(PipedWriter writer) {
this.writer = writer;
}
@Override
public void run() {
System.out.println("this is writer");
int receive=0;
try {
writer.write("test");
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws IOException, InterruptedException {
PipedWriter writer = new PipedWriter();
PipedReader reader = new PipedReader();
writer.connect(reader); //连接才能通信
new Thread(new ReaderThread(reader)).start();
Thread.sleep(1000);
new Thread(new WriterThread(writer)).start();
}
}
输出:
this is reader
this is writer
t
e
s
t
我们通过线程的构造函数,传⼊了 PipedWrite 和 PipedReader 对象。可以简单分析 ⼀下这个示例代码的执⾏流程:
- 线程ReaderThread开始执行
- 线程ReaderThread使用管道reader.read() 进⼊”阻塞“
- 线程WriterThread开始执⾏
- 线程WriterThread⽤writer.write(“test”)往管道写⼊字符串
- 线程WriterThread使⽤writer.close()结束管道写⼊,并执⾏完毕
- 线程ReaderThread接受到管道输出的字符串并打印
- 线程ReaderThread执⾏完毕。
管道通信的应⽤场景:
这个很好理解。使⽤管道多半与I/O流相关。当我们⼀个线程需要先另⼀个线程发 送⼀个信息(⽐如字符串)或者⽂件等等时,就需要使⽤管道通信了。
4.5 其它通信相关 4.5.1 join⽅法join()⽅法是Thread类的⼀个实例⽅法。它的作⽤是让当前线程陷⼊“等待”状态,等 join的这个线程执⾏完成后,再继续执⾏当前线程。
有时候,主线程创建并启动了⼦线程,如果⼦线程中需要进⾏⼤量的耗时运算,主线程往往将早于⼦线程结束之前结束。
如果主线程想等待⼦线程执⾏完毕后,获得⼦线程中的处理完的某个数据,就要⽤ 到join⽅法了。
public class Join {
static class ThreadA implements Runnable{
@Override
public void run() {
try {
System.out.println("我是子线程,我先睡一秒");
Thread.sleep(1000);
System.out.println("我是子线程,我已睡一秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadA());
thread.start();
thread.join();
System.out.println("如果不加join⽅法,我会先被打印出来,加了就不⼀样了");
}
}
注意join()⽅法有两个重载⽅法,⼀个是join(long), ⼀个是join(long, int)。 实际上,通过源码你会发现,join()⽅法及其重载⽅法底层都是利⽤了wait(long)这个⽅法。 对于join(long, int),通过查看源码(JDK 1.8)发现,底层并没有精确到纳秒,⽽是对第⼆个参数做了简单的判断和处理。4.5.2 sleep⽅法
sleep⽅法是Thread类的⼀个静态⽅法。它的作⽤是让当前线程睡眠⼀段时间。它有这样两个⽅法:
- Thread.sleep(long)
- Thread.sleep(long, int)
sleep⽅法是不会释放当前的锁的,⽽wait⽅法会。这也是最 常⻅的⼀个多线程⾯试题。
它们还有这些区别:
- wait可以指定时间,也可以不指定;⽽sleep必须指定时间。
- wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁。
- wait必须放在同步块或同步⽅法中,⽽sleep可以再任意位置
ThreadLocal是⼀个本地线程副本变量⼯具类。内部是⼀个弱引⽤的Map来维护。 这⾥不详细介绍它的原理,⽽是只是介绍它的使⽤,后面有单独介绍原理的文章。
有些朋友称ThreadLocal为线程本地变量或线程本地存储。严格来说,ThreadLocal 类并不属于多线程间的通信,⽽是**让每个线程有⾃⼰”独⽴“的变量,线程之间互不 影响。**它为每个线程都创建⼀个副本,每个线程可以访问⾃⼰内部的副本变量。
ThreadLocal类最常⽤的就是set⽅法和get⽅法。
public class ThreadLocalDemo {
static class ThreadA implements Runnable {
private ThreadLocal threadLocal;
public ThreadA(ThreadLocal threadLocal) {
this.threadLocal = threadLocal;
}
@Override
public void run() {
threadLocal.set("A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadA输出:"+threadLocal.get());
}
}
static class ThreadB implements Runnable{
private ThreadLocal threadLocal;
public ThreadB(ThreadLocal threadLocal) {
this.threadLocal = threadLocal;
}
@Override
public void run() {
threadLocal.set("B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadB输出:"+threadLocal.get());
}
}
public static void main(String[] args) {
ThreadLocal threadLocal=new ThreadLocal<>();
new Thread(new ThreadA(threadLocal)).start();
new Thread(new ThreadB(threadLocal)).start();
}
}
输出:
ThreadA输出:A
ThreadB输出:B
可以看到,虽然两个线程使⽤的同⼀个ThreadLocal实例(通过构造⽅法传⼊), 但是它们各⾃可以存取⾃⼰当前线程的⼀个值。
那ThreadLocal有什么作⽤呢?如果只是单纯的想要线程隔离,在每个线程中声明 ⼀个私有变量就好了呀,为什么要使⽤ThreadLocal?
如果开发者希望将类的某个静态变量(user ID或者transaction ID)与线程状态关 联,则可以考虑使⽤ThreadLocal。
最常⻅的ThreadLocal使⽤场景为⽤来解决数据库连接、Session管理等。数据库连 接和Session管理涉及多个复杂对象的初始化和关闭。如果在每个线程中声明⼀些 私有变量来进⾏操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接。



