在这篇文章中,我会为大家介绍一下synchronized关键字的用法。同样,我会尝试用比较容易理解的方式去介绍,以求让大家都能够看明白,想清楚。
synchronized是什么
synchronized是一种锁机制,那么肯定有童鞋会问:什么是锁?为什么要用锁?怎么使用锁? 接下来我就分别针对这个经典三连问来详细解释一下。
1.什么是锁?
计算机的世界中并没有锁这个实体概念,而所谓锁就是一组特殊的指令(根据不同锁的类型对应的是不同的指令,比如synchronized其实对应的就是两条CPU指令 #Monitor enter / #Monitor exit)
2.为什么要使用锁?
大家在生活中肯定看到过许多的锁,比如自行车锁、门锁、箱子锁等等,那么我们为什么要用锁?很简单,我们是要防止自己的东西被别人偷走,而我们自己是有钥匙可以打开锁的,当我们想把锁上的东西送给别人时,只需要直接把钥匙给那个人就可以,即强调了被锁住的物品在同一时间只能被持有锁钥匙的那一个人所拥有的概念。
OK,类比到程序世界中的锁,它锁住的其实就是一段代码,那么为什么要用锁去锁住代码?同样很简单,防止某段代码被一个线程执行时,又被其他的线程去执行。当一个线程想要执行被锁住的代码时,必须先获取“开锁权”,只有取得开锁权的线程才能打开锁,然后去执行被锁住的代码。执行完毕后,当前线程必须重新把锁给锁起来,并让出“开锁权”,好让其他需要执行这段代码的线程去争抢“开锁权”。
注意,上面所说的防止某段代码被一个线程执行时又被其他线程执行,只是针对于多个线程会对同一个对象的同一个共享变量,至少有一个线程会执行写操作的情况(线程不安全的万恶之源),才需要使用锁。而如下的情况即时代码同一时刻会被多个线程执行,也是不需要使用锁的:
(1)代码中所有的变量都不是共享变量。如果某个对象所有的变量都是局部变量,那么每个线程执行时在自己的内存区域都会有这些局部变量的副本,而不论当前线程怎么去更改这种变量,都只不过是在线程自己的内存区域中去玩,当方法或线程结束后,这些变量就会被销毁,所以这种情况下多个线程其实是“自己玩自己的”,谁都不会影响谁,当然也就不需要使用锁了。另外,如果某个对象全是由这种变量组成的,那么这个对象就被称为“无状态对象“,即无状态对象一定是线程安全的。
(2)变量本身是不可变的。比如 private final static String str = "HELLO_WORLD",这里的str变量因为有final修饰就变为了不可变的常量,常量无法被进行修改,所以也就不满足“至少有一个线程执行写操作”这个条件,即常量str可以在多线程之间安全的进行共享。
(3)如果某段代码全部都是对一些变量的读操作,同样不满足 “至少有一个线程执行写操作”这个条件,那么此时这段代码也是无需使用锁的。
3.怎么使用锁?
到这里我们就对锁有了一些基本概念了,其实JDK中锁的实现有很多种,而synchronized只是其中的一种实现方式。为了顺应文章主题,这里就先讲一下synchronized在日常开发中是怎么使用的吧 : )
synchronized的使用方法
方法一:使用synchronized修饰实例方法。这种使用方法会以当前对象作为锁,而被synchronized修饰的方法在同一时刻只能被一个线程访问。示例代码如下:
1 public class SynchronizedMethod { 2 3 private int a = 0; 4 5 private synchronized void synchronizedMethodAdd(){ 6 a++; 7 } 8 9 }
方法二:使用synchronized修饰代码块。这种使用方法会以synchronized后面括号中的对象作为锁,从而保证被synchronized修饰的代码块在同一时刻只能被一个线程访问。示例代码如下:
1 public class SynchronizedMethod { 2 3 private int a = 0; 4 5 private void synchronizedBlockAdd(){ 6 synchronized (this){ 7 a++; 8 } 9 } 10 11 }
可以看到synchronized后面括号中的this,其实就代表的是以当前对象作为锁(和方法一使用的是同一个锁)。
方法二的另一种情况是不使用当前对象作为锁,而是使用自定义的对象作为锁,示例代码如下:
1 public class SynchronizedMethod { 2 3 private int a = 0; 4 5 private MyLock lock; 6 7 private void synchronizedBlockAdd(){ 8 synchronized (lock){ 9 a++; 10 } 11 } 12 13 }
可以看到这里synchronized使用的是自定义的lock对象作为锁,而不再是当前对象(SynchronizedMethod )作为锁。
注意:如果两个线程持有的锁不同,就无法保证线程的安全性,执行结果就会变得不可预测!!!如下代码示例:
1 import java.util.concurrent.locks.Lock; 2 import java.util.concurrent.locks.ReentrantLock; 3 4 public class SynchronizedMethod { 5 6 private int a = 0; 7 8 private Lock lock; 9 10 public void synchronizedBlockAdd(boolean param){ 11 if(param){ 12 lock = new ReentrantLock(); 13 }else{ 14 lock = new ReentrantLock(); 15 } 16 17 synchronized (lock){ 18 a++; 19 System.out.println("a = " + a); 20 } 21 } 22 23 public static void main(String[] args) { 24 SynchronizedMethod synchronizedMethod = new SynchronizedMethod(); 25 NonSameLockObjectThread1 thread1 = new NonSameLockObjectThread1(synchronizedMethod); 26 NonSameLockObjectThread2 thread2 = new NonSameLockObjectThread2(synchronizedMethod); 27 thread1.start(); 28 thread2.start(); 29 } 30 } 31 32 33 class NonSameLockObjectThread1 extends Thread{ 34 35 private SynchronizedMethod synchronizedMethod; 36 37 public NonSameLockObjectThread1(SynchronizedMethod synchronizedMethod){ 38 this.synchronizedMethod = synchronizedMethod; 39 } 40 41 @Override 42 public void run() { 43 synchronizedMethod.synchronizedBlockAdd(true); 44 } 45 } 46 47 class NonSameLockObjectThread2 extends Thread{ 48 49 private SynchronizedMethod synchronizedMethod; 50 51 public NonSameLockObjectThread2(SynchronizedMethod synchronizedMethod){ 52 this.synchronizedMethod = synchronizedMethod; 53 } 54 55 @Override 56 public void run() { 57 synchronizedMethod.synchronizedBlockAdd(false); 58 } 59 }
执行结果:
可以看到,当两个线程使用不同的锁对象时,锁就失去了它应有的效力!
方法三:使用synchronized修饰静态方法。和方法一修饰实例方法用法一样,但是修饰静态方法synchronized使用的是当前类的class对象作为锁,而不是当前对象作为锁,这点同样需要注意!示例代码如下:
1 public class StaticSynchronized { 2 3 public static synchronized void sayHello(){ 4 System.out.println("hello!"); 5 } 6 7 }
synchronized的使用方法就上面三种,总之使用这个关键字时最重要的就是要区分清楚当前线程使用的是哪个对象作为锁!
synchronized的几个小Tips
1.当一个线程正在执行synchronized修饰的方法或代码块时,其他线程无法执行同一个对象中其他synchronized修饰且与当前正在执行方法拥有相同锁的方法或代码块,但可以执行没有synchronized修饰的普通方法。这样说有点抽象,以下面的代码为例:
1 import java.util.concurrent.locks.Lock; 2 import java.util.concurrent.locks.ReentrantLock; 3 4 public class StaticSynchronized { 5 6 private Lock lock = new ReentrantLock(); 7 8 public static synchronized void a(){ 9 System.out.println("hello!"); 10 } 11 12 public synchronized void b(){ 13 14 } 15 16 public void c(){ 17 synchronized (this){ 18 19 } 20 } 21 22 public void d(){ 23 synchronized (lock){ 24 25 } 26 } 27 28 public void e(){ 29 30 } 31 }
如果线程A在执行a()方法,那么线程B在同一时刻不能执行a()方法,因为a()方法是以 StaticSynchronized的class对象作为锁,同一时刻只能有一个线程执行被同一个锁锁住的代码。但是线程B可以在同一时刻执行b()或c()方法,因为这两个方法使用的是当前对象作为锁,而不是对象的class类作为锁。同样线程B也可以执行d()方法(d方法是以自定义的Lock对象作为锁),也可以执行e()方法(普通方法没有锁)。具体判断能不能执行,只需看是否用的是同一个锁即可。
2.synchronized修饰的方法或代码块在正常结束或发生异常时,都会自动释放锁。这点同样容易理解,如果不释放锁,那么其他的线程就永远无法获得执行权。
3.synchronized默认是可重入的。所谓可重入,就是当前线程获取锁然后执行代码,在执行过程中又需要获取相同的锁(比如while循环中需要获取锁),此时synchronized是默认支持同一个线程多次获取同一个锁的。JVM有一个计数器在计算当前线程获取这个锁的次数,只有当计数器变成0时,其他线程才有可能获取到锁。
synchronized的效率与锁升级机制
大家都知道synchronized这个关键字很早就已经存在于JDK中了,而在JDK1.6之前这个关键字的效率一直为人诟病,原因就在于JDK1.6之前synchronized是纯粹的重量级锁,也就是说哪怕在只有极少数线程竞争的情况下,所有的线程获取锁/释放锁时都要执行#MonitorEnter和#MonitorExit这对指令,而这个操作其实时非常消耗性能的,所以说synchronized在竞争较少的场景下性能并不佳。JDK开发者当然也知道这个情况,所以在JDK1.6之后就引进了一种叫“锁升级”的机制来提高synchronized关键字在低并发场景下的性能表现,而这个机制主要引入了两种锁:偏向锁以及轻量级锁。所以现在的锁按照竞争情况从低到高排序共有4种状态:无锁、偏向锁、轻量级锁、重量级锁。那在介绍这几种状态锁相关的特性之前,先贴两张对象头存储内容的图片,以便更好的理解:
32位虚拟机的对象头存储情况:
64位虚拟机对象头的存储情况:
大家可以参考下各种状态的锁在32位/64位的虚拟中在对象头中的存储情况。接下来就讲一下这几种状态的锁对应的特性:
1.无锁。这种是最简单的情况,即系统中不存在并发情况,全部都是单线程在执行。也正因为如此,没有任何获取/释放锁带来的消耗,所以这种场景下程序的性能其实是最高的。
2.偏向锁。这种锁主要提高的是无竞争情况下线程获取锁的性能。它的做法是在对象头中存储当前获取了偏向锁线程的ID(可参考上图偏向锁在32bit/64bit虚拟机中的存储情况)。这样做的好处是,如果下次获取该偏向锁的是同一个线程(这种情况在低竞争情况下很常见),那么这个线程就不再需要做获取锁的操作(偏向锁的对象头中已经存储了该线程ID),这大大减少了低竞争场景下线程执行同步代码的性能消耗。偏向锁因为其特性,所以比较适合用于大部分时间锁都只有一个线程会获取的场景。
3.轻量级锁。这种锁主要提高在很少的线程竞争场景下获取锁的效率。那么它是怎么做的呢?举个栗子:线程A获取了锁,然后对象头中记录的A的线程ID(此时为偏向锁),线程A在执行过程中,线程B也想要获取这个锁,但发现这个锁已经被线程A获取了,此时这个锁就会自动升级成轻量级锁,然后线程A会暂停并释放锁,同时将当前锁的对象头MarkWord置为空,然后同线程B一起使用CAS自旋不断尝试获取这个锁(其实就是替换锁对象头的MarkWord为当前线程的指针存储的对象头MarkWord),一旦某个线程替换对象头的MarkWord成功就表示当前线程获取了锁,则结束自旋并执行同步代码。所以轻量级锁主要是用来在极少数线程竞争场景下,可以让线程通过自旋的方式尝试获取锁以避免调用操作系统层面的获取锁的方式带来的性能消耗。正因为其自旋的特点,所以轻量级锁一般是用在同步代码执行速度非常快(以便更快的让自旋操作成功并获取锁),并且追求响应速度快的场景。轻量级锁也有缺点,因为自旋是通过CPU去操作的,如果线程长时间自旋但得不到锁的话,会非常消耗CPU的性能,所以轻量级锁不适用于同步代码执行时间较长的场景。
4.重量级锁。上面说了,如果某个线程不断自旋超过一定次数也无法获取锁时,此时这个锁就会膨胀为重量级锁。当一个锁变为重量级锁后,所有无法获取该锁的线程都不能通过CAS自旋获取,而只能进入阻塞状态,等待执行同步代码成功的线程释放锁并唤醒自己后,才能继续尝试获取锁。这种锁适用于同步代码执行时间较长的场景。
好了,关于synchronzied的知识点到这里就全部说完了。下篇文章会讲一下 ThreadLocal 这个工具类的使用方式。