本系列文章:
多线程(一)线程与进程、Thread
多线程(二)Java内存模型、同步关键字
多线程(三)线程池
多线程(四)显式锁、队列同步器
多线程(五)可重入锁、读写锁
多线程(六)线程间通信机制
多线程(七)原子操作、阻塞队列
多线程(八)并发容器
多线程(九)并发工具类
多线程(十)多线程编程示例
线程上下文切换,简单来说,指的是CPU保存现场,执行新线程,恢复现场,继续执行原线程的一个过程
。 图示:
可以看出:串行是利用一个资源,依次、首尾相接地把不同的事情做完;并发也是利用一个资源,在做一件事时的空闲时间去做另一件事;并行是投入多个资源,去做多件事。
多线程编程的实质就是将任务的处理方式由串行改成并发
。
一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是交替地为每个线程分配时间片。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就是上下文切换
。
概括来说:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换
。
在时间片切换到别的任务和切换到当前任务的时候,操作系统需要保存和恢复相应线程的进度信息。这个进度信息就是上下文
,它一般包括通用寄存器的内容和程序计数器的内容。
使用vmstat可以测量上下文切换的次数。示例:
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 127876 398928 2297092 0 0 0 4 2 2 0 0 99 0 0
0 0 0 127868 398928 2297092 0 0 0 0 595 1171 0 1 99 0 0
0 0 0 127868 398928 2297092 0 0 0 0 590 1180 1 0 100 0 0
0 0 0 127868 398928 2297092 0 0 0 0 567 1135 0 1 99 0 0
CS(Content Switch)表示上下文切换的次数,例子中的上下文每1秒切换1000多次。
上下文切换可以分为自发性上下文切换和非自发性上下文切换(通常说的上下文切换指的是第一种):
类型 | 含义 | 原因 |
---|---|---|
自发性上下文切换 | 由于自身因素导致的切出 | Thread.sleep(long mills); Object.wait(); Thread.yiels(); Thread.join(); LockSupport.park(); 线程发起了IO操作; 等待其他线程持有的锁 。 |
非自发性上下文切换 | 由于线程调度器的原因被迫切出 | 当前线程的时间片用完; 有一个比当前线程优先级更高的线程需要运行; Java虚拟机的垃圾回收动作。 |
sudo -u admin /opt/ifeve/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17
[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
| sort | uniq -c
39 RUNNABLE
21 TIMED_WAITING(onobjectmonitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(onobjectmonitor)
3 WAITING(parking)
"http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in
Object.wait() [0x0000000052423000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
at java.lang.Object.wait(Object.java:485)
at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464)
- locked <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489)
at java.lang.Thread.run(Thread.java:662)
[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
| sort | uniq -c
44 RUNNABLE
22 TIMED_WAITING(onobjectmonitor)
9 TIMED_WAITING(parking)
36 TIMED_WAITING(sleeping)
130 WAITING(onobjectmonitor)
1 WAITING(parking)
线程安全问题概括来说表现为3个方面:原子性、可见性和有序性。
若该操作从其执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,相应地我们称该操作具有原子性
。
- 原子操作是针对共享变量的操作而言的;
- 原子操作是在多线程环境下才有意义。
原子操作的“不可分割”具有两层含义:
在Java中,对基本数据类型数据(long/double除外,仅包括byte、boolean、short、char、float和int)的变量和引用型变量的写操作都是原子的
。volatile关键字仅能保证变量写操作的原子性,并不能保证其他操作(如read-modify-write操作和check-then-act操作)的原子性
。
- 读取变量counter的值;
- 对counter加一;
- 将新值赋值给变量counter。
Atomic开头的原子类、synchronized、LOCK等,都可以解决原子性问题
。一个线程对共享变量的修改,另一个线程能够立刻看到
)。
volatile
,通过在汇编语言中添加lock指令,来实现内存可见性。synchronized
,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。final,被final关键字修饰的字段在构造器中一旦初始化完成,并且没有发生this逃逸(其它线程通过 this 引用访问到初始化了一半的对象)
,那么其它线程就能看见 final 字段的值。
- 父线程在启动子线程之前对共享变量的更新对于子线程来说是可见的。
- 一个线程终止后该线程对共享变量的更新对于调用该线程的join方法的线程而言是可见的。
有序性指的是:程序执行的顺序按照代码的先后顺序执行
。有序性问题由编译优化导致。
volatile和synchronized都可以保证有序性
:
- volatile关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
- synchronized关键字同样可以保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。
- 主动轮询异步调用的结果;
- 被调用方通过callback来通知调用方调用结果(常用)。
比如在超市购物,如果一件物品没了,你等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用就像网购,在网上付款下单后就不用管了,当货物到达后你收到通知去取就好。
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。
进程是程序向操作系统申请资源的基本单位,线程是进程中可独立执行的最小单位
。通常一个进程可以包含多个线程,至少包含一个线程,同一个进程中所有线程共享该进程的资源。
进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
。同一进程的线程共享本进程的地址空间和资源,而线程之间的地址空间和资源是相互独立的
。线程不能独立执行,必须依存在应用程序中
,由应用程序提供多个线程执行控制,两者均可并发执行。 一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。多线程的并发运行,指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。
线程调度模型有两种:
Java虚拟机(JVM)采用抢占式调度模型
。
1)给线程命名,这样可以帮助调试。
2)最小化同步的范围,而不是将整个方法同步,只对关键部分做同步。
3)如果可以,更偏向于使用volatile而不是synchronized。
4)使用更高层次的并发工具,而不是使用wait()和notify()来实现线程间通信,如BlockingQueue、CountDownLatch及Semeaphore。
5)优先使用并发集合,而不是对集合进行同步。并发集合提供更好的可扩展性。
6)使用线程池。
使用"top -H -p pid"+"jps pid"可以很容易地找到某条占用CPU高的线程的线程堆栈,从而定位占用CPU高的原因,一般是因为不当的代码操作导致了死循环。
最后提一点,"top -H -p pid"打出来的LWP是十进制的,"jps pid"打出来的本地线程号是十六进制的,转换一下,就能定位到占用CPU高的线程的当前线程堆栈了。
在Java中创建一个线程,可以理解创建一个Thread类(或其子类)的实例。线程的任务处理逻辑可以在Thread类的run实例方法中实现,运行一个线程实际上就是让Java虚拟机执行该线程的run方法。run方法相当于线程的任务处理逻辑的入口方法,它由虚拟机在运行相应线程时直接调用,而不是由相应代码进行调用
。
启动一个线程的方法是调用start方法,其实质是请求Java虚拟机运行相应的线程,而这个线程具体何时运行是由线程调度器决定的
。因此,start方法调用结束并不意味着相应线程已经开始运行。
创建线程有4种方式。
使用方式:
- 继承Thread类;
- 重写run方法;
- 创建Thread对象;
- 通过start()方法启动线程。
示例:
/*继承Thread类*/
public class WelcomeThread extends Thread{
@Override
public void run() {
System.out.printf("test");
}
}
public class ThreadTest1 {
public static void main(String[] args) {
// 创建线程
Thread welcomeThread = new WelcomeThread();
// 启动线程
welcomeThread.start();
}
}
JDK1.8后,可以使用Lambda表达式来创建,示例:
new Thread(()->{
System.out.println("Lambda Thread Test!");
}).start();
使用方式:
- 实现Runnable接口;
- 重写run方法;
- 创建Thread对象,将实现Runnable接口的类作为Thread的构造参数;
- 通过start()进行启动。
此种方式用到了代理模式,示例:
public class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
}
}
前两种比较的话, 推荐使用第二种方式,原因:
- Java是单继承,将继承关系留给最需要的类。
Runnable可以实现多个相同的程序代码的线程去共享同一个资源
。当以Thread方式去实现资源共享时,实际上Thread内部,依然是以Runnable形式去实现的资源共享。
前两种方式比较常见,Callable的使用方式:
- 创建实现Callable接口的类;
- 以Callable接口的实现类为参数,创建FutureTask对象;
- 将FutureTask作为参数创建Thread对象;
- 调用线程对象的start()方法。
示例:
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
return 1;
}
}
public class CallableTest {
public static void main(String[] args) {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
}
}
使用该方法创建线程时,核心方法是call(),该方法有返回值,其返回值类型就是Callable接口中泛型对应的类型
。
由于线程的创建、销毁是一个比较消耗资源的过程,所以在实际使用时往往使用线程池。
在创建线程池时,可以使用现成的Executors工具类来创建,该工具类能创建的线程池有4种:newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool。此处以newSingleThreadExecutor为例,其步骤为:
- 使用Executors类中的newSingleThreadExecutor方法创建一个线程池;
- 调用线程池中的execute()方法执行由实现Runnable接口创建的线程;或者调用submit()方法执行由实现Callable接口创建的线程;
- 调用线程池中的shutdown()方法关闭线程池。
示例:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
}
}
public class SingleThreadExecutorTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
MyRunnable runnableTest = new MyRunnable();
for (int i = 0; i < 5; i++) {
executorService.execute(runnableTest);
}
System.out.println("线程任务开始执行");
executorService.shutdown();
}
}
线程类实现Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况。
当线程的run方法执行结束,相应的线程的运行也就结束了。
线程每次只能使用一次,即只能调用一次start方法
。在线程未结束前,多次调用start方法会抛出IllegalThreadStateException,Thread类中的start方法中可以看出该逻辑:
public synchronized void start() {
checkNotStarted();
hasBeenStarted = true;
nativeCreate(this, stackSize, daemon);
}
private void checkNotStarted() {
if (hasBeenStarted) {
throw new IllegalThreadStateException("Thread already started");
}
}
可以看出:start()方法使用synchronized关键字修饰,说明start()方法是同步的,它会在启动线程前检查线程的状态,如果不是初始化状态,则直接抛出异常。所以,一个线程只能启动一次,多次启动是会抛出异常的。
两者的区别:
start()方法用于启动线程,run()方法用于实现具体的业务逻辑
。run()可以重复调用,而start()只能调用一次
。
调用start()方法来启动一个线程,无需等待run()方法体代码执行完毕,可以直接继续执行其他的代码, 此时线程是处于就绪状态,并没有运行。
当调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
新建一个线程,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。
如果直接执行run()方法,会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作
。示例:
public class JavaTest {
public static void main(String[] args) {
System.out.println("main方法中的线程名:"
+Thread.currentThread().getName()); //main方法中的线程名:main
Thread welcomeThread = new WelcomeThread();
System.out.println("以start方法启动线程");
welcomeThread.start(); //Thread子类中的线程名:Thread-0
System.out.println("以run方法启动线程");
welcomeThread.run(); //Thread子类中的线程名:main
}
}
class WelcomeThread extends Thread{
@Override
public void run() {
System.out.println("Thread子类中的线程名:"
+Thread.currentThread().getName());
}
}
总结: 调用start方法方可启动线程并使线程进入就绪状态
,而run方法只是thread的一个普通方法调用,还是在主线程里执行。
线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:
- Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
- Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的。
Thread类的私有属性有许多,了解几个常用的即可:线程的编号(ID)、名称(Name)、线程类别(Daemon)和优先级(Priority)。
这几个属性中,ID仅可读,其他都是可读写
。具体:
属性 | 属性类型 | 用途 | 注意事项 |
---|---|---|---|
编号(ID) | long |
用于标识不同的线程,不同的线程拥有不同的编号 | 某个编号的线程运行结束后,该编号可能被后续创建的线程使用,因此该属性的值不适合用作某种唯一标识 |
名称(Name) | String |
用于区分不同的线程,默认值的格式为“Thread-线程编号” | 尽量为不同的线程设置不同的值 |
线程类别(Daemon) | boolean |
值为true表示相应的线程为守护线程,否则表示相应的线程为用户线程。该属性的默认值与相应线程的父线程的该属性的值相同 | 该属性必须在相应线程启动之前设置,即调用setDaemon方法必须在调用start方法之前,否则会出现IllegalThreadStateException |
优先级(Priority) | int |
优先级高的线程一般会被优先运行。优先级从1到10,默认值一般为5(普通优先级),数字越大,优先级越高。 对于具体的一个线程而言,其优先级的默认值与其父线程的优先级值相等。 |
一般使用默认的优先级即可,不恰当地设置该属性值可能会导致严重的问题(线程饥饿) |
获取4个属性值示例:
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
//10,Thread-0,5,false
System.out.println(Thread.currentThread().getId()+","
+Thread.currentThread().getName()+","
+Thread.currentThread().getPriority()+","
+Thread.currentThread().isDaemon());
}
}).start();
}
Java线程的优先级属性本质上只是一个给线程调度器的提示信息,以便于线程调度器决定优先调度哪些线程运行
。每个线程的优先级都在1到10之间,1的优先级为最低,10的优先级为最高,在默认情况下优先级都是Thread.NORM_PRIORITY(常数 5)。
虽然开发者可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。
线程优先级特性:
在不同的JVM以及OS上,线程规划会存在差异,有些OS会忽略对线程优先级的设定。
设置和获取线程优先级的方法:
//为线程设定优先级
public final void setPriority(int newPriority)
//获取线程的优先级
public final int getPriority()
示例:
public class RunnableDemo implements Runnable {
@Override
public void run() {
int nowPriority = Thread.currentThread().getPriority();
System.out.println("1.优先级:"+nowPriority); //1.优先级:5
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
nowPriority = Thread.currentThread().getPriority();
System.out.println("2.优先级:"+nowPriority); //2.优先级:10
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
}
}
Java 中的线程分为两种:守护线程和用户线程。任何线程都可以设置为守护线程和用户线程,通过方法setDaemon(true)
可以把该线程设置为守护线程,反之则为用户线程。
用户线程
:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。
守护线程
:运行在后台,为其他前台线程服务,比如垃圾回收线程,JIT(编译器)线程就可以理解为守护线程。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。
守护线程应该永远不去访问固有资源 ,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
注意事项:
setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException
。- 在守护线程中产生的新线程也是守护线程。
- 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑。
- 守护线程中不能依靠finally块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作,所以守护线程中的finally语句块可能无法被执行。
设置和获取线程是否是守护线程的方法:
//设置线程是否为守护线程
public final void setDaemon(boolean on)
//判断线程是否是守护线程
public final boolean isDaemon()
相比于上面的两个属性,实际运用中,往往线程名称会被修改,目的是为了调试。获取和设置线程名称的方法:
//获取线程名称
public final String getName()
//设置线程名称
public final synchronized void setName(String name)
示例:
public class RunnableDemo implements Runnable {
@Override
public void run() {
String nowName = Thread.currentThread().getName();
System.out.println("1.线程名称:"+nowName); //1.线程名称:Thread-0
Thread.currentThread().setName("测试线程");
nowName = Thread.currentThread().getName();
System.out.println("2.线程名称:"+nowName); //2.线程名称:测试线程
}
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo();
Thread thread = new Thread(runnableDemo);
thread.start();
}
}
在Thread类中,线程状态是一个枚举类型:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED
}
线程的状态可以通过public State getState()
来获取,该方法的返回值是一个枚举类型,线程状态定义如下:
在实际开发中,往往将线程的状态理解为5种:新建、可运行、运行、阻塞、死亡。
等待阻塞
(位于对象等待池中的阻塞)
运行状态中的线程执行wait()方法,JVM会把该线程放入等待队列中,使本线程进入到等待阻塞状态;同步阻塞
(位于对象锁池中的阻塞)
线程在获取synchronized同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池中,线程会进入同步阻塞状态;其他阻塞
通过调用线程的sleep()或 join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
以下是Thread类中较常用的几个方法,并不包含线程间协作的方法(如await、notify等),这些方法的使用随后介绍。其中的yield方法并不常用,但常常拿来和sleep、await等方法进行比较,所以也介绍下。
方法 | 功能 | 备注 |
---|---|---|
static Thread currentThread() | 返回当前线程,即当前代码的执行线程 | |
void run() | 用于实现线程的任务处理逻辑 | 该方法由Java虚拟机直接调用 |
void start() | 启动线程 | 调用该方法并不代表相应的线程已经被启动,线程是否启动是由虚拟机去决定的 |
void join() | 等待相应线程运行结束 | 若线程A调用线程B的join方法,那么线程A的运行会被暂停,直到线程B运行结束 |
static void yield() | 使当前线程主动放弃其对处理器的占用,这可能导致当前线程被暂停 | 这个方法是不可靠的,该方法被调用时当前线程仍可能继续运行 |
void interrupt() | 中断线程 |
|
static void sleep(long millis) | 使当前线程休眠(暂停运行)指定的时间 |
中断
可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作,常常被用于线程间的协作
。
其他线程可以调用指定线程的interrupt()方法对其进行中断操作,同时指定线程可以调用isInterrupted()来感知其他线程对其自身的中断操作,从而做出响应。
另外,也可以调用Thread的静态方法interrupted()对当前线程进行中断操作,该方法会清除中断标志位。需要注意的是,当抛出InterruptedException时候,会清除中断标志位
,此时再调用isInterrupted,会返回false。
和中断相关的方法有3个:
方法名 | 详细解释 | 备注 |
---|---|---|
public void interrupt() | 中断该线程对象 |
如果该线程被调用了Object wait/Object wait(long),或者被调用sleep(long),join()/join(long)方法时会抛出interruptedException并且中断标志位将会被清除 |
public boolean isinterrupted() | 测试该线程对象是否被中断 |
中断标志位不会被清除 |
public static boolean interrupted() | 查看当前中断信号是true还是false并且清除中断信号 |
中断标志位会被清除 |
关于interrupt和isinterrupted的使用,示例:
public class JavaTest {
public static void main(String[] args) throws InterruptedException {
//sleepThread睡眠1000ms
final Thread sleepThread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
super.run();
}
};
//busyThread一直执行死循环
Thread busyThread = new Thread() {
@Override
public void run() {
while (true) ;
}
};
sleepThread.start();
busyThread.start();
sleepThread.interrupt();
busyThread.interrupt();
while (sleepThread.isInterrupted()) ;
System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
}
}
在上面的代码中,开启了两个线程分别为sleepThread和BusyThread, sleepThread睡眠1s,BusyThread执行死循环。然后分别对着两个线程进行中断操作,可以看出sleepThread抛出InterruptedException后清除标志位,而busyThread就不会清除标志位。
另外,可以通过中断的方式实现线程间的简单交互,因为可以通过isInterrupted()
方法监控某个线程的中断标志位是否清零,针对不同的中断标志位进行不同的处理。
join方法也是一种线程间协作的方式
,很多时候,一个线程的输入可能非常依赖于另一个线程的输出。如果在一个线程threadA中执行了threadB.join(),其含义是:当前线程threadA会等待threadB线程终止后,threadA才会继续执行
。
方法名 | 详细注释 | 备注 |
---|---|---|
public final void join() throws InterruptedException | 等待这个线程死亡。 | 如果任何线程中断当前线程,如果抛出InterruptedException异常时,当前线程的中断状态将被清除 |
public final void join(long millis) throws InterruptedException | 等待这个线程死亡的时间最多为millis毫秒。 如果参数为 0,意味着永远等待。 |
如果millis为负数,抛出IllegalArgumentException异常 |
public final void join(long millis, int nanos) throws InterruptedException | 等待最多millis毫秒加上这nanos纳秒。 | 如果millis为负数或者nanos不在0-999999范围抛出IllegalArgumentException异常 |
看个例子:
public class JoinDemo {
public static void main(String[] args) {
Thread previousThread = Thread.currentThread();
for (int i = 1; i <= 10; i++) {
Thread curThread = new JoinThread(previousThread);
curThread.start();
previousThread = curThread;
}
}
static class JoinThread extends Thread {
private Thread thread;
public JoinThread(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
System.out.println(thread.getName() + " terminated.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试结果:
在上面的例子中一个创建了10个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程…
sleep方法为:
public static native void sleep(long millis)
显然sleep是Thread的静态方法,它的作用是:让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁
。
Thread.sleep方法经常拿来与Object.wait()方法进行比较,sleep和wait两者主要的区别:
- sleep()方法是Thread的静态方法,而wait是Object实例方法;
wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁
。而sleep()方法没有这个限制可以在任何地方使用。wait()方法会释放占有的对象锁,使得该线程进入等待池中
,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;- sleep()方法在休眠时间达到后,如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。
关于sleep方法的使用,示例:
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("第一个线程的执行时间:"+new Date());
}
}).start();
System.out.println("sleep2秒");
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("第二个线程的执行时间:"+new Date());
}
}).start();
}
结果示例:
可以看出,第2个线程的执行时间是晚于第1个线程2秒的。
yield方法为:
public static native void yield()
yield方法的作用:使当前线程从执行状态(运行状态)变为可执行态(就绪状态)
。
yield方法是一个静态方法,一旦执行,它会是当前线程让出CPU。但是,让出了CPU并不是代表当前线程不再运行了。线程调度器可能忽略此此消息,并且如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。另外,让出的时间片只会分配给大于等于当前线程优先级的线程。
在线程中,用priority来表示优先级,priority的范围从1~10。在构建线程的时候可以通过 setPriority(int) 方法进行设置,默认优先级为5,优先级高的线程相较于优先级低的线程优先获得处理器时间片。
需要注意的是,sleep()和yield()方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而yield()方法只允许大于等于当前线程优先级的线程,竞争CPU时间片
。
interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态
。
线程中断仅仅是设置线程的中断状态标识,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态标识被置为“中断状态”,就会抛出中断异常。
interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号
。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
isInterrupted:查看当前中断信号是true还是false
。
sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会
;线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态
; 如果线程运行中产生了异常,首先会生成一个异常对象。我们平时throw抛出异常,就是把异常交给JVM处理。JVM首先会去找有没有能够处理该异常的处理者(首先找到当前抛出异常的调用者,如果当前调用者无法处理,则会沿着方法调用栈一路找下去),能够处理的调用者实际就是看方法的catch关键字,JVM会把该异常对象封装到catch入参,允许开发者手动处理异常。
若找不到能够处理的处理者(实际就是没有手动catch异常,比如未受检异常),就会交该线程处理;JVM会调用Thread类的dispatchUncaughtException()方法,该方法调用了getUncaughtExceptionHandler(),uncaughtExceptoin(this,e)来处理了异常,如果当前线程设置了自己的UncaughtExceptionHandler,则使用该handler,调用自己的uncaughtException方法。如果没有,则使用当前线程所在的线程组的Handler的uncaughtExceptoin()方法,如果线程中也没有设置,则直接把异常定向到System.err中,打印异常信息(控制台红色字体输出的异常就是被定向到System.err的异常)。
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
不推荐
这个方法,因为stop是过期作废的方法。 JDK1.5之后,引入了一个枚举TimeUnit,对sleep方法提供了很好的封装。
比如要休眠2小时22分55秒899毫秒,两种写法:
Thread.sleep(8575899);
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(22);
TimeUnit.SECONDS.sleep(55);
TimeUnit.MILLISECONDS.sleep(899);
获取线程上下文类加载器:
public ClassLoader getContextClassLoader()
设置线程类加载器(可以打破Java类加载器的父类委托机制):
public void setContextClassLoader(ClassLoader cl)
Thread类的sleep()和yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。
在Thread类中有一个静态方法叫holdsLock(Object o),返回true表示:当且仅当当前线程拥有某个具体对象的锁。
线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:
1、线程体中调用了yield方法让出了对CPU的占用权利;
2、线程体中调用了sleep方法使线程进入睡眠状态;
3、线程由于IO操作受到阻塞;
4、另外一个更高优先级线程出现;
5、在支持时间片的系统中,该线程的时间片用完。
1)使用top命令查找java命令下cpu占用最高的进程:
例如pid为9595的进程是占用cpu使用率最大的。
2)使用top -H -p 9595
查看当前pid为9595进程下各线程占用cpu情况:
可以看到,pid为10034的线程占用cpu是最高的。
3)将线程的pid由10进制转成16进制:
4)把进程的全部堆栈信息导入到临时文件中:
jstack 9595 > /tmp/a.txt
Thread的join()方法:
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
可以看到,有一个long类型参数的join()方法使用了synchroinzed修饰,说明这个方法同一时刻只能被一个实例或者方法调用。由于,传递的参数为0,所以,程序会进入如下代码逻辑。
if (millis == 0) {
while (isAlive()) {
wait(0);
}
}
首先,在代码中以while循环的方式来判断当前线程是否已经启动处于活跃状态,如果已经启动处于活跃状态,则调用同类中的wait()方法,并传递参数0。继续跟进wait()方法,如下所示。
public final native void wait(long timeout) throws InterruptedException;
wait()方法是一个本地方法,通过JNI的方式调用JDK底层的方法来使线程等待执行完成。
调用线程的wait()方法时,会使主线程处于等待状态,等待子线程执行完成后再次向下执行。也就是说,在ThreadSort02类的main()方法中,调用子线程的join()方法,会阻塞main()方法的执行,当子线程执行完成后,main()方法会继续向下执行,启动第二个子线程,并执行子线程的业务逻辑,以此类推。
使用多线程下载文件,可以简单分为以下几步:
示例:
package ThreadTest;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
public class DownloadTest {
private static final String path = "http://down.360safe.com/se/360se9.1.0.426.exe";
public static void main(String[] args) throws Exception {
/*第一步:获取目标文件的大小*/
int totalSize = new URL(path).openConnection().getContentLength();
System.out.println("目标文件的总大小为:"+totalSize+"B");
/*第二步:确定开启几个线程。开启线程的总数=CPU核数+1;例如:CPU核数为4,则最多可开启5条线程*/
int availableProcessors = Runtime.getRuntime().availableProcessors();
System.out.println("CPU核数是:"+availableProcessors);
int threadCount = 3;
/*第三步:计算每个线程要下载多少个字节*/
int blockSize = totalSize/threadCount;
/*每次循环启动一条线程下载*/
for(int threadId=0; threadId<3;threadId++){
/*第四步:计算各个线程要下载的字节范围*/
/*每个线程下载的开始索引*/
int startIndex = threadId * blockSize;
/*每个线程下载的结束索引*/
int endIndex = (threadId+1)* blockSize-1;
/*如果是最后一条线程*/
if(threadId == (threadCount -1)){
endIndex = totalSize -1;
}
/*第五步:启动子线程下载*/
new DownloadThread(threadId,startIndex,endIndex).start();
}
}
private static class DownloadThread extends Thread{
private int threadId;
private int startIndex;
private int endIndex;
public DownloadThread(int threadId, int startIndex, int endIndex) {
super();
this.threadId = threadId;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
public void run(){
System.out.println("第"+threadId+"条线程,下载索引:"+startIndex+"~"+endIndex);
/*每条线程要去找服务器拿取一段数据*/
try {
URL url = new URL(path);
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
/*设置连接超时时间*/
connection.setConnectTimeout(5000);
/*第六步:获取目标文件的[startIndex,endIndex]范围*/
connection.setRequestProperty("range", "bytes="+startIndex+"-"+endIndex);
connection.connect();
/*获取响应码,当服务器返回的是文件的一部分时,响应码不是200,而是206*/
int responseCode = connection.getResponseCode();
if (responseCode == 206) {
//拿到目标段的数据
InputStream is = connection.getInputStream();
/*第七步:创建一个RandomAccessFile对象,将返回的字节流写到文件指定的范围*/
String fileName = getFileName(path);
/*创建一个可读写的文件,即把文件下载到D盘*/
RandomAccessFile raf = new RandomAccessFile("d:/"+fileName, "rw");
/*注意:让raf写字节流之前,需要移动raf到指定的位置开始写*/
raf.seek(startIndex);
/*将字节流数据写到文件中*/
byte[] buffer = new byte[1024];
int len = 0;
while((len=is.read(buffer))!=-1){
raf.write(buffer, 0, len);
}
is.close();
raf.close();
System.out.println("第 "+ threadId +"条线程下载完成 !");
} else {
System.out.println("下载失败,响应码是:"+responseCode);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*获取文件的名称*/
private static String getFileName(String path){
int index = path.lastIndexOf("/");
String fileName = path.substring(index+1);
return fileName ;
}
}
测试结果:
目标文件的总大小为:48695168B
CPU核数是:4
第0条线程,下载索引:0~16231721
第1条线程,下载索引:16231722~32463443
第2条线程,下载索引:32463444~48695167
第 1条线程下载完成 !
第 0条线程下载完成 !
第 2条线程下载完成 !
下载文件:
线程活性故障是由于资源稀缺性或程序自身的问题和缺陷导致线程一直处于非RUNNABLE状态,或者线程虽然处于RUNNABLE状态,但是其要执行的任务却一直无法进展的故障现象。
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去
。
如图所示,线程A持有资源2,线程B持有资源1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态:
当线程产生死锁时,这些线程及相关的资源将满足如下全部条件:
一个资源只能被一个线程(进程)占用
,直到被该线程(进程)释放。一个线程(进程)因请求被占用资源(锁)而发生阻塞时,对已获得的资源保持不放
。线程(进程)已获得的资源
,在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
。当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞
。用一句话该概括:
两个或多个线程持有并且不释放独有的锁,并且还需要竞争别的线程所持有的锁,导致这些线程都一直阻塞下去。
这些条件是死锁产生的必要条件而非充分条件,也就是说只要产生了死锁,那么上面的这些条件一定同时成立
,但是上述条件即便同时成立也不一定产生死锁。
如果把锁看作一种资源,这种资源正好符合“资源互斥”和“资源不可抢夺”的要求。那么,可能产生死锁的特征就是在持有一个锁的情况下去申请另外一个锁,通常是锁的嵌套,示例:
//内部锁
public void deadLockMethod1(){
synchronized(lockA){
//...
synchronized(lockB){
//...
}
}
}
//显式锁
public void deadLockMethod2(){
lockA.lock();
try{
//...
lockB.lock();
try{
//...
}finally{
lockB.unlock();
}
}finally{
lockA.unlock();
}
}
示例:
private static Object lockObject1 = new Object();
private static Object lockObject2 = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
test1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
test2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
public static void test1() throws InterruptedException{
synchronized (lockObject1) {
System.out.println(Thread.currentThread().getName()
+"获取到lockObject1,正在获取lockObject2");
Thread.sleep(1000);
synchronized (lockObject2) {
System.out.println(Thread.currentThread().getName()
+"获取到lockObject2");
}
}
}
public static void test2() throws InterruptedException{
synchronized (lockObject2) {
System.out.println(Thread.currentThread().getName()
+"获取到lockObject2,正在获取lockObject1");
Thread.sleep(1000);
synchronized (lockObject1) {
System.out.println(Thread.currentThread().getName()
+"获取到lockObject1");
}
}
}
以下结果表明已经出现了死锁:
Thread-1获取到lockObject2,正在获取lockObject1
Thread-0获取到lockObject1,正在获取lockObject2
由上文可知,要产生死锁需要同时满足四个条件,所以,只要打破其中一个条件就可以避免死锁的产生。常用的规避方法有如下几种:
用一个粒度较粗的锁
替代原来的多个粒度较细的锁,这样涉及的线程都只需要申请一个锁从而避免了死锁。粗锁法的缺点是它明显降低了并发性并可能导致资源浪费。使用ReentrantLock.tryLock(long timeout, TimeUnit unit) 来申请锁可以避免一个线程无限制地等待另外一个线程持有的资源
,从而最终能够消除死锁产生的必要条件中的“占用并等待资源”。示例: boolean locked = false;
try {
locked = lock.tryLock(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(locked) lock.unlock();
}
使用一些锁的替代品
(无状态对象、线程特有对象以及volatile关键字等
),在条件允许的情况下,使用这些替代品在保障线程安全的前提下不仅能够避免锁的开销,还能够直接避免死锁。 线程饥饿是指一直无法获得其所需的资源而导致任务一直无法进展的一种活性故障。
线程饥饿的一个典型例子是在争用的情况下使用非公平模式的读写锁。此种情况下,可能会导致某些线程一直无法获取其所需的资源(锁),即导致线程饥饿。
把锁看作一种资源的话,其实死锁也是一种线程饥饿。死锁的结果是故障线程都无法获得其所需的全部锁中的一个锁,从而使其任务一直无法进展,这相当于线程无法获得其所需的全部资源(锁)而使得其任务一直无法进展,即产生了线程饥饿。由于线程饥饿的产生条件是一个(或多个)线程始终无法获得其所需的资源,显然这个条件的满足并不意味着死锁的必要条件(而不是充分条件)的满足,因此线程饥饿并不会导致死锁。
Java中导致饥饿的原因:
- 高优先级线程抢占了所有的低优先级线程的 CPU 时间。
- 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
- 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。
线程饥饿涉及的线程,其生命周期不一定就是WAITING或BLOCKED状态,其状态也可能是RUNNING(说明涉及的线程一直在申请宁其所需的资源),这时饥饿就会演变成活锁。
活锁指线程一直处于运行状态,但是其任务却一直无法进展的一种活性故障。
死锁
是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
活锁
任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,却一直获得不了锁。
饥饿
一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
1、活锁与死锁的区别
活锁和死锁的区别在于:处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能
。活锁可以认为是一种特殊的饥饿。
2、死锁活锁与饥饿的区别
进程会处于饥饿状态是因为持续地有其它优先级更高的进程请求相同的资源。不像死锁或者活锁,饥饿能够被解开。例如,当其它高优先级的进程都终止时并且没有更高优先级的进程强占资源。