先用一个简单的例子描述一下多线程可能带来的问题。
假设有一个银行账户,里面有10000块钱,小张和他妻子都知道密码,某一次小张和他妻子同时操作该账户,小张准备存1000块钱,而他妻子准备取1000块钱。
假设小张手速快了那么一点点,先进入银行账户,开始存钱,但是还没有结算完的时候,他妻子进入银行账户,开始取钱,代码如下:
//初始金额为10000
money = 10000
//小张准备开始的操作
Thread zhang = new Thread(){
public void run(){
money += 1000;
try{
Thread.sleep(1000)
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
//妻子准备开始的操作
Thread wife = new Thread(){
public void run(){
money -= 1000;
try{
Thread.sleep(1000)
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
//小张开始操作
zhang.start();
//妻子也开始操作
wife.start();
System.out.println(money);
运行之后可以发现,最终钱数并不一定为10000,这数据是不准确的,也就是产生了“脏数据”。
产生脏数据的原因很简单,对于这个共享的money数据,小张操作时,从主内存中获取数据money=10000,进行计算得到money=11000,这个数据操作完后需要等待存到主内存中,但是在还没有存回时,妻子也开始了操作,从主内存中同样获取money=10000,并进行计算之后得到money=9000的结果,同样等到存到主内存中,此时小张操作完的数据已经存回主内存了,使得主内存中的money=11000;然后妻子操作完的数据也开始存回主内存了,使得money=9000。所以最终结果可能是9000!=10000。这就是多线程同时操作数据可能带来的问题。这种读取行为称为“脏读”。
如何解决上述问题呢?只需要将该账户锁定起来,允许同一时间段只能有一个人操作,想要操作这个账户,则必须要拿到这把锁,才能对数据进行操作,我们称为同步。JAVA中有给账户加锁的关键字,synchronized。
synchronized有两种使用方式,第一种是同步代码块。
synchronized(object){
//此处的代码只有拿到object对象的锁才可以执行
...
}
该种方式进行加锁,锁的是括号中的对象,即如果需要调用该对象或者该对象中的资源,就需要先拿到锁,拿不到锁的必须等到当前拿到锁的线程操作完成释放锁才可以去争抢。线程获得了锁,才可以执行同步代码块中的内容。
第二种方式是同步方法,这种方式锁的是当前的对象。
synchronized void withdraw(){
...
}
这种方式进行加锁之后,外部的线程想要访问该方法,则需要拿到该对象的锁。拿到锁之后才可以调用该方法。如果该对象有多个方法且都已经同步,而外部有多个线程需要分别调用不同的方法,则必须先拿到该锁,才可以进行自己的方法调用。也就是说,当对多个方法都加锁时,不仅是不可能有两个线程同时调用一个方法,也不可能有两个线程同时调用两个不同的方法。
对静态方法进行加锁,静态方法是在类加载的时候就加载的,此时还没有对象,所以很显然锁的不应该是对象,而应该是类。或者说该类的Class对象。想要操作同步的静态方法,则必须要拿到这个Class对象的锁才可以进行操作。需要注意的是,类的锁和类的实例对象的锁无关,所以拿到类的锁并不影响另外一个线程获取类的实例对象的锁。