高并发多线程之线程基础中生命周期线程封闭cpu缓存
Posted 踩踩踩从踩
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高并发多线程之线程基础中生命周期线程封闭cpu缓存相关的知识,希望对你有一定的参考价值。
前言
本篇文章主要介绍线程的基础,什么是线程、线程的状态、线程封闭、cpu缓存和内存屏障、指令重排等概念
什么是java线程
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
现在的项目中,如果只使用单线程,是肯定不满足项目的需求,而多线程充分压榨计算机的cpu和内存降低任务处理时间,也就是一个任务需要40分钟,可以将任务分为5个线程,同时运行,虽然增加内存和cpu的消耗,但是任务处理时间是降低;这其中就涉及了许多概念,如何去处理多线程之间协同,以及如何充分利用cpu,管理多线程。这里面非常多的概念,需要从基础开始慢慢深入理解。
线程状态
java中线程状态分为6个状态:6个状态中定义
java.lang.Thread.State
- New :尚未启动的线程状态
例如 Thread thread=new Thread(); 从new开始的状态 还没有start 之前就是 new的状态
- Runnable:可运行线程的线程状态,等待cpu状态
也就是 start的时候,这个时候是可以执行,并不是马上执行。 这里面也区分开两种状态,也就是cpu正在执行 ,可以被执行 还在等待着。等待cpu的调度
- Blocked 状态:线程阻塞等待监视器锁定的线程状态
处于synchronized同步代码块或方法中阻塞。这只针对synchronied
- waiting状态:线程等待状态
不带timeout参数的方式调用object.wait thread.join locksupport.park.线程等待,如果没有唤醒则会处于一直等待的状态
- timewariting状态:具有等待时间的等待线程的线程状态。例如下面方式中有超时属性的。
thread.sleep object.wait thread.join locksupport.parknanos. locksupport.parkuntil
- terminated:终止线程状态。线程正常完成执行或者出现异常
从代码中来看 首先 新建 运行 到终止的代码
public static void test1() throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
public void run() {
System.out.println("子线程运行。。。");
}
});
System.out.println("1、调用start方法前,thread1状态:" + thread1.getState().toString());
Thread.sleep(1000L);
thread1.start();
System.out.println("2、调用start方法后,thread1状态:" + thread1.getState().toString());
Thread.sleep(1000L); // 等待thread1执行结束,再看状态
System.out.println("3、1s后,thread1状态:" + thread1.getState().toString());
}
而得到的结果是 ,在start前后的状态,和 线程执行成过后的状态 变化
1、调用start方法前,thread1状态:NEW
2、调用start方法后,thread1状态:RUNNABLE
子线程运行。。。
3、1s后,thread1状态:TERMINATED
然后在其中等待唤醒的状态
public static void test2() throws InterruptedException {
Thread thread2 = new Thread(new Runnable() {
public void run() {
try {// 将线程2移动到等待状态,1500后自动唤醒
Thread.sleep(5000);
System.out.println("3、Sleep结束," + Thread.currentThread().getName() + "当前状态:" + Thread.currentThread().getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread2.start();
System.out.println("1、调用start后,thread2状态:" + thread2.getState().toString());
Thread.sleep(2000L); // 等待2000毫秒,再看状态
System.out.println("2、等待2s后,thread2状态:" + thread2.getState().toString());
}
这里需要获取到线程,并让sleep在那里 去获取到状态
1、调用start后,thread2状态:RUNNABLE
2、等待2s后,thread2状态:TIMED_WAITING
3、Sleep结束,Thread-0当前状态:RUNNABLE
对于blocked状态,这里需要用对象给锁住
public static void test4() throws InterruptedException {
Thread thread2=new Thread(new Runnable() {
public void run() {
System.out.println("2、抢锁之前,thread2状态:"+Thread.currentThread().getState().toString());
synchronized (Demo2_ThreadState.class) {
System.out.println("5、抢锁之后,thread2状态:"+Thread.currentThread().getState().toString());
}
}
});
synchronized (Demo2_ThreadState.class) {
System.out.println("1、主线程拿到锁,启动thread2");
thread2.start();
Thread.sleep(2000L);
System.out.println("3、thread2的状态" + thread2.getState());
Thread.sleep(4000L);
}
System.out.println("4、主线程释放锁");
}
得到的结果,这里就能看出synchronized进行锁住,互斥锁
1、主线程拿到锁,启动thread2
2、抢锁之前,thread2状态:RUNNABLE
3、thread2的状态BLOCKED
4、主线程释放锁
5、抢锁之后,thread2状态:RUNNABLE
中止线程
- 中止线程的方式直接使用stop命令,进行中止线程。
因为stop方法是很暴力的,可能会导致不可控的情况,例如在线程执行中有存在某个值相加的情况,不能保证数据的原子性,例如下面种情况
MyThread mythread=new MyThread();
mythread.start();
Thread.sleep(1000);
mythread.stop();
while (mythread.isAlive()) {
}
mythread.print();
class MyThread extends Thread{
private int i=0,j=0;
@Override
public void run() {
synchronized (this) {
++i;
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
++j;
}
}
public void print() {
System.out.print("打印出i的值为:"+i+",j的值为"+j);
}
得到的结果却是
打印出i的值为:1,j的值为0
两个值明显不一致。并且stop方法使得synchronized都无法保证原子性。因此被弃用了
- interrupt 方法进行中断线程,在timewaiting状态下也可以捕获到异常,达到终止线程的情况
mythread.interrupt();
然后得到的结果就是
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.dongnao.concurrent.period1.MyThread.run(Demo2_ThreadState.java:120)
打印出i的值为:1,j的值为1
保证了数据的原子性,打断了会继续执行。
- 使用状态位进行中断线程
修改flag的值,来中断,只有线程里面是while 循环时,就可以使用这种方式进行中断线程。y个flag一定是全局变量
public volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
while (flag) { // 判断是否运行
System.out.println("运行中");
Thread.sleep(1000L);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 3秒之后,将状态标志改为False,代表不继续运行
Thread.sleep(3000L);
flag = false;
System.out.println("程序运行结束");
}
比如某些需要实时监控的线程需要进行停止线程扫描。
线程封闭
多线程访问共享可变数据时,涉及到线程间数据同步的问题。
我们想要的方案是,某个线程在访问共享区域的变量时,其他线程不会访问变量;
并不是所有时候,都要用到共享数据,若数据都被封闭在各自线程中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步技术称为线程封闭;
ThreadLocal实现线程封闭
通过下面代码来实现
/** threadLocal变量,每个线程都有一个副本,互不干扰 */
public static ThreadLocal<String> value = new ThreadLocal<>();
public void threadLocalTest() throws Exception {
// threadlocal线程封闭示例
value.set("123"); // 主线程设置值
String v = value.get();
System.out.println("主线程,v:" + v);
new Thread(new Runnable() {
@Override
public void run() {
String v = value.get();
System.out.println("子线程:线程1取到的值:" + v);
// 设置 threadLocal
value.set("456");
v = value.get();
System.out.println("子线程:重新设置之后,线程1取到的值:" + v);
System.out.println("子线程 执行结束");
}
}).start();
Thread.sleep(12000L); // 等待所有线程执行结束
v = value.get();
System.out.println("主线程,v:" + v);
}
threadLocal的源码,他里面还是通过一个全局map来达到线程封闭的。key则是线程,value是对应设置的值。
相当于每个线程互存副本的。
通过栈封闭达到线程封闭的效果
局部变量的固有属性之一就是封闭在线程之中。它们位于执行线程的栈中,其他线程无法访问这个栈。
局部变量的属性。
共享的对象。
cpu缓存和指令重排
cpu性能优化和缓存
cpu对程序执行进行优化,cpu高速缓存; cpu高速缓存中的数据是内存中的一小部分,但这一小部分是短时间内cpu即将访问的,当cpu调用大量数据时,就避开内存而直接访问高速缓存。
多级缓存
l1 cache时cpu第一层高速缓存,它的容量很小,技术难度增加和成本增加非常大
l2 cache l3 cache 这些
cpu查找时,也是通过一级缓存、二级缓存、三级缓存、往下查找的
缓存同步协议
就如jvm内存协议规范中规定一样,有个缓存一致性协议(MESI协议) 多数厂商都对它进行了实现
多处理器时,单个cpu对缓存中数据进行改动,需要通知其他cpu,也就是意味着,cpu处理要控制自己的读写操作,还要监听其他cpu发出的通知,从而保持最终一致性。
cpu在优化运行效率-运行时指令重排
指令重排,这其中涉及到cpu指令重排和jit编译指令重排。
出现场景,还是cpu在执行某个操作的时候,发现当前缓存区块被占用,因此他就优先执行后面的区块,提高了cpu的处理性能。
as-if-serial语义:指令在重排时,不管怎么做,单个线程的结果不能被改变
出现的问题
高速缓存会导致数据短暂得不一致;
cpu指令重排,在多线程得情况下可能会出现数据混乱。
内存屏障
处理器提供了两个内存屏障指令来解决 数据缓存和多线程下指令重排问题。
写内存屏障:
在写指令后插入store barrier ,能让写入缓存中的最新数据更新写入主内存,让其他线程马上看到。强制写入主内存,这种显示调用,cpu就不会因为性能考虑而去对指令重排。
读内存屏障:
在读指令前插入load barrier,可以让高速缓存中的数据失效,强制新从的主内存加载数据,强制读取主内存内容,让cpu高速缓存的内容和主内存保持一致,避免缓存不一致导致的一致性问题
其实这两个指令 jvm在运行时,会根据java程序中 volatile 关键字修饰的变量,自动添加指令的运行。
总结
整篇文章描述的比较基础,如果说有什么没描述的,我觉得是java怎么创建线程的方式,这个我觉得不需要在本篇文章描述,比较简单;本篇文章描述的是、线程状态、如何优雅的中止线程、线程封闭的概念和实现、cpu缓存和指令重排;主要是为多线程打基础。线程池、以及线程间通信这些内容我们放到下篇文章中写入
以上是关于高并发多线程之线程基础中生命周期线程封闭cpu缓存的主要内容,如果未能解决你的问题,请参考以下文章