【Java多线程】对象及变量的并发访问——synchronized关键字(上)

    多线程编程中,一定会经常接触到一个概念,那就是“线程安全”。而线程安全是多线程编程所要关注的重要问题,而“非线程安全”的一个主要表现就是“脏读”,比如一个线程改变了对象内的变量值,但还没有执行完所有操作,这是另一个线程开始执行并取出了被改变过的这个变量,这时取出的数据就就称为“脏数据”。而Java中提供了一些并发访问的的方式,本文主要研究了其中的synchronized关键字的用法。

一、线程安全/非线程安全的变量

    一般来说非线程安全的问题存在于实例变量中,而如果这个变量是存在于方法内部的私有变量,那么这个变量是线程安全的。因为方法内部的私有变量是不允许其他方法访问的。就算两个线程同时调用了同一个实例变量中的同一个方法,这两个方法在执行时是异步执行,但是两个方法会各自创造自己的私有变量,所以不存在线程安全问题。

public void apple(String name){

try{

int num = 0;

if(name.equals("jack"))

num = 2;

else

num = 1;

}

int num = 0;

if(name = "jack")

catch(InterruptException e){

e.printStackTrace();

}

}

    如上述方法,每次调用方法的时候,都会重新创建一个int变量num,所以多个线程操作的是不同的num。

    但是如果不是方法内部的私有变量,而是类内部的实例变量,那么在多个线程同时访问的时候就会出现所谓的非线程安全问题了。这里有一个方法可以解决多个线程调用同一个方法访问相同的实例变量时的非线程安全的问题,那就是在这个方法前面加synchronized关键字,将其变为同步方法。多个线程访问同一个同步方法的时候一定是线程安全的。

二、对象锁

    锁的概念是多线程编程中的一个重要概念,当一个线程对一个对象进行同步访问时,会获得这个对象的锁,这样当别的方法要访问同一个对象的时候,因为无法获得锁,所以要等待上一个线程将此对象的锁释放。当创建同一个类的不同的实例化对象时,JVM会为每个对象创建不同的对象锁,所以synchronized关键字获得的都是对象锁而不是将一个函数或者一段代码当作锁,同步的执行这段代码或者同步的执行这个函数。而是针对一个实例对象进行锁操作,理解这个概念十分重要。

    当出现异常的时候,锁会自动释放,从而使得其他线程有机会竞争对象锁,保证程序正常运行。

三、synchronized方法

    synchronized修饰过的方法称为同步方法,当一个synchronized方法(方法A)被调用的时候,当前线程会获得此方法所属的对象(object)的对象锁,此时,其他线程不能在调用这个对象的这个同步方法。但是,当另一个线程调用object对象的非同步方法B时,这个方法B却可以和方法A同步执行。也就是说调用方法B不需要获得object的对象锁。

    此时将B方法使用synchronized修饰,再进行A、B方法调用的时候,就会变成同步执行了。即一个线程先获得object的锁执行完相应方法,释放锁,再由另一个线程获得锁,执行方法。

    这个概念可以用来解决脏读的问题,脏读一般会出现在操作非同步的实例变量的情况下,会有不同的线程争抢资源。我们可以将可能发生脏读的两个方法修饰为同步方法,则在执行方法时,这两个方法会被同步的调用,从而杜绝了脏读现象的发生。

    synchronized修饰的方法不能够继承。如果父类声明了synchronized方法,要在子类中进行重写,则要在方法重写的时候将方法再次用synchronized修饰,否则重写后的方法不是同步方法。但是如果不重写方法,直接在子类中调用此类方法的话,则调用的方法还是synchronized方法。

四、synchronized锁重入

    synchronized拥有锁重入的功能,也就是说当一个同步方法在获得object的锁后,再次请求对象锁是可以再次得到的,这也说明在一个synchronized方法/代码块的内部调用本类的其他synchronized方法/代码块时,是永远可以得到锁的。

    试想一下如果synchronized不可重入的话会发生什么情况。线程1调用了object的synchronized方法A,在方法A执行的时候需要使用到这个类的另一个同步方法B,但是此时对象锁时还没有被释放的,要执行方法B的话又需要先获得object的对象锁,这时,程序就会等待object的锁释放,而方法A还没有执行完,他在等待方法B返回所需要的结果,所以不会释放锁。这样两个方法相互等待就会发生死锁。同样的,当存在父子继承关系的时候,子类中synchronized方法在调用父类的synchronized方法时,也可以进行锁的重入。

五、synchronized同步语句块

    synchronized方法是有弊端的,试想一下,如果一个方法要执行一个很长的任务,这个任务是可以异步执行的,而这个方法又是一个同步方法,那么B线程就要等待很长的时间。这样的情况下可以使用synchronized同步语句块解决问题。

    这就好比我们要去办证件,办证件盖章的地方在一个房间内,每次只能有一个人进入这个房间盖章。但是在盖章之前要先填一个表格,所以我们就排队在房间外面等待,每次进去一个人,领取表格,然后填表,填好表之后交给工作人员给材料盖章。但是明明填表的过程是可以大家各自进行的,只有盖章的工作人员是需要同步的共享资源,把这两个任务全都安排在房间里进行很浪费时间。那么怎么办呢,我们可以把盖章的过程看作被synchronized修饰的代码块,而让填表过程独立出来。大家在房间外面领表填写,然后排队进入房间盖章,这样一来效率就高很多了。

    这就是同步语句块的作用,我们通常在需要同步的代码部分用

    synchronized(this)

    {

    同步代码;

    } 

    这样的方法来设置同步代码块,当程序执行的时候,线程间处于“一半同步、一半异步”的状态,也就是说在没有执行到同步代码块部分的时候,线程之间异步执行,然后轮流进入同步代码块内执行任务。换句话说使用同步代码块比使用同步方法缩小了同步范围,因此提高了效率。

    值得注意的是,synchronized(this)获得的是当前对象的对象锁,对象监视器为此object。当其他线程访问这个对象的其他的同步代码块或同步方法时都将被阻塞,因为他们使用的对象监视器是同一个。也就是说,synchronized(this)和同步方法一样,都是锁定当前对象的。

    除了synchronized(this)之外,我们还可以将任意对象作为对象监视器,也就是synchronized(object)的形式,此时当多个线程持有同一个对象锁的时候,同一时间内只有一个线程可以访问同步代码块内的代码。使用这种方法的好处是,当程序执行synchronized(object)内的代码块时,因为获得的并不是当前对象的对象锁,所以程序可以和当前对象的同步方法之间异步执行,不需要等待当前对象的锁,从而效率大大提高。

    我们可以得出以下几个结论:

    1)当多个线程执行synchronized(object){} 中的代码的时候,同步执行;

    2)当其他线程执行object中的synchronized方法时呈同步效果;

    3)当其他线程执行object中的synchronized(this)代码块时呈同步效果(非同步方法中的代码块);

    PS:如果其他线程调用不加synchronized的方法时依旧是异步调用,要记得调用非同步方法是不用获得对象锁的,而调用同步方法以及同步代码块中的代码的时候一定要获得对象锁。

六、class类锁

    除了修饰普通方法,synchronized关键字还可以修饰静态方法也就是类方法,而同步静态方法在执行的时候,线程获得的是class类的锁而不是对象锁,于是当一个线程A调用一个实例对象object的类方法的时候,另一个线程B可以异步调用这个object类的非静态同步方法,因为线程A获得的是class类的锁而不是object的对象锁,所以是不影响线程B去获得object的锁的。

    而类锁也有一个需要注意的地方,那就是如果实例化一个类的两个不同对象的时候,调用这两个对象中的静态方法也应该是同步执行的,因为调用时要获得的是同一个类锁。

class Apple{

public synchronized static void eat(){...};

}

Apple a1 = new Apple();

Apple a2 = new Apple();

当两个线程调用a1、a2中的eat方法时,依次获得Apple的class锁,同步执行。

    还有一种获得class锁的方法是synchronized(this.class)或者synchronized(xxx.class),可以达到同样的效果,同步的规则与对象锁相同。


猜你喜欢

转载自blog.csdn.net/u012198209/article/details/80258224
今日推荐