互斥锁的特性
互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块进行访问。互斥性也称为操作的原子性。
可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
对于 Java 来讲,关键字 synchronized 满足了上面两种特性,在线程同步中扮演了非常重要的作用,它可以保证在同一时刻只有一个线程可以执行某个方法或某个代码块(主要是对共享数据的操作),同时 synchronized 也可以保证一个线程的变化(主要是共享数据的变化)被其他线程所看到,也就是说保证共享数据的可见性。
需要明确的是:synchronized 锁的不是代码,锁的都是对象。
Synchronized 基本使用
synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。
Synchronized 作用范围
-
作用于方法时,锁住的是对象的实例(this);
-
当作用于静态方法时,锁住的是 Class 实例,又因为 Class 的相关数据存储在永久代 PermGen(jdk1.8 则是 metaspace),永久代是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
-
synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
synchronized 关键字测试:
package Thread;
public class SynDemo implements Runnable {
@Override
public void run() {
String name = Thread.currentThread().getName();
try {
switch (name.charAt(0)) {
case 'A':
synMethod();
break;
case 'B':
synBlock();
break;
case 'C':
synClassMethod();
break;
case 'D':
synClassBlock();
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void synBlock() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " --> 进入 synBlock");
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " --> synBlock_start");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " --> synBlock_end");
}
}
private void synClassBlock() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " --> 进入 synClassBlock");
synchronized (SynDemo.class) {
System.out.println(Thread.currentThread().getName() + " --> synClassBlock_start");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " --> synClassBlock_end");
}
}
private synchronized void synMethod() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " --> synMethod_start");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " --> synMethod_end");
}
private synchronized static void synClassMethod() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " --> synClassMethod_start");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " --> synClassMethod_end");
}
public static void main(String[] args) {
SynDemo synDemo = new SynDemo();
new Thread(synDemo, "A method 1").start();
new Thread(synDemo, "A method 2").start();
new Thread(synDemo, "B block 1").start();
new Thread(synDemo, "B block 2").start();
new Thread(synDemo, "C class method 1").start();
new Thread(synDemo, "C class method 2").start();
new Thread(synDemo, "D class block 1").start();
new Thread(synDemo, "D class block 2").start();
}
}
对象锁和类锁的总结
-
有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块
-
若锁住的是同一个对象,一个线程在访问对象的同步代码块或同步方法时,另一个访问对象的同步代码块或同步方法的线程会被阻塞
-
同一个类的不同对象的对象锁互不干扰
-
类锁由于也是一种特殊的对象锁,因此表现和上述 1、2 一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的
-
类锁和对象锁互不干扰
Synchronized 底层原理
1、synchronized 同步语句块的情况
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
反编译结果:
monitorenter :
每个对象有一个监视器锁(monitor)。当 monitor 被占用时就会处于锁定状态,线程执行 monitorenter 指令时尝试获取 monitor 的所有权,过程如下:
1、如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为 1,该线程即为 monitor 的所有者。
2、如果线程已经占有该 monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权。
monitorexit:
执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。
指令执行时,monitor 的进入数减 1,如果减 1 后进入数为 0,那线程退出 monitor,不再是这个monitor 的所有者。其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出 Synchronized 的实现原理,Synchronized 的语义底层是通过一个 monitor 的对象来完成,其实 wait/notify 等方法也依赖于monitor 对象,这就是为什么只有在同步块或者方法中才能调用 wait/notify 等方法,否则会抛出 java.lang.IllegalMonitorStateException 的异常的原因。
2、synchronized 修饰方法的情况
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
反编译结果:
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取而代之的却是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
JDK1.6之后 synchronized 关键字底层优化
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。