Java并发编程(二)synchronized关键字

注意

以下内容源自互联网相关资料及本人学习与工作经验,仅为学习及技术分享所用,切勿用于商业用途,转载请注明出处。

1. 线程同步

多个线程之间可能会竞争相同的资源,比如两个读取数据库操作的线程就会竞争数据库连接这个资源,这个被竞争的资源叫作共享资源或者临界资源。如何协调各个线程对共享资源的使用,称为线程同步。线程同步需要考虑以下几点:

  • 存在竞争就需要同步,同步的概念指的不是同时执行,而是协同步调,按一定的规矩将线程“排队”,确定好“谁先消费共享资源”,而这个规矩必须满足于业务的要求。
  • 共享资源才需同步,只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。
  • 只对“变量”进行同步,只有“变量”(这里的变量指的是资源存在可变性)才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。
  • 共享资源并非同份代码,多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。

2. synchronized关键字

Java提供了原生的synchronized关键字来修饰临界资源,对共享资源加锁,使同一时间只能有一个线程得到共享资源,其它线程只能阻塞等待该线程释放共享资源后,才可以重新竞争获得共享资源的机会。
有一个线典的例子:多个人上公厕,假设只有一个坑位,这里的坑位就是共享资源,多个人就是多个线程,同时只能有一个人竞争得到上坑的机会,上坑后,这个人会把厕所门锁上,其他人只能在外等这个人上完厕所后,打开厕所后,才能“蜂拥而上”竞争下一次上坑的机会。在这个例子里,厕所门锁就相当于synchronized关键字,它将厕所这个共享资源加上了锁,同时能允许一个线程进入。

2.1 用法1 synchronized修饰实例方法

synchronized修饰实例方法,锁是加在这个方法的实例对象上的,当多个线程访问同一个实例对象的这个方法时,只能有一个线程进入该方法。注意:这种场景下,一个实例对象对应一把锁,多个线程只有调用同一个实例对象时才有意义。
下面的代码是synchroinzed修饰实例方法的例子:

public synchronized void addCount(){
        for(int i = 0;i<50;i++){
            count +=1;
        }
    }

2.2 用法2 synchronized修饰静态方法

synchronized修饰静态方法与修饰实例方法在语法上是相同的,参见下例:

public synchronized static void addCount(){
        for(int i = 0;i<50;i++){
            count +=1; //count必须为静态变量
        
        }
    }

我们知道,在JVM中每一个类只存在一个类对象(class object),在这种场景下,synchronized所加的锁就在修饰的静态方法所属的类对象上,所有线程调用此静态方法时,同一时间只有一个线程能获得这把锁,进行这个静态方法。

2.3 用法3 synchronized修饰实例方法内代码块

上面示例代码中,共享资源其实只是count这个变量,我们没有必要对整个方法加锁,这样反而会影响整个程序的执行效率,这时候,我们可以将synchronized关键字加在操作count变量的代码块上,实现更细粒度的控制。如下:

public void addCount(){
        for(int i = 0;i<50;i++){
            synchronized(this){
                          count +=1;
                        }
        }
    }

synchronized修饰代码块时,需要指定一个“监视对象”(monitor object),也就是要对哪个对象加锁。上面示例代码中使用this指向本实例对象,表示对实例对象加锁,多个线程访问此实例对象时,只能有一个线程进行synchronized修饰的代码块。
监视对象不一定要是this,也可以是任何对象,如下例所示:

public final Object monitor = new Object();//final修饰,表示监视对象不可变
public void addCount(){
        for(int i = 0;i<50;i++){
            synchronized(monitor){
                          count +=1;
                        }
            
        }
    }

上例中,我们用final修饰了监视对象,为什么呢?因为monitor这个变量实际是只是指向监视对象所在内存中的地址,如果不修饰成final的话,当我们修改这个monitor变量,让它指向其他对象,比如 在synchronized代码块中,我们让monitor指向其他对象(monitor = new OtherObject();) 那么monitor就不是原本我们指定的监视对象了,锁也就失去了意义。
如果监视对象不是同一个实例对象的话,则表示有多个不同的锁,不同的线程可以进入不同的锁限制的代码块。

2.4 用法4 synchronized修饰静态方法内代码块

同样也可以使用synchronized来修饰静态方法内的代码块,此时监视对象必须是类对象(class object)。
注意,如果监视的类对象不同,则表示有多个不同的锁,不同的线程可以进入不同的锁限制的代码块。

public synchronized static void addCount(){
        for(int i = 0;i<50;i++){
                synchronized(SynchronizedUsage.class){//SynchronizedUsage.class是类对象
            count +=1; //count必须为静态变量
                 }
        }
    }

2.5 释放锁的两种情况

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  • 获取锁的线程执行完了该代码块,然后线程释放对锁的占有。
  • 线程执行发生异常,此时JVM会让线程自动释放锁。

你可能感兴趣的:(Java并发编程(二)synchronized关键字)