通过一个实例,在多线程情况下去分析安全隐患并解决。
需求分析:有两个储户去银行存钱,每次存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);
}
}
}
运行结果:
同步函数:在上面的代码中,函数add()和synchronized都对代码进行了封装,但synchronized是带有同步特性的封装。让函数具备同步性就解决了上述问题。就是将同步关键字作为函数的修饰符:
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 表示。