什么是线程
为了说明什么是线程,首先需要强调一点,什么是进程。所谓进程,就是正在运行的程序代码。每个进程之间是相互隔离的,两个进程有不同的栈,堆,寄存器组,PC,虚拟地址空间等。而线程是进程中更小的单位,与进程相类似,每一个线程也有他自己的context,包括它自己的寄存器,PC,栈等。但是一个线程会共享创建它的父进程的中的堆,打开的文件(即文件描述符,但是在java中没有这个)。
线程的创建
在java中,主要有三种创建线程的方法,即:继承Thread,实现runnable接口,实现callable接口。
- 继承Thread类:
class Foo extends Thread {
@Override
public void run() {
System.out.println("hello world")
}
}
因为java只支持单继承,所以可能以这种方式来创建,代码不怎么灵活。
- 实现runnable接口
class Foo implements Runnable {
@Override
public void run() {
System.out.println("hello world")
}
}
public class Bar {
public static void main() {
Thread t = new Thread(new Foo());
t.start();
}
}
继承Thread和实现Runnable都没返回值,另外一个接口叫作Callable,可以有返回值。
- 实现Callable接口
class Bar1 implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("hello world");
return "ok";
}
}
public class Bar {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> task = new FutureTask<String>(new Bar1());
new Thread(task).start();
String result = task.get();
System.out.println(result);
}
}
线程的通知与等待
在java中,线程的挂起调用wait()函数,唤醒某个线程使用notify()或者notifyAll()。理解这几个函数的操作,最经典的模型就是生产者消费者模型。观察生产者消费者模型可以帮助我们理解这两个函数的使用。代码如下:
public class Test {
static final int BUF_SIZE = 10;
private static final int[] buffer = new int[BUF_SIZE];
//count记录缓冲区中还有多少数据可以读取
static int useIndex = 0, fillIndex = 0,count;
static class Producer extends Thread {
@Override
public void run() {
int i = 0;
synchronized (buffer) {
while (true) {
while (count == BUF_SIZE) {
try {
buffer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
put(i);
i++;
buffer.notifyAll();
}
}
}
public void put(int value) {
buffer[fillIndex] = value;
fillIndex = (fillIndex + 1) % BUF_SIZE;
count ++;
}
}
static class Consumer extends Thread {
@Override
public void run() {
//buffer是共享资源,我们要保证对Buffer的访问,最多只能有一个线程
synchronized (buffer) {
while (true) {
while (count == 0) {
try {
buffer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int tmp = get();
System.out.printf("Thread ID: %s,get value:%d\n",this.getId(),tmp);
//通知producer来往里面插入数据
buffer.notifyAll();
}
}
}
public int get() {
int tmp = buffer[useIndex];
useIndex = (useIndex + 1) % BUF_SIZE;
count --;
return tmp;
}
}
public static void main(String[] args){
new Producer().start();
new Consumer().start();
new Consumer().start();
}
}
wait()的作用对象往往是一个共享变量,如这里的缓冲区buffer
。调用一个共享变量的wait()方法会使得当前线程挂起,然后调用共享变量的notifyAll()函数会使得因为这个共享变量而挂机的线程被唤醒。
Note:
如果要调用wait()函数,首先需要获得这个共享变量的监视器锁,否则会抛异常。获得监视器锁的两种方法是:对这个共享变量上锁,或者是调用了该对象变量的synchronized修饰的方法。在上面例子中,synchronized(buffer)
就获得了监视器锁,于是我们可以成功地调用buffer.wait()
实际上,对于wait()的调用会释放当前的监视器锁,这与pthread的共享变量相类似这篇文章介绍了共享变量的内容。如果wait()不会释放某个对象的监视器锁,那么其他线程就无法获得这个锁,那么就陷入死锁了。此外,对于某个对象的wait()函数的调用只会只会释放该对象的监视器锁,不会释放其他对象的监视器锁。
调用共享对象的wait()函数之后,使得调用者线程陷入休眠。如果在其他线程中调用该线程的interrupt(),会抛出InterruptedException。
代码如下:
public class Bar {
public static void main(String[] args) throws InterruptedException {
Integer a = 1;
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("begin ");
synchronized (a) { //获得a的监视器锁
try {
a.wait(); //释放对于a的监视器锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t = new Thread(r);
t.start();
Thread.sleep(1000);
t.interrupt();
}
}
//output:
begin
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at Bar$1.run(Bar.java:14)
at java.lang.Thread.run(Thread.java:748)
其他函数:
wait(long timeouts),如果等待timeouts之后还未接收到唤醒,那么就会直接返回。wait(0)和wait()是一样的。notify(),随机唤醒一个因为调用共享变量的wait()而陷入挂机的线程。与wait()类似,只有获得监视器锁之后才可以调用notify()。notifyAll()唤醒所有挂起的线程。
等待线程终止的方法
调用线程的join方法来阻塞主线程,等待子线程的运行结束。才继续往下执行。代码如下:
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("hello world");
}
}
});
t.start();
t.join();
System.out.println("main done");
如果没有join,那么可能main done会先输出,再输出一百条hello world。但是调用join之后,会等待子线程先运行结束,在输出main done。
在看一个join的例子,因为主线程和子线程是同时运行的,如果希望输出子线程中的某个int,如果不使用join,那么可能会输出0,如下:
static int j = 0
public static void main () {
Thread t = new Thread(() -> {
for (int i = 0; i < 100; i++) j++;
});
t.start();
t.join();//如果没有join,j可能会输出0
System.out.println(j);
}
让线程sleep方法
一个线程可以调用sleep来使得线程休眠。但是在休眠期间不会释放它自己的锁。该线程在休眠期间,如果其他线程调用它的interrupt()函数,会抛InterruptedException。文档中说到:
Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers. The thread does not lose ownership of any monitors.
书中所用的是Reentrantlock,不过应该都差不多。
线程中断
调用interrupt()函数并不能中断一个线程,而是设置了线程的标志位。所以该线程根据是否被interrupted来自行处理。
- void interrupt():中断线程。例如线程A使用interrupt()来中断一个线程B,但是线程B实际上没有被中断,而是继续向下执行,interrupt()只是设置了标志位。此外,如果线程B因为调用了wait(),sleep(),join()而被挂起,那么interrupt()调用会使得在这些方法抛出InterruptedException。
- boolean isInterrupted():检测该线程是否被中断,如果是返回true,如果不是返回false。文档:Tests whether this thread has been interrupted.
- boolean interrupted():判断当前线程是否被中断。文档:Tests whether this thread has been interrupted.
主要是第2和3个函数有点拗口:
public class Bar {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true){}
}
});
t.start();
t.interrupt();
//判断调用isInterrupted()的线程是否被中断,所以这里返回的是true
System.out.println(t.isInterrupted());
//输出false,因为它判断的是当前线程,当前线程是主线程,它未被中断
System.out.println(Thread.interrupted());
//interruped是一个静态函数,所以和上一句其实没区别
System.out.println(t.interrupted());
}
}
守护线程和用户线程
在Java中主要有两类线程。守护线程和用户线程,比如说main线程就是一个用户线程。守护线程和用户线程之间的区别是:当最后用户线程退出后,JVM就会退出了,无论守护线程是否还在运行。下面以代码来解释:
public class Foo{
public static void main(String[] args) {
Thread t = new Thread(()-> {
for (;;) {}
});
//t.setDaemon(true);
t.start();
}
}
如果没有设置为守护线程,那么这个程序就永远不会结束,因为子线程死循环卡住了,JVM会一直在运行。但是设置为守护线程之后,主线程结束了,Thread t
也会被退出。
ThreadLocal
如果是一个普通的共享变量,那么多个线程对其修改的时候就各自影响。在JDK中可以使用一个TheradLocal类,让他们操作的对象都是自己的共享变量的副本。代码如下,我开启了两个线程来操作一个ThreadLocal对象,但是会发现它们互相不会影响。可以将TheadLocal理解为是一个Map,key是当前线程,value是对应的值(不过事实确不是这样,只是助于理解)。结合代码来看:
public class Test {
static ThreadLocal<String> name = ThreadLocal.withInitial(() -> "tim");
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name.get());
});
Thread t1= new Thread(()-> {
name.set("jack");
System.out.println(name.get());
name.remove();
});
t.start();
t1.start();
}
}
//output:
jack //t1的输出结果
tim //t2的输出结果
在上述代码中,一个线程立即运行,一个线程一秒后运行。Thread t1
中我们使用set()设置了ThreadLocal name
,想来看看是否会影响在Thread t
中的结果。可以看到,对于ThreadLocal的操作只会影响当前线程内的副本,不会影响其他线程的ThreadLocal。
稍微分析下ThreadLocal:
前面说过,可以将ThreadLocal理解为是一个Map,不过并不是完全正确。确实,在Thread类中有一个ThreadLocalMap,当前线程操作(使用set,get函数)的任何ThreadLocal对象都会被存到当前对象的ThreadLocalMap中。然后,在get()的时候,会从当前线程中的threadLocalMap中,ThreadLocal为key,获得对应的值。set和remove都是与之类似,代码如下,先讲set():
//Thread类中的ThreadLocalMap,用于保存当前类操作的ThreadLocal变量
ThreadLocal.ThreadLocalMap threadLocals = null;
//获取当前线程的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//在ThreadLocal类中,
public void set(T value) {
Thread t = Thread.currentThread();
//得到当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map 不为null,那么就往ThreadLcalMap中插入,以ThreadLocal为key
//此时插入的值作为value
if (map != null)
map.set(this, value);
else
//如果map为空,那么就创建当前线程的ThreadLocalMap
createMap(t, value);
}
//创建当前线程ThreadLocalMap,以当前的ThreadLocal作为key,set()的参数就是value
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get()的流程。
//在ThreadLocal类当中
public T get() {
Thread t = Thread.currentThread();
//调用getMap()来获得当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map不为空,那么就直接从里面,以ThreadLocal为key,拿到它对应的value
if (map != null) {
//以ThreadLocal作为key,从当前线程的ThreadLocalMap中获得对应的value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//在最开始,我们往往没有指定ThradLocal的初始值,所以这里直接返回初始值
return setInitialValue();
}
remove()的流程,也与之类似,源码如下:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
从源码来看,每个线程在操作ThreadLocal的时候,都会将其塞入到自己的ThreadLocalMap当中,因此每个线程操作的ThreadLocal并不是相同的。
ThreadLocal不能被继承
假如在主线程中对一个ThreadLoca进行操作,并不会影响到子线程中的ThreadLocal。这应该是很明确的,因为前面说过了每一个线程中都会有自己的ThreadLocalMap,将ThreadLocal作为key,和value映射起来。代码如下:
public class Bar {
static ThreadLocal<String> local = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
local.set("main");
System.out.println(local.get());
Thread t = new Thread(() -> {
System.out.println(local.get());
});
t.start();
t.join();
}
}
//output:
main
null
InheritableThreadLocal
如果想让当前的线程的ThreadLocal变量被子线程继承,那么就需要使用InheritableThreadLocal类。将上述代码修改为:
static ThreadLocal<String> local = new InheritableThreadLocal<>();
//output:
main
main
接下来稍微关注以下源码。
//在Thread中,和Thread中变量threadLocals相类似,不过它是可继承
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
InheritableThreadLocal
类继承了ThreadLocal类,并且重写了几个函数,其中比较关键的就是createMap
,所以在set()
和get()
等函数中,调用的createMap()
都将是被重写的。如下:
//调用的getMap不再返回ThreadLocalMap,而是返回一个InheritableThreadLocals
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
//给当前线程的inheritableThreadLocals创建初值。
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
当一个进程被创建的时候,在Thread
类的init
方法中,会去判断当前进程的父进程是否创建过InheritableThreadLocal,在前面的代码中我们已经为父线程创建了inheritableThreadLocals 。它的源码如下:
//Thread(new Runnable(){...})对应的构造函数调用的init方法
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
/*
inheritThreadLocals:true
parent.inheritableThreadLocals:调用get或者set()之后,父进程的inheritableThreadLocals就会被初始化了
所以不为null。
*/
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
//从父进程中继承thradLocalMap
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
一个疑惑之处:为什么要将父进程的map复制到子进程的this.inheritableThreadLocals
,而不是this.threadLocals
呢?
回看前面的,代码此时父进程和子进程中使用的变量local
都是InheritableThreadLocal
,调用get()
函数会去调用getMap()
。因为InheritableThreadLocal
调用getMap()
返回的是t.inheritableThreadLocals;
代码如下:
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
所以,复制的时候,应该复制到this.inheritableThreadLocals