您的位置:首页 > 编程语言 > Java开发

Java 并发编程之对象的共享

2014-08-19 13:53 316 查看

可见性

看下面这段代码

public class Init {
public static boolean ready = false;
public static int number = 0;

public static class Readerthread extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
while (!ready)
Thread.yield();
System.out.println(number);
}

}

public static void main(String[] args) {
new Readerthread().start();
number = 42;
ready = true;

}
}


按照推论来讲,程序应该会输出42这个数字,Thread.yield();是将线程打回就绪状态,等待系统的下一次调度。实际上可能不是,程序可能输出0,或者 永远循环下去。读线程可能看到了ready的值,但却没有看到写入后number的值 。这种现象被叫做“重排序”,也就是说实际的执行顺序可能是先写入ready后写入Number。我也运行了几十次这个程序,倒是都会输出42.可能出错的机率比较小吧。解决这个问题的方法就是添加足够的同步。

几天之后我在项目中碰到了比这更好的例子

public class Init {
public static boolean ready = false;
public static int number = 0;

public static class Readerthread extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
number = 42;
ready = true;
}

}

public static void main(String[] args) {
while (!ready)
Thread.yield();
System.out.println(number);
new Readerthread().start();

}
}这个循环会100%的死循环下去。如何使此对象共享于两个线程之间呢
我想到了volatile关键字,用于确保可见性

public class Init {
public volatile boolean ready = false;
public volatile int number = 0;

public class Readerthread extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
number = 42;
ready = true;
}
}
public void test() {
while (!this.ready) {
Thread.yield();
System.out.println(this.number);
}
System.out.println(this.number);
new Readerthread().start();
}

public static void main(String[] args) {
Init init = new Init();
init.test();
}
}

结果却很出乎意料。程序依旧死循环,再仔细一看就明白了。。原来这和并发编程没什么 关系。只是单纯的卡住了
如何解决。。很简单

public class Init {
public volatile boolean ready = false;
public volatile int number = 0;

public class Readerthread extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
number = 42;
ready = true;
System.out.println(ready);
}
}

public void test() {
new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
while (!ready) {
Thread.yield();
System.out.println(number);
}
}
}).start();

System.out.println(this.number);
new Readerthread().start();
}

public static void main(String[] args) {
Init init = new Init();
init.test();
}
}

失效数据

public class Init {

private int vlaue;

/**
* @return the vlaue
*/
public int getVlaue() {
return vlaue;
}

/**
* @param vlaue
*            the vlaue to set
*/
public void setVlaue(int vlaue) {
this.vlaue = vlaue;
}
}

上面是一个非线程安全的整数类,第一眼看完可能都会认为应该对set方法进行同步 ,其实 不然。因为可能两个线程一个在调用set,另一个再用get.那么用get的那个线程可能看到更新后的value的值 ,也可能看不到。所以如果仅对set方法进行同步是不对的,也应该对get方法进行同步

public class Init {

private int vlaue;

/**
* @return the vlaue
*/
public synchronized int getVlaue() {
return vlaue;
}

/**
* @param vlaue
*            the vlaue to set
*/
public synchronized void setVlaue(int vlaue) {
this.vlaue = vlaue;
}
}


非原子的64位操作

读到一个失效值 也比读到一个随机值好吧。这也是一种安全性保证 ,不过被称为最低安全性。

存在一个例外。java的内存模型要求,变量的读取和写入操作都必须是原子操作,但对于非volatile类型的Long和double变量 ,jvm允许将64位的读写 操作分解为两个32位的操作。只读一半的话,,,结果可想而知。解决办法则是用volatile关键字声明它们,或者加锁保护起来。

加锁与可见性

加锁的含义不仅仅局限于互斥行为,还包括内存可见性,为确保所有线程都能看到共享变量的最新值,那么所有执行读或写操作的线程都必须在同一个锁上进行同步 。换句话说就是在加锁和解锁之间的代码块里的内容都是线程可见的。

Volatile变量

这是java提供的一种稍弱的同步机制,用于确保变量的更新通知到其它线程。它只确保可见性而不可确保原子性。用于循环中的中止变量等地方比较合适,等其它一些不需要准确的数值的地方。以下三种情况才应该使用

对变量的写入操作不依赖变量的当前值,(如Count++就不行)或者能确保是在单线程中更新变量值
该变量不会与其他状态变量 一起纳入不变性条件中。(这个的意思 是不会影响到其它变量为前提吧?)
在访问变量时不需要加锁

发布与逸出

发布一个对象 的意思 是指,使对象能够在当前作用域之外的代码中使用,例如,将一个指向该对象的引用保存到其他代码可以访问的地方。许多情况下我们要确保对象的内部状态不被 发布。如果发布了那么就会破坏线程安全性。当不该发布的对象被发布时这种情况被 叫做逸出。如下面这种情况。

public class unsafe {

private String[] state = new String[] { "sdf", "fdsf" };

public String[] getstate() {
return state;
}
}
常见的还有隐式的this逸出



button.setonclicklistener(new onclicklistener(){

public void onEvent(Event e){

dosomething(e);

}

});


参考资料:《java并发编程实战》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 多线程