前几天再写synchronized的JVM实现时候,想起若是是2个线程同时想得到对象锁,即修改对象头的lock位时候,同时发现对象锁可用,同时修改lock的值,天了噜想一想真是太可怕,想解决互斥却在解决方法中也有互斥的问题。后来发现:java
任何互斥尝试必须基于一些基础硬件的互斥!算法
任何互斥尝试必须基于一些基础硬件的互斥!数组
任何互斥尝试必须基于一些基础硬件的互斥!并发
重要的话要说3遍!!jvm
那咱们来看看硬件的支持吧。spa
一、中断禁用操作系统
在单处理器中,并发进程不能同时执行,只能交替。所以要保证互斥,只须要保证一个进程不被中断就行,这种能力能够经过系统内核启用中断和禁止中判定义的原语实现。线程
禁用中断 临界区 启用中断
这种方法代价很高,由于处理器只能交替执行程序。还有个问题对于多处理器并无什么用。设计
二、专用机器指令code
在多处理器中,全部处理器都是共享内存。因此要保证互斥,在硬件级别上,对存储单位的访问要排斥对相同单位的其余访问。基于这一点,处理器设计者设计了一些机器指令,来保证两个动做的完整性,如咱们喜闻乐见的compareAndSwap。
机器指令方法使用了忙等待。可能饥饿,致使某些进程无限期进入临界区。最重要的是可能产生死锁,假设进程P1经过CAS进入临界区,可是在临界区的时候被迫中断把处理器让给优先级别更高的P2,若是P2想使用同一资源,因为互斥机制,那么P2将永远没法进入临界区。
对存储单位的访问要排斥对相同单位的其余访问又是怎么实现的呢?举个列子,在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCK pin,若是汇编语言的程序中在一条指令前面加上前缀"LOCK",通过汇编之后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能经过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
软件方法(见下文)和硬件方法都有明显缺陷,咱们须要寻找其余机制。
信号量
其基本原理是:两个或者多个进程能够经过简单的信号进行合做,一个进程能够被迫停在某个位置,直到它收到特定的信号。为了达到预期效果,咱们能够把信号量当作一个整型变量,在它之上定义3个操做
1)一个信号量初始化非负整数
2)semWaite操做使信号量减一。若是信号量变为负数,则执行semWaite的进程被阻塞。
3)semSignal操做使信号量加一。若是信号量不为负数,则被semWaite阻塞的一个进程解除阻塞
固然semWaite和semSignal都是原语,并且都具备原子性。至于还有二元信号量,管程之类的机制,你们有兴趣能够看看操做系统,下面咱们要讲讲软件实现的互斥。
软件实现的互斥固然也要按照基本法,对存储单位的访问要排斥对相同单位的其余访问这个假设确定是要有的。那一拍脑壳,确定说设置个变量,每一个进程检查变量,若是为true则把值设为false,访问临界区,访问完之后把值设为true,以下:
volatile boolean flag=true; while(flag) flag=false; //访问临界区 flag=true;
再仔细一想,这基本的互斥都作不到,假设P1线程先访问flag,此时flag为true,线程P1还没修改flag值,线程P2访问flag值,而后P1,P2就一块儿愉快的访问临界区了,但是咱们不乐意啊,因此咱们要修改程序,分开P1,P2这对小婊砸。
P1: while(flag) //访问临界区 flag=true; P2: while(!flag) //访问临界区 flag=false;
这样是分开了P1,P2,可是并非很优雅,P1,P2老是交替执行,更可怕的是若是P1在访问临界区的时候不当心退出,那么P2将永远不能访问临界区。接下来我就不卖蠢了,直接贴出Peterson算法的java实现。
public class Petersen { private volatile static boolean flags[]; private volatile static int signal; public static void main(String args[]) { flags = new boolean[2]; new Thread(new Runnable() { public void run() { while (true) { flags[0] = true; signal = 0; while (flags[1] && signal == 0) { } //临界区,do something for (int i = 0; i < 10; i++) { try { Thread.sleep(100l); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getId()); } //退出临界区 flags[0]=false; break; } } }).start(); new Thread(new Runnable() { public void run() { while (true) { flags[1] = true; signal = 1; while (flags[0] && signal == 1) { } //临界区,do something for (int i = 0; i < 10; i++) { try { Thread.sleep(100l); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getId()); } //退出临界区 flags[1]=false; break; } } }).start(); } }
原理是利用一个boolean数组记录各线程的状态,而后用signal来控制来控制同步并发。假设两进程都想进入临界区,把本身flag设置为true,把signal设置为本身线程ID,咱们能够看见总有一个线程在while那边忙循环,从而保持了互斥。据说peterson算法很容易推广到N线程,可是试了试3线程,老是不能很好控制,有好心人知道麻烦告知一声。
在看java互斥的实现下咱们先来看看java的内存模型(对理解volatile有极大用处):
Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每一个线程都有本身独立的工做内存,线程只能访问本身的工做内存,不能够访问其它线程的工做内存。工做内存中保存了主内存共享变量的副本,线程要操做这些共享变量,只能经过操做工做内存中的副原本实现,操做完毕以后再同步回到主内存当中。如何保证多个线程操做主内存的数据完整性是一个难题,Java内存模型也规定了工做内存与主内存之间交互的协议,首先是定义了8种原子操做:
(1) lock:将主内存中的变量锁定,为一个线程所独占
(2) unclock:将lock加的锁定解除,此时其它的线程能够有机会访问此变量
(3) read:将主内存中的变量值读到工做内存当中
(4) load:将read读取的值保存到工做内存中的变量副本中。
(5) use:将值传递给线程的代码执行引擎
(6) assign:将执行引擎处理返回的值从新赋值给变量副本
(7) store:将变量副本的值存储到主内存中。
(8) write:将store存储的值写入到主内存的共享变量当中。
那synchronized是怎样获取对象锁的呢,固然它可使用lock原子性操做,也能够用peterse同样的算法,具体实现得看看jvm的源码。以上——