概念
线程与进程(process、Thread)
**进程:**是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
**线程:**是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
* 进程与线程的区别
进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
复制代码
线程调度:
1. 分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
2. 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
复制代码
Java使用的为抢占式调度。
很多多线程是模拟出来的,真正的多线程是值多个CPU,即多核,如服务器。
如果是模拟出来的多线程,即在一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,以为切换的很快,所以就有同时执行的错觉
- 线程就是独立的执行单元
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程。如:main线程、GC线程
- main()称之为主线程,为系统的入口,用于执行整个程序
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器始于操作系统紧密相关的,先后顺序时不能人为的干预
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
- 线程会带来额外的开销,如CPU调度时间(线程的上下文切换),并发控制开销
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的**生命周期(一个对象从创建到销毁期间一系列的轨迹)**中,有多个状态。
状态有
- 创建(new)
- 就绪(runnable)
- 运行(running)
- 阻塞(blocked)
- 计时等待(time waiting)
- 无限等待(waiting)
- 消亡(dead、Teminated)
线程状态 | 详解 |
---|---|
New(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 MyThread myThread = new MyThread()只有线程对象,没有线程特征 |
Runnable(可运行) | 线程可以再java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用thread.start() 方法:就绪状态 |
Running(运行) | 线程正在运行 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程将编程Runnable状态 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒 |
TimedWaiting(计时等待) | 同Waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting 状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep()、Object.Wait() |
Teminated(被终止) | 因为run()方法正常退出而死亡,或者因为没有捕获的异常终止了run() 方法而死亡。 |
当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的JVM内存区域划分一篇博文中知道程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。
当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。
线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。
阻塞与等待的区别
阻塞
:当一个线程试图获取对象锁(非java.util.concurrent库中的锁,即synchronized),而该锁被其他线程持有,则该线程进入阻塞状态。它的特点是使用简单,由JVM调度器来决定唤醒自己,而不需要由另一个线程来显式唤醒自己,不响应中断。等待
:当一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态。它的特点是需要等待另一个线程显式地唤醒自己,实现灵活,语义更丰富,可响应中断。例如调用:Object.wait()、Thread.join()以及等待Lock或Condition。
需要强调的是虽然synchronized和JUC里的Lock都实现锁的功能,但线程进入的状态是不一样的。synchronized会让线程进入阻塞态,而JUC里的Lock是用LockSupport.park()/unpark()来实现阻塞/唤醒的,会让线程进入等待态。但话又说回来,虽然等锁时进入的状态不一样,但被唤醒后又都进入runnable态,从行为效果来看又是一样的。
上下文切换
对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换
(对于进程也是类似)。
由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。
因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行
。
程序计数器(PC寄存器):
记录线程执行到的字节码行数(运行的位置),当线程上下文切换时使用
Thread类
public class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
// Thread的名字,可以通过Thread类的构造器中的参数来指定线程名字
private volatile String name;
// 线程的优先级(最大值为10,最小值为1,默认值为5)
private int priority;
private Thread threadQ;
private long eetop;
// 是否单步执行此线程
private boolean single_step;
// 线程是否是守护线程
private boolean daemon = false;
/* JVM state */
private boolean stillborn = false;
// 要执行的任务
private Runnable target;
/* The group of this thread */
private ThreadGroup group;
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
/* The inherited AccessControlContext of this thread */
private AccessControlContext inheritedAccessControlContext;
/* For autonumbering anonymous threads. */
private static int threadInitNumber;
}
复制代码
主要方法
start()
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
新启一个线程执行其run()方法,一个线程只能start一次。主要是通过调用native start0()来实现。
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) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
复制代码
run()
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
sleep()
线程休眠sleep()方法有两个重载版本:
sleep(long millis) //参数为毫秒
sleep(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
复制代码
sleep()相当于让线程睡眠,
交出CPU
,让CPU去执行其他的任务,但是sleep方法**不会释放锁
**。也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。看下面这个例子就清楚了:
public class Test {
private int i = 10;
private Object object = new Object();
public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread1 = test.new MyThread();
MyThread thread2 = test.new MyThread();
thread1.start();
thread2.start();
}
class MyThread extends Thread{
@Override
public void run() {
synchronized (object) {
i++;
System.out.println("i:"+i);
try {
System.out.println("线程"+Thread.currentThread().getName()+"进入睡眠状态");
Thread.currentThread().sleep(10000);
} catch (InterruptedException e) {
// TODO: handle exception
}
System.out.println("线程"+Thread.currentThread().getName()+"睡眠结束");
i++;
System.out.println("i:"+i);
}
}
}
}
// 输出结果:
i:11
线程Thread-0进入睡眠状态
线程Thread-0睡眠结束
i:12
i:13
线程Thread-1进入睡眠状态
线程Thread-1睡眠结束
i:14
复制代码
从上面输出结果可以看出,当Thread-0进入睡眠状态之后,Thread-1并没有去执行具体的任务。只有当Thread-0执行完之后,此时Thread-0释放了对象锁,Thread-1才开始执行。
注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。
总结:
- sleep(参数) 指定当前线程阻塞的毫秒数
- sleep()存在异常InterruptedException
- sleep时间达到后,线程进入就绪状态(Runnable)
- sleep()可以模拟网络延时,倒计时等
每一个对象都有一个锁,sleep()只会让出CPU的执行权,但不会释放锁。
yield()
线程礼让、退让。yield()只是假装是否锁,这个线程还是会去争抢锁(类似于篮球争球一样,自己也参加)。
因为yield()方法并不会让线程进入阻塞状态,而是让线程
重回就绪状态(Runnable)
,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
public class TestYield {
public static void main(String[] args) throws IOException {
TestYield test = new TestYield();
MyThread thread1 = test.new MyThread();
thread1.setName("a");
thread1.start();
MyThread thread2 = test.new MyThread();
thread2.setName("b");
thread2.start();
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println(currentThread().getName()+"---start");
Thread.yield();
System.out.println(currentThread().getName()+"---end");
}
}
}
// 执行结果1
a---start
a---end
b---start
b---end
// 执行结果2
a---start
b---start
a---end
b---end
复制代码
可以看出yield()方法不一定礼让成功,线程从Running进入Runnable后,有可能会重新获得CPU的执行权
总结
- 礼让线程,让当前正在执行的线程重回就绪状态(Runnable),该方法不会让线程进入阻塞状态(与sleep()的区别)。
- yield()会让出CPU重新调度,礼让不一定成功,看下次那个线程抢到CPU执行权
join()
线程强制执行 (插队)join:
join合并线程,带此线程执行完成后,再执行其他线程,其他线程阻塞 。【类似于**
插队
**】
join方法有三个重载版本:
join()
join(long millis) //参数为毫秒
join(long millis, int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
复制代码
假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的事件。
看下面一个例子:
public class TestJoin {
public static void main(String[] args) throws IOException {
System.out.println("进入线程"+Thread.currentThread().getName());
TestJoin test = new TestJoin();
MyThread thread1 = test.new MyThread();
thread1.start();
try {
System.out.println("线程"+Thread.currentThread().getName()+"等待");
thread1.join();
System.out.println("线程"+Thread.currentThread().getName()+"继续执行");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("进入线程"+Thread.currentThread().getName());
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
// TODO: handle exception
}
System.out.println("线程"+Thread.currentThread().getName()+"执行完毕");
}
}
}
// 执行结果:
进入线程main
线程main等待
进入线程Thread-0
线程Thread-0执行完毕
线程main继续执行
复制代码
可以看出,当调用thread1.join()方法后,main线程会进入等待,然后等待thread1执行完之后再继续执行。
实际上调用join方法是调用了Object的wait方法,这个可以通过查看join()源码得知:
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;
}
}
}
复制代码
wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。
由于wait方法会让线程释放对象锁,所以join方法同样会让线程释放对一个对象持有的锁。具体的wait方法使用在后面文章中给出。
interrupt()
interrupt,顾名思义,即**中断
**的意思。单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就说,它可以用来中断一个正处于阻塞状态的线程;另外,通过interrupt方法和isInterrupted()方法来停止正在运行的线程。
此操作会将线程的中断标志位置位,至于线程作何动作那要看线程了。
- 如果线程sleep()、wait()、join()等处于阻塞状态,那么线程会定时检查中断状态位如果发现中断状态位为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断状态位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。
- 如果线程正在运行、争用synchronized、lock()等,那么是不可中断的,他们会忽略。
可以通过以下三种方式来判断中断:
1)isInterrupted()
此方法只会读取线程的中断标志位,并不会重置。
2)interrupted()
此方法读取线程的中断标志位,并会重置。
3)throw InterruptException
抛出该异常的同时,会重置中断标志位。
public class TestInterrupt {
public static void main(String[] args) throws IOException {
TestInterrupt test = new TestInterrupt();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
try {
System.out.println("进入睡眠状态");
Thread.currentThread().sleep(10000);
System.out.println("睡眠完毕");
} catch (InterruptedException e) {
System.out.println("得到中断异常");
}
System.out.println("run方法执行完毕");
}
}
}
// 执行结果:
进入睡眠状态
得到中断异常
run方法执行完毕
复制代码
public class TestInterrupt2 {
public static void main(String[] args) throws IOException {
TestInterrupt2 test = new TestInterrupt2();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
int i = 0;
while(i<Integer.MAX_VALUE){
System.out.println(i+" while循环");
i++;
}
}
}
}
复制代码
运行该程序会发现,while循环会一直运行直到变量i的值超出Integer.MAX_VALUE。所以说直接调用interrupt方法不能中断正在运行中的线程。
但是如果配合isInterrupted()能够中断正在运行的线程,因为调用interrupt方法相当于将中断标志位置为true,那么可以通过调用isInterrupted()判断中断标志是否被置位来中断线程的执行。比如下面这段代码:
/**
* interrupt方法配合isInterrupted()能够中断正在运行的线程中断处于
*/
public class TestInterrupt3 {
public static void main(String[] args) throws IOException {
TestInterrupt3 test = new TestInterrupt3();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
int i = 0;
while(!isInterrupted() && i<Integer.MAX_VALUE){
System.out.println(i+" while循环");
i++;
}
}
}
}
复制代码
运行会发现,打印若干个值之后,while循环就停止打印了。
但是一般情况下不建议通过这种方式来中断线程,一般会在MyThread类中增加一个属性 isStop来标志是否结束while循环,然后再在while循环中判断isStop的值。
public class TestInterrupt4 {
public static void main(String[] args) throws IOException {
TestInterrupt3 test = new TestInterrupt3();
TestInterrupt3.MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
class MyThread extends Thread{
private volatile boolean isStop = false;
@Override
public void run() {
int i = 0;
while(!isStop){
i++;
}
}
public void setStop(boolean stop){
this.isStop = stop;
}
}
}
复制代码
如此就可以在外面通过调用setStop方法来终止while循环。
stop()和 destroy()
stop方法已经是一个废弃的方法,它是一个不安全的方法。因为调用stop方法会直接终止run方法的调用,并且会抛出一个ThreadDeath错误,如果线程持有某个对象锁的话,会完全释放锁,导致对象状态不一致。所以stop方法基本是不会被用到的。
destroy方法也是废弃的方法。基本不会被使用到。
JDK不推荐使用stop()和destroy()来停止线程。
一般让线程自己停止下来,或者使用标志位进行终止变量,当标志位为false时,线程停止。
/**
* 使用标志位进行终止变量,当标志位为false时,线程停止。
*/
public class TestStop implements Runnable{
// 1.线程中定义线程体中使用的标识
private boolean flag = true;
@Override
public void run() {
// 2.线程体使用该标识
while (true){
System.out.println("业务逻辑");
}
}
// 对外提供方法,改变标识
public void stop() {
this.flag = false;
}
}
复制代码
suspend()/resume()
挂起线程,直到被resume,才会苏醒。
但调用suspend()的线程和调用resume()的线程,可能会因为争锁的问题而发生死锁,所以JDK 7开始已经不推荐使用了。
其他方法
以下是关系到线程属性的几个方法:
1. getId
用来得到线程ID
2. getName和setName
用来得到或者设置线程名称。
3. getPriority和setPriority
用来获取和设置线程优先级。
注:优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了。线程的执行与否,取决于CPU的调度
4. setDaemon和isDaemon
用来设置线程是否成为守护线程和判断线程是否是守护线程。
守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。
Thread类有一个比较常用的静态方法currentThread()用来获取当前线程。