提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
个人学习笔记,仅供参考!
1.如何优雅的结束线程
- 可使用stop()方法结束线程。stop()方法会直接结束线程并释放做资源,以下代码可进行验证。但是由于stop()方法过于粗暴,无论线程处于何时都会直接结束线程,因此可能在同步方法中进行一半时,被结束,且无善后。所以可能导致数据一致性出现问题。现已废弃,了解即可。
public class day04 {
static class SleepHelper {
public static void sleepSeconds(int seconds) {
seconds *= 1000;
try {
Thread.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static final Object o = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (o) {
while (true) {
System.out.println("t1 go on");
SleepHelper.sleepSeconds(1);
}
}
});
Thread t2 = new Thread(() -> {
synchronized (o) {
while (true) {
System.out.println("t2 go on");
SleepHelper.sleepSeconds(1);
}
}
});
t1.start();
SleepHelper.sleepSeconds(1);
t2.start();
SleepHelper.sleepSeconds(5);
t1.stop();
}
}
- 使用suspend();与resume();方法组合,可以使线程停止和恢复。但此方法同样过于粗暴,且线程停止时不会释放锁。以下代码可进行验证。在代码中容易因使用不当或特殊情况,造成死锁。已废弃,了解即可。
public class day04 {
static class SleepHelper {
public static void sleepSeconds(int seconds) {
seconds *= 1000;
try {
Thread.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static final Object o = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (o) {
while (true) {
System.out.println("t1 go on");
SleepHelper.sleepSeconds(1);
}
}
});
Thread t2 = new Thread(() -> {
synchronized (o) {
while (true) {
System.out.println("t2 go on");
SleepHelper.sleepSeconds(1);
}
}
});
t1.start();
SleepHelper.sleepSeconds(1);
t2.start();
SleepHelper.sleepSeconds(5);
t1.suspend();
SleepHelper.sleepSeconds(5);
t1.resume();
}
}
- 使用volatile结束线程。volatile是一种较为优雅的结束线程的一种方式。尽可能不依赖于循环中,中间状态,例如在往一个容器中加入元素,加到第四个时停止,这种情况volatile很难做到精确控制,容易出现偏差;或者说如果,在代码中使用了wait();则可能导致阻塞,无法跳入下一次循环而导致线程无法结束。在特定情况下volatile可以起到不错的作用,且使用较为简便。
public class day04 {
static class SleepHelper {
public static void sleepSeconds(int seconds) {
seconds *= 1000;
try {
Thread.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static final Object o = new Object();
private static volatile boolean running = true;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (o) {
while (running) {
System.out.println("t1 go on");
SleepHelper.sleepSeconds(1);
// try {
// o.wait();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
});
// Thread t2 = new Thread(() -> {
// synchronized (o) {
// while (true) {
// System.out.println("t2 go on");
// SleepHelper.sleepSeconds(1);
// }
// }
//
// });
t1.start();
SleepHelper.sleepSeconds(1);
// t2.start();
SleepHelper.sleepSeconds(5);
running = false;
}
}
- interrupt结束线程,这是一种非常优雅的结束线程的方式。且就算代码处于sleep();或处于wait();时只要可以正确处理因为interrupt();产生的报错,线程依然能优雅的结束。
public class day04 {
static class SleepHelper {
public static void sleepSeconds(int seconds) {
seconds *= 1000;
try {
Thread.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("t1 go on");
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// Thread.currentThread().interrupt();
// }
}
System.out.println("end");
});
t1.start();
SleepHelper.sleepSeconds(5);
t1.interrupt();
}
}
对于线程结束的方式,以上四种为比较普通的结束方式。如果要做到精确的结束,就需要业务线程与其他线程配合。一些比较精细或者高级的方法,之后会进行记录与讲解。
2.并发编程三大特性
- 可见性(visibility)
从一个程序谈起:
以下程序,注释volatile与打印语句时,线程不会得到停止。但放开任一注释,线程即可跳出循环继续执行直到结束。这是由于打印语句内部使用了synchronize关键字,而volatile可以保障可见性。
public class day04 {
static class SleepHelper {
public static void sleepSeconds(int seconds) {
seconds *= 1000;
try {
Thread.sleep(seconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static /*volatile*/ boolean running = true;
private static void m(){
System.out.println("m start");
while (running){
//System.out.println("hello");
}
System.out.println("m end");
}
public static void main(String[] args) {
new Thread(day04::m,"t1").start();
SleepHelper.sleepSeconds(1);
running = false;
}
}
在主存中的running为true,当两个线程读取running时,会取出以后copy一份放在线程缓存中, 之后while时读的都是缓存中的copy值。所以线程可见性:一个线程对共享变量值得修改,能够及时的被其他线程看到。
线程可见性原理:
线程一对共享变量的改变想要被线程二看见,就必须执行下面两个步骤:
将工作内存1中的共享变量的改变更新到主内存中
将主内存中最新的共享变量的变化更新到工作内存2中。
关于synchronized:1.线程解锁前,必须把共享变量的最新值刷新到主内存。2.线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中读取最新的值。(加锁与解锁是同一把锁)
关于volatile保障可见性:volatile变量每次被线程访问时,都强迫从主内存中读取该变量的值,而当变量发生变化时会强迫线程将最新的值刷新到主内存中,这样不同的变量总能看到最新的值。
volatile关键字:
能够保证volatile变量的可见性。
只能保证单个volatile变量的原子性,对于volatile++这种复合操作不具有原子性。
volatile引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性。
volatile实现共享变量内存可见性有一个条件,就是对共享变量的操作必须具有原子性。比如一个整数变量num ; 这个操作具有原子性,但是 num++ 或者num–由3步组成,并不具有原子性,所以是不行的。例如:如有一个num,此时有线程1从主内存中获取num的值,并执行++,但还未修改写入主内存,又有线程2取得num的值,进行++操作,造成丢失修改,执行了2次++,num的值只增加1。所以volatile不具有原子性,不适用于计数场景,可以保证原子性操作时,可以尽量的选择使用volatile。在其他不能保证其操作的原子性时,考虑使用synchronized。
关于缓存:
这是一个多核CPU,左侧为CPU1,右侧为CPU2,L1、L2位于核内部,L3位于CPU内部。几个CPU共享主存。当寄存器需要一个数据时。会去L1读取,L1没有去L2,L2没有去L3,L3也没有就会去主存中读取。当放数据时也是一样,从L3放到L1再到寄存器。我们可见性中说的缓存就是这里的缓存,并不是说的ThreadLocal。
缓存行概念:计算机将数据从主存读入Cache时,是把要读取数据附近的一部分数据都读取进来,这样一次读取的一组数据就叫做CacheLine,每一级缓存中都能放很多的CacheLine,缓存行越大,局部空间效率越高,读取时间越慢!缓存行越小,局部空间效率越低,读取时间越快!
一个缓存行存储的字节是2的倍数。不同机器上,缓存行大小也不一样,通常来说为64字节。。
空间局部性原理:按块读取,程序局部性原理,可以提高效率,充分发挥总线CPU针脚等一次性读取更多数据的能力。
我们可以通过一个小程序来认识:缓存一致性。
public class day04 {
public static long COUNT = 1000_0000_0000L;
private static class T{
// private long p1=1L,p2=1L,p3=1L,p4=1L,p5=1L,p6=1L,p7=1L;
public long x = 0L;
// private long p9=1L,p10=1L,p11=1L,p12=1L,p13=1L,p14=1L,p15=1L;
}
public static T[] arr = new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new Thread(() -> {
for (long i = 0; i < COUNT; i++) {
arr[0].x = i;
}
latch.countDown();
});
Thread t2 = new Thread(() -> {
for (long i = 0; i < COUNT; i++) {
arr[1].x = i;
}
latch.countDown();
});
final long start = System.nanoTime();
t1.start();
t2.start();
latch.await();
System.out.println((System.nanoTime()-start)/100_000L);
}
}
当我们放开注释的两行代码时,会发现效率提升了。这就是由于缓存一致性所产生的现象。
但如果将注释放开,数据被进行了填充,则两条数据不可能位于统一缓存行,则会变成下列这种情况。
在Disruptor中也使用到过这种特点。
在JDK8中提供了一个注解:@Contended,被这个注解修饰的变量不会和其他数据处于同一缓存行,相当于自动为其填充了空白数据。使用时需要添加参数:-XX:-RestrictContended (但只有JDK1.8有)
MESI 是Cache一致性协议中的一种,是Inter的Cpu设计,比较有名。
MESI协议是一个基于失效的缓存一致性协议,是支持写回(write-back)缓存的最常用协议。与写穿(write through)缓存相比,回写缓冲能节约大量带宽。总是有“脏”(dirty)状态表示缓存中的数据与主存中不同。MESI协议要求在缓存不命中(miss)且数据块在另一个缓存时,允许缓存到缓存的数据复制。与MSI协议相比,MESI协议减少了主存的事务数量。这极大改善了性能。
总结
本次学习,学习了,如何结束一个线程,以及并发编程三大特性之一的可见性。由此学习了volatile保障线程可见性,缓存行的概念和机制,以及大概了解了缓存一致性协议。