java-concurrency 之 JMM

自从JSR-133在jdk5中完善后,java的并发能力大大提升。我们可以使用concurrent包来完成很多线程工作,而不用处理线程所带来的复杂性。这里要说的自然是JMM。这是JSR-133中描述的,并且在jdk1.5后得到完善。

如果抛开JMM谈java并发,就显得毫无底气了。因为JSR-133正是为了线程才针对JMM的规范。

如果你了解计算机的缓存机制,其实JMM很好理解。为了使得各个线程的数据不被其他线程无意或恶意修改,JMM把各个线程的私有内存和主要内存(可以看作是堆上的内存)分开。需要使用到主存的数据,就首先read到线程内存中,然后在load到变量里。于是,就多了8种操作。
lock 锁住主存变量,线程独享
unlock 解锁主存变量,对应lock
read 从主内存中读取变量到工作内存(线程内存)
load 从工作内存中读取read到的变量赋值给工作内存中的变量副本。
use 执行引擎使用到工作内存的变量(读取)
assign 执行引擎对变量的修改后赋值给工作内存变量(改变)
store 把工作内存中的变量放入到主内存中
write 把从工作内存中store得到的变量赋值给主内存变量。

可以看到,这几种操作,是必须遵守一定的顺序的。
1,成对出现。比如 read 和 write, load 和 store
2,确保assign后调用store,不允许没有assign操作,而store
3,新变量只能在主存中产生。也就是 use 和 store之前,必须有 load 和 assign
4,一次只能有一个线程lock操作,同一个线程可以多次lock(可重入)
5,lock之前,必须同步变量到主存。store 和 write
6,lock清空工作内存中的变量,重新 load 或者 assign
7,unlock不能单独使用,必须先有lock。

至此,JMM就初现雏形了。
详细的可以看 Doug Lea大牛的文章:http://g.oswego.edu/dl/jmm/cookbook.html

接下来,我们就要问了,JMM是要解决些什么具体问题呢。
1、原子性 atomicity
2、可见性 visibility
3、有序性 ordering

原子性,就是保证程序的执行不被打断(线程中断)。read,load, assign ,use, store, write 对于基本数据类型都是原子性的(除去long,double,但是多数虚拟机的实现都尽量使得这两个类型的读写是原子性)。如果要对更大的范围保证原子性,有lock和unlock(为开放),这里有更高层次的 monitorenter 和 monitorexit,这就是synchronized 关键字的指令。

可见性,就是一个线程修改了某个变量,其他线程可以立即见到。由于每个线程所做的操作,只能是修改自己线程中的工作内存,所以并不是其他线程立即可见的。volatile关键字可以使得对他的修改,对其他线程立即可见。后面会讲到。当然,synchronized也可一,因为在unlock了之前,是必须要store 和 write的。还有一个final,也放在后面说。

有序性,线程内的看到自己的执行总是有序的,而线程之外都是无序的。这里有编译和JIT优化的问题,也有工作内存和主存同步的问题。volatile 也是禁止重排序的(虚拟机支持)synchronized因为 lock 一个线程独享,所以也是可以的。

对于有序性,这里如何判断jvm是否要reordering,这里有一个先行发生原则。也就是先发生的操作,可以被之后发生的操作观察到,这样就不许要使用之前说的那些关键字了。但是这样的原则(happens-before)只适用以下几个细则中:
1,(Program Order Rule)在同一个线程中,按代码顺序执行。也就是根据控制流,执行到哪里就是哪里。
2,(Monitor Lock Rule) unlock 操作先行发生后面的 lock ,针对同一个锁。
3,(Volatile Variable Rule) 对于一个volatileo变量的写先于后面对这个变量的读。
4,(Thread start Rule) 线程的start方法先行发生与此线程的其他动作
5,(Thread Termination Rule)线程的所有操作都优先于对此线程的终止检测。
6,(Thread Interruption Rule)对interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
7,(Finalizer Rule) 一个对象的初始化优先与finalize方法调用
8,(Transitivity) A优先与B , B 优先于 C , 则A 优先于 C
9 如不符合以上几点,JVM都可能reordering
分析:

private int value;
public int getValue() { return value;}
public void setValue(int value) { this.value = value; }

假设当前value值为1, 线程A 调用setValue(2) 然后(很快)线程B调用getValue(),那么线程B 会得到什么值呢?
首先,不在同一个线程,第一条不适用,也没有synchronized,volatile,等关键字,也不是发生在线程的启动终止等,所以2,3,4,5,6,7,都不适用。没有第三个参考线程,8 也不适用。所以,就是9,那么JVM就不能保证顺序性,也就是说,B 可能得到1,也可能得到2.

现在返回来看 volatile 关键字,他主要用于Visibility和Ordering
volatile的Visibility可以用JMM中的几条操作来说明。也就是use前,必须首先load,并且如果最后一个操作不是use,就不去load。这就要求每次使用前要刷新变量值。
其次,只有assign的时候,才store,如果最后一个操作不是store,就不允许assign。这就是说明每次修改后的变量要被更新,如果变量不能被store,就不被更新。
然后是volatile的Ordering确保。如果一个线程对volatile变量(V)的使用先于对另一个votaile变量(W)的使用,那么这个线程对V的read,write也一样要先于对W的read,write。这样保证不会被指令重排序优化。
那么 volatile 应用在什么地方呢。
首先看这样的代码

private volatile boolean run;
public void stop(){
run = false;
}
...
public void exec() {
while(run) { dosomething... }
}


但是有一点,并非支持Visibility的就一定是线程安全的。
再看一个例子

public class Counter {
private volatile int visitorCount;

public Counter(){
this.visitorCount = 0;
}

public void increcement(){
visitorCount++;
}

public int getCount(){
return this.visitorCount;
}

public static void main(String[] args) throws Exception {
final Counter counter = new Counter();
for(int i = 0; i < 20 ;i++){
Thread t = new Thread(new Runnable(){
public void run(){
for(int j = 0; j < 100;j++)
counter.increcement();
}
});
t.start();
}
while(Thread.activeCount() > 1)
Thread.yield();
System.out.println(counter.getCount());
}
}

看看这个输出多少。理想情况是2000吧,实际会小于这个数字。这个问题的原因就是,虽然volatile是线程可见的,但是由于我们的改变不是原子操作,而且,在修改之前,我们需要获取到这个值,但是当我们修改的时候,可能这个值已经改变了。所以必须对增加值进行同步,才能保证正确性。这样说明,对于线程可见的变量,依然要同步才能确保线程安全。
补从一点,其实++操作不是原子性的,我们可以这样改:


import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
private volatile AtomicInteger visitorCount;

public Counter(){
this.visitorCount = new AtomicInteger(0);
}

public void increcement(){
visitorCount.incrementAndGet();
}

public int getCount(){
return visitorCount.get();
}

public static void main(String[] args) throws Exception {
final Counter counter = new Counter();
for(int i = 0; i < 20 ;i++){
Thread t = new Thread(new Runnable(){
public void run(){
for(int j = 0; j < 100;j++)
counter.increcement();
}
});
t.start();
}
while(Thread.activeCount() > 1)
Thread.yield();
System.out.println(counter.getCount());
}
}


对AtomicInteger的操作是原子性的。也就是我们在取得值和操作值中,是只允许一个线程在操作的。除此之外,我们就是用synchronized了。

对于final关键字,他本身就是使得变量是不可变的。所以对于基本数据是线程安全的,对于对象引用,则对引用是不可变的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值