Head First Java(第2版)P526程序:
/*代码示例一*/
public class TestThread {
public static void main(String args[]){
ThreadOne t1 = new ThreadOne();
ThreadTwo t2 = new ThreadTwo();
Thread one = new Thread(t1);
Thread two = new Thread(t2);
one.start();
two.start();
}
}
class ThreadOne implements Runnable {
Accum a = Accum.getAccum();
@Override
public void run() {
for (int x= 0; x < 98; x ++) {
a.updateCounter(1000);
try {
Thread.sleep(50);
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println("one " + a.getCounter());
}
}
class ThreadTwo implements Runnable {
Accum a = Accum.getAccum();
@Override
public void run() {
for (int x= 0; x < 99; x ++) {
a.updateCounter(1);
try {
Thread.sleep(50);
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println("Two " + a.getCounter());
}
}
class Accum {
private static Accum a = new Accum();
private int counter = 0;
private Accum() {}
public static Accum getAccum() {
return a;
}
public int getCounter() {
return counter;
}
public void updateCounter(int add) {
counter += add;
}
}
运行结果可以有N种可能:
(照片没有对齐,估计又要被wen老师嫌弃了)
当然题目中给的也是其中一种运行结果,给了99099不过是提示罢了,假如理解了这段代码的话,你就会知道运行出99099的概率几近于零T_T。
且来看看这段代码,虽然线程one,two都引用了静态变量Accum a,但是当我们对counter做修改的时候依然面临着并发性问题。那么,把counter也改为static总可以吧?
当然不行。静态与同步是两回事!!!
所以,假如要正确地修改counter的值,就需要使用synchronized来修饰方法/代码块,使它每次只能被单一的线程存取。
于是,代码修改部分如下:
public synchronized void updateCounter(int add) {
counter += add;
}
现在就能正确修改counter了。
Head First Java中有提到Singleton模式(创建静态实例,私有构造函数)。一般,我们是那么写:
(当然,也已经不是原来的效果了)
/*代码示例二*/
public class TestThread {
public static void main(String args[]){
// ThreadOne t1 = new ThreadOne();
// ThreadTwo t2 = new ThreadTwo();
Accum accum = new Accum();
Thread one = new Thread(accum);
Thread two = new Thread(accum);
one.start();
two.start();
}
}
//class ThreadOne implements Runnable {
// Accum a = Accum.getAccum();
// public void run() {
// for (int x= 0; x < 98; x ++) {
// a.updateCounter(1000);
//
// try {
// Thread.sleep(50);
// } catch (Exception ex) {
// ex.printStackTrace();
// }
// }
//
// System.out.println("one " + a.getCounter());
// }
//}
//class ThreadTwo implements Runnable {
// Accum a = Accum.getAccum();
//
//
// @Override
// public void run() {
// for (int x= 0; x < 99; x ++) {
// a.updateCounter(1);
//
// try {
// Thread.sleep(50);
// } catch (Exception ex) {
// ex.printStackTrace();
// }
// }
//
// System.out.println("Two " + a.getCounter());
// }
//}
class Accum implements Runnable{
// private static Accum a = new Accum();
private int counter = 0;
public Accum() {}
// public static Accum getAccum() {
// return a;
// }
public int getCounter() {
return counter;
}
public synchronized void updateCounter(int add) {
counter += add;
}
@Override
public void run() {
for (int x= 0; x < 98; x ++) {
updateCounter(1000);
try {
Thread.sleep(50);
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println("Result " + getCounter());
}
}
(为了方便看到对比,仅注释掉原来的代码)
那么运行结果是:
同步方法的锁,同步方法分为静态同步方法与非静态同步方法。
所有的非静态同步方法用的都是同一把锁——实例对象本身,也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁。见代码实例二,Thread类one,two公用Accum实例对象a。
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。见下面的代码实例三:
/*代码实例三*/
public class TestThread {
public static void main(String args[]){
Accum accum1 = new Accum();
Accum accum2 = new Accum();
Thread one = new Thread(accum1);
Thread two = new Thread(accum2);
//one,two分别拥有实例accum1,acccum2
one.start();
two.start();
}
}
class Accum implements Runnable{
// private static Accum a = new Accum();
private int counter = 0;
public Accum() {}
// public static Accum getAccum() {
// return a;
// }
public int getCounter() {
return counter;
}
public synchronized void updateCounter(int add) {
counter += add;
}
@Override
public void run() {
for (int x= 0; x < 98; x ++) {
updateCounter(1000);
try {
Thread.sleep(50);
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println("Result " + getCounter());
}
}
运行结果:
所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!见下面的代码实例四:
/*代码实例四*/
public class TestThread {
public static void main(String args[]){
Accum accum1 = new Accum();
Accum accum2 = new Accum();
Thread one = new Thread(accum1);
Thread two = new Thread(accum2);
one.start();
two.start();
}
}
class Accum implements Runnable{
private static int counter = 0;
public Accum() {}
public int getCounter() {
return counter;
}
/*在此处加了sychronized关键字,并且将counter改为static变量,那么假如我只是把counter改为静态变量,而没有加sychronized会如何呢?就会引发并发性问题,记住,同步与静态没有关系!!!*/
public synchronized static void updateCounter(int add) {
counter += add;
}
@Override
public void run() {
for (int x= 0; x < 98; x ++) {
updateCounter(1000);
try {
Thread.sleep(50);
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println("Result " + getCounter());
}
}
运行结果是:
而对于同步块,由于其锁是可以选择的,所以只有使用同一把锁的同步块之间才有着竞态条件,这就得具体情况具体分析了,但这里有个需要注意的地方,同步块的锁是可以选择的,但是不是可以任意选择的!!!!这里必须要注意一个物理对象和一个引用对象的实例变量之间的区别!使用一个引用对象的实例变量作为锁并不是一个好的选择,因为同步块在执行过程中可能会改变它的值,其中就包括将其设置为null,而对一个null对象加锁会产生异常,并且对不同的对象加锁也违背了同步的初衷!这看起来是很清楚的,但是一个经常发生的错误就是选用了错误的锁对象,因此必须注意:同步是基于实际对象而不是对象引用的!多个变量可以引用同一个对象,变量也可以改变其值从而指向其他的对象,因此,当选择一个对象锁时,我们要根据实际对象而不是其引用来考虑!作为一个原则,不要选择一个可能会在锁的作用域中改变值的实例变量作为锁对象!!!!
参考:http://topmanopensource.iteye.com/blog/1738178
折腾了那么久,肘子终于搞明白了head first java里的这样一段话:
如果有3个Dog对象在堆上,则总共有4个与Dog有关的锁,3个是Dog实例的锁,1个是类。当你对静态方法做同步化的时,Java会使用类本身的锁。
以上。祝周末能够愉快地编程~