Java多线程-2-可重入锁以及Synchronized的其他基本特性

Synchronized锁重入

关键字Synchronized拥有锁重入的功能,也就是在使用Synchronized的时候,当一个线程得到一个对象的锁后,在该锁里执行代码的时候可以再次请求该对象的锁时可以再次得到该对象的锁。

也就是说,当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。

一个简单的例子就是:在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的,示例代码A如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SyncDubbo {

public synchronized void method1() {
System.out.println("method1-----");
method2();
}

public synchronized void method2() {
System.out.println("method2-----");
method3();
}

public synchronized void method3() {
System.out.println("method3-----");
}

public static void main(String[] args) {
final SyncDubbo syncDubbo = new SyncDubbo();
new Thread(new Runnable() {
@Override
public void run() {
syncDubbo.method1();
}
}).start();
}
}

执行结果为:

1
2
3
method1-----
method2-----
method3-----

示例代码A向我们演示了,如何在一个已经被synchronized关键字修饰过的方法再去调用对象中其他被synchronized修饰的方法。

  • 为什么要引入可重入锁这种机制?

    上一篇文章中介绍了“一个对象一把锁,多个对象多把锁”,可重入锁的概念就是:自己可以获取自己的内部锁

    假如有1个线程T获得了对象A的锁,那么该线程T如果在未释放前再次请求该对象的锁时,如果没有可重入锁的机制,是不会获取到锁的,这样的话就会出现死锁的情况。

    就如代码A体现的那样,线程T在执行到method1()内部的时候,由于该线程已经获取了该对象syncDubbo 的对象锁,当执行到调用method2() 的时候,会再次请求该对象的对象锁,如果没有可重入锁机制的话,由于该线程T还未释放在刚进入method1() 时获取的对象锁,当执行到调用method2() 的时候,就会出现死锁。

  • 可重入锁到底有什么用?

    正如代码A和上面问题中所解释的那样,最大的作用是避免死锁。假如有一个场景:用户名和密码保存在本地txt文件中,则登录验证方法和更新密码方法都应该被加synchronized,那么当更新密码的时候需要验证密码的合法性,所以需要调用验证方法,此时是可以调用的。

  • 可重入锁的其他特性:父子可继承性

    可重入锁支持在父子类继承的环境中,示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class SyncDubbo {

    static class Main {
    public int i = 5;
    public synchronized void operationSup() {
    i--;
    System.out.println("Main print i =" + i);
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

    static class Sub extends Main {
    public synchronized void operationSub() {
    while (i > 0) {
    i--;
    System.out.println("Sub print i = " + i);
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }

    public static void main(String[] args) {
    new Thread(new Runnable() {
    public void run() {
    Sub sub = new Sub();
    sub.operationSub();
    }
    }).start();
    }
    }

Synchronized的其他特性

出现异常时锁自动释放

就是说,当一个线程执行的代码出现异常的时候,其所持有的锁会自动释放,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SyncException {

private int i = 0;

public synchronized void operation() {
while (true) {
i++;
System.out.println(Thread.currentThread().getName() + " , i= " + i);
if (i == 10) {
Integer.parseInt("a");
}
}
}

public static void main(String[] args) {
final SyncException se = new SyncException();
new Thread(new Runnable() {
public void run() {
se.operation();
}
}, "t1").start();
}
}

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
t1 , i= 2
t1 , i= 3
t1 , i= 4
t1 , i= 5
t1 , i= 6
t1 , i= 7
t1 , i= 8
t1 , i= 9
t1 , i= 10
java.lang.NumberFormatException: For input string: "a"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
//其他输出信息

可以看出,当执行代码报错的时候,程序不会再执行,即释放了锁。

将任意对象作为监听器

Java还支持对“任意对象”作为“对象监视器”来实现同步的功能。这个“任意对象”大多数是实例变量及方法的参数,使用synchronized(非this对象)
synchronized(非this对象)格式的作用只有1中:synchronized(非this对象x)同步代码块

  1. 在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码;
  2. 当持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class StringLock {

private String lock = "lock";

public void method() {
synchronized (lock) {
try {
System.out.println("当前线程: " + Thread.currentThread().getName() + "开始");
Thread.sleep(1000);
System.out.println("当前线程: " + Thread.currentThread().getName() + "结束");
} catch (InterruptedException e) {

}
}
}

public static void main(String[] args) {
final StringLock stringLock = new StringLock();
new Thread(new Runnable() {
public void run() {
stringLock.method();
}
}, "t1").start();

new Thread(new Runnable() {
public void run() {
stringLock.method();
}
}, "t2").start();
}
}

执行结果:

1
2
3
4
当前线程: t1开始
当前线程: t1结束
当前线程: t2开始
当前线程: t2结束

单例模式-双重校验锁

普通的加锁的单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {

private static Singleton instance = null; //懒汉模式
//private static Singleton instance = new Singleton(); //饿汉模式

private Singleton() {

}

public static synchronized Singleton newInstance() {
if (null == instance) { //判断实例是否已经被其他线程创建了
instance = new Singleton();
}
return instance;
}
}

使用上述的方式可以实现多线程的情况下获取到正确的实例对象,但是每次访问newInstance()方法都会进行加锁和解锁操作,也就是说该锁可能会成为系统的瓶颈,为了解决这个问题,有人提出了“双重校验锁”的方式,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DubbleSingleton {

private static DubbleSingleton instance;

public static DubbleSingleton getInstance(){
if(instance == null){ //判断实例是否已经被其他线程创建了,如果没有则创建
try {
//模拟初始化对象的准备时间...
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//类上加锁,表示当前对象不可以在其他线程的时候创建
synchronized (DubbleSingleton.class) {
//如果不加这一层判断的话,这样的话每一个线程会得到一个实例
//而不是所有的线程的到的是一个实例
if(instance == null){ //从第一次判断是否为null到加锁之间的时间内判断实例是否已经被创建
instance = new DubbleSingleton();
}
}
}
return instance;
}
}

那么问题来了,为什么volatile关键字可以实现禁止指令的重排序优化 以及什么是指令重排序优化哪?

在Java内存模型中我们都是围绕着原子性、有序性和可见性进行讨论的。为了确保线程间的原子性、有序性和可见性,Java中使用了一些特殊的关键字申明或者是特殊的操作来告诉虚拟机,在这个地方,要注意一下,不能随意变动优化目标指令。关键字volatile就是其中之一。

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度(比如:将多条指定并行执行或者是调整指令的执行顺序)。编译器、处理器也遵循这样一个目标。注意是单线程。可显而知,多线程的情况下指令重排序就会给程序员带来问题。最重要的一个问题就是程序执行的顺序可能会被调整,另一个问题是对修改的属性无法及时的通知其他线程,已达到所有线程操作该属性的可见性。

根据编译器的优化规则,如果不使用volatile关键字对变量进行修饰的,那么这个变量被修改后,其他线程可能并不会被通知到,甚至在别的想爱你城中,看到变量修改顺序都会是反的。一旦使用volatile关键字进行修饰的话,虚拟机就会特别小心的处理这种情况。

参考文献

  1. 徐刘根:Java多线程编程-(2)-可重入锁以及Synchronized的其他基本特性
  2. 霓裳梦竹:synchronized将任意对象作为对象监视器

土豪将鼓励我继续创作和搬运!