多线程之两阶段终止、以及临界问题
两阶段终止问题
什么是优雅的终止
即如何在线程t1中优雅的终止线程t2,优雅是指给t2线程处理自身关键内容的机会而不是直接强制终止。
错误思路
- 使用线程对象的stop()方法停止线程
stop() 方法会真正杀死线程,如果此时线程锁住了共享资源,那么当它被杀死后,没有机会释放锁,则其他线程将永远无法获得锁。
使用System.exit()方法停止线程
目的仅是停止一个线程,但是这种做法会让整个程序都听下来。
所以比较推荐的方法,是需要终止线程时,给线程发送interrupt(),并且线程判断IsInterrupted()来判断是不是发送了打断指令。这里需要注意的是,线程在sleep()的时候,执行interrupt()会抛出异常,但是isInterrupted()仍然是false,所以,要注意捕获这个异常,并且对应终止线程。
以上提到思路,更适用于线程while(true)时终止的情况,如果正常执行,那么更希望线程可以正常运行完成。
多线程上下文切换的问题
临界区
一个程序运行多个线程本身是没有问题的,问题出现在多个线程访问一个共享资源
多个线程读共享资源也是没有问题的,但是多个线程对共享资源读写操作时,因为时间片切换的问题,会发生指令交错,就会出现问题
一段代码块内,如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
应用互斥
为了避免临界区的竞态条件发生,有多种方法可以达到目的。
阻塞式的解决方案:synchronized,Lock
非阻塞式的解决方案: 原子变量
synchronized俗称对象锁,采用互斥的方法,让同一时刻至多只有一个线程能够持有对象锁,其他线程想再获得这个对象锁,就会被阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
synchronized实际上是用对象锁保证了临界区代码的原子性,临界区的代码对外是不可分割的,不会被线程切换打断。
public class test {
static int r =0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
r++;
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
r--;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("r="+r);
}
}
结果为:
r=-423
r=-615。。。。。。
这个代码就是多个线程读写了共享的资源 r。
因为r++和r--操作,对应的底层代码有取数、加1、存数的操作,所以,在执行到中途时,时间片可能用完,而切换为另一个线程,而线程保存当前的执行结果。如线程t1 执行到第二步,r=200,然后等待存数,这时t2执行,r--,r应该为199,但是在切换t1时,继续执行r=200的存数操作,导致出错。
改进:
public class test {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (object) {
r++;
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (object) {
r--;
}
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("r=" + r);
}
}
其实synchronized要求锁住一个对象即可,在这段代码中,只要保证线程1和线程2之间由于锁住同一个对象,从而导致互斥即可,所以这里声明了一个object对象,使t1和t2形成互斥即可。