一 同步的概念
线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。
例如:两个线程ThreadA、ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据。
MyRunnable.java
1 package Thread; 2 public class MyRunnable implements Runnable{ 3 private Foo foo=new Foo(); 4 public static void main(String[] args){ 5 MyRunnable r=new MyRunnable(); 6 Thread ta=new Thread(r,"Thread-A"); 7 Thread tb=new Thread(r,"Thread-B"); 8 ta.start(); 9 tb.start(); 10 } 11 public void run(){ 12 for(int i=0;i<3;i++){ 13 this.fix(30); 14 try{ 15 Thread.sleep(1); 16 } 17 catch(InterruptedException e){ 18 e.printStackTrace(); 19 } 20 System.out.println(Thread.currentThread().getName()+":当前foo对象的值="+foo.getX()); 21 } 22 } 23 public int fix(int y){ 24 return foo.fix(y); 25 } 26 }
Foo.java
1 package Thread; 2 public class Foo { 3 private int x=100; 4 public int getX(){ 5 return x; 6 } 7 public int fix(int y){ 8 x=x-y; 9 return x; 10 } 11 }
运行结果:
1 Thread-A:当前foo对象的值=40 2 Thread-B:当前foo对象的值=40 3 Thread-A:当前foo对象的值=-20 4 Thread-B:当前foo对象的值=-50 5 Thread-A:当前foo对象的值=-80 6 Thread-B:当前foo对象的值=-80
从结果看出,这样的输出值明显不合理。原因是两个线程不加控制的访问Foo对象并修改器数据所致。因此,应该对Foo的访问加以限制,每次只能有一个线程在访问。
1 、同步方法
线程的同步是保证多线程安全访问竞争资源的一种手段。 对于同步具体的Java代码中需要完成两个操作:
1、把竞争访问的资源标识为private;
2、同步哪些修改变量的代码,使用synchronized关键字同步方法或代码。(对于synchronized而言,只能标记非抽象方法,不能标识成员变量。)
为了演示同步方法的使用,构建了一个信用卡账户,起初信用额度为100w,然后模拟透支、存款等操作。显然银行账户User对象是个竞争资源,而多个并发操作的是账户方法oper(int x),当然应该在此方法上加上同步,并将账户余额设为私有变量,禁止直接访问。
BankTest.java
1 package Thread; 2 3 public class BankTest { 4 public static void main(String[] args){ 5 User u=new User("小二",100); 6 MyThread t1=new MyThread("线程1",u,20); 7 MyThread t2=new MyThread("线程2",u,-60); 8 MyThread t3=new MyThread("线程3",u,-80); 9 MyThread t4=new MyThread("线程4",u,-30); 10 MyThread t5=new MyThread("线程5",u,32); 11 MyThread t6=new MyThread("线程6",u,21); 12 t1.start(); 13 t2.start(); 14 t3.start(); 15 t4.start(); 16 t5.start(); 17 t6.start(); 18 } 19 } 20 class MyThread extends Thread{ 21 private User u; 22 private int y=0; 23 MyThread(String name,User u,int y){ 24 super(name); 25 this.u=u; 26 this.y=y; 27 } 28 public void run(){ 29 u.oper(y); 30 } 31 } 32 class User{ 33 private String code; 34 private int cash; 35 User(String code,int cash){ 36 this.code=code; 37 this.cash=cash; 38 } 39 public String getCode(){ 40 return code; 41 } 42 /** 43 * 业务方法 44 * @param x 添加x万元 45 * */ 46 public synchronized void oper(int x){ 47 try{ 48 Thread.sleep(10L); 49 this.cash+=x; 50 System.out.println(Thread.currentThread().getName()+"运行结果,增加"+x+",当前余额为:"+cash); 51 Thread.sleep(10L); 52 } 53 catch(InterruptedException e){ 54 e.printStackTrace(); 55 } 56 } 57 public String toString(){ 58 return "User{" + "code=" + code + ",cash=" + cash +'}'; 59 } 60 }
结果为:
1 线程1运行结果,增加20,当前余额为:120 2 线程2运行结果,增加-60,当前余额为:60 3 线程6运行结果,增加21,当前余额为:81 4 线程5运行结果,增加32,当前余额为:113 5 线程4运行结果,增加-30,当前余额为:83 6 线程3运行结果,增加-80,当前余额为:3
但是如果把同步关键字synchronized去掉,结果为:
1 线程2运行结果,增加-60,当前余额为:60 2 线程6运行结果,增加21,当前余额为:1 3 线程4运行结果,增加-30,当前余额为:3 4 线程5运行结果,增加32,当前余额为:3 5 线程1运行结果,增加20,当前余额为:60 6 线程3运行结果,增加-80,当前余额为:-20
显然是错误的,多个线程并发访问了竞争资源u,并对u的属性做了修改。可见同步的重要性。
2、同步块
有时候,同步块比同步方法有更好的效果。在上个例子的基础上对oper方法做了改变,由同步方法改为同步块模式。
BankTest.java
1 public void oper(int x){ 2 try{ 3 Thread.sleep(10L); 4 synchronized(this){ 5 this.cash+=x; 6 System.out.println(Thread.currentThread().getName()+"运行结果,增加"+x+",当前余额为:"+cash); 7 } 8 Thread.sleep(10L); 9 } 10 catch(InterruptedException e){ 11 e.printStackTrace(); 12 } 13 }
结果和上例是一样的。具体的变化思想为:
1 public syncronized int getX(){ 2 return x++; //同步方法 3 } 4 与 5 public int getX(){ 6 synchronized(this){ 7 return x;//非同步方法 8 } 9 }
注意:
在使用synchronized时,应该避免在synchronized方法或synchronized块中使用sleep或yield方法。因为synchronized程序块占用着对象锁,你休息那么其他线程只能等待。这样效率不高。同样,在同步程序块内调用yield方法让出CPU资源也没有意义,因为你占用着锁,其他资源无法访问。
二、静态方法同步
要同步静态方法,需要一个用整个类对象的锁,这个对象就是这个类(xxx.class),如:
1 public static synchronized int setName(String name){
2 xxx.name=name;
3 }
4 等价于
5 public static int setName(){
6 synchronized(xxx.class){
7 xxx.name=name;
8 }
9 }
三、何时需要同步
1、多个线程同时访问互斥(可交换)数据时,应该同步保护数据,确保两个线程不会同时更改它。
2、非静态字段中可更改的数据,通常用非静态方法访问。
3、静态字段中可更改的数据,用静态方法访问。
四、总结
1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
2、线程同步方法是通过锁来实现,每个对象都仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程无法再访问该对象的其他同步方法。
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁不干预。一个线程获得锁,当在一个同步方法中访问另外对象的同步方法时,会获取两个对象的锁。
4、对于同步,要时刻清醒哪个对象上同步,这是关键。