volatile与Java内存模型
java
public class Demo09 {
public static boolean flag = true;
public static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
System.out.println("线程" + this.getName() + " in");
while (flag) {
;
}
System.out.println("线程" + this.getName() + "停止了");
}
}
public static void main(String[] args) throws InterruptedException {
new T1("t1").start();
//休眠1秒
Thread.sleep(1000);
//将flag置为false
flag = false;
}
}
运行上面代码,会发现程序无法终止。
线程t1的run()方法中有个循环,通过flag来控制循环是否结束,主线程中休眠了1秒,将flag置为false,按说此时线程t1会检测到flag为false,打印"线程t1停止了",为何和我们期望的结果不一样呢?运行上面的代码我们可以判断,t1中看到的flag一直为true,主线程将flag置为false之后,t1线程中并没有看到,所以一直死循环。
那么t1中为什么看不到被主线程修改之后的flag?
要解释这个,我们需要先了解一下java内存模型(JMM),Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:
从上图中可以看出,线程A需要和线程B通信,必须要经历下面2个步骤:
- 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去
- 然后,线程B到主内存中去读取线程A之前已更新过的共享变量
下面通过示意图来说明这两个步骤:
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
对JMM了解之后,我们再看看文章开头的问题,线程t1中为何看不到被主线程修改为false的flag的值,有两种可能:
- 主线程修改了flag之后,未将其刷新到主内存,所以t1看不到
- 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中获取flag最新的值
对于上面2种情况,有没有什么办法可以解决?
是否有这样的方法:线程中修改了工作内存中的副本之后,立即将其刷新到主内存;工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。
java帮我们提供了这样的方法,使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:
- 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
- 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
我们修改一下开头的示例代码:
java
public volatile static boolean flag = true;
使用volatile修饰flag变量,然后运行一下程序,输出:
线程t1 in
线程t1停止了
这下程序可以正常停止了。
volatile解决了共享变量在多线程中可见性的问题,可见性是指一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。
线程组
我们可以把线程归属到某个线程组中,线程组可以包含多个线程 以及线程组,线程和线程组组成了父子关系,是个树形结构,如下图:
使用线程组可以方便管理线程,线程组提供了一些方法方便方便我们管理线程。
创建线程关联线程组
创建线程的时候,可以给线程指定一个线程组,代码如下:
java
import java.util.concurrent.TimeUnit;
public class Demo1 {
public static class R1 implements Runnable {
@Override
public void run() {
System.out.println("threadName:" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadGroup threadGroup = new ThreadGroup("thread-group-1");
Thread t1 = new Thread(threadGroup, new R1(), "t1");
Thread t2 = new Thread(threadGroup, new R1(), "t2");
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("活动线程数:" + threadGroup.activeCount());
System.out.println("活动线程组:" + threadGroup.activeGroupCount());
System.out.println("线程组名称:" + threadGroup.getName());
}
}
输出结果:
java
threadName:t1
threadName:t2
活动线程数:2
活动线程组:0
线程组名称:thread-group-1
activeCount() 方法可以返回线程组中的所有活动线程数,包含下面的所有子孙节点的线程,由于线程组中的线程是动态变化的,这个值只能是一个估算值。
为线程组指定父线程组
创建线程组的时候,可以给其指定一个父线程组,也可以不指定,如果不指定父线程组,则父线程组为当前线程的线程组,java api有2个常用的构造方法用来创建线程组:
java
public ThreadGroup(String name)
public ThreadGroup(ThreadGroup parent, String name)
第一个构造方法未指定父线程组,看一下内部的实现:
java
public ThreadGroup(String name) {
this(Thread.currentThread().getThreadGroup(), name);
}
系统自动获取当前线程的线程组作为默认父线程组。
上一段示例代码:
java
import java.util.concurrent.TimeUnit;
public class Demo2 {
public static class R1 implements Runnable {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println("所属线程组:" + thread.getThreadGroup().getName() + ",线程名称:" + thread.getName());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadGroup threadGroup1 = new ThreadGroup("thread-group-1");
Thread t1 = new Thread(threadGroup1, new R1(), "t1");
Thread t2 = new Thread(threadGroup1, new R1(), "t2");
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("threadGroup1活动线程数:" + threadGroup1.activeCount());
System.out.println("threadGroup1活动线程组:" + threadGroup1.activeGroupCount());
System.out.println("threadGroup1线程组名称:" + threadGroup1.getName());
System.out.println("threadGroup1父线程组名称:" + threadGroup1.getParent().getName());
System.out.println("----------------------");
ThreadGroup threadGroup2 = new ThreadGroup(threadGroup1, "thread-group-2");
Thread t3 = new Thread(threadGroup2, new R1(), "t3");
Thread t4 = new Thread(threadGroup2, new R1(), "t4");
t3.start();
t4.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("threadGroup2活动线程数:" + threadGroup2.activeCount());
System.out.println("threadGroup2活动线程组:" + threadGroup2.activeGroupCount());
System.out.println("threadGroup2线程组名称:" + threadGroup2.getName());
System.out.println("threadGroup2父线程组名称:" + threadGroup2.getParent().getName());
System.out.println("----------------------");
System.out.println("threadGroup1活动线程数:" + threadGroup1.activeCount());
System.out.println("threadGroup1活动线程组:" + threadGroup1.activeGroupCount());
System.out.println("----------------------");
threadGroup1.list();
}
}
输出结果:
java
所属线程组:thread-group-1,线程名称:t1
所属线程组:thread-group-1,线程名称:t2
threadGroup1活动线程数:2
threadGroup1活动线程组:0
threadGroup1线程组名称:thread-group-1
threadGroup1父线程组名称:main
----------------------
所属线程组:thread-group-2,线程名称:t4
所属线程组:thread-group-2,线程名称:t3
threadGroup2活动线程数:2
threadGroup2活动线程组:0
threadGroup2线程组名称:thread-group-2
threadGroup2父线程组名称:thread-group-1
----------------------
threadGroup1活动线程数:4
threadGroup1活动线程组:1
----------------------
java.lang.ThreadGroup[name=thread-group-1,maxpri=10]
Thread[t1,5,thread-group-1]
Thread[t2,5,thread-group-1]
java.lang.ThreadGroup[name=thread-group-2,maxpri=10]
Thread[t3,5,thread-group-2]
Thread[t4,5,thread-group-2]
代码解释:
- threadGroup1未指定父线程组,系统获取了主线程的线程组作为threadGroup1的父线程组,输出结果中是:main
- threadGroup1为threadGroup2的父线程组
- threadGroup1活动线程数为4,包含了threadGroup1线程组中的t1、t2,以及子线程组threadGroup2中的t3、t4
- 线程组的list()方法,将线程组中的所有子孙节点信息输出到控制台,用于调试使用
根线程组
获取根线程组
java
public class Demo3 {
public static void main(String[] args) {
System.out.println(Thread.currentThread());
System.out.println(Thread.currentThread().getThreadGroup());
System.out.println(Thread.currentThread().getThreadGroup().getParent());
System.out.println(Thread.currentThread().getThreadGroup().getParent().getParent());
}
}
运行上面代码,输出:
css
Thread[main,5,main]
java.lang.ThreadGroup[name=main,maxpri=10]
java.lang.ThreadGroup[name=system,maxpri=10]
null
从上面代码可以看出:
- 主线程的线程组为main
- 根线程组为system
看一下ThreadGroup的源码:
java
private ThreadGroup() { // called from C code
this.name = "system";
this.maxPriority = Thread.MAX_PRIORITY;
this.parent = null;
}
发现ThreadGroup默认构造方法是private的,是由c调用的,创建的正是system线程组。
批量停止线程
调用线程组interrupt() ,会将线程组树下的所有子孙线程中断标志置为true,可以用来批量中断线程。
示例代码:
java
import java.util.concurrent.TimeUnit;
public class Demo4 {
public static class R1 implements Runnable {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println("所属线程组:" + thread.getThreadGroup().getName() + ",线程名称:" + thread.getName());
while (!thread.isInterrupted()) {
;
}
System.out.println("线程:" + thread.getName() + "停止了!");
}
}
public static void main(String[] args) throws InterruptedException {
ThreadGroup threadGroup1 = new ThreadGroup("thread-group-1");
Thread t1 = new Thread(threadGroup1, new R1(), "t1");
Thread t2 = new Thread(threadGroup1, new R1(), "t2");
t1.start();
t2.start();
ThreadGroup threadGroup2 = new ThreadGroup(threadGroup1, "thread-group-2");
Thread t3 = new Thread(threadGroup2, new R1(), "t3");
Thread t4 = new Thread(threadGroup2, new R1(), "t4");
t3.start();
t4.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("-----------threadGroup1信息-----------");
threadGroup1.list();
System.out.println("----------------------");
System.out.println("停止线程组:" + threadGroup1.getName() + "中的所有子孙线程");
threadGroup1.interrupt();
TimeUnit.SECONDS.sleep(2);
System.out.println("----------threadGroup1停止后,输出信息------------");
threadGroup1.list();
}
}
输出:
java
所属线程组:thread-group-1,线程名称:t1
所属线程组:thread-group-1,线程名称:t2
所属线程组:thread-group-2,线程名称:t3
所属线程组:thread-group-2,线程名称:t4
-----------threadGroup1信息-----------
java.lang.ThreadGroup[name=thread-group-1,maxpri=10]
Thread[t1,5,thread-group-1]
Thread[t2,5,thread-group-1]
java.lang.ThreadGroup[name=thread-group-2,maxpri=10]
Thread[t3,5,thread-group-2]
Thread[t4,5,thread-group-2]
----------------------
停止线程组:thread-group-1中的所有子孙线程
线程:t4停止了!
线程:t2停止了!
线程:t1停止了!
线程:t3停止了!
----------threadGroup1停止后,输出信息------------
java.lang.ThreadGroup[name=thread-group-1,maxpri=10]
java.lang.ThreadGroup[name=thread-group-2,maxpri=10]
停止线程之后,通过list() 方法可以看出输出的信息中不包含已结束的线程了。
多说几句,建议大家再创建线程或者线程组的时候,给他们取一个有意义的名字,对于计算机来说,可能名字并不重要,但是在系统出问题的时候,你可能会去查看线程堆栈信息,如果你看到的都是t1、t2、t3,估计自己也比较崩溃,如果看到的是httpAccpHandler、dubboHandler类似的名字,应该会好很多。
用户线程和守护线程
守护线程 是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程 、JIT线程 都是守护线程 。与之对应的是用户线程 ,用户线程可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作。如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出。
java线程分为用户线程和守护线程,线程的daemon属性为true表示是守护线程,false表示是用户线程。
下面我们来看一下守护线程的一些特性。
程序只有守护线程时,系统会自动退出
typescript
public class Demo1 {
public static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.getName() + "开始执行," + (this.isDaemon() ? "我是守护线程" : "我是用户线程"));
while (true) ;
}
}
public static void main(String[] args) {
T1 t1 = new T1("子线程1");
t1.start();
System.out.println("主线程结束");
}
}
运行上面代码,结果如下:
可以看到主线程已经结束了,但是程序无法退出,原因:子线程1是用户线程,内部有个死循环,一直处于运行状态,无法结束。
再看下面的代码:
typescript
public class Demo2 {
public static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.getName() + "开始执行," + (this.isDaemon() ? "我是守护线程" : "我是用户线程"));
while (true) ;
}
}
public static void main(String[] args) {
T1 t1 = new T1("子线程1");
t1.setDaemon(true);
t1.start();
System.out.println("主线程结束");
}
}
运行结果:
程序可以正常结束了,代码中通过 t1.setDaemon(true); 将t1线程设置为守护线程,main方法所在的主线程执行完毕之后,程序就退出了。
结论:当程序中所有的用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出。
设置守护线程,需要在start()方法之前进行
java
import java.util.concurrent.TimeUnit;
public class Demo3 {
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
t1.setDaemon(true);
}
}
t1.setDaemon(true);是在t1的start()方法之后执行的,执行会报异常,运行结果如下:
线程daemon的默认值
我们看一下创建线程源码,位于Thread类的init() 方法中:
java
Thread parent = currentThread();
this.daemon = parent.isDaemon();
dameon的默认值为为父线程的daemon,也就是说,父线程如果为用户线程,子线程默认也是用户现场,父线程如果是守护线程,子线程默认也是守护线程。
示例代码:
java
public class Demo4 {
public static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.getName() + ".daemon:" + this.isDaemon());
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + ".daemon:" + Thread.currentThread().isDaemon());
T1 t1 = new T1("t1");
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
System.out.println(this.getName() + ".daemon:" + this.isDaemon());
T1 t3 = new T1("t3");
t3.start();
}
};
t2.setName("t2");
t2.setDaemon(true);
t2.start();
TimeUnit.SECONDS.sleep(2);
}
}
运行代码,输出:
arduino
main.daemon:false
t1.daemon:false
t2.daemon:true
t3.daemon:true
t1是由主线程(main方法所在的线程)创建的,main线程是t1的父线程,所以t1.daemon为false,说明t1是用户线程。
t2线程调用了 setDaemon(true);
将其设为守护线程,t3是由t2创建的,所以t3默认线程类型和t2一样,t2.daemon为true。
总结
- java中的线程分为用户线程 和守护线程
- 程序中的所有的用户线程结束之后,不管守护线程处于什么状态,java虚拟机都会自动退出
- 调用线程的实例方法setDaemon()来设置线程是否是守护线程
- setDaemon()方法必须在线程的start()方法之前调用,在后面调用会报异常,并且不起效
- 线程的daemon默认值和其父线程一样
线程安全和synchronized关键字
什么是线程安全?
当多个线程去访问同一个类(对象或方法)的时候,该类都能表现出正常的行为(与自己预想的结果一致),那我们就可以所这个类是线程安全的。
看一段代码:
java
public class Demo1 {
static int num = 0;
public static void m1() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
public static class T1 extends Thread {
@Override
public void run() {
Demo1.m1();
}
}
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
T1 t2 = new T1();
T1 t3 = new T1();
t1.start();
t2.start();
t3.start();
//等待3个线程结束打印num
t1.join();
t2.join();
t3.join();
System.out.println(Demo1.num);
/**
* 打印结果:
* 25572
*/
}
}
Demo1中有个静态变量num,默认值是0,m1()方法中对num++执行10000次,main方法中创建了3个线程用来调用m1()方法,然后调用3个线程的join()方法,用来等待3个线程执行完毕之后,打印num的值。我们期望的结果是30000,运行一下,但真实的结果却不是30000。上面的程序在多线程中表现出来的结果和预想的结果不一致,说明上面的程序不是线程安全的。
线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点:
- 一是存在共享数据(也称临界资源)
- 二是存在多条线程共同操作共享数据
因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据 ,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁 ,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作) ,同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代volatile功能) ,这点确实也是很重要的。
那么我们把上面的程序做一下调整,在m1()方法上面使用关键字synchronized,如下:
java
public static synchronized void m1() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
然后执行代码,输出30000,和期望结果一致。
synchronized主要有3种使用方式
- 修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁
- 修饰静态方法,作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁
- 修饰代码块,需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁
synchronized作用于实例对象
所谓实例对象锁就是用synchronized修饰实例对象的实例方法,注意是实例方法 ,不是静态方法,如:
java
public class Demo2 {
int num = 0;
public synchronized void add() {
num++;
}
public static class T extends Thread {
private Demo2 demo2;
public T(Demo2 demo2) {
this.demo2 = demo2;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
this.demo2.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
Demo2 demo2 = new Demo2();
T t1 = new T(demo2);
T t2 = new T(demo2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(demo2.num);
}
}
main()方法中创建了一个对象demo2和2个线程t1、t2,t1、t2中调用demo2的add()方法10000次,add()方法中执行了num++,num++实际上是分3步,获取num,然后将num+1,然后将结果赋值给num,如果t2在t1读取num和num+1之间获取了num的值,那么t1和t2会读取到同样的值,然后执行num++,两次操作之后num是相同的值,最终和期望的结果不一致,造成了线程安全失败,因此我们对add方法加了synchronized来保证线程安全。
注意:m1()方法是实例方法,两个线程操作m1()时,需要先获取demo2的锁,没有获取到锁的,将等待,直到其他线程释放锁为止。
synchronize作用于实例方法需要注意:
- 实例方法上加synchronized,线程安全的前提是,多个线程操作的是同一个实例,如果多个线程作用于不同的实例,那么线程安全是无法保证的
- 同一个实例的多个实例方法上有synchronized,这些方法都是互斥的,同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法
synchronized作用于静态方法
当synchronized作用于静态方法时,锁的对象就是当前类的Class对象。如:
java
public class Demo3 {
static int num = 0;
public static synchronized void m1() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
public static class T1 extends Thread {
@Override
public void run() {
Demo3.m1();
}
}
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
T1 t2 = new T1();
T1 t3 = new T1();
t1.start();
t2.start();
t3.start();
//等待3个线程结束打印num
t1.join();
t2.join();
t3.join();
System.out.println(Demo3.num);
/**
* 打印结果:
* 30000
*/
}
}
上面代码打印30000,和期望结果一致。m1()方法是静态方法,有synchronized修饰,锁用于与Demo3.class对象,和下面的写法类似:
csharp
public static void m1() {
synchronized (Demo4.class) {
for (int i = 0; i < 10000; i++) {
num++;
}
}
}
synchronized同步代码块
除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:
java
public class Demo5 implements Runnable {
static Demo5 instance = new Demo5();
static int i = 0;
@Override
public void run() {
//省略其他耗时操作....
//使用同步代码块对变量i进行同步操作,锁对象为instance
synchronized (instance) {
for (int j = 0; j < 10000; j++) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:
java
//this,当前实例对象锁
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//class对象锁
synchronized(Demo5.class){
for(int j=0;j<1000000;j++){
i++;
}
}
分析代码是否互斥的方法,先找出synchronized作用的对象是谁,如果多个线程操作的方法中synchronized作用的锁对象一样,那么这些线程同时异步执行这些方法就是互斥的。如下代码:
java
public class Demo6 {
//作用于当前类的实例对象
public synchronized void m1() {
}
//作用于当前类的实例对象
public synchronized void m2() {
}
//作用于当前类的实例对象
public void m3() {
synchronized (this) {
}
}
//作用于当前类Class对象
public static synchronized void m4() {
}
//作用于当前类Class对象
public static void m5() {
synchronized (Demo6.class) {
}
}
public static class T extends Thread{
Demo6 demo6;
public T(Demo6 demo6) {
this.demo6 = demo6;
}
@Override
public void run() {
super.run();
}
}
public static void main(String[] args) {
Demo6 d1 = new Demo6();
Thread t1 = new Thread(() -> {
d1.m1();
});
t1.start();
Thread t2 = new Thread(() -> {
d1.m2();
});
t2.start();
Thread t3 = new Thread(() -> {
d1.m2();
});
t3.start();
Demo6 d2 = new Demo6();
Thread t4 = new Thread(() -> {
d2.m2();
});
t4.start();
Thread t5 = new Thread(() -> {
Demo6.m4();
});
t5.start();
Thread t6 = new Thread(() -> {
Demo6.m5();
});
t6.start();
}
}
分析上面代码:
- 线程t1、t2、t3中调用的方法都需要获取d1的锁,所以他们是互斥的
- t1/t2/t3这3个线程和t4不互斥,他们可以同时运行,因为前面三个线程依赖于d1的锁,t4依赖于d2的锁
- t5、t6都作用于当前类的Class对象锁,所以这两个线程是互斥的,和其他几个线程不互斥
线程中断的几种方式
通过一个变量控制线程中断
代码:
java
import java.util.concurrent.TimeUnit;
public class Demo1 {
public volatile static boolean exit = false;
public static class T extends Thread {
@Override
public void run() {
while (true) {
//循环处理业务
if (exit) {
break;
}
}
}
}
public static void setExit() {
exit = true;
}
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
TimeUnit.SECONDS.sleep(3);
setExit();
}
}
代码中启动了一个线程,线程的run方法中有个死循环,内部通过exit变量的值来控制是否退出。 TimeUnit.SECONDS.sleep(3);
让主线程休眠3秒,此处为什么使用TimeUnit?TimeUnit使用更方便一些,能够很清晰的控制休眠时间,底层还是转换为Thread.sleep实现的。程序有个重点:volatile关键字,exit变量必须通过这个修饰,如果把这个去掉,程序无法正常退出。volatile控制了变量在多线程中的可见性,关于volatile前面的文章中有介绍,此处就不再说了。
通过线程自带的中断标志控制
示例代码:
java
public class Demo2 {
public static class T extends Thread {
@Override
public void run() {
while (true) {
//循环处理业务
if (this.isInterrupted()) {
break;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
TimeUnit.SECONDS.sleep(3);
t.interrupt();
}
}
运行上面的程序,程序可以正常结束。线程内部有个中断标志,当调用线程的interrupt()实例方法之后,线程的中断标志会被置为true,可以通过线程的实例方法isInterrupted()获取线程的中断标志。
线程阻塞状态中如何中断?
示例代码:
java
public class Demo3 {
public static class T extends Thread {
@Override
public void run() {
while (true) {
//循环处理业务
//下面模拟阻塞代码
try {
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
}
}
运行上面代码,发现程序无法结束。
在此先补充几点知识:
- 调用线程的interrupt()实例方法,线程的中断标志会被置为true****
- 当线程处于阻塞状态时,调用线程的interrupt()实例方法,线程内部会触发InterruptedException异常,并且会清除线程内部的中断标志(即将中断标志置为false)****
那么上面代码可以调用线程的interrupt()方法来引发InterruptedException异常,来中断sleep方法导致的阻塞,调整一下代码,如下:
java
public class Demo3 {
public static class T extends Thread {
@Override
public void run() {
while (true) {
//循环处理业务
//下面模拟阻塞代码
try {
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
this.interrupt();
}
if (this.isInterrupted()) {
break;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t = new T();
t.start();
TimeUnit.SECONDS.sleep(3);
t.interrupt();
}
}
运行结果:
java
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.itsoku.chat05.Demo3$T.run(Demo3.java:17)
程序可以正常结束了,分析一下上面代码,注意几点:
- main方法中调用了t.interrupt()方法,此时线程t内部的中断标志会置为true
- 然后会触发run()方法内部的InterruptedException异常,所以运行结果中有异常输出,上面说了,当触发InterruptedException异常时候,线程内部的中断标志又会被清除(变为false),所以在catch中又调用了this.interrupt();一次,将中断标志置为false
- run()方法中通过this.isInterrupted()来获取线程的中断标志,退出循环(break)
总结
- 当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,可以使用
Thread.interrupt()
方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态) - 内部有循环体,可以通过一个变量来作为一个信号控制线程是否中断,注意变量需要volatile修饰
- 文中的几种方式可以结合起来灵活使用控制线程的中断