通过一个实例,在多线程情况下去分析安全隐患并解决。
需求分析:有两个储户去银行存钱,每次存100,共存三次。
class Bank { private int sum; //银行共有多少钱 public void add(int num) { sum=sum+num; System.out.println("sum="+sum); } } class Customer implements Runnable //顾客存钱的行为可以封装为线程任务 { public void run() { Bank b=new Bank(); for(int x=0;x<3;x++) { b.add(100); //add()是Bank类对象的方法,所以要创建一个Bank类对象 } } } class BankDemo { public static void main(String[] args) { Customer c=new Customer(); //创建任务对象 Thread t1=new Thread(c); //创建线程 Thread t2=new Thread(c); t1.start(); t2.start(); } }
运行结果:
我们看到,sum=100出现两次,sum=200出现两次,而两位顾客每一次存钱100元都会导致sum增加100,即运行结果发生错误。这是因为我们在Customer类的run()方法中创建了一个Bank类对象,而两个线程(顾客)在每次执行任务代码(存钱)时都会重新创建Bank()对象,实则就创建了两个Bank类对象,而并分操作同一个银行。在Customer类中应该这样编写:
class Customer implements Runnable //顾客存钱的行为可以封装为线程任务 { private Bank b=new Bank(); //保证多个线程操作同一个Bank类对象 public void run() { for(int x=0;x<3;x++) { b.add(100); //add()是Bank类对象的方法,所以要创建一个Bank类对象 } } }运行结果:
现在显示的结果是正确的,但在现在的程序中仍然存在线程安全隐患。
通过分析,Bank类的b对象和Bank类的sum成员都是多线程的共享数据,Bank类的add()方法是线程任务代码。在本例中,对共享数据的操作不止一条,就可能存在安全问题。我们调用sleep()方法来验证这个问题:
class Bank { private int sum; //银行共有多少钱 public void add(int num) { sum=sum+num; try{Thread.sleep(10);}catch(InterruptedException e){} System.out.println("sum="+sum); } }运行结果:
出现了两次sum=200,sum=400,sum=600,发生了安全问题。
所以用同步代码块将线程代码进行封装:
class Bank { private int sum; private Object obj=new Object(); public void add(int num) { synchronized(obj) //此时不能直接使用new Object()来创建对象,原理如上所讲,即每个线程使用自己的锁。 { sum=sum+num; try{Thread.sleep(10);}catch(InterruptedException e){} System.out.println("sum="+sum); } } }
运行结果:
class Bank { private int sum; public synchronized void add(int num) //同步函数-->解决线程安全问题 { sum=sum+num; try{Thread.sleep(10);}catch(InterruptedException e){} System.out.println("sum="+sum); } }运行结果:
注:同步函数使用的锁是this。
引申:1.同步函数和同步代码块的区别:同步函数的锁是固定的this(当前对象),同步代码块的锁是任意的对象。
2.静态的同步函数使用的锁是,该函数所属字节码文件对象,可以用 getClass()方法获取,也可以用当前 类名.class 表示。