线程
接下来的内容与下面这张图有关
由图可知,TIMED_WAITING、WAITING、BLOCKED都属于线程阻塞,他们共同的特点是就是线程不执行代码,也不参与CPU的争夺,除此之外,它们还有各自的特点:(重要)
(1)阻塞1,线程运行时,调用sleep或者join方法后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE状态,条件是线程被打断了、或者指定的时间到了,或者join的线程结束了
(2)阻塞2,线程运行时,发现锁不可用后,进入这种阻塞,该阻塞状态可以恢复到RUNNABLE状态,条件是线程需要争夺的锁对象变为可用了(别的线程把锁释放了)
(3)阻塞3,线程运行时,调用了wait方法后,线程先释放锁后,再进入这种阻塞,该阻塞状态可以恢复到BLOCKED状态(也就是阻塞2的情况),条件是线程被打断了、或者是被别的线程唤醒了(notify方法)
1、Thread中常用的方法
1.1 Sleep()方法
public static native void sleep(long millis) throws InterruptedException;
作用:
该静态方法可以让当前执行的线程暂时休眠指定的毫秒数
如下:
样例1:
public class Demo01 {
public static void main(String[] args) {
TestThread t = new TestThread();
t.start();
}
}
//创建一个线程类
class TestThread extends Thread{
@Override
public void run() {
System.out.println("休眠前。。。");
try {
TestThread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("休眠一段时间后。。。");
}
}
可以看出,第二句话是隔了一段时间(1秒)后输出的,这就是Sleep方法的作用
样例2:
public class Demo02 {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程") {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//获取启动前状态:这里NEW状态
System.out.println(t1.getState());
//启动t1线程
t1.start();
for(int i = 0; i<1000; i++) {
System.out.println(t1.getState());
}
}
}
结果
NEW
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
。。。
TIMED_WAITING
TIMED_WAITING
1.2 join()和join(参数)方法
作用:
让当前线程阻塞,等待另一个指定的线程运行结束后,当前线程才可以继续运行。
1、无参join方法
public class Demo03 {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程"){
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1线程结束");
}
};
Thread t2 = new Thread("t2线程") {
@Override
public void run() {
try {
t1.join(); //让t2线程阻塞,等t1先执行完成t2再执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2线程结束");
}
};
t1.start();
t2.start();
//先让主线程休眠一段时间,来让t1和t2线程进入状态
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t2.getState());
}
}
结果
WAITING
t1线程结束
t2线程结束
出现这种结果的原因:
t2线程调用了t1对象的join()方法,那t2线程就会从RUNNABLE状态进入WAITING(无限期等待)状态,等t1线程运行结束后t2线程才开始运行说人话就是join方法出现在哪个线程中,那这个线程就会进入阻塞,先让被调用对象的线程执行。。。
2、含参join()方法
public class Demo03 {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程"){
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1线程结束");
}
};
Thread t2 = new Thread("t2线程") {
@Override
public void run() {
try {
//让t1线程休眠1秒钟
t1.join(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2线程结束");
}
};
t1.start();
t2.start();
//先让主线程休眠一段时间,来让t1和t2线程进入状态
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t2.getState());
}
}
结果
TIMED_WAITING
t1线程结束
t2线程结束
可以看到,线程执行了join(long million)方法后,会从RUNNABLE状态进入到TIMED_WAITING(有限期等待)状态。
总结:
- 如果调用了无参join()方法,线程会一直阻塞着,直到某个条件满足时才会自动恢复,这种情况下,线程的状态为WAITING(无限期等待)
- 如果调用了含参join(long million)方法,线程在阻塞一段时间后,就会自动恢复到RUNNABLE状态,这种情况下,线程的状态为TIMED_WAITING(有限期等待)
1.3 interrupt()方法
1.3.1 作用
打断某个线程
1.3.2 工作原理
interrupt方法是通过改变线程对象中的一个标识的值(true|false),来达到打断阻塞状态的效果。一个线程在阻塞状态下,会时刻监测这个标识的值是不是true,如果一旦发现这个值变为true,那么就抛出异常结束阻塞状态,并再把这个值改为false。
1.3.3 关于InterruptedException异常
中文名:被打断异常
背景
其实前面的两个方法sleep方法和join方法都可能会抛出InterruptedException类型的异常,说明调用sleep和join使线程进入阻塞状态的情况下,是有可能抛出InterruptedException类型的异常的。
介绍
InterruptedException异常是指线程A中调用了线程B的interrupted方法,而此时线程B处于阻塞状态,那此时sleep方法或者join方法就会抛出被打断异常。
interrupt本质
从Thread源码可以看出,interrupt方法其实就是调用了interrupt0这个方法,这个方法上的注释为:Just to set the interrupt flag,是被native修饰的本地方法,interrupt方法的最终结果只是改变了线程对象中一个标识flag的值
具体演示看如下代码:
public class Demo04 {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程") {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("t1线程结束");
}
};
t1.start();
//让主线程休眠500毫秒
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//打断t1由于调用sleep方法而进入的阻塞状态
t1.interrupt();
}
}
结果:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at blog_xiancheng_2.Demo04$1.run(Demo04.java:10)
t1线程结束
可以看出,t1线程调用了sleep方法进入了阻塞状态,,需要100毫秒才恢复,但是主线程中调用了t1线程对象的打断方法interrupt,此时Thread.sleep(10000)方法就会抛出被打断的异常,同时t1线程从阻塞状态恢复到RUNNABLE状态,继续执行代码,最后输出“t1线程结束”。
1.4 查看线程对象中“打断标识”值的两个方法
1.4.1 isInterrupted()方法
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
功能介绍
这是个非静态方法,只是返回这个“打断标识”值(true/false),并且不会对这个值进行清除,因为所传参数ClearInterrupted的值为false。默认情况下一个线程对象的打断标识值为false。
代码演示:
样例一:
public class Demo05 {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程") {
@Override
public void run() {
for(int i = 0; i<10; i++) {
//判断是否有其他线程调用了自己的interrupt方法
//调用类中的非静态方法:isInterrupted
System.out.println(this.isInterrupted());
//为什么用this?你个铺盖,这都不晓得,首先这是个匿名内部类,不能用类名直接调用,而this的功能之一就是代替类名
}
System.out.println("t1线程结束!");
}
};
t1.start();
}
}
结果:
false
false
false
false
false
false
false
false
false
false
t1线程结束!
注意 无论线程是否处于阻塞状态,其他线程都可以调用这歌线程的interrupt方法,这个方法只是 **改变打断标识值(true/false )**而已
样例2:
public class Demo06 {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程") {
@Override
public void run() {
for(int i = 0; i<10; i++) {
//判断是否有其他线程调用了自己的interrupt方法
//调用类中的非静态方法:isInterrupted
System.out.println(this.isInterrupted());
}
System.out.println("t1线程结束!");
}
};
//启动线程
t1.start();
//打断线程
t1.interrupt();
}
}
结果
true
true
true
true
true
true
true
true
true
true
t1线程结束!
1.4.2 interrupted()方法
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
private native boolean isInterrupted(boolean ClearInterrupted);
功能介绍
这是个静态方法,返回这个打断标识值,并且会对这个值进行清除,因为传的参数ClearInterrupted的值是true
样例:
public class Demo07 {
public static void main(String[] args) {
Thread t1 = new Thread("t1线程") {
@Override
public void run() {
for(int i = 0; i<10; i++) {
//判断是否有其他线程调用了自己的interrupt方法
//调用类中的非静态方法:isInterrupted
System.out.println(Thread.interrupted());
}
System.out.println("t1线程结束!");
}
};
//启动线程
t1.start();
//打断线程
t1.interrupt();
}
}
结果:
true
false
false
false
false
false
false
false
false
false
t1线程结束!
可以看出,第一次返回true之后,后面在调用方法查看这个“打断标识”值,都是false,就是说t1线程被打断后,通过interrupted()方法判断,确实被打断了就会返回一个true,并且之后的返回值会被interrupted()方法修改成false。
总结:Interrupt()方法、isInterrupted()方法、interrupted()看起来比较像,但功能还是有差别的,
1、Interrupt()方法用来打断线程
2、isInterrupted()方法用来返回打断标识值,并且不会清除这个值
3、interrupted()方法也是用返回打断标识值,但打断后会清除这个值
2、线程安全与线程同步
2.1 线程安全
定义
如果有多个线程,他们在一段时间内,并发访问堆区中的同一个变量,并且有写入的操作,那么最终可能会出现数据的结果和预期不符的情况,这种情况就是线程安全问题。
案例
首先我们知道,JVM内存中的堆区,是一个共享的区域,是所有线程都可以访问的内存空间。JVM内存中的栈区,是线程的私有空间,每个线程都有自己的栈区,别的线程无法访问到自己栈区的数据。在多线程环境中,如果有俩个线程并发访问堆区中一个对象中的数据,那么这个数据可能会出现和预期结果不符的情况。
如下,如果有两个线程同时访问同一个对象里面的一个成员属性,结果可能就会出现错乱的问题。
public class Demo01 {
public static void main(String[] args) {
myData myData = new myData();
Thread t1 = new Thread("线程1") {
@Override
public void run() {
//获取当前线程的名字
String name = Thread.currentThread().getName();
for(int i = 0; i<10; i++) {
myData.num = i;
System.out.println(name + " : " + myData.num);
}
}
};
Thread t2 = new Thread("线程2") {
@Override
public void run() {
//获取当前线程的名字
String name = Thread.currentThread().getName();
for(int i = 100; i<2000; i++) {
myData.num = i; //在线程2中又一次给num属性赋值
}
}
};
t1.start();
t2.start();
}
}
class myData{
int num;
}
结果:
线程1 : 1999
线程1 : 1
线程1 : 2
线程1 : 3
线程1 : 4
线程1 : 5
线程1 : 6
线程1 : 7
线程1 : 8
线程1 : 9
可以看到每次运行的结果中t1线程输出的num值和预期不一样,这就是出现了线程安全问题,如图:
t1和t2并发访问的时候,争夺CPU的时间片,运行完时间片,退出后再次争夺下一个时间片,也就是说t1和t2都是“断断续续”的运行的。在这期间,可能t1线程有一次拿到时间片运行的时候,给num赋值为1,然后时间片用完退出了,结果下次t2线程拿到了时间片,又将num的值赋成了1999,然后t1线程又拿到了时间片,本来预期的是输出1,但是结果却是输出了1999。核心的原因是,t1线程操作一下变量num,然后时间片用完退出去,t2先过来又操作了变量num,等t1线程再过来的时候,这值已经被t2线程给“偷偷”修改了,那么就出现了和预期不符的情况。
补充:
成员变量是在内存中的堆区,堆区是所有线程可以访问的内存空间,所以成员变量也是所有线程所共享的。
2.2 线程同步
使用背景
当使用多个线程访问同一个共享变量的时候,并且线程中对变量有写的操作,这时就容易出现线程安全问题。
定义
Java中实现线程同步的方式,是给需要同步的代码进行synchronized关键字加锁。线程同步的效果,就是一段加锁的代码,每次只能有一个拿到锁的线程,才有资格去执行,没有拿到的锁的线程,只能等拿到锁的线程把代码执行完,再把锁给释放了,它才能去拿这个锁然后再运行代码。
synchronized关键字
synchronized修饰代码块
格式如下:
synchronized (锁对象){
//操作共享变量的代码,这些代码需要线程同步,否则会有线程安全问题
//...
}
先看一个案例,如下:
public class Demo01 {
public static void main(String[] args) {
myData myData = new myData();
Thread t1 = new Thread("线程1") {
@Override
public void run() {
//获取当前线程的名字
String name = Thread.currentThread().getName();
synchronized(myData) {
for(int i = 0; i<10; i++) {
myData.num = i;
System.out.println(name + " : " + myData.num);
}
}
}
};
Thread t2 = new Thread("线程2") {
@Override
public void run() {
//获取当前线程的名字
String name = Thread.currentThread().getName();
synchronized(myData) {
for(int i = 0; i<10; i++) {
myData.num = i;
}
}
}
};
t1.start();
t2.start();
}
}
class myData{
int num;
}
结果:
线程1 : 0
线程1 : 1
线程1 : 2
线程1 : 3
线程1 : 4
线程1 : 5
线程1 : 6
线程1 : 7
线程1 : 8
线程1 : 9
现在运行这个程序,每次输出的结果都是和预期的一样:线程1每次输出的值都是直接0~9,没有让t2来打扰到自己的执行
分析如下:
当有多个线程时,使用synchronized对象给这些线程中的某段代码上锁,并且这些锁使用的都是相同的锁对象,对于这些加锁的代码,每次只能有一个拿到锁的线程,才有资格去执行,没有拿到锁的线程,只能等拿到锁的线程把代码执行完,再把锁给释放了,它才能去拿这个锁然后再运行代码。这段代码中线程1拿到了锁,就可以进入到加锁的代码块中,去执行代码,执行很短的一个时间片,然后退出,但是锁并不释放,也就意味着,即使下次是t2线程抢到CPU的使用权,它也无法运行代码,因为t2没有拿到锁
说人话,只有一把锁,拿到锁的才可以执行。没拿到锁的等拿到锁的执行完或者等拿到锁的主动释放了锁后才能去抢锁再执行。
这时候t2线程的阻塞状态,和之前学习的调用sleep或join方法进入的阻塞不同,这种阻塞属于锁阻塞,(结合最前面的图看)。需要等待另一个线程把锁释放了,t2线程才能恢复。如果t2线程处于这种阻塞,那么调用线程对象的getState方法返回的状态名称为:BLOCKED
再看一个案例:
public class Demo02 {
public static void main(String[] args) {
//这里创建Object类的对象,并把它作为锁对象
Object obj = new Object();
//线程1
Thread t1 = new Thread("t1") {
@Override
public void run() {
synchronized(obj) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//线程2
Thread t2 = new Thread("t2") {
@Override
public void run() {
synchronized(obj) {
}
}
};
t1.start();
//这里让t主线程休息1秒钟,给t1线程点时间,让他先拿到锁,然后去休眠100秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
//分别获取两个线程的当前状态
System.out.println("t1线程状态:" + t1.getState());
System.out.println("t2线程状态:" + t2.getState());
}
}
结果如下:
t1线程状态:TIMED_WAITING
t2线程状态:BLOCKED分析:
t1线程需要拿到锁对象obj才能运行加锁的代码块,t2线程也需要拿到锁对象obj,才能运行加锁的代码块,锁对象obj只有一个,所以t1和t2只能有一个线程先拿到,拿到后执行代码,那么另一个就拿不到了,拿不到就阻塞,此时线程的状态为:BLOCKED
synchronized修饰方法
修饰非静态方法,默认使用this,并且自己不另外指定
修饰静态方法,默认使用当前类的Class对象当做锁对象,并且不能自己另外指定
案例:
public class MyData{
private int[] arr = new int[20];
//当前数据可以存放的位置,也表示当前存放的元素个数
private int current;
//添加数据
public void add(int num){
String name = Thread.currentThread().getName();
arr[current] = num;
System.out.println(name+"线程本次写入的值为"+num+",写入后取出的值为"+arr[current]); current++;
}
}
public class Test {
public static void main(String[] args) {
MyData myData = new MyData();
Thread t1 = new Thread("t1"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
myData.add(i);
//计算机运行10次运行太快了,让它执行慢一些,好观察效果
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//t2线程的名字前面加个制表符\t,打印的时候好观察
Thread t2 = new Thread("\tt2"){
@Override
public void run() {
for (int i = 10; i < 20; i++) {
myData.add(i);
//计算机运行10次运行太快了,让它执行慢一些,好观察效果
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
t2.start();
}
}
运行结果:
可以看出,在某一次add方法执行的时候,会出现写入的数据和当前的数据不一致的情况。
当我们使用synchronized关键字给add方法上锁后,结果就是写入数据和当前数据一致了
public class MyData{
private int[] arr = new int[20];
//当前数据可以存放的位置,也表示当前存放的元素个数
private int current;
//添加数据
public synchronized void add(int num){
String name = Thread.currentThread().getName();
arr[current] = num;
System.out.println(name+"线程本次写入的值为"+num+",写入后取出的值为"+arr[current]); current++;
}
}
分析:
该代码表示,拿到锁对象this的线程,才可以进入到add方法中执行代码,代码执行完,会释放锁,这时锁变的可用了,所有需要这把锁的线程都恢复到RUNABLE状态(它们之前在锁阻塞状态),这些线程一起重新争夺CPU执行权,谁先拿到CPU执行权,就会先过去拿到锁,进入代码去执行。
线程同步的效果的关键点在于,让t1和t2俩个线程去争夺同一把锁对象
3、关于wait()方法和notify()、notifyAll()方法
这三个方法是Object类中的方法
- 任何对象中都一定有这三个方法
- 只有对象作为锁对象的时候,才可以调用
- 只有在同步的代码块中,才可以调用
其他情况下,调用一个对象的这三个方法,都会报错!
3.1 wait()方法
作用:让拿到的锁的线程,即使代码没执行完,也可以把锁立即给释放。
如下几个代码就能帮助你理解这个方法的基本用法
代码1:
public class Test {
public static void main(String[] args) {
final Object obj = new Object();
Thread t1 = new Thread("t1"){
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (obj){
for (int i = 0; i < 10; i++) {
System.out.println(name+"线程: i = "+i);
}
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (obj){
for (int j = 10; j < 20; j++) {
System.out.println(name+"线程: j = "+j); }
}
}
};
t1.start();
t2.start();
}
}
代码1的结果是:
t1和t2俩个线程,争夺同一把锁对象obj,所以程序的运行结果是:要么t1先拿到锁输处0~ 9,然后t2再拿到锁输出10~ 19,要么就是t2先拿到锁输入10~19,然后t1再拿到锁输出0 ~9
代码二
如果希望t1线程中i=5的时候,先释放锁,让t2拿到锁去运行,在t2线程中,当j=15的时候,释放锁,让t1拿到锁去运行
public class Test {
public static void main(String[] args) {
final Object obj = new Object();
Thread t1 = new Thread("t1"){
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (obj){
for (int i = 0; i < 10; i++) {
System.out.println(name+"线程: i = "+i);
if(i == 5)
try{
//obj是锁对象,在同步代码块中,可以调用wait方法
//让当前拿到锁的线程,立即释放锁
obj.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (obj){
for (int j = 10; j < 20; j++) {
System.out.println(name+"线程: j = "+j);
if(j == 15){
try{
obj.wait();
}catch{InterruptedException e){
e.printStackTrace();
}
}
}
}
};
t1.start();
t2.start();
//主线程休眠1秒钟,给t1和t2点时间,等它们调用wait方法
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1线程当前的状态为:"+t1.getState());
System.out.println("t2线程当前的状态为:"+t2.getState());
}
}
//运行结果:
t1线程: i = 0
t1线程: i = 1
t1线程: i = 2
t1线程: i = 3
t1线程: i = 4
t1线程: i = 5
t2线程: j = 10
t2线程: j = 11
t2线程: j = 12
t2线程: j = 13
t2线程: j = 14
t2线程: j = 15
t1线程当前的状态为:WAITING
t2线程当前的状态为:WAITING
可以看到,t1线程和t2线程都没有运行完,但是代码不运行了,JVM也没停住 这是因为,当前调用锁对象的wait方法后,当前线程释放锁,然后进入到阻塞状态,并且等待其他线程先唤醒自己,如果没有其他线程唤醒自己,那么就一直等着。所以现在的情况是,俩个线程t1和t2都是在处于阻塞状态,等待别人唤醒自己,所以程序不运行了,但是也没结束。调用t1和t2的getState方法,返回的状态为:WAITING
3.2 notify()方法和notifyAll()方法
作用
锁对象.notify(),该方法可以在等待池中,随机唤醒一个等待指定锁对象的线程,使得这个线程进入到锁池中,而进入到锁池的线程, 一旦发现锁可用,就可以自动恢复到RUNNABLE状态了,notifyAll()是唤醒等待池中所有锁对象的线程。
案例:
修改上述代码,加入notify方法的调用
public class Test {
public static void main(String[] args) {
final Object obj = new Object();
Thread t1 = new Thread("t1"){
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (obj){
for (int i = 0; i < 10; i++) {
System.out.println(name+"线程: i = "+i);
if(i==5){
try {
//在释放锁对象之前,叫醒等待池中等待obj锁对象的线程
//意思是告诉对方,我要释放锁了,你准备去抢把
obj.notify();
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//最后在执行完所有代码之前,再叫醒一次,防止等待池中还有其他线程在等待obj这个锁对象4
obj.notify();
}
}
};
Thread t2 = new Thread("t2"){
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (obj){
for (int j = 10; j < 20; j++) {
System.out.println(name+"线程: j = "+j);
if(j==15){
try {
//在释放锁对象之前,叫醒等待池中等待obj锁对象的线程
//意思是告诉对方,我要释放锁了,你准备去抢把
obj.notify();
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//最后在执行完所有代码之前,在叫醒一次,防止等待池中还有线程在等待obj这个锁对象
obj.notify();
}
}
};
t1.start();
t2.start();
//主线程休眠1秒钟,给t1和t2点时间,等它们调用wait方法
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1线程当前的状态为:"+t1.getState());
System.out.println("t2线程当前的状态为:"+t2.getState());
}
}
结果:
t1线程: i = 0
t1线程: i = 1
t1线程: i = 2
t1线程: i = 3
t1线程: i = 4
t1线程: i = 5
t2线程: j = 10
t2线程: j = 11
t2线程: j = 12
t2线程: j = 13
t2线程: j = 14
t2线程: j = 15
t1线程: i = 6
t1线程: i = 7
t1线程: i = 8
t1线程: i = 9
t2线程: j = 16
t2线程: j = 17
t2线程: j = 18
t2线程: j = 19
t1线程当前的状态为:TERMINATED
t2线程当前的状态为:TERMINATED