进程&线程
1 进程&线程
1.1 进程
在计算机中,进程代表了内存中正在运行的应用程序,计算机中的资源(cpu、内存、磁盘、网络等),会按照需求,分配给每个进程,从而这个进程对应的应用程序就可以使用这些资源了。
在操作系统中,启动一个应用程序的的时候,会有一个或多个进程同时被创建,这些进程其实就表示了当前这个应用程序在系统中的资源使用情况以及程序运行的情况。如果关闭这个进程,那么对应的应用程序也就关闭了。
所以,进程就是系统中,运行一个应用程序的基本单位。
1.2 线程
线程是进程中的一个代码执行单元,负责当前进程中代码程序的执行,一个进程中有一个或者多个线程。
当一个进程中启动了多个线程去分别执行代码的时候,这个程序就是多线程程序。
例如,当前我们使用java命令去运行一个类的时候,会先启动JVM,这个JVM对于计算机来讲,就是一个应用程序,所以同时系统中也会启动一个进程和这个JVM对应。
在桌面新建文件Hello.java
public class Hello{
public static void main(String[] args)throws Exception{
System.out.println("hello");
long time = 1000*100L;
Thread.sleep(time);
System.out.println("world");
}
}
Thread.sleep方法,可以让当前执行代码的线程暂时的休眠一会
注意记得抛异常
运行Hello之后,会先输出hello,此时JVM并没有直接结束,而是让当前线程休眠了100秒,所以这时JVM还在运行着,我们可以在任务管理器中可以看到JVM对应的进程。
如果把当前进程强行关闭,那么JVM就停止了,那么程序的运行也就停止了。
当前代码的执行JVM,它执行的线程名字叫做main,他的任务就是调用执行类中的main方法
1.3 小结
一个进程对应一个应用程序
一个进程包含了1~n个线程
线程就是用来执行应用程序代码的
但是线程并不是越多越好
2 并发和并行
-
线程的并发执行,是指在一个时间段内,两个或多个线程,使用一个CPU,进行交替运行。
-
线程的并行执行,是指在同一时刻,两个或多个线程,各自使用一个CPU,同时进行运行。
如果计算机是单核CPU的话,那么同一时刻只能有一个线程使用CPU来执行代码
如果计算机是多核CPU的话,那么同一时刻有可能是两个线程同时使用不同的CPU执行代码
可以看出,多核CPU确定可以提高程序的运行速度
但是,加入我们在程序中编写了两个线程,启动并运行它们,那么我们是无法控制也无法知道,计算机中到底是使用了一个CPU去运行它们,还是使用两个CPU去运行它们。这是计算机内核中对资源进行调度的事情,我们从应用程序的层面是无法干涉的。
所以,在一般情况下,我们编写多线程代码时,可以用单核CPU的情况来考虑问题,去设计并编写多线程的代码。
将来计算机如果真的使用多个CPU来运行我们的多线程代码的话,那么也只是提高了代码的执行效率,基本不会影响我们代码的设计和实现的。
3 时间片
3.1 概述
时间片,当前一个线程要使用CPU的时候,CPU会分配给这个线程一小段时间(毫秒级别),这段时间就叫做时间片,也就是该线程允许使用CPU来运行的时间,在这个期间,线程拥有CPU的使用权。
如果在一个时间片结束时,线程还在运行,那么这时候,该线程就需要停止运行,并交出CPU的使用权,然后等待下一个CPU时间片的分配。
所以,在宏观上,一段时间内,我们感觉两个线程在同时运行代码,其实在微观中,这两个线程在使用一个CPU的时候,他们是交替着运行的,每个线程每次都是运行一个很小的时间片,然后就交出CPU使用权,只是他们两个交替运行的速度太快了,给我们的感觉,好像是他们两个线程在同时运行。
3.2 调度
当两个或多个线程使用一个CPU来运行代码的时候,在操作系统的内核中,就会有相应的算法来控制线程获取CPU时间片的方式,从而使得这些线程可以按照某种顺序来使用CPU运行代码,这种情况被称为线程调度。
常见的调度方式:
- 时间片轮转:
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间 - 抢占式调度:
系统会让优先级高的线程优先使用CPU(提高抢占到的概率),但是如果线程的优先级相同,那么会随机选择一个线程获取当前CPU的时间片
注:JVM中的线程,使用的调度方式是抢占式调度
例:
public static void main(String[] args) {
// 创建线程对象t1
Thread t1 = new Thread() {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("hello" + i);
}
}
};
// 创建线程对象t2
Thread t2 = new Thread() {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("world" + i);
}
}
};
// 启动线程t1,t2
t1.start();
t2.start();
}
上述代码运行结果:
从上图看出,当前是先执行了线程t1输出了7次,之后cpu被线程t2抢走执行了t2一次再次被线程t1抢走。
上述代码重新运行结果:
有上述两个结果可知:JVM中的线程,使用的调度方式是抢占式调度
4 main线程
使用java命令来运行一个类的时候,首先会启动JVM(进程),JVM会创建一个名字叫做main的线程,来执行类中的main方法(程序入口)
public static void main(String[] args) {
// 获取执行当前方法的线程对象
Thread currentThread = Thread.currentThread();
System.out.println("执行当前方法的线程名字为:" + currentThread.getName());
}
运行结果:
所以,我们卸载main方法中的代码,其实都是由名字叫做main的线程去执行的。
Thread.currentThread()
可以写在任意方法中,返回的就是执行这个方法的线程对象
上述代码使用java命令运行过程:
- 使用java命令运行Test类,会先启动JVM
- 应用类加载器通过CLASSPATH环境变量配置的路径,找到Test.class文件,并加载到方法区
注意:这里会同时产生一个Class类型对象,来代表这个Test类型,并且会优先处理类中的静态代码(静态属性、静态方法、静态代码块)- JVM创建并启动一个名字叫main的线程
- main线程将Test中的main方法加载到栈区中
- 在栈里面,main线程就可以一行行的执行方法中的代码了
- 如果在执行代码中,遇到了方法调用,那么线程会继续把调用的方法,加载到栈中(压栈操作),然后执行栈顶这个最新添加进来的方法,栈顶方法执行完,就释放(出栈操作),然后再执行当前最新的栈顶方法
- 代码执行过程输出执行结果
- 当前是单线程程序,main线程结束了,JVM就停止了,如果是多线程程序,那么JVM要等所有线程都结束了才会停止
例:
public class Test2 {
public static void show() {
Thread currentThread = Thread.currentThread();
System.out.println("执行show方法的线程名字为:" + currentThread.getName());
}
public static void main(String[] args) {
// 获取执行当前方法的线程对象
Thread currentThread = Thread.currentThread();
System.out.println("执行当前方法的线程名字为:" + currentThread.getName());
System.out.println("调用show方法");
show();
}
}
show方法被main方法调用,当前JVM只创建了main线程,所以show方法也是被main线程所执行的哦
5 线程的创建和启动
java.lang.Thread
是java中的线程类,所有的线程对象都必须是Thread类或其子类的实例。
每个线程的作用,就是完成我们给它指定的任务,实际上就是执行一段我们指定的代码。我们只需要在thread类的子类中重写run方法,把执行的代码写入到run方法中即可,这就是线程的执行任务。
5.1 继承Thread类
Java中通过集成Thread类来创建并启动一个新的线程的步骤如下:
- 定义Thread类的子类(可以是匿名内部类),并重写Thread类中的run方法,run方法中的代码就是线程的执行任务。
- 创建Thread子类的对象,这个对象就代表了一个要独立运行的新线程。
- 调用线程对象的start方法来启动该线程。
- 方法一:
public class Test3 {
public static void main(String[] args) {
// 2. 创建线程类对象
Thread t = new MyThread();
// 3. 调用start方法启动线程
t.start();
}
}
//1. 子类继承父类Thread,并重写run方法(指定线程的执行任务)
class MyThread extends Thread {
@Override
public void run() {
System.out.println("执行当前方法的线程名为:" + Thread.currentThread().getName());
for (int i = 0; i < 10; i++) {
System.out.println("hello world");
try {
// 可以让当前执行的代码的线程睡眠1000毫秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 方法二:
使用匿名内部类
public class Test4 {
public static void main(String[] args) {
Thread t= new MyThread() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
}
}
main线程在执行main方法的过程中,创建并启动了t线程,并且t线程启动后,和mian线程就没有关系了,这时候main线程和t线程都是自己独立的运行,并且他们两个是要争夺CPU的时间片(使用权)的
上述代码在内存中的情况:
注意1:之前所提到的栈区,又被称为方法调用栈,是线程专门执行方法中代码的地方,并且每一个线程,都有自己独立的栈空间,和别的线程互不影响
注意2:最先启动的线程是主线程(main线程),因为它要执行程序的入口main方法,在主线程中,创建并且启动了t线程,启动之后main线程和t线程将各自独立运行,并且争夺CPU的时间片。
注意3:线程启动之后(调用start方法),会开始争夺CPU的时间片,然后自动执行run方法,如果子类对象重写了,那么就调用到重写后的run方法
注意4:堆区是对所有线程共享的,每个线程中如果创建了对象,那么对象就会存放到堆区中。
注意5:线程对象t被创建出来的时候,他还只是一个普通的对象,但是当调用了t.start()方法之后,线程对象t可以说才真正的“现出原形”,开辟了单独的栈空间,供线程t调用方法使用
多线程比单线程的优势在于提高了程序运行速度。
一般会将执行时间较长的代码交给多线程去执行处理
5.2 实现Runnable接口
给一个线程对象指定要执行的任务,除了继承Thread类后重写run方法之外,还可以利用Runnable接口来完成线程任务的指定
java.lang.Runnable
,该接口中只有一个抽象方法run()
public interface Runnable {
public abstract void run();
}
其实thread类也是Runnable接口的实现类,其代码结构大致为:
public class Thread implements Runnable {
private Runnable target;
public Thread() {
//..
}
public Thread(Runnable target) {
this,target = target;
}
public void run() {
if(target != null) {
target.run();
}
}
}
可以看出,子类重写Thread中的run方法,这个run方法其实也来自于Runnable接口
通过以上的代码结构,可知,我们还可以直接创建Thread对象,在调用构造器的时候,传一个Runnable接口的实现类对象进来,然后调用线程的对象run方法,那么默认就会调用到Runnable接口实现类重写的方法
例1:
public class Test5 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
Thread t = new Thread(myThread1);
t.start();
System.out.println("main");
}
}
class MyThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("win-run" + i);
}
}
}
运行结果:
main
win-run0
win-run1
win-run2
win-run3
win-run4
例2:
public class Test6 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("win" + i);
}
}
});
t.start();
System.out.println("main");
}
}
运行结果同上
实现Runnable接口比继承Thread类所具有的优势:
- 可以把相同的一个执行任务(Runnable接口的实现),交给不同的线程对象去执行
- 可以避免java中的单继承的局限性
- 线程和执行代码各自独立,实现代码解耦
6 线程的名字
通过Thread类中的currentThread()
方法,可以获取当前线程的对象,然后调用线程对象的getName()
方法,可以获取当前线程的名字。
String name = Thread.currentThread().getName();
注意,这里说的当前线程,指定是执行当前方法的线程,因为获取线程名字的代码肯定是写在某个方法中的,并且这个方法一定是由某个线程调用执行的
例:
public class Test7 {
public static void main(String[] args) {
String name = Thread.currentThread().getName();
System.out.println("执行当前main方法的线程是:" + name);
Runnable run = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println("执行当前run方法的线程是:" + name);
}
};
Thread t = new Thread(run);
t.start();
// 注意!
// run.run();
}
}
运行结果:
执行当前main方法的线程是:main
执行当前run方法的线程是:Thread-0
注意,一定要记得start方法启动线程之后,线程会自动执行run方法。
如果在main方法中直接调用run方法,这样就不是启动线程执行任务,而是普通的方法调用,和调用sayHello没区别
默认情况下,主线程中,创建出的线程,他们都会有一个默认的名字:
public Thread(){
init(null, null, "Thread-" + nextThreadNum(), 0);
}
其中,“Thread-” + nextThreadNum()
就是在拼接处这个线程默认的名字,Thread-0 Thread-1 Thread-2等等
自定义线程名字
除了默认的线程名字,我们也可以设置一个指定的名字给它
//方法一:
Thread t = new Thread("t线程");
//方法二:
Thread t = new Thread(new Runnable(){
public void run(){
//执行任务
}
}, "t线程");
//方法三:
Thread t = new Thread();
t.setName("t线程");
注意:
线程的名字是在创建线程的时候指定的,所以main线程的名字不能改变
7 线程的分类
java中,线程可以分为:
- 前台线程,又叫做执行线程、用户线程
- 后台线程,又叫做守护线程、精灵线程
前台线程
这种线程专门用来执行用户编写的代码,地位比较高,JVM是否会停止运行,就是要看当前是否还有前台线程在执行,如果还剩下任意一个前台线程在执行,那么JVM就不能停止
后台线程
这种线程是用来给前台线程服务的,给前台线程提供一个良好的运行环境,地位比较低,JVM是否停止运行,根本不关心后台线程的运行情况和状态。
例如,垃圾回收器就是一个后台线程,他一直在背后默默的执行着垃圾回收的代码,为我们前台线程在执行用户代码的时候,提供一个良好的内存环境。
小结:
前台线程:main线程/开发人员编写的线程,JVM是否关闭取决于前台线程是否全部执行完毕
后台线程:给前台线程提供良好的运行环境,例如:内存环境、类加载环境。JVM是否关闭和他无关
在主线程中,创建出来的线程对象,默认就是前台线程,在他启动之前,我们可以将它设置为后台线程:
public class Test8 {
public static void main(String[] args) {
Thread thread = new Thread("t线程") {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
System.out.println(name + ":hello" + i);
}
}
};
// 在启动线程之前,可以将其设置为后台线程,否则默认是前台线程
thread.setDaemon(true);
thread.start();
}
}
8 线程优先级
线程类Thread中,有一个属性,表示线程的优先级
可看出,最终设置线程优先级的方法,是一个native方法,并不是java语言实现的
线程的优先级使用int类型数字表示,最大为10,最小为1,默认为5.
当两个线程争夺CPU时间片时:
- 优先级相同,获得CPU使用权的概率相同
- 优先级不同,那么高优先级的线程有更高的概率获取到CPU的使用权
(只是概率会高,但不表示一定会先执行优先级高的)
public class Test9 {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程") {
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 10000; i++) {
}
System.out.println(name + "线程执行完毕");
}
};
Thread t2 = new Thread("t2线程") {
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 10000; i++) {
}
System.out.println(name + "线程执行完毕");
}
};
t2.setPriority(10);
System.out.println("t1线程的优先级" + t1.getPriority());
System.out.println("t2线程的优先级" + t2.getPriority());
t1.start();
t2.start();
}
}
上述代码其中一次执行结果:
由上图可知,即便t2线程优先级比t1大,但t2不一定会是第一个执行的
注意1,默认情况下,俩个线程的优先级都是5,那么俩个线程争夺到CPU的使用权的概率一样,那么基本上俩个线程都有相同的概率先执行完10000次循环
注意2,其实t1先稍微占了那么一点点的优势,因为毕竟在主线程的代码中,先启动了t1线程,然后又启动了t2线程
注意3,设置t1和t2优先级之后,在运行查看结果,会明显看到优先级高的线程,有更高的概率先执行完代码
9 线程组
Java中使用java.lang.ThreadGroup
类来表示线程组,它可以对一批线程进行管理,对线程组进行操作,同时也会对线程组里面的这一批线程操作。
java.lang.ThreadGroup
:
public class ThreadGroup{
public ThreadGroup(String name){
//...
}
public ThreadGroup(ThreadGroup parent, String name){
//...
}
}
创建线程组的时候,需要指定该线程组的名字。
也可以指定其父线程组,如果没有指定,那么这个新创建的线程组的父线程组就是当前线程组。
例:
public class Test1 {
public static void main(String[] args) {
// 获取当前线程对象
Thread thread = Thread.currentThread();
// 获取当前线程所属的线程组
ThreadGroup threadGroup = thread.getThreadGroup();
System.out.println(threadGroup);
System.out.println(threadGroup.getName());
}
}
运行结果:
java.lang.ThreadGroup[name=main,maxpri=10]
main
可以看出,当前线程组的名字为main,并且线程组中的线程最大优先级可以设置为10
例:用户在主线程中创建新的线程,属于默认线程组(也就是名为“main”的线程组)
public class Test2 {
public static void main(String[] args) {
// 创建新线程
Thread t = new Thread();
ThreadGroup threadGroup = t.getThreadGroup();
System.out.println(threadGroup);
}
}
运行结果:
java.lang.ThreadGroup[name=main,maxpri=10]
例:
public class Test3 {
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("myThreadGroup");
// 指定线程所属的线程组
Thread thread = new Thread(threadGroup, "t线程");
ThreadGroup threadGroup2 = thread.getThreadGroup();
System.out.println(threadGroup2);
}
}
运行结果:
java.lang.ThreadGroup[name=myThreadGroup,maxpri=10]
例:
package com.test.demo1;
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
// 新建线程组
ThreadGroup group = new ThreadGroup("我的线程组");
Runnable run = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 创建新的线程
Thread t1 = new Thread(group, run, "t1线程");
Thread t2 = new Thread(group, run, "t2线程");
Thread t3 = new Thread(group, run, "t3线程");
// 启动三个线程
t1.start();
t2.start();
t3.start();
// 返回当前线程组中还没有“死亡”的线程个数
System.out.println("线程组中还在存活的线程个数为:" + group.activeCount());
// 创建数组,用于存放线程组中仍旧存活的线程
Thread[] arr = new Thread[group.activeCount()];
// 将存活的线程存放到数组中,并返回本次存放数组的个数
System.out.println("arr数组中存放的线程个数为:" + group.enumerate(arr));
// 输出数组中的内容
System.out.println("arr数组中的内容为:" + Arrays.toString(arr));
System.out.println("--------------");
try {
//main线程睡眠101毫秒
Thread.sleep(101);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 返回当前线程组中还没有“死亡”的线程个数
System.out.println("线程组中还在存活的线程个数为:" + group.activeCount());
// 创建数组,用于存放线程组中仍旧存活的线程
Thread[] arr1 = new Thread[group.activeCount()];
// 将存活的线程存放到数组中,并返回本次存放数组的个数
System.out.println("arr数组中存放的线程个数为:" + group.enumerate(arr1));
// 输出数组中的内容
System.out.println("arr数组中的内容为:" + Arrays.toString(arr1));
}
}
运行结果:
线程组中还在存活的线程个数为:3
arr数组中存放的线程个数为:3
arr数组中的内容为:[Thread[t1线程,5,我的线程组], Thread[t2线程,5,我的线程组], Thread[t3线程,5,我的线程组]]
--------------
线程组中还在存活的线程个数为:0
arr数组中存放的线程个数为:0
arr数组中的内容为:[]
上述代码中所用到的线程组的方法有:
group.activeCount();
//获取线程组中还存活的线程个数group.enumerate(arr);
//将线程组中还存活的线程存入指定数组中
注意:只有在创建线程对象的时候,才能指定其所在的线程组,线程运行中途不能改变他所属的线程组
10 线程状态
线程状态 | 名称 | 描述 |
---|---|---|
NEW | 新建 | 线程刚被创建,还没调用start方法,或者刚刚调用了start方法,调用start方法不一定“立即”改变线程状态,中间可能需要一些步骤才能完成一个线程的启动 |
RUNNABLE | 可行 | start方法调用结束,线程由NEW变成RUNNABLE,线程存活着,并尝试抢占CPU资源,或者已经抢占到CPU资源正在运行,这两种情况的状态都显示为RUNNABLE |
BLOCKED | 锁阻塞 | 线程A和线程B都要执行方法test,而且方法test被加了锁,线程A先拿到了锁去执行test方法,线程B这时候需要等待线程A把锁释放。此时线程B就是处于BLOCKED状态 |
WAITING | 无限期等待 | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能唤醒。 |
TIMED_WAITING | 有限期等待 | 和WAITING状态类似,但是有一个时间期限,时间到了,自己也会主动醒来。 |
TERMINATED | 终止(死亡) | run方法执行结束的线程处于该状态 |
注:BLOCKED,WAITING,TIMED_WAITING这三种都属于线程阻塞,只是触发条件不同,以及从阻塞状态中恢复过来的条件也不同。
这三种阻塞状态,所具有的相同点:线程不执行代码;线程也不参与CPU时间片的争夺
10.1 线程状态关系图
注:
-
刚创建好的线程对象,处于NEW状态
-
线程启动后(start),处于RUNNABLE状态
-
RUNNABLE状态包含两种情况:
–就绪状态:此时这个线程没有运行,因为没有抢到CPU的执行权
–运行状态 :线程正在运行中,已经抢到CPU的执行权 -
JavaAPI中并没有定义就绪状态和运行状态,而是把这两种情况统一叫做RUNNABLE(可运行状态),但是一般我们为了能更清楚的描述问题,会用上就绪状态和运行状态
-
在线程多次抢到CPU执行权,“断断续续”把run方法执行完之后,就变成了TERMINATED状态(死亡),之所以是“断断续续”的运行,是因为每次抢到CPU执行权的时候,只是运行很小的一个时间片,完了之后还要重新抢占下一个时间片,并且中间还有可能抢不到的情况。
-
死亡后的线程,不能重新启动。
- 最基本的状态变化:
10.2 sleep方法
线程类Thread中的sleep()
方法:
public static native void sleep(long millis) throuws InterruptedException;
该静态方法可以让当前执行的线程暂时休眠指定的毫秒数
-
Thread.sleep(10000);
//以毫秒为单位 -
当线程执行了sleep方法,会从RUNNABLE状态进入到TIMED_WAITING状态(当前状态不占用CPU)
-
TIMED_WAITING状态(阻塞状态)特点:阻塞结束后,线程会自动回到RUNNABLE状态
状态图:
例:
public static void main(String[] args) {
Thread t1 = new Thread("t1线程") {
@Override
public void run() {
try {
//t1线程休眠10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
System.out.println(t1.getState());
t1.start();
for (int i = 0; i < 1000; i++) {
System.out.println(t1.getState());
}
}
10.3 join方法
线程类Thread中的join()
方法:
public final synchronized void join(long millis)throws InterruptedException{
//...
}
public final void join() throws InterruptedException{
//...
}
join()
:调用该方法,可以让当前线程阻塞,等待另一个指定的线程运行结束后,当前线程才可以继续运行- 状态:RUNNABLE --> WAITING
join(long time)
:调用该方法,当前线程阻塞对应的时间,等到时间结束,线程继续运行- 状态:RUNNABLE --> TIMED_WAITING
用法:
假设有线程t1、t2
- 在线程t2中调用:
t1.join();
等到t1线程执行结束t2才可以抢占CPU - 在线程t2中调用:
t1.join(100);
t2线程要等待100毫秒后即可和t1线程一同抢占CPU
状态图:
sleep()&join()总结:
- 如果指定了时间,线程阻塞一定的时间后,会自动恢复到RUNNABLE状态,这种情况下,线程的状态为TIMED_WAITING(有限期等待)
- 如果没有指定时间,线程会一直阻塞,直到某个条件满足时,才会自动恢复,这种情况下,线程的状态为WAITING(无限期等待)
10.4 interrupt方法
线程类Thread中的interrupt
方法
//Interrupts this thread
public void interrupt(){
//...
}
该方法可以让线程从阻塞状态恢复到RUNNABLE状态
根据上面介绍sleep方法和join方法可知,这两个方法都会抛出InterruptedException
类型的异常,说明调用slee和join使线程进入阻塞状态的情况下,是有可能抛出InterruptedException
类型的异常的。
InterruptedExceptin
异常类型指的是:线程A中,调用了线程B的interrupt方法,而此时线程B处于阻塞状态,那么此时sleep方法或者join方法就会抛出被打断的异常
package com.test.demo1;
public class Test2 {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程") {
@Override
public void run() {
try {
// t1线程休眠100秒
Thread.sleep(100000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("\nt1线程结束");
}
};
t1.start();
try {
// 让主线程休眠500毫秒,目的是为了给t1时间,让它调用slee方法而进入阻塞状态
Thread.sleep(500);
System.out.println(t1.getName() + "当前状态为:" + t1.getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打断t1由于调用sleep方法而进入的阻塞状态
t1.interrupt();
/*
* main线程调用t1线程的t1.interrupt方法,强制打断t1线程的阻塞状态。
* 也正因为有这个方法,sleep才会有异常需要处理
*/
}
}
运行结果:
t1线程当前状态为:TIMED_WAITING
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.test.demo1.Test2$1.run(Test2.java:10)
t1线程结束
- interrupt方法的工作原理:
interrupt方法是通过改变线程对象中的一个标识的值(true | false),来达到打断阻塞状态的效果。
一个线程在阻塞状态下,会时刻监测这个标识的值是不是true,如果一旦发现这个值变为true,那么就抛出异常结束阻塞状态,并再把这个值改为false。
从Thread类的源码中可以看到:
interrupt
方法中其实是调用了interrupt0
这个本地方法,而interrupt0的注释为:Just to set the interrupt flag
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
private native void interrupt0();
可以看出,interrupt方法只是改变了线程对象中一个标识flag的值
查看线程对象中“打断标识”值的两个方法:
- 线程类Thread的
isInterrupted()
- 线程类Thread的
interrupted()
isInterrupted()
public boolean isInterrupted(){
return isInterrupted(false);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
注意,这个非静态方法
isInterrupted()
,只是返回这个“打断标识”值,并不会对这个值进行清除(true->false),因为所传参数ClearInterrupted的值为false。
该方法在没有调用interrupt方法之前,默认返回false
当调用了interrupt方法之后,返回值为true,且不会变回false
-
例1:
-
例2:
interrupted()
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
注意,这个静态方法
interrupted()
,返回这个“打断标识”值,并且会对这个值进行清除(true->false),因为所传参数ClearInterrupted的值为true
没调用interrupt方法之前返回值也是false
调用了interrupt方法之后返回值为true,但之后该打断标识会变回false
- 例1:
Thread类中的三个方法:interrupt()
、isInterrupted()
、interrupted()
的结构关系如下:
public class Thread{
public void interrupt() {
//...
interrupt0(); // Just to set the interrupt flag
}
private native void interrupt0();
public boolean isInterrupted() {
return isInterrupted(false);
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
public static native Thread currentThread();
}
10.5 线程安全
JVM内存中的堆区,是一个共享的区域,是所有线程都可以访问的内存空间。
JVM内存中的栈区,是线程的私有空间,每个线程都有自己的栈区,别的线程无法访问到自己栈区的数据。
在多线程环境中,如果有两个线程并发访问堆区中一个对象中的数据,那么这个数据可能会出现和预期结果不符的情况。
例如:
package com.test.demo1;
class MyData {
int num;
}
public class Test3 {
public static void main(String[] args) {
final MyData myData = new MyData();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
// 先给num赋值
myData.num = i;
// 再输出
System.out.println(name + ":" + myData.num);
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 100; i < 20000; i++) {
// 给num赋值
myData.num = i;
}
}
}, "t2");
t1.start();
t2.start();
}
}
运行结果:(每次结果都不一样)
t1:2146
t1:14524
t1:16622
t1:18233
t1:19664
t1:5
t1:6
t1:7
t1:8
t1:9
可以看出,每次运行结果中,t1线程输出的num的值可能和预期都不一样
总结:
如果有多个线程,他们在一段时间内,并发访问堆区中的同一个变量,并且有写入的操作,那么最终可能会得出数据的结果和预期不符的情况,这种情况就是线程安全问题。
我们经常会进行这样的描述:这段代码是线程安全的,那段代码是非线程安全的。其实就是在说,这段代码在多线程并发访问的环境中,是否会出现上述情况,也就是结果和预期不符的情况。
思考,方法中的局部变量和对象中的成员变量分别在内存中什么地方?那些变量可能被多个线程共享?
局部变量存在虚拟机栈
成员变量(非静态)存在堆区
静态成员变量存在方法区
只有成员变量和成员方法才会多个线程共享,就会存在线程安全问题
10.6 线程同步
当使用多个线程访问同一个共享变量的时候,并且线程中对变量有写的操作,这时就容易出现线程安全问题。
java中,提供了线程同步的机制,来解决上述的线程安全问题。
Java中实现线程同步的方式,是给需要同步的代码进行synchronized
关键字加锁。
例:
class MyData{
int num;
}
public class Test {
public static void main(String[] args) {
MyData myData = new MyData();
Thread t1 = new Thread("t1"){
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized(myData) {
for(int i = 0; i < 10; i++) {
myData.num = 1;
System.out.println(name + ": " + myData.num);
}
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
synchronized(myData) {
for(int i = 0; i < 10; i++) {
myData.num = 1;
}
}
}
};
t1.start();
t2.start();
}
}
分析:
线程同步的效果,就是一段加锁的代码,每次只能有一个拿到锁的线程,才有资格去执行,没有拿到锁的线程,只能等拿到锁的线程把代码执行完,再把锁给释放了,他才能拿这个锁然后在运行代码。
这样一来,本来这段代码是两线程并发访问,“争先恐后”的去执行的,现在线程同步之后,这段代码就变成了先有一个拿到锁的线程先执行,执行完了,再有另一个线程拿到锁去执行。
如此一来就不会出现线程安全问题了。
synchronized
修饰代码块的使用格式为:
sychronized(锁对象) {
//操作共享变量的代码,这些代码需要线程同步,否则会有线程安全问题
//...
}
对应这样加锁的代码,如果两个线程进行并发访问的话:
- 假设线程t1是第一个该代码的线程,那么他会率先拿到这把锁,其实就是在这个锁对象中写入自己线程的信息,相当于告诉其他线程,这把锁现在是我的,你们都不能使用。
- 这时候t1线程拿着锁,就可已进入到加锁的代码块中,执行代码,执行很短的一个时间片,然后退出,但是锁并不释放,也就意味着,即使下次是t2线程拿到CPU,他也无法运行代码,因为t2线程没有拿到锁。
- 就这样,t1线程开心的拿着锁,抢到CPU的执行权,抢到了就去执行,抢不到也不用担心,因为没有其他线程可以执行这段代码,因为其他线程拿不到锁。
- 而对于t2线程来说,即使有一次抢到了CPU,来到代码面前,要执行的时候才发现,锁被t1线程拿走了,自己无法进入到代码块中执行,这时t2线程就会从运行状态进入阻塞状态,直到t1运行完,把锁释放了,t2线程才会恢复到RUNNABLE状态,抢到CPU,再拿到锁然后进入代码块中执行。
注意,此时t2线程的阻塞状态,和调用sleep或join方法进入的阻塞不同,这种阻塞属于锁阻塞,需要等待另一个线程把锁释放了,t2线程才能恢复。如果t2线程处于这种阻塞,那么调用线程对象的getState
方法返回的状态名称为:BLOCKED
java中,任意一个对象,只要是对象,就可以用来当作,加锁代码块中的锁对象。然后让多个线程去抢着拿这把锁就可以了,此时就达到了线程同步的效果,因为拿到锁的线程能执行代码,其他拿不到的线程就不执行,并且进入阻塞状态。
注意,线程“拿到”锁,只是一种形象的说法,如同引用“指向”对象一样。其实就是线程把自己的信息写入到了锁对象中,用这种方式告诉其他线程,这个锁对象已经被我“拿走了”。
10.7 synchronized
synchronized
可以修饰一个代码块,并指定谁是锁对象;还可以直接修饰一个方法,表示这个方法中的所有代码都需要线程同步。
synchronized直接修饰方法:
synchronized
关键字修饰非静态方法,默认使用this
当作锁对象,并且不能自己另外指定。synchronized
关键字修饰静态方法,默认使用当前类的Class对象
当作锁对象,并且不能自己另外指定。
上述两种情况的同步效果一样,只是锁对象不同。
synchronized
直接修饰方法的使用格式:
public synchronized void 方法名(){
...
}
10.8 wait方法和notify方法
Object类中有三个方法:wait()、notify()、notifyAll()
当一个对象,在线程同步的代码中,充当锁对象的时候,在synchronized
同步的代码块中,就可以调用这个锁对象的这三个方法了。
三个核心点:
- 任何对象中都一定有这三个方法
- 只有对象作为锁对象的时候,才可以调用
- 只有在同步代码块中,才可以调用
其他情况下调用一个对象的这三个方法,都会报错!
synchronized
关键字,虽然可以达到线程同步的效果,但是太“霸道",只要一个线程拿到了锁对象,那么这个线程无论是在运行状态,还是时间片用完,回到就绪状态,还是sleep休眠,这个线程都是死死拿着这个锁对象不释放,只有这个线程把线程同步的代码执行完,才会释放锁对象让别的线程使用。
那么,有没有一个方法,可以让拿到锁的线程,即使代码没有执行完,也可以把锁立即给释放了呢?
有,就是wait()
方法。
例1:
package com.test.demo1;
public class Test {
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread("t1") {
public void run() {
String name = Thread.currentThread().getName();
synchronized (object) {
for (int i = 0; i < 10; i++) {
System.out.println(name + "线程: i = " + i);
if (i == 5) {
try {
// object是锁调用,在同步代码块中,可以调用wait方法
// 让当前拿到锁的线程,立即释放锁
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
Thread t2 = new Thread("t2") {
public void run() {
String name = Thread.currentThread().getName();
synchronized (object) {
for (int j = 0; j < 20; j++) {
System.out.println(name + "线程: j = " + j);
if (j == 15) {
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
t1.start();
t2.start();
}
}
运行结果:
通过上述代码运行结果可知,t1线程和t2线程都没有运行完,但是代码都不运行了,JVM也没有停止。
这是因为,当前调用锁对象的wait方法后,当前线程释放锁,然后进入到阻塞状态,并且等待其他线程唤醒自己,如果没有其他线程唤醒自己,那么就一直等待。所以现在的情况就是,两个线程t1和t2都是处于阻塞状态,等待别人唤醒自己,所以程序不运行了,但是也没有结束。
当前线程调用getState()
方法返回状态为:WAITING
线程状态图:
可以看出,此时线程调用了wait方法,释放了锁,变为阻塞状态(WIAITING),并进入了等待池,等待其他线程唤醒自己或者打断自己,如果有线程调用了notify方法进行了唤醒,或者interrupt方法进行了打断,那么这个线程就会从等待池进入到锁池,而进入到锁池的线程,会时刻关注锁对象是否可用,一旦可用,这个线程就会立刻自动恢复到RUNNABLE状态。
TIMED_WAITING、WAITING、BLOCKED都属于线程阻塞,共同特点就是线程不执行代码,也不参与CPU的争夺,除此之外还有各自的特点:
- TIMED_WAITING:线程运行时,调用sleep或者join方法后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE状态,条件是线程被打断了、或者指定的时间到了,或者join的线程结束了。
- BLOCKED:线程运行时,发现锁不可用后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE状态,条件是线程需要争夺的锁对象变为可用了(别的线程把锁释放了)
- WAITING:线程运行时,调用了wait方法后,线程先释放锁后,再进入这种阻塞,该阻塞状态可以恢复到BLOCKED状态,条件是线程被打断了、或者是被别的线程唤醒了(notify方法)
综上,修改例1代码:
package com.test.demo1;
public class Test {
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread("t1") {
public void run() {
String name = Thread.currentThread().getName();
synchronized (object) {
for (int i = 0; i < 10; i++) {
System.out.println(name + "线程: i = " + i);
if (i == 5) {
try {
/*唤醒等待池中任意一个线程*/
object.notify();
// object是锁调用,在同步代码块中,可以调用wait方法
// 让当前拿到锁的线程,立即释放锁
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*唤醒等待池中任意一个线程*/
object.notify();
}
}
};
Thread t2 = new Thread("t2") {
public void run() {
String name = Thread.currentThread().getName();
synchronized (object) {
for (int j = 0; j < 20; j++) {
System.out.println(name + "线程: j = " + j);
if (j == 15) {
try {
/*唤醒等待池中任意一个线程*/
object.notify();
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*唤醒等待池中任意一个线程*/
object.notify();
}
}
};
t1.start();
t2.start();
}
}
锁对象.notify(),该方法可以在等待池中,随机唤醒一个等待指定锁对象的线程,使得这个线程进入锁池中,而进入到锁池的线程,一旦发现锁可用,就可以自动恢复到RUNNABLE状态了。
锁对象.notifyAll(),该方法可以在等待池中,唤醒所有等待指定锁对象的线程,使得这些线程进入到锁池中,而进入到锁池的线程,一旦发现锁可用,就自动回复到RUNNABLE状态。
10.9 死锁
在程序中要尽量避免出现死锁情况,一旦发生那么只能手动停止JVM的运行,然后查找并修改产生死锁的问题代码
简单的描述死锁就是:两个线程t1和t2,t1拿着t2需要等待的锁不释放,而t2又拿着t1需要等待的锁不释放,两个线程就这样一直僵持下去。
可以通过jcosole查看到线程死锁的情况。