临界区 – 临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程 使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。
阻塞(Blocking)和非阻塞(Non-Blocking) – 阻塞和非阻塞通常用来形容多线程间的相互影响。阻塞,当一个线程进入临界区后,其他线程必须等待 。 – 非阻塞允许多个线程同时进入临界区
无障碍(Obstruction-Free) – 无障碍是一种最弱的非阻塞调度 – 自由出入临界区 – 无竞争时,有限步内完成操作 – 有竞争时,回滚数据 。
无锁(Lock-Free) – 是无障碍的 – 保证有一个线程可以胜出。
无等待(Wait-Free) – 无锁的 – 要求所有的线程都必须在有限步内完成 – 无饥饿的。
原子性–是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就 不会被其它线程干扰。
i++ 是原子操作吗?
当多个线程同时操作i++ 例如A B线程同时读到i 并都加1 ,i则变成3。
在java中,对基本数据类型的变量的读取和赋值操作是原子性操作有点要注意的是,对于32位系统的来说,long类型数据和double类型数据(对于基本数据类型,
byte,short,int,float,boolean,char读写是原子操作),它们的读写并非原子性的,也就是说如果存在两条线程同时对long类型或者double类型的数据进行读写是存在相互干扰的,因
为对于32位虚拟机来说,每次原子读写是32位的,而long和double则是64位的存储单元,这样会导致一个线程在写时,操作完前32位的原子操作后,轮到B线程读取时,恰好只读取到了后32位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它可能是“半个变量”的数值,即64位数据被两个线程分成了两次读取。但也不必太担心,因为读取到“半个变量”的情况比较少见,至少在目前的商用的虚拟机中,几乎都把64位的数据的读写操作作为原子操作来执行,因此对于这个问题不必太在意,知道这么回事即可。
有序性
一条指令的执行是可以分为很多步骤的 :
– 取指 IF
– 译码和取寄存器操作数 ID
– 执行或者有效地址计算 EX
– 存储器访问 MEM
– 写回 WB
指令1 IF ID EX MEM WB
指令2 IF ID EX MEM WB
指令2每必要再指令1执行完再执行,可以在指令1的第一个步骤开始执行。每过一个时钟周期可以跑一条指令。
add指令出现气泡的部分,是因为C还没有读取到,SW指令出现气泡是因为如果ID前移会导致EX冲突。
气泡会影响性能,所以可以将没有依赖关系的指令提前 如LW e ,LW f,指令重排序后:
可见性 --是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。 – 编译器优化 – 硬件优化(如写吸收,批操作)。
对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。
但在多线程环境中可就不一定了,前面我们分析过,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题,通过前面的分析,我们知道无论是编译器优化还是处理器优化的重排现象,在多线程环境下,确实会导致程序轮序执行的问题,从而也就导致可见性问题。
Happen-Before规则
程序顺序原则:一个线程内保证语义的串行性
volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
传递性:A先于B,B先于C,那么A必然先于C
线程的start()方法先于它的每一个动作
线程的所有操作先于线程的终结(Thread.join())
线程的中断(interrupt())先于被中断线程的代码
对象的构造函数执行结束先于finalize()方法
什么是多线程环境下的伪共享(false sharing)?
伪共享是多线程系统(每个处理器有自己的局部缓存)中一个众所周知的性能问题。缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。