阅读地址:http://concurrent.redspider.group/article/01/1.html
基础篇
进程与线程基本概念
进程、线程、程序
进程:操作系统进行资源分配的基本单位
线程:操作系统进行调度的基本单位,即CPU分配时间的单位
程序:用某种编程语言编写,能够完成一定任务或者功能的代码集合,是指令和数据的有序集合,是一段静态代码
多进程的方式也可以实现并发,为什么我们要使用多线程?
- 进程间的通信比较复杂,而线程间的通信比较简单,通常情况下,我们需要使用共享资源,这些资源在线程间的通信比较容易。
- 进程是重量级的,而线程是轻量级的,故多线程方式的系统开销更小。
ProcessBuilder类是J2SE 1.5在Java.lang中新添加的一个新类,用于创建多进程
进程和线程的区别
进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O):
- 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
- 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
- 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。
Java多线程入门类和接口
在Java中如何使用多线程
- 继承
Thread 类,并重写run 方法; - 实现
Runnable 接口的run 方法;
Runnable 接口添加了@FunctionalInterface 注解,表示它是一个函数式接口,我们可以使用Java 8的函数式编程 来简化代码
public class Demo {
public static void main(String[] args) {
new Thread(() -> {
System.out.println("Java 8 匿名内部类");
}).start();
}
}
实际情况下,我们大多是直接调用下面两个构造方法:
Thread(Runnable target)
Thread(Runnable target, String name)
Thread类的几个常用方法
- currentThread():Thread类的静态方法,返回对当前正在执行的线程对象的引用
- start():开始执行线程的方法,java虚拟机会调用线程内的run()方法
- yield():yield在英语里有放弃的意思,同样,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,
就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的 - sleep():Thread类的静态方法,使当前线程睡眠一段时间
- join():使当前线程等待另一个线程执行完毕之后再继续执行,
内部调用的是Object类的wait方法实现的
Thread类与Runnable接口的比较(迷)
Thread 类或者实现Runnable 接口这两种实现多线程的方式,它们之间有什么优劣呢?
- 由于Java
单继承,多实现 的特性,Runnable接口使用起来比Thread更灵活 - Runnable接口出现更符合面向对象,
将线程单独进行对象的封装 - Runnable接口出现,降低了
线程对象和线程任务的耦合性 - 如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口
更为轻量
Callable、Future与FutureTask
通常来说,我们使用Runnable 和Thread 来创建一个新的线程。但是它们有一个弊端,就是run 方法是没有返回值的。而有时候我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值。
JDK提供了Callable 接口与Future 接口为我们解决这个问题,这也是所谓的“异步”模型。
Callable接口
Callable 与Runnable 类似,同样是只有一个抽象方法的函数式接口。不同的是,Callable 提供的方法是有返回值的,而且支持泛型。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Callable 一般是配合线程池工具ExecutorService 来使用的。ExecutorService 可以使用submit 方法来让一个Callable 接口执行。它会返回一个Future ,我们后续的程序可以通过这个Future 的get 方法得到结果。
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return 2;
}
public static void main(String args[]) throws Exception {
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> result = executor.submit(new Task());
System.out.println(result.get());
}
}
Future接口
Future 接口只有几个比较简单的方法:
cancel :试图取消 一个线程的执行。注意是试图取消 ,并不一定能取消成功 。因为任务可能已完成、已取消、或者一些其它因素不能取消,存在取消失败的可能。isCancelled :线程是否已经取消isDone :线程是否已经完成
有时候,为了让任务有能够取消的功能,就使用Callable 来代替Runnable 。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。
FutureTask类
上面介绍了Future 接口。这个接口有一个实现类叫FutureTask 。FutureTask 是实现的RunnableFuture 接口的,而RunnableFuture 接口同时继承了Runnable 接口和Future 接口:
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
那FutureTask 类有什么用?为什么要有一个FutureTask 类?前面说到了Future 只是一个接口,而它里面的cancel ,get ,isDone 等方法要自己实现起来都是非常复杂的。所以JDK提供了一个FutureTask 类来供我们使用。
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return 2;
}
public static void main(String args[]) throws Exception {
ExecutorService executor = Executors.newCachedThreadPool();
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
executor.submit(futureTask);
System.out.println(futureTask.get(5,TimeUnit.SECONDS));
}
}
这个代码上与第一个Demo有一点小的区别:
- 这里调用
submit 方法是没有返回值的。这里实际上是调用的submit(Runnable task) 方法,而上面的Demo,调用的是submit(Callable<T> task) 方法。 - 这里是使用
FutureTask 直接取get 取值,而上面的Demo是通过submit 方法返回的Future 去取值。
在很多高并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能够在高并发环境下确保任务只执行一次 。
FutureTask的几个状态
/**
*
* state可能的状态转变路径如下:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
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是任务完成后的瞬时状态。
线程组和线程优先级
线程组(ThreadGroup)
Java中用ThreadGroup来表示线程组,我们可以使用线程组对线程进行批量控制。ThreadGroup和Thread的关系就如同他们的字面意思一样简单粗暴,每个Thread必然存在于一个ThreadGroup中 ,Thread不能独立于ThreadGroup存在。执行main()方法线程的名字是main ,如果在new Thread时没有显式指定 ,那么默认将父线程 (当前执行new Thread的线程)线程组设置为自己的线程组。
public class Demo {
public static void main(String[] args) {
Thread testThread = new Thread(() -> {
System.out.println("testThread当前线程组名字:" +
Thread.currentThread().getThreadGroup().getName());
System.out.println("testThread线程名字:" +
Thread.currentThread().getName());
});
testThread.start();
System.out.println("执行main所在线程的线程组名字: " + Thread.currentThread().getThreadGroup().getName());
System.out.println("执行main方法线程名字:" + Thread.currentThread().getName());
}
}
ThreadGroup管理着它下面的Thread,ThreadGroup是一个标准的向下引用 的树状结构,这样设计的原因是防止"上级"线程被"下级"线程引用而无法有效地被GC回收。
线程的优先级
Java中线程优先级可以指定,范围是1~10 。但是并不是所有的操作系统都支持10级优先级的划分(比如有些操作系统只支持3级划分:低,中,高),Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。
Java默认的线程优先级为5,线程的执行顺序由调度程序来决定,线程的优先级会在线程被调用之前设定。
通常情况下,高优先级的线程将会比低优先级的线程有更高的几率得到执行。我们使用方法Thread 类的setPriority() 实例方法来设定线程的优先级。
public class Demo {
public static void main(String[] args) {
Thread a = new Thread();
System.out.println("我是默认线程优先级:"+a.getPriority());
Thread b = new Thread();
b.setPriority(10);
System.out.println("我是设置过的线程优先级:"+b.getPriority());
}
}
但实际上真正的业务并不会采用这种方法来指定一些线程执行的先后顺序,因为Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定会采纳。而真正的调用顺序,是由操作系统的线程调度算法决定的 。
public class Demo {
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();
});
}
}
Java提供一个线程调度器 来监视和控制处于RUNNABLE状态 的线程。线程的调度策略采用抢占式 ,优先级高的线程比优先级低的线程会有更大的几率优先执行。
在优先级相同的情况下,线程执行按照“先到先得”的原则。每个Java程序都有一个默认的主线程,就是通过JVM启动的第一个线程main线程。
如果某个线程优先级大于线程所在线程组的最大优先级 ,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级
守护线程(Daemon)
守护线程默认的优先级比较低,通过Thread类的setDaemon(boolean on)来将非守护线程设置成守护线程。如果某线程是守护线程,那如果所有的非守护线程都结束了,这个守护线程也会自动结束。
应用场景是:当所有非守护线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。
线程组的常用方法
获取当前的线程组名字
Thread.currentThread().getThreadGroup().getName()
复制线程组
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
Thread[] threads = new Thread[threadGroup.activeCount()];
threadGroup.enumerate(threads);
线程组统一异常处理
public class ThreadGroupDemo {
@Test
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("group") {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + ": " + e.getMessage());
}
};
Thread thread = new Thread(threadGroup, new Runnable() {
@Override
public void run() {
throw new RuntimeException("测试异常");
}
});
thread.start();
}
}
线程组的数据结构
线程组还可以包含其他的线程组,不仅仅是线程。
首先看看 ThreadGroup 源码中的成员变量
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
private final ThreadGroup parent;
String name;
int maxPriority;
boolean destroyed;
boolean daemon;
boolean vmAllowSuspension;
int nUnstartedThreads = 0;
int nthreads;
Thread threads[];
int ngroups;
ThreadGroup groups[];
}
然后看看构造函数:
private ThreadGroup() {
this.name = "system";
this.maxPriority = Thread.MAX_PRIORITY;
this.parent = null;
}
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 方法,这里看看这个方法的源码:
private static Void checkParentAccess(ThreadGroup parent) {
parent.checkAccess();
return null;
}
public final void checkAccess() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkAccess(this);
}
}
其实Thread类也有一个checkAccess()方法,不过是用来当前运行的线程是否有权限修改被调用的这个线程实例。
总结来说,线程组是一个树状的结构,每个线程组下面可以有多个线程或者线程组。线程组可以起到统一控制线程的优先级和检查线程的权限的作用。
Java线程的状态及主要转化方法
操作系统中的线程状态转换
在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态是一致的。
操作系统线程主要有以下三个状态:
- 就绪状态(ready):线程正在等待使用CPU,经调度程序调用之后可进入running状态。
- 执行状态(running):线程正在使用CPU。
- 等待状态(waiting): 线程经过等待事件的调用或者正在等待其他资源(如I/O)。
Java线程的6个状态
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
NEW
处于NEW状态的线程此时尚未启动。这里的尚未启动指的是还没调用Thread实例的start()方法。
private void testStateNew() {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState());
}
反复调用同一个线程的start()方法是否可行?
在调用一次start()之后,threadStatus的值会改变(threadStatus !=0),此时再次调用start()方法会抛出IllegalThreadStateException异常
假如一个线程执行完毕(此时处于TERMINATED状态),再次调用这个线程的start()方法是否可行?
threadStatus为2代表当前线程状态为TERMINATED(threadStatus !=0),此时再次调用start()方法会抛出IllegalThreadStateException异常
RUNNABLE
表示当前线程正在运行中。处于RUNNABLE状态的线程在Java虚拟机中运行,也有可能在等待CPU分配资源。
看了操作系统线程的几个状态之后我们来看看Thread源码里对RUNNABLE状态的定义:
Java线程的RUNNABLE状态其实是包括了传统操作系统线程的ready和running两个状态的。
BLOCKED
阻塞状态。处于BLOCKED状态的线程正等待锁的释放以进入同步区。我们用BLOCKED状态举个生活中的例子:
假如今天你下班后准备去食堂吃饭。你来到食堂仅有的一个窗口,发现前面已经有个人在窗口前了,此时你必须得等前面的人从窗口离开才行。
假设你是线程t2,你前面的那个人是线程t1。此时t1占有了锁(食堂唯一的窗口),t2正在等待锁的释放,所以此时t2就处于BLOCKED状态。
WAITING
等待状态。处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。
调用如下3个方法会使线程进入等待状态:
- Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
- Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法;
- LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。
我们延续上面的例子继续解释一下WAITING状态:
你等了好几分钟现在终于轮到你了,突然你们有一个“不懂事”的经理突然来了。你看到他你就有一种不祥的预感,果然,他是来找你的。
他把你拉到一旁叫你待会儿再吃饭,说他下午要去作报告,赶紧来找你了解一下项目的情况。你心里虽然有一万个不愿意但是你还是从食堂窗口走开了。
此时,假设你还是线程t2,你的经理是线程t1。虽然你此时都占有锁(窗口)了,“不速之客”来了你还是得释放掉锁。此时你t2的状态就是WAITING。然后经理t1获得锁,进入RUNNABLE状态。
要是经理t1不主动唤醒你t2(notify、notifyAll..),可以说你t2只能一直等待了。
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等待期间就属于TIMED_WATING状态。t1等待10分钟后,就自动唤醒,拥有了去争夺锁的资格。
TERMINATED
终止状态。此时线程已执行完毕。
线程状态的转换
BLOCKED与RUNNABLE状态的转换
处于BLOCKED状态的线程是因为在等待锁的释放。假如这里有两个线程a和b,a线程提前获得了锁并且暂未释放锁,此时b就处于BLOCKED状态。
WAITING状态与RUNNABLE状态的转换
根据转换图我们知道有3个方法可以使线程从RUNNABLE状态转为WAITING状态。我们主要介绍下Object.wait() 和Thread.join() 。
# Object.wait()
调用wait()方法前线程必须持有对象的锁。线程调用wait()方法时,会释放当前的锁,直到有其他线程调用notify()/notifyAll()方法唤醒等待锁的线程。
需要注意的是,其他线程调用notify()方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用wait()方法的线程。
同样,调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。
# Thread.join()
调用join()方法,其他的线程会一直等待这个线程执行完毕(转换为TERMINATED状态)。一般是先start然后join
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状态。
线程中断
在某些情况下,我们在线程启动后发现并不需要它继续执行下去时,需要中断线程。目前在Java里还没有安全直接的方法来停止线程,但是Java提供了线程中断机制来处理需要中断线程的情况 。
线程中断机制是一种协作机制。需要注意,通过中断操作并不能直接终止一个线程 ,而是通知需要被中断的线程自行处理 。
简单介绍下Thread类里提供的关于线程中断的几个方法:
- Thread.interrupt():中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为true(默认是flase);
- Thread.interrupted():测试当前线程是否被中断。线程的中断状态受这个方法的影响,意思是调用一次使线程中断状态设置为true,连续调用两次会使得这个线程的中断状态重新转为false;
- Thread.isInterrupted():测试当前线程是否被中断。与上面方法不同的是调用这个方法并不会影响线程的中断状态。
|