我们在找工作时,经常在招聘信息上看到有这么一条:要求多线程并发经验。无论是初级程序员,中级程序员,高级程序员,也无论是大厂,小厂,并发编程肯定是少不了的。
但是网上很多博文直接上来就讲JUC,没有从基础出发,所以该篇旨在讲明并发基础,主要为计算机原理,线程常见方法,Java虚拟机方法的知识,为后面的学习保驾护航,话不多说,开始吧。
CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU,所以才引入了缓存的概念。我们可以从下图看出在CPU和主内存之间加了一个缓存,用来提升交互速度。
随着CPU的速率越来越快,人们对计算机性能要求越来越高,传统的缓存已经满足不了,所以引入了多级缓存,包括一级缓存,二级缓存,三级缓存,具体如图所示。
一级缓存:基本上都是内置在cpu内部,和cpu一个速度运行,能有效的提升cpu的工作效率。当然数量越多,cpu工作效率就会越高,但是由于cpu的内部结构限制了其大小,所以一级缓存的数据并不大。
二级缓存:主要作用是协调一级缓存和内存之间的工作效率。cpu首先用的是一级内存,当cpu的速度慢慢提升之后,一级缓存就不够cpu的使用量了,这就需要用到二级内存。
三级缓存:和一级缓存与二级缓存的关系差不多,是为了在读取二级缓存不够用的时候而设计的一种缓存手段,在有三级缓存cpu之中,只有大约百分之五的数据需要在内存中调取使用,这能提升cpu不少的效率,从而cpu能够高速的工作。
我们可以看下本机的缓存情况。
只有一级缓存情况:
我们可以将CPU当做我们本人,缓存区当做超市,主内存当做工厂,如果想要买东西(取数据)就先去超市(缓存区)买(取),如果超市(缓存区)没有,就去工厂(主内存)里面买(取)。
多级缓存情况:
我们可以将CPU当做本人,一级缓存当做楼下小区里面的小卖部,二级缓存当做普通超市,三级缓存当做大型超市,主内存当做工厂,如果想买东西先去楼下小卖部(一级缓存),小卖部(一级缓存)没有的话,就去普通超市(二级缓存),如果普通超市(二级缓存)还没有,就去大型超市(三级缓存),如果大型超市(三级缓存)还没有,就直接去工厂(主内存)取。这些缓存的出现使得我们不必每次都去工厂(主内存)买东西(取数据),节省了时间,提升了速度。
CPU速率太快,快到内存跟不上,在处理器处理周期内,CPU常常等待内存,造成资源的浪费。
对于多核系统来说, 每个核中缓存数据不一致的问题。
CPU从主内存读取数据到缓存区,并在总线对这个数据进行加锁,其他CPU无法去读写这个数据,直到这个CPU使用完数据,锁被释放了才访问。就比如我想去超市买一个辣条,但是张三也想买,在我买的过程中,就给辣条加了锁,张三根本碰不到辣条,我买的过程非常慢,那张三不急死啦嘛。
针对上面缓存数据不一致的情况,提出了MESI协议用以保证多个CPU缓存中共享数据的一致性,定义了缓存行Cache Line四个状态,分表是M(Modified),E(Exclusive),S(Share),I(Invalid)四种。
MESI状态之间的迁移
:
这图一看是很懵逼的,咱慢慢来看哈,慢慢体会这些变化哈。
当前状态是Modified
当前状态是Exclusive
当前状态是Share
当前状态是Invalid
并发:同一时刻只能有一个指令执行,但多个指令被CPU轮换执行,因为时间间隔很短,会造成同时执行的错觉。
并行:同一时刻多条指令在多个处理器同时执行,不管是微观,还是宏观上,都是同时执行的。
举个例子,并发就是一个家庭主妇既要烧饭,也要带娃,也要打扫房间,如果每个事情只做一分钟,然后轮换,从宏观上来说,会造成同时执行的错觉。并行就是该家庭主妇请了两个保姆,一个专职负责烧饭,一个专职负责带娃,自己专职负责打扫卫生,不管从宏观还是微观上来看,他们都是同时执行的。
某位大佬曾经说两者的区别,并发是同一时间应对
多件事情的能力,并行是同一时间去做
多件事情的能力。作为一个工科生,不知道如何夸大佬,只知道喊666。
进程是用来加载指令,管理内存,执行语句的。
线程是进程的一部分,一个进程可以分为1个或多个线程。
网易云音乐的打开,就是开启了一个进程,而播放,查找,评论等都是线程。
线程之间的通信比较简单,可以通过他们的共享内存通信,具体可以看下面Java内存模式部分。
进程之间的通信比较复杂,对于同一台计算机而言,其通信称为IPC;对于不同计算机,其通信需要网络并遵循彼此约定的协议,如HTTP等。这部分偏硬件,咱也不敢说,咱也不敢问。
初始状态:新建new一个线程,还没有进行任何步骤,还未和硬件关联上。
可运行状态:当调用start方法,即进行可运行状态(就绪状态),但是这个时候还没获取到时间片,具体什么时候运行取决于硬件。
运行状态:当CPU分配的时间片到某个线程了,该线程即可进入运行状态。
阻塞状态:当线程调用阻塞API,线程并没有用到CPU,其进入阻塞状态。
终止状态:当一个线程运行结束了,即进入终止状态。
线程和任务合并
Thread thread=new Thread(){
public void run(){
System.out.println("开始");
}
};
线程和任务分开
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("开始");
}
};
Thread thread=new Thread(runnable);
FutureTask返回执行结果
FutureTask<String> futureTask=new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
return "线程的返回值";
}
});
Thread thread=new Thread(futureTask);
thread.start();
这里start是进入就绪状态,即可运行状态,具体什么时候要看CPU。
未加join情况:
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("线程开始");
try {
sleep(4000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束");
}
};
//创建线程
Thread thread=new Thread(runnable);
//启动线程
System.out.println("主线程开始");
thread.start();
System.out.println("主线程结束");
运行结果:
使用join的情况:
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("线程开始");
try {
sleep(4000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程结束");
}
};
//创建线程
Thread thread=new Thread(runnable);
//启动线程
System.out.println("主线程开始");
thread.start();
thread.join();
System.out.println("主线程结束");
运行结果:
没有用join方法的第一情况,主线程开始和主线程结束都在前面,并靠在一起,而线程开始和线程结束则在后面,因为他们是两个不同的线程,彼此互不干扰。而用了join方法的第二种情况,主线程结束在最后一行,因为join方法需要等待子线程结束后才能继续执行后面代码。
//创建线程 Thread thread=new Thread(){
public void run(){
System.out.println("线程开始");
}
};
//启动线程
thread.start();
System.out.println("id:"+thread.getId());
System.out.println("name:"+thread.getName());
System.out.println("priority:"+thread.getPriority());
运行结果:
跟多级缓存差不多意思,每个线程里面都有工作内存,其存储的是主内存中数据的副本,如下图。那如果主内存中有变量a=1,现在线程A,B,C都存了a=1的副本,线程A对其进行加1操作,并刷新到主内存。可是线程B,C并不知道这种情况,那么就出问题啦。那如何解决这个问题呢?下面将慢慢说,不急。
下面罗列的是8种原子操作,大家大概看看,下面将详细描述。
咱以上面的例子画了个图,请原谅偶我笨,画的丑了点。
1.read读取:将主内存中的a=1读取出来。
2.load载入:将从主内存中a=1载入到线程A的工作内存中。
3.use使用:将线程A工作内存的a=1读取到,并进行自增操作。
4.assign赋值:将a=2写入到线程A的工作内存中。
5.store存储:将a=2存储到主内存中。
6.write写入:将a=2写入到主内存的a变量中。
7.lock锁定:在上面CPU缓存解决不一致的方法一中,线程A操作的时候,对主内存a变量进行加锁操作(lock),线程B根本读不了a变量。
8.unlock解锁:线程A操作解锁之后,对主内存a变量进行解锁操作(unlock),线程B可以读到a变量并对其操作。
注意:lock和unlock存在着一个性能问题,我们发现写的代码明明是多线程并发操作,但是底层还是串行化,并没有真正实现并发。
上面说的MESI协议是在总线那边实践的,线程A,B可以同时获取主内存a的值,a进行自增操作之后在进行操作6write写入的时候,会经过总线。线程B一直使用嗅探监控总线中自己感兴趣的变量a,一旦发现a值有修改,立刻将自己工作内存中a置为无效Invalid(利用MESI协议),并立刻从主内存中读取a值,这个时候总线中a还没有写入内存,所以有个短暂的lock过程,等到a写入内存了,进行unlock操作,线程B即可读取新的a值。
该过程虽然也有lock与unlock操作,但是锁的粒度降低啦。
优势:
风险:
看到这里的都是真爱,先行谢过。此篇是并发系列的基础,主要聊了硬件的MESI协议,原子的八种操作,线程和进程的关系,线程的一些基础操作,JMM的基础等。如果有什么错误,或者不对的地方,欢迎指正。
Java并发编程入门与高并发面试
CPU多级缓存与缓存一致性
Java高并发编程精髓Java内存模型JMM详解全集