2.2 Java中的synchronized同步锁

导致线程安全问题的根本原因在于,存在多个线程同时操作一个共享资源,要想解决这个问题,就需要保证对共享资源访问的独占性,因此人们在Java中提供了synchronized关键字,我们称之为同步锁,它可以保证在同一时刻,只允许一个线程执行某个方法或代码块。

synchronized同步锁具有互斥性,这相当于线程由并行执行变成串行执行,保证了线程的安全性,但是损失了性能。下面我们先来看一下synchronized的使用方法。

2.2.1 synchronized的使用方法

synchronized的使用方法比较简单,修饰方式有如下两种。

• 作用在方法级别,表示针对m1()方法加锁,当多个线程同时访问m1()方法时,同一时刻只有一个线程能执行。

• 作用在代码块级别,表示针对某一段线程不安全的代码加锁,只有访问到synchronized(this)这行代码时,才会去竞争锁资源。

了解了synchronized的基本使用语法之后,我们来看如图2-4所示的流程,它针对2.1节的案例增加了synchronized同步锁之后的执行流程。简单地说,当多个线程同时访问加synchronized关键字修饰的方法时,需要先抢占一个锁标记,只有抢到锁标记的线程才有资格调用incr()方法。这就使得在同一时刻只有一个线程执行i++操作,从而解决了原子性问题。

图2-4 增加了synchronized同步锁之后的执行流程

2.2.2 了解synchronized同步锁的作用范围

我们对一个方法增加synchronized关键字后,当多个线程访问该方法时,整个执行过程会变成串行执行,这种执行方式很明显会影响程序的性能,那么如何做好安全性及性能的平衡呢?

实际上,synchronized关键字只需要保护可能存在线程安全问题的代码,因此,我们可以通过控制同步锁的作用范围来实现这个平衡机制。在synchronized中,提供了两种锁,一是类锁,二是对象锁。

类锁

类锁是全局锁,当多个线程调用不同对象实例的同步方法时会产生互斥,具体实现方式如下。

• 修饰静态方法:

• 修饰代码块,synchronized中的锁对象是类,也就是Lock.class。

下面这段程序使用类锁来实现跨对象实例,从而实现互斥的功能。

• 该程序中定义了一个m1()方法,该方法中实现了一个循环打印当前线程名称的逻辑,并且这段逻辑是用类锁来保护的。

• 在main()方法中定义了两个SynchronizedExample对象实例se1和se2,又分别定义了两个线程来调用这两个实例的m1()方法。

根据类锁的作用范围可以知道,即便是多个对象实例,也能够达到互斥的目的,因此最终输出的结果是:哪个线程抢到了锁,哪个线程就持续打印自己的线程名称。

对象锁

对象锁是实例锁,当多个线程调用同一个对象实例的同步方法时会产生互斥,具体实现方式如下。

• 修饰普通方法:

• 修饰代码块,synchronized中的锁对象是普通对象实例。

下面这段程序演示了对象锁的使用方法,代码如下。

我们先来看一下打印结果。

从以上结果中我们发现,对于几乎相同的代码,在使用对象锁的情况下,当两个线程分别访问两个不同对象实例的m1()方法时,并没有达到两者互斥的目的,看起来似乎锁没有生效,实际上并不是锁没有生效,问题的根源在于synchronized(lock)中锁对象lock的作用范围过小。

Class是在JVM启动过程中加载的,每个.class文件被装载后会产生一个Class对象,Class对象在JVM进程中是全局唯一的。通过static修饰的成员对象及方法的生命周期都属于类级别,它们会随着类的定义被分配和装载到内存,随着类被卸载而回收。

实例对象的生命周期伴随着实例对象的创建而开始,同时伴随着实例对象的回收而结束。

因此,类锁和对象锁最大的区别是锁对象lock的生命周期不同,如果要达到多个线程互斥,那么多个线程必须要竞争同一个对象锁。

在上述代码中,通过Object lock=new Object();构建的锁对象的生命周期是由Synchronized-ForObjectExample对象的实例来决定的,不同的SynchronizedForObjectExample实例会有不同的lock锁对象,由于没有形成竞争,所以不会实现互斥的效果。如果想要让上述程序达到同步的目的,那么我们可以对lock锁对象增加static关键字。