JAVA Synchronized 同步 详解(一)

使用synchronized

在编写一个类时,如果该类中的代码可能运行于多线程环境下,那么就要考虑同步的问题。

在Java中内置了语言级的同步原语--synchronized,这也大大简化了Java中多线程同步的使用。

我们首先编写一个非常简单的多线程的程序,是模拟银行中的多个线程同时对同一个储蓄账户进行存款、取款操作的;在程序中我们使用了一个简化版本的Account类,代表了一个银行账户的信息;在主程序中我们首先生成了1000个线程,然后启动它们,每一个线程都对John的账户进行存100元,然后马上又取出100元。这样,对于John的账户来说,最终账户的余额应该是还是1000元才对。然而运行的结果却超出我们的想像,首先来看看我们的演示代码:

  1. package com.sync.test;  
  2.   
  3. public class Account {  
  4.         String name;  
  5.     float amount;  
  6.   
  7.     public Account(String name, float amount) {  
  8.         this.name = name;  
  9.         this.amount = amount;  
  10.     }  
  11.   
  12.     public void inAccount(float amt) {  
  13.         float tmp = amount;  
  14.         tmp += amt;  
  15.   
  16.         try {  
  17.             Thread.sleep((int) (Math.random() * 1000));// 模拟其它处理所需要的时间,比如刷新数据库等  
  18.         } catch (InterruptedException e) {  
  19.             System.out.println("inAccount excetion: " + e.toString());  
  20.         }  
  21.         amount = tmp;  
  22.     }  
  23.   
  24.     public void outAccount(float amt) {  
  25.         float tmp = amount;  
  26.         tmp -= amt;  
  27.         try {  
  28.             Thread.sleep((int) (Math.random() * 1000));// 模拟其它处理所需要的时间,比如刷新数据库等  
  29.         } catch (InterruptedException e) {  
  30.             System.out.println("outAccount excetion: " + e.toString());  
  31.         }  
  32.         amount = tmp;  
  33.     }  
  34.   
  35.     public float getBalance() {  
  36.         return amount;  
  37.     }  
  38. }   
package com.sync.test;

public class Account {
        String name;
	float amount;

	public Account(String name, float amount) {
		this.name = name;
		this.amount = amount;
	}

	public void inAccount(float amt) {
		float tmp = amount;
		tmp += amt;

		try {
			Thread.sleep((int) (Math.random() * 1000));// 模拟其它处理所需要的时间,比如刷新数据库等
		} catch (InterruptedException e) {
			System.out.println("inAccount excetion: " + e.toString());
		}
		amount = tmp;
	}

	public void outAccount(float amt) {
		float tmp = amount;
		tmp -= amt;
		try {
			Thread.sleep((int) (Math.random() * 1000));// 模拟其它处理所需要的时间,比如刷新数据库等
		} catch (InterruptedException e) {
			System.out.println("outAccount excetion: " + e.toString());
		}
		amount = tmp;
	}

	public float getBalance() {
		return amount;
	}
} 
  1. package com.sync.test;  
  2.   
  3. public class AccountTest {  
  4.   
  5.     private static int NUM_OF_THREAD = 1000;  
  6.     static Thread[] threads = new Thread[NUM_OF_THREAD];  
  7.   
  8.     public static void main(String[] args) throws Exception {  
  9.   
  10.         final Account acc = new Account("John"1000.0f);  
  11.   
  12.         for (int i = 0; i < NUM_OF_THREAD; i++) {  
  13.             threads[i] = new Thread(new Runnable() {  
  14.                 public void run() {  
  15.                     acc.inAccount(100.0f);  
  16.                     acc.outAccount(100.0f);  
  17.                 }  
  18.             });  
  19.             threads[i].start();  
  20.         }  
  21.   
  22.         for (int i = 0; i < NUM_OF_THREAD; i++) {  
  23.             try {  
  24.                 threads[i].join(); // 等待所有线程运行结束  
  25.             } catch (InterruptedException e) {  
  26.                 System.out.println("threads[" + i + "] excetion: " + e.toString());  
  27.             }  
  28.         }  
  29.         System.out.println("Finally, John's balance is:" + acc.getBalance());  
  30.     }  
  31.   
  32. }  
package com.sync.test;

public class AccountTest {

	private static int NUM_OF_THREAD = 1000;
	static Thread[] threads = new Thread[NUM_OF_THREAD];

	public static void main(String[] args) throws Exception {

		final Account acc = new Account("John", 1000.0f);

		for (int i = 0; i < NUM_OF_THREAD; i++) {
			threads[i] = new Thread(new Runnable() {
				public void run() {
					acc.inAccount(100.0f);
					acc.outAccount(100.0f);
				}
			});
			threads[i].start();
		}

		for (int i = 0; i < NUM_OF_THREAD; i++) {
			try {
				threads[i].join(); // 等待所有线程运行结束
			} catch (InterruptedException e) {
				System.out.println("threads[" + i + "] excetion: " + e.toString());
			}
		}
		System.out.println("Finally, John's balance is:" + acc.getBalance());
	}

}

注意:上面在Account的inAccount和outAccount方法中之所以要把对amount的运算使用一个临时变量首先存储,随机sleep一段时间,然后,再赋值给amount,是为了模拟真实运行时的情况。因为在真实系统中,账户信息肯定是存储在持久媒介中,比如RDBMS中,此处的睡眠的时间相当于比较耗时的数据库操作,最后把临时变量tmp的值赋值给amount相当于把amount的改动写入数据库中。

运行AccountTest,结果如下(每一次结果都会不同):
Finally, John's balance is:1200.0

为什么会出现这样的问题?
这就是多线程中的同步的问题。在我们的程序中,Account中的amount会同时被多个线程所访问,这就是一个竞争资源,通常称作竞态条件。对于这样的多个线程共享的资源我们必须进行同步,以避免一个线程的改动被另一个线程所覆盖。
在我们这个程序中,Account中的amount是一个竞态条件,所以所有对amount的修改访问都要进行同步,我们将inAccount()和outAccount()方法进行同步,
修改为:

  1. public synchronized  void inAccount(float amt) {  
  2.     float tmp = amount;  
  3.     tmp += amt;  
  4.   
  5.     try {  
  6.         Thread.sleep((int)(Math.random()*1000)); // 模拟其它处理所需要的时间,比如刷新数据库等  
  7.     } catch (InterruptedException e) {  
  8.         System.out.println("inAccount excetion: " + e.toString());  
  9.     }  
  10.   
  11.     amount = tmp;  
  12. }  
  13.   
  14. public synchronized void outAccount(float amt) {  
  15.     float tmp = amount;  
  16.     tmp -= amt;  
  17.   
  18.     try {  
  19.         Thread.sleep((int)(Math.random()*1000)); // 模拟其它处理所需要的时间,比如刷新数据库等  
  20.     } catch (InterruptedException e) {  
  21.         System.out.println("outAccount excetion: " + e.toString());  
  22.     }  
  23.   
  24.     amount = tmp;  
  25. }  
	public synchronized  void inAccount(float amt) {
		float tmp = amount;
		tmp += amt;

		try {
			Thread.sleep((int)(Math.random()*1000)); // 模拟其它处理所需要的时间,比如刷新数据库等
		} catch (InterruptedException e) {
			System.out.println("inAccount excetion: " + e.toString());
		}

		amount = tmp;
	}

	public synchronized void outAccount(float amt) {
		float tmp = amount;
		tmp -= amt;

		try {
			Thread.sleep((int)(Math.random()*1000)); // 模拟其它处理所需要的时间,比如刷新数据库等
		} catch (InterruptedException e) {
			System.out.println("outAccount excetion: " + e.toString());
		}

		amount = tmp;
	}

此时,再运行,我们就能够得到正确的结果了。Account中的getBalance()也访问了amount,为什么不对getBalance()同步呢?因为getBalance()并不会修改amount的值,所以,同时多个线程对它访问不会造成数据的混乱。

同步加锁的是对象,而不是代码。
因此,如果你的类中有一个同步方法,这个方法可以被两个不同的线程同时执行,只要每个线程自己创建一个的该类的实例即可。
参考下面的代码:

  1. package com.sync.test;  
  2.   
  3. public class Foo extends Thread {  
  4.     private int val;  
  5.   
  6.     public Foo(int v) {  
  7.         val = v;  
  8.     }  
  9.   
  10.     public synchronized void printVal(int v) {  
  11.         while (true)  
  12.             System.out.println(v);  
  13.     }  
  14.   
  15.     public void run() {  
  16.         printVal(val);  
  17.     }  
  18.   
  19. }  
package com.sync.test;

public class Foo extends Thread {
	private int val;

	public Foo(int v) {
		val = v;
	}

	public synchronized void printVal(int v) {
		while (true)
			System.out.println(v);
	}

	public void run() {
		printVal(val);
	}

}

  1. package com.sync.test;  
  2.   
  3. public class FooTest {  
  4.   
  5.     public static void main(String[] args) {  
  6.         Foo f1 = new Foo(1);  
  7.         f1.start();  
  8.         Foo f2 = new Foo(3);  
  9.         f2.start();  
  10.     }  
  11. }  
package com.sync.test;

public class FooTest {

	public static void main(String[] args) {
		Foo f1 = new Foo(1);
		f1.start();
		Foo f2 = new Foo(3);
		f2.start();
	}
}

运行FooTest产生的输出是1和3交叉的。如果printVal是断面,你看到的输出只能是1或者只能是3而不能是两者同时出现。程序运行的结果证明两个线程都在并发的执行printVal方法,即使该方法是同步的并且由于是一个无限循环而没有终止。


类的同步:
要实现真正的断面,你必须同步一个全局对象或者对类进行同步。
下面的代码给出了一个这样的范例:

  1. package com.sync.test;  
  2.   
  3. public class Foo extends Thread {  
  4.     private int val;  
  5.   
  6.     public Foo(int v) {  
  7.         val = v;  
  8.     }  
  9.   
  10.     public void printVal(int v) {  
  11.         synchronized (Foo.class) {  
  12.             while (true)  
  13.                 System.out.println(v);  
  14.         }  
  15.     }  
  16.   
  17.     public void run() {  
  18.         printVal(val);  
  19.     }  
  20.   
  21. }  
package com.sync.test;

public class Foo extends Thread {
	private int val;

	public Foo(int v) {
		val = v;
	}

	public void printVal(int v) {
		synchronized (Foo.class) {
			while (true)
				System.out.println(v);
		}
	}

	public void run() {
		printVal(val);
	}

}

上面的类不再对个别的类实例同步而是对类进行同步。

对于类Foo而言,它只有唯一的类定义,两个线程在相同的锁上同步,因此只有一个线程可以执行printVal方法。

这个代码也可以通过对公共对象加锁。例如给Foo添加一个静态成员。两个方法都可以同步这个对象而达到线程安全。

同步公共对象的两种通常方法:

第一种

  1. package com.sync.test;  
  2.   
  3. public class Foo extends Thread {  
  4.     private int val;  
  5.     private static Object lock = new Object();  
  6.   
  7.     public Foo(int v) {  
  8.         val = v;  
  9.     }  
  10.   
  11.     public void printVal(int v) {  
  12.         synchronized (lock) {  
  13.             while (true) {  
  14.                 System.out.println(v);  
  15.             }  
  16.         }  
  17.     }  
  18.   
  19.     public void run() {  
  20.         printVal(val);  
  21.     }  
  22. }  
package com.sync.test;

public class Foo extends Thread {
	private int val;
	private static Object lock = new Object();

	public Foo(int v) {
		val = v;
	}

	public void printVal(int v) {
		synchronized (lock) {
			while (true) {
				System.out.println(v);
			}
		}
	}

	public void run() {
		printVal(val);
	}
}

上面的这个例子比原文给出的例子要好一些,因为原文中的加锁是针对类定义的,一个类只能有一个类定义,而同步的一般原理是应该尽量减小同步的粒度以到达更好的性能。笔者给出的范例的同步粒度比原文的要小。

 

第二种

  1. package com.sync.test;  
  2.   
  3. public class Foo extends Thread {  
  4.     private String name;  
  5.     private String val;  
  6.   
  7.     public Foo(String name, String v) {  
  8.         this.name = name;  
  9.         val = v;  
  10.     }  
  11.   
  12.     public void printVal() {  
  13.         synchronized (val) {  
  14.             while (true){  
  15.                 System.out.println(name + val);  
  16.             }  
  17.         }  
  18.     }  
  19.   
  20.     public void run() {  
  21.         printVal();  
  22.     }  
  23. }  
package com.sync.test;

public class Foo extends Thread {
	private String name;
	private String val;

	public Foo(String name, String v) {
		this.name = name;
		val = v;
	}

	public void printVal() {
		synchronized (val) {
			while (true){
				System.out.println(name + val);
			}
		}
	}

	public void run() {
		printVal();
	}
}
  1. package com.sync.test;  
  2.   
  3. public class FooTest {  
  4.   
  5.     public static void main(String[] args) {  
  6.         Foo f1 = new Foo("Foo 1:""printVal");  
  7.         f1.start();  
  8.         Foo f2 = new Foo("Foo 2:""printVal");  
  9.         f2.start();  
  10.     }  
  11. }  
package com.sync.test;

public class FooTest {

	public static void main(String[] args) {
		Foo f1 = new Foo("Foo 1:", "printVal");
		f1.start();
		Foo f2 = new Foo("Foo 2:", "printVal");
		f2.start();
	}
}

上面这个代码需要进行一些额外的说明,因为JVM有一种优化机制,因为String类型的对象是不可变的,因此当你使用""的形式引用字符串时,如果JVM发现内存已经有一个这样的对象,那么它就使用那个对象而不再生成一个新的String对象,这样是为了减小内存的使用。

上面的main方法其实等同于:

  1. public static void main(String[] args) {  
  2.     String value = "printVal";  
  3.     Foo f1 = new Foo("Foo 1:", value);  
  4.     f1.start();  
  5.     Foo f2 = new Foo("Foo 2:", value);  
  6.     f2.start();  
  7. }  
public static void main(String[] args) {
	String value = "printVal";
	Foo f1 = new Foo("Foo 1:", value);
	f1.start();
	Foo f2 = new Foo("Foo 2:", value);
	f2.start();
}


总结:   

        1、synchronized关键字的作用域有二种:
               (1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
               (2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。   

        2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的作用域是当前对象;   

        3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值