什么是线程
在讨论什么是线程前有必要先说下什么是进程,因为线程是进程中的一个实体,线程 本身是不会独立存在的。进程是代码在数据集合上的一次运行活动,是系统进行资源分配 和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中 的多个线程共享进程的资源。
线程创建与运行
Java中有三种线程创建方式,分别为实现Runnable接口的run方法,继承Thread类 并重写run的方法,使用FutureTask方式。
(1)通过继承Thread类
public class ThreadTest {
//继承Thread类并重写run方法
public static class MyThread extends Thread (
@Override
public void run() (
System.out.printin ("I am a child thread**);
}
}
public static void main(String[] args) (
//创建线程
MyThread thread = new MyThread();
//启动线程
thread.start();
}
)
使用继承方式的好处是,在run()方法内获取当前线程直接使用this就可以了,无须 使用Thread.currentThread()方法;不好的地方是Java不支持多继承,如果继承了 Thread类, 那么就不能再继承其他类。而Runnable则没有这个限制,下面看看Runnablel方式:
(2)通过实现Runnable接口方式
public static class RunableTask implements Runnable(
@Override
public void run() {
System. out. printin (*' I am a child thread");
}
}
public static void main(String[] args) throws InterruptedException(
RunableTask task = new RunableTask();
new Thread(task).start();
new Thread(task).start();
)
上面介绍的两种方式 都有一个缺点,就是任务没有返回值。下面看最后一种,即使用FutureTask的方式。
(3)使用FutureTask方式
//创建任务类,类似Runable
public static class CallerTask implements Callable<String>(
@Override
public String call() throws Exception (
return "hello'*;
)
}
public static void main(String[] args) throws InterruptedException (
//创建异步任务
FutureTask<String> futureTask = new FutureTasko(new CallerTask());
//启动线程
new Thread(futureTask).start();
try (
//等待任务执行完毕,并返回结果
String result = futureTask.get();
System.out.printin(result);
} catch (ExecutionException e) (
e.printStackTrace();
)
}
线程通知与等待
Object是所有类的父类,Java把所有类都需要的方法放 到了 Object类里面,其中就包含本节要讲的通知与等待系列函数。
1.wait()函数
当一个线程调用一个共享变量的wait。方法时,该调用线程会被阻塞挂起,同时会释放锁(只会释放当前共享变量上 的锁)。当直到发生下面几件事情之一才返回:
(1)其他线程调用了该共享对象的notify()或者notifyAll()方法;(注:notify()会导致死锁,而notifyAll()不会。原因后面详说)
(2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。
另外需要注意的是,一个线程可以从挂起状态变为可以运行状态(也就是被唤醒), 即使该线程没有被其他线程调用notify()> notifyAll()方法进行通知,或者被中断,或者等 待超时,这就是所谓的虚假唤醒。
虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线 程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进 行防范。退出循环的条件是满足了唤醒该线程的条件。
synchronized (obj) (
while (条件不满足)(
obj .wait ();
}
)
2.wait(long timeout)函数
该方法相比wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共 享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的 notify()或者notifyAll()方法唤醒,那么该函数还是会因为超时而返回
- notify()函数
一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列 方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线 程是随机的。
此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对 象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤 醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起 竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
4.notifyAII()函数
在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程, notifyAHO方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。
注意:wait()、notify()、notifyAll()方法必须在同步代码块/同步方法中调用,否则报IllegalMonitorStateException异常。因为这几个方法都是对于某个共享资源来说的:wait()是挂起线程,等待共享资源;notify()/notifyAll是唤醒正在等待该共享资源的线程。
等待线程执行终止的join方法
在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执 行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。Thread类中有 一个join方法就可以做这个事情,前面介绍的等待通知方法是Object类中的方法,而join 方法则是Thread类直接提供的。看下面一个简单的例子
public static void main(String[] args) throws InterruptedException (
Thread threadOne = new Thread(new Runnable() (
@Override
public void run() (
try {
Thread.sleep(1000);
) catch (InterruptedException e) (
e.printStackTrace();
}
System.out.printin("child threadOne over!");
)
});
Thread threadTwo = new Thread(new Runnable() (
@Override
public void run() {
try (
Thread.sleep(1000);
} catch (InterruptedException e) (
e.printStackTrace();
}
System, out .printin ("child threadTwo over! '*);
}
));
//启动子线程
threadOne.start();
threadTwo.start();
System.out.printin(nwait all child thread over!n);
//等待子线程执行完毕,返回
threadOne.join();
threadTwo.join();
System.out.printin("all child thread over!n);
)
如上代码在主线程里面启动了两个子线程,然后分别调用了它们的join。方法,那 么主线程首先会在调用threadOne.join()方法后被阻塞,等待threadOne执行完毕后返回。 threadOne执行完毕后threadOne.join()就会返回,然后主线程调用threadTwo.join()方法后 再次被阻塞,等待threadTwo执行完毕后返回。上面代码的执行结果为:
wait all child thread over!
child threadOne over!
child threadTwo over!
all child thread over!
让线程睡眠的sleep方法
Thread类中有一个静态的sleep方法,当一个执行中的线程调用了 Thread的sleep方 法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但 是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数 会正常返回,线程就处于就绪状态,然后参与CPU的调度,获取到CPU资源后就可以继 续运行了。如果在睡眠期间其他线程调用了该线程的interrupto方法中断了该线程,则该 线程会在调用sleep方法的地方抛出InterruptedException异常而返回。
让出CPU执行权的yield方法
Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际就是在暗示 线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗 示。我们知道操作系统是为每个线程分配一个时间片来占有CPU的,正常情况下当一个 线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个 线程调用了 Thread类的静态方法yield时,是在告诉线程调度器自己占有的时间片中还没 有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。
当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线 程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到 刚刚让出CPU的那个线程来获取CPU执行权。
sleep与yield方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂 起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是 让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度 时就有可能调度到当前线程执行。
线程中断
Java中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
・void interrupt()方法:中断线程,例如,当线程A运行时,线程B可以调用线程A 的interrupt()方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设 置标志,线程A实际并没有被中断,它会继续往下执行。
如果线程A因为调用了 wait系列函数、join方法或者sleep方法而被阻塞挂起,这时候若线程B调用线程 A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异 常而返回。
, boolean isInterrupted()方法:检测当前线程是否被中断,如果是返回true,否则返 回 false o
• boolean intemipted()方法:检测当前线程是否被中断,如果是返回true,否则返 回false =与islnterrupted不同的是,该方法如果发现当前线程被中断,则会清除 中断标志,
并且该方法是static方法,可以通过Thread类直接调用。另外从下面 的代码可以知道,在interrupted()内部是获取当前调用线程的中断标志而不是调用 interrupted()方法的实例对象的中断标志。
通过例子1了解线程中断;
public static void main(String[] args) throws InterruptedException {
// write your code here
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//如果当前线程被中断则退出循环
while (!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread() + " hello");
}
}
});
//启动子线程
thread.start();
//主线程休眠Is,以便中断前让子线程输出
Thread.sleep(1000);
//中断子线程
System.out.println("main thread interrupt thread");
thread.interrupt();
//等待子线程执行完毕
thread.join ();
System.out.println("main is over");
}
输出:
Thread[Thread-0,5,main] hello
Thread[Thread-0,5,main] hello
main thread interrupt thread
main is over
在如上代码中,子线程thread通过检查当前线程中断标志来控制是否退出循环(注意:子线程退出不是因为thread.interrupt()代码,而是执行代码后,while条件不满足,子线程执行完毕),主线 程在休眠Is后调用thread的interrupt。方法设置了中断标志,所以线程thread退出了循环。
上面例子简单介绍了interrupt()方法使用,下面通过例子2来了解interrupted()与islnterrupted()方法的不同之处:
public static void main(String[] args) throws InterruptedException {
// write your code here
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
for(;;){
}
}
});
//启动线程
threadOne.start();
//设置中断标志
threadOne.interrupt ();
//获取中断标志
System.out.println("isInterrupted:"+threadOne.isInterrupted());
//获取中断标志并重置
System.out.println("isInterrupted:"+threadOne.interrupted());
//获取中断标志并重置
System.out.println ("isInterrupted: "+Thread.interrupted());
//获取中断标志
System.out.println("islnterrupted:"+threadOne.isInterrupted());
threadOne.join();
System.out.println("main thread is over");
}
输出:
isInterrupted:true
isInterrupted:false
isInterrupted: false
islnterrupted:true
第一行输出true这个大家应该都可以想到,但是下面三行为何是false, false> true呢?不应该是true、false、false吗?如果你有这个疑问,则说明你对这两个函数的区别还是不太清楚。上面我们介绍了在interrupted。方法内部是获取当前线程的中断状态,这里虽然 调用了 threadOne的interrupted()方法,但是获取的是主线程的中断标志,因为主线程是 当前线程。threadOne.interrupted()和Thread.interrupted()方法的作用是一样的,目的都是 获取当前线程的中断标志。下面附上interrupted()方法源码:
//Thread类的interrupted()方法源码
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
线程上下文切换
在多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个 线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转 的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前 线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换, 从当前线程的上下文切换到了其他线程。那么就有一个问题,让出CPU的线程等下次轮 到自己占有CPU时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存 当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。
线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程 被其他线程中断时。
线程死锁
什么是线程死锁
锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象, 在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
那么为什么会产生死锁呢?学过操作系统的朋友应该都知道,死锁的产生必须具备以 下四个条件。
•互斥条件:指线程对己经获取到的资源进行排它性使用,即该资源同时只由一个线 程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资 源的线程释放该资源。
・请求并持有条件:指一个线程己经持有了至少一个资源,但又提出了新的资源请求, 而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己 已经获取的资源。
・不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有 在自己使用完毕后才由自己释放该资源。
・环路等待条件:指在发生死锁时,必然存在一个线程一资源的环形链,即线程集合 {TO, Tl, T2, •••, T"}中的TO正在等待一个T1占用的资源,T1正在等待T2占 用的资源,……T〃正在等待已被TO占用的资源。
如何避免线程死锁
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,但是学过操作系统 的读者应该都知道,目前只有请求并持有和环路等待条件是可以被破坏的。
造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁,那么什么是资源申请的有序性呢?我们通过下面的例子来了解一下。
//创建资源
private static Object resourceA = new Object();
private static Object resourcesB = new Object();
public static void main(String[] args) throws InterruptedException {
// write your code here
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println(Thread.currentThread() + " get ResourceA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get sourceB");
synchronized(resourcesB){
System.out.println(Thread.currentThread() + " get ResourceB");
}
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println(Thread.currentThread() + " get ResourceA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get sourceB");
synchronized(resourcesB){
System.out.println(Thread.currentThread() + " get ResourceB");
}
}
}
});
threadA.start();
threadB.start();
}
输出:
Thread[Thread-0,5,main] get ResourceA
Thread[Thread-0,5,main]waiting get sourceB
Thread[Thread-0,5,main] get ResourceB
Thread[Thread-1,5,main] get ResourceA
Thread[Thread-1,5,main]waiting get sourceB
Thread[Thread-1,5,main] get ResourceB
如上代码让在线程B中获取资源的顺序和在线程A中获取资源的顺序保持一致,其 实资源分配有序性就是指,假如线程A和线程B都需要资源1, 2, 3,...,n时,对资源 进行排序,线程A和线程B只有在获取了资源n-1时才能去获取资源n。
我们可以简单分析一下为何资源的有序分配会避免死锁,比如上面的代码,假如线程 A和线程B同时执行到了 synchronized (resourceA),只有— 线程可以获取到resourceA 上的监视器锁,假如线程A获取到了,那么线程B就会被阻塞而不会再去获取资源B, 线程A获取到resourceA的监视器锁后会去申请resourceB的监视器锁资源,这时候线程 A是可以获取到的,线程A获取到resourceB资源并使用后会放弃对资源resourceB的持有, 然后再释放对resourceA的持有,释放resourceA后线程B才会被从阻塞状态变为激活状态。
所以资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。
守护线程和用户线程
Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。 在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实在JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。那么守护线程和用户线程有什么区 别呢?区别之一是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有 守护线程,也就是说守护线程是否结束并不影响JVM的退出。言外之意,只要有一个用 户线程还没结束,正常情况下JVM就不会退出。
那么如何创建一个守护进程?只需要设置线程的daemon参数为true即可。
Thread daemonThread = new Thread(new Runnable() (
public void run () (
)
});
//设置为守护线程
daemonThread.setDaemon(true); daemonThread.start();
ThreradLocal
ThreadLocal介绍
多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个 共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的 同步。
同步的措施一般是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的 负担。那么有没有一种方式可以做到,当创建一个变量后,每个线程对其进行访问的时候 访问的是自己线程的变量呢?其实ThreadLocal就可以做这件事情,虽然ThreadLocal并 不是为了解决这个问题而出现的。
ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个 ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多 个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。
ThreadLocal的使用
// (1) print函数
static void print(String str){
//1.1打印当前线程本地内存中1 ocalVariable变量的值
System.out.println (str + ": " +localvariable.get ());
//1.2清除当前线程本地内存中的localvariable变量
localvariable.remove();
}
static ThreadLocal<String> localvariable = new ThreadLocal();
public static void main(String[] args) throws InterruptedException {
// write your code here
Thread threadOne = new Thread(new Runnable(){
@Override
public void run() {
//3.1设置线程One中本地变量localvariable的值
localvariable.set("threadOne local variable");
//3.2调用打印函数
print("threadOne");
//3.3打印本地变量值
System.out.println("threadOne remove after: "+localvariable.get());
}
});
Thread threadTwo = new Thread(new Runnable(){
@Override
public void run() {
//4.1设置线程One中本地变量localvariable的值
localvariable.set("threadTwo local variable");
//4.2调用打印函数
print("threadTwo");
//4.3打印本地变量值
System.out.println("threadTwo remove after: "+localvariable.get());
}
});
threadOne.start();
threadTwo.start();
}
输出:
threadOne: threadOne local variable
threadOne remove after: null
threadTwo: threadTwo local variable
threadTwo remove after: null
线程One中的代码3.1通过set方法设置了 localVariable的值,这其实设置的是线程 One本地内存中的一个副本,这个副本线程Two是访问不了的。然后代码3.2调用了 print 函数,代码1.1通过get函数获取了当前线程(线程One)本地内存中localVariable的值。
线程Two的执行类似于线程One。
ThreadLocal的实现原理
首先我们来看一下ThreadLocal相关类的类图结构ThreadLocal相关类的类图结构。
由该图可知,Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals,它们都 是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmapo在默认情 况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set或 者get方法时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal实例里面, 而是存放在调用线程的threadLocals变量里面。另外,Thread里面的threadLocals为何被设计为map结构?很明显是因为每个线程可以关 联多个ThreadLocal变量。
结合我们上个例子来说明,当我们在threadOne调用localvariable.set("threadOne local variable");时,会把值放入threadOne线程对象的threadLocals里面;threadTwo线程调用localvariable.set("threadOne local variable");时,会把值放入threadTwo线程对象的threadLocals里面。
我们来看一下ThreadLocal的set、get及remove方法的源码实现逻辑。
void set(T value)
public void set (T value) (
//(I)获取当前线程
Thread t = Thread.currentThread();
//(2)将当前线程作为key,去查找对应的线程变量,找到则设置
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// (3)第一次调用就创建当前线程对应的HashMap
createMap(t, value);
}
代码(1)首先获取调用线程,然后使用当前线程作为参数调用getMap(t)方法, getMap(Thread t)的源码如下。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到,getMap(t)的作用是获取线程自己的变量threadLocals, threadlocal变量被 绑定到了线程的成员变量上。
T get()
public T get() {
//(4)获取当前线程
Thread t = Thread.currentThread();
// (5)获取当前线程的threadLocals变量
ThreadLocalMap map = getMap(t);
// (6)如果threadLocals不为null,则返回对应本地变量的值
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// (7) threadLocals为空则初始化当前线程的threadLocals成员变量
return setInitialValue();
}
代码(4)首先获取当前线程实例,如果当前线程的threadLocals变量不为null,则直 接返回当前线程绑定的本地变量,否则执行代码(7)进行初始化。setlnitialValue。
void remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
使用完 ThreadLocal ,最好手动调用 remove() 方法 ,否则 可能造成内存泄漏。
ThreadLocal应用场景
InheritableThreadLocal 类
同一个ThreadLocal变量在父线程被设值后,在子线程是获取不到的。为了解决这个问题,InheritableThreadLocal类应运而生,它继承ThreadLocal类,提供一个特性,就是让子线程可以访问在父线程中设置的本地变量。为了避免篇幅过长,这个不展开这个类的介绍了。