最近开始学习java并发方面的知识了,主要就是java的多线程。
就多线程而言,java和其他语言相比,如c++等有挺大的区别,因为java是直接集成多线程的,JVM对多线程有较大的优化。而不像c++一样调用其
他的线程库来实现多线程。
但是注意:c++11开始加入了线程库,也就是说c++11开始也有所谓的”内存模型“了,但是它的优化程度是否能和很早就有内存模型的java语言相比,
还不好说,有机会会比较下的^_^
今天主要简单的聊聊 java多线程的内存模型,因为只有知道了JVM帮我们偷偷干了什么,才能写出更安全健壮的多线程并发程序。
ps:java的内存模型一开始就是为了融入多线程而设计的。
一.预备知识
1.并发与并行的区别:
并发:同一时间,同时发生的事情
并行:同一时间,同时在做的事情
简单的来说就并发是指同一时间我们能接受多少,并行是同一时间我们能处理多少。
举个例子:老妈在做饭,让我帮忙干活,短短的一分钟她说了四件事,去超市买盐,去收衣服,去扫地,并嘱咐我喝完桌子的饭,
没办法我只能答应- -,这就是并发
我不会分身术,我只有两只手,我可以边扫地边喝桌子上的饭,这是并行。
再说个socket的例子吧,都知道epoll IO复用解决了C10K问题,它可以同时接受10000左右的连接数,这指的是并发, 但是一台计算机只有两个CPU,
同时只能有几个线程在上面跑,这是并行。
2.同步与互斥:
互斥:指的是同一件事或者同一个东西,在同一时刻在只允许一个用户在使用或者在做。
同步:在互斥的基础上有序的进行访问。注意同步并没有说什么同一时刻之类的。
3.竞态条件:
从多线程的角度来说就是多个线程对同一块资源进行访问时,最终结果取决于线程执行的顺序,但是线程执行的顺序不固定,每一执行的结果也就是不确定的。
4.死锁:
这个概念应该经常听见,简单的来说就是两方或多方因为彼此占据对方的资源而形成的阻塞。
可以看看哲学家就餐问题,讲死锁这个问题比较清晰
5.内存可见性
在JVM中一个线程改变了内存是否另外一个线程可以知道。
二.简述内存模型
1.java为什么要有内存模型
目的是为了让我们所有的CPU在任意时刻都能访问到相同的数据。
现在已经是多核时代了,不同于单核时代的伪并行,实现的是真正的并行,在多个CPU上面跑数据,那么问题就来了,
我们如何保证一个线程改变了数据,另外一个线程可以及时知道呢?另一个问题,CPU都有寄存器和缓存,如果数据在
寄存器或者缓存中修改了且没有同步到内从中我们如何及时知道呢?
先不管是什么语言,这都是多线程的一个问题,现在我们只关心java的。
java有虚拟机JVM,是为了实现跨平台,前面我们也说了java的多线程是融合在语言中的,也就是说java必须让JVM来
完成上面所说的问题,也就是抽象出自己的内存模型,从而达到跨平台且可以实现多线程的目的。
JMM(java memory model)也就是java语言级别的内存模型,保证在不同平台上的内存一致性
2.java的内存模型
如图
每个线程都有自己的私有空间,一般是主存中部分变量的副本。
如果两个线程想通信的话,必须是先写到线程的私有空间地址,然后写入主内存中,然后另外一个线程从主内存中读取数据。
3.指令重排
指令重排:将即将执行的指令进行重新排序,从而加快执行速度。
指令重排一般 分为编译器优化指令重排,字节码指令重排,内存重排等等。
x86对指令重排的要求比较严格,禁止的指令重排种类比较多。
比如:
ret = getret();
i = 10;
如果getret()会阻塞一段时间,CPU可能进行指令重排,从而先执行i = 10编译后的指令。
普通的CPU有指令重排,会针对编译出来的指令进行重新排序。
java的内存模型主要是针对指令重排进行限定,因为在多线程条件下,指令重排可能会引发不确定性。
更具体的内容参见http://ifeve.com/jmm-cookbook-reorderings/
4. 内存屏障
有时也被成为“内存栅栏”, 简单的讲就是严格的限定指令的执行顺序,也就是禁止特定类型的处理器重排。
也是为了避免多线程并发受到影响。
下图是根据指令的不同添加不同的内存屏障,第一步第二步是指令,中间是插入的屏障
指令和屏障的含义更具体的参见http://ifeve.com/jmm-cookbook-mb/
5.关键字volatile,synchronized,final
这三个关键字都和多线程内存模型有关。
volatile:单词的意思是可变的,易变的,作用是当我们写一个变量时,它会被立刻刷新到主内存中,保证了变量的可见性,不会发生
前面所说的,在线程自己的私有内存中改变却没有刷新到主内存中的问题。
那么简单的说就是volatile会立刻刷新当前执行线程的内部缓冲区或者寄存器等,然后同步到内存中去,保证其他线程访问的是最新的。
并且volatile增强语义后它会限定部分内存重排的规则来保证线程安全性。但是volatile修饰的变量不保证具有原子性。
volatile 最适合的是一个线程写,多个线程读的场合。如果有多个线程并发写操作,仍然需要使用锁或线程安全容器或原子变量来代替。
synchronized:同步的,其实就是加上了锁,保证了访问区域的原子性,准备访问时加上锁,访问结束时去掉锁。
final:final在java内可修饰类,方法,变量和引用。
修饰类:是此类不能被继承.
修饰方法:是此方法不能被继承的类修改,或者有时会将短的方法进行内敛从而加快执行速度。类的private方法会自动转化为final的
修饰变量:指的是此变量值不能改变
修饰引用:此引用不能指向其他对象。
但是final在修饰一个变量或引用时有可能定义时我们没有初始化,编译器自动初始化为0或者其他默认值,而此时在我们修改给它赋初值时
线程访问了,所以存在线程访问的是默认初始化值而不是我们赋的初值。
所以后来增强了final关键字的语义在JSR-133文档中
不需要显示同步final修饰的不可变对象也是线程安全的。
JSR-133是针对java多线程内存模型的文档.
第一篇就简单的介绍到这里
此篇文章是自己简单的理解,接触java的时间并不长,如果有什么错误,还忘指出^_^
参考博客:ifeve.com的java内存模型板块.