在Java中,线程同步使用最多的方法是使用synchronized关键字。每个Java对象都隐含有一把锁,这里称为Java内置锁(或者对象锁、隐式锁)。使用synchronized(syncObject)调用相当于获取 syncObject 的内置锁,所以可以使用内置锁对临界区代码段进行排他性保护。
synchronized 同步方法
synchronized 关键字是Java的保留字,当使用synchronized关键字修饰一个方法的时候,该方法被声明为同步方法,具体的例子如下:
//同步方法
public synchronized void selfplus(){
amount++;
}
关键字synchronized的位置处于同步方法的返回类型之前。回到前面的线程安全小实验,现在使用synchronized关键字对临界区代码段进行保护,代码如下:
package com.crazymakercircle.plus;// 省略import
public class SafePlus{
private Integer amount -0:
//临界区代码段使用synchronized进行保护
public synchronized void selfplus () {
amount++;
}
}
再次运行测试用例程序,累加10000次之后,最终的结果不再有偏差,与预期的结果(10000)是相同的。
在方法声明中设置synchronized同步关键字,保证其方法的代码执行流程是排他性的。任何时间只允许一个线程进入同步方法(临界区代码段),如果其他线程需要执行同一个方法,那么只能等待和排队。
synchronized 同步块
对于小的临界区,我们直接在方法声明中设置synchronized同步关键字,可以避免竞态条件的问题。但是对于较大的临界区代码段,为了执行效率,最好将同步方法分为小的临界区代码段。通过下面这个例子来具体讲述:
public class TwoPlus {
private int sum1 = 0;
private int sum2 =0;
//同步方法
public synchronized void plus(int vall,int val2){
//临界区代码段
this.sum1 += vall;
this.sum2 += va12;
}
}
在以上代码中,临界区代码段包含对两个临界区资源的操作,这两个临界区资源分别为sum1和 sum2。使用synchronized对plus(int vall, int val2)进行同步保护之后,进入临界区代码段的线程拥有sum1和sum2的操作权,并且是全部占用。一旦线程进入,当线程在操作sum1而没有操作sum2时,也将sum2的操作权白白占用,其他的线程由于没有进入临界区,只能看着sum2被闲置而不能去执行操作。
所以,将synchronized加在方法上,如果其保护的临界区代码段包含的临界区资源(要求是相互独立的)多于一个,就会造成临界区资源的闲置等待,进而会影响临界区代码段的吞吐量、为了提升吞吐量,可以将synchronized关键字放在函数体内,同步一个代码块。synchronized 同步块的写法是:
synchronized (syncObject) //同步块而不是方法
{
//临界区代码段的代码块
}
在synchronized同步块后边的括号中是一个syncObject对象,代表着进入临界区代码段需要获取 syncObject对象的监视锁,或者说将syncObject对象监视锁作为临界区代码段的同步锁。由于每一个Java对象都有一把监视锁,因此任何Java对象都能作为synchronized的同步锁。
单个线程在synchronized同步块后面的同步锁后,才能进入临界区代码段;反过来说,当一个线程获得syncObject对象的监视锁后,其他线程就只能等待。
使用synchronized同步块对上面的TwoPlus类进行吞吐量的提升改造,具体的代码如下:
public class TwoPlus {
private int suml0;
private int sum2 =0;
private Integer sumllock - new Integer(1); //同步锁一
private Integer sum2Lock = new Integer(2); //同步锁二
public void plus(int val1, int val2){
//同步块1
synchronized (this.sumlLock){
this.sum1 += vall;
}
//同步块2
synchronized(this.sum2Lock){
this.sum2 += val2;
}
}
}
改造之后,对两个独立的临界区资源sum1 和sum2的加法操作可以并发执行了,在某一个时刻,不同的线程可以对sum1和sum2同时进行加法操作,提升了plus(方法的吞吐量。
在TwoPlus代码中,由于同步块1和同步块2保护着两个独立的临界区代码段,需要两把不同的 syncObject对象锁,因此TwoPlus代码新加了sum1Lock和sum2Lock两个新的成员属性。这两个属性没有参与业务处理,TwoPlus仅仅利用了sum1Lock 和 sum2Lock的内置锁功能。
synchronized 方法和synchronized同步块有什么区别呢?总体来说,synchronized方法是一种粗粒度的并发控制,某一时刻只能有一个线程执行该synchronized方法;而synchronized代码块是一种细粒度的并发控制,处于synchronized块之外的其他代码是可以被多个线程并发访问的。在一个方法中,并不一定所有代码都是临界区代码段,可能只有几行代码会涉及线程同步问题。所以synchronized 代码块比synchronized方法更加细粒度地控制了多个线程的同步访问。 synchronized 方法和synchronized代码块有什么联系呢?在Java的内部实现上,synchronized方法实际上等同于用一个synchronized代码块,这个代码块包含同步方法中的所有语句,然后在 synchronized 代码块的括号中传入this关键字,使用this对象锁作为进入临界区的同步锁。
例如,下面两种实现多线程同步的plus方法版本编译成JVM内部字码后结果是一样的。版本一,使用synchronized代码块对方法内部全部代码进行保护,具体代码如下:
public void plus () {
synchronized(this) { //对方法内部全部代码进行保护
amount++;
}
}
版本二,使用synchronized方法对方法内部全部代码进行保护,具体代码如下:
public synchronized void plus() {
amount++;
}
综上所述,synchronized方法的同步锁实质上使用了this对象锁,这样就免去了手工设置同步锁的工作。而使用synchronized代码块需要手工设置同步锁。
静态的同步方法
在Java世界里一切皆对象。Java有两种对象:Object实例对象和Class对象。每个类运行时的类型信息用Class对象表示,它包含与类名称、继承关系、字段、方法有关的信息。JVM将一个类加载入自己的方法区内存时,会为其创建一个Class对象,对于一个类来说其Class对象是唯一的。 Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。
所有的类都是在第一次使用时被动态加载到JVM中的(懒加载),其各个类都是在必需时才加载的。这一点与许多传统语言(如C++)都不同,JVM为动态加载机制配套了一个判定一个类是否已经被加载的检查动作,使得类加载器首先检查这个类的Class对象是否已加载。如果尚未加载,类加载器就会根据类的全限定名查找.class文件,验证后加载到JVM的方法区内存并构造其对应的Class对象。
普通的synchronized实例方法,其同步锁是当前对象this的监视锁。如果某个synchronized方法是static(静态)方法,而不是普通的对象实例方法,其同步锁又是什么呢?
下面展示一个使用synchronized关键字修饰static方法的例子,具体如下:
package com.crazymakercircle.plusi
//省略import
public class SafeStaticMethodPlus{
//静态的临界区资源
private static Integer amount = 0;
//使用synchronized关键字修饰 static方法
public static synchronized void selfPlus (){
amount++;
}
}
大家都知道,静态方法属于Class实例而不是单个Object实例,在静态方法内部是不可以访
问Object实例的this引用(也叫指针、句柄)的。所以,修饰static方法的synchronized关键字就没有办法获得Object 实例的this对象的监视锁。
实际上,使用synchronized关键字修饰static方法时,synchronized的同步锁并不是普通Object 对象的监视锁,而是类所对应的 Class 对象的监视锁。
为了以示区分,这里将Object对象的监视锁叫作对象锁,将Class对象的监视锁叫作类锁。当synchronized 关键字修饰static方法时,同步锁为类锁:当synchronized关键字修饰普通的成员方法(非静态方法)时,同步锁为类锁。由于类的对象实例可以有很多,但是每个类只有一个Class实例,因此使用类锁作为synchronized的同步锁时会造成同一个JVM内的所有线程只能互斥地进入临界区段。
//对JVM内的所有线程同步
public static synchronized void selfplus (){
//临界区代码
}
所以,使用synchronized关键字修饰static方法是非常粗粒度的同步机制。
通过synchronized关键字所抢占的同步锁什么时候释放呢?一种场景是synchronized块(代码块或者方法)正确执行完毕,监视锁自动释放;另一种场景是程序出现异常,非正常退出 synchronized块,监视锁也会自动释放。所以,使用synchronized块时不必担心监视锁的释放问题。