(2)可重入锁以及Synchronized的其他基本特性

上一篇基本介绍了进程和线程的区别、实现多线程的两种方式、线程安全的概念以及如何使用Synchronized实现线程安全,下边介绍一下关于Synchronized的其他基本特性。

一、Synchronized锁重入

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

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

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

public class SyncDubbo {
   
   
<span class="hljs-keyword">public</span> <span class="hljs-keyword">synchronized</span> <span class="hljs-keyword">void</span> <span class="hljs-title">method1</span>() {
    System.out.println(<span class="hljs-string">"method1-----"</span>);
    method2();
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">synchronized</span> <span class="hljs-keyword">void</span> <span class="hljs-title">method2</span>() {
    System.out.println(<span class="hljs-string">"method2-----"</span>);
    method3();
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">synchronized</span> <span class="hljs-keyword">void</span> <span class="hljs-title">method3</span>() {
    System.out.println(<span class="hljs-string">"method3-----"</span>);
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span>(String[] args) {
    <span class="hljs-keyword">final</span> SyncDubbo syncDubbo = <span class="hljs-keyword">new</span> SyncDubbo();
    <span class="hljs-keyword">new</span> Thread(<span class="hljs-keyword">new</span> Runnable() {
        <span class="hljs-annotation">@Override</span>
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span>() {
            syncDubbo.method1();
        }
    }).start();
}

}

  • 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

执行结果:

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

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

(4)那么,为什么要引入可重入锁这种机制哪?

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

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

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

(5)那么可重入锁到底有什么用哪?

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

(6)关于可重入锁的实现原理,是一个大论题,在这里篇幅有限不再学习,有兴趣可以移步至:http://www.cnblogs.com/pureEve/p/6421273.html 进行学习。

(7)可重入锁的其他特性:父子可继承性

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

public class SyncDubbo {
   
   
<span class="hljs-keyword">static</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Main</span> {<!-- --></span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> i = <span class="hljs-number">5</span>;
    <span class="hljs-keyword">public</span> synchronized <span class="hljs-keyword">void</span> operationSup() {
        i--;
        System.out.println(<span class="hljs-string">"Main print i ="</span> + i);
        <span class="hljs-keyword">try</span> {
            Thread.sleep(<span class="hljs-number">100</span>);
        } <span class="hljs-keyword">catch</span> (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

<span class="hljs-keyword">static</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Sub</span> <span class="hljs-inheritance"><span class="hljs-keyword">extends</span></span> <span class="hljs-title">Main</span> {<!-- --></span>
    <span class="hljs-keyword">public</span> synchronized <span class="hljs-keyword">void</span> operationSub() {
        <span class="hljs-keyword">while</span> (i &gt; <span class="hljs-number">0</span>) {
            i--;
            System.out.println(<span class="hljs-string">"Sub print i = "</span> + i);
            <span class="hljs-keyword">try</span> {
                Thread.sleep(<span class="hljs-number">100</span>);
            } <span class="hljs-keyword">catch</span> (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> main(String[] args) {
    <span class="hljs-keyword">new</span> Thread(<span class="hljs-keyword">new</span> Runnable() {
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> run() {
            Sub sub = <span class="hljs-keyword">new</span> Sub();
            sub.operationSub();
        }
    }).start();
}

}

  • 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

一、Synchronized的其他特性

(1)出现异常时,锁自动释放

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

public class SyncException {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>;

<span class="hljs-keyword">public</span> synchronized <span class="hljs-keyword">void</span> <span class="hljs-title">operation</span>() {
    <span class="hljs-keyword">while</span> (<span class="hljs-keyword">true</span>) {
        i++;
        System.<span class="hljs-keyword">out</span>.println(Thread.currentThread().getName() + <span class="hljs-string">" , i= "</span> + i);
        <span class="hljs-keyword">if</span> (i == <span class="hljs-number">10</span>) {
            Integer.parseInt(<span class="hljs-string">"a"</span>);
        }
    }
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span>(String[] args) {
    final SyncException se = <span class="hljs-keyword">new</span> SyncException();
    <span class="hljs-keyword">new</span> Thread(<span class="hljs-keyword">new</span> Runnable() {
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span>() {
            se.operation();
        }
    }, <span class="hljs-string">"t1"</span>).start();
}

}

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

执行结果如下:

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)
    //其他输出信息
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

(2)将任意对象作为监视器

public class StringLock {
<span class="hljs-keyword">private</span> String <span class="hljs-keyword">lock</span> = <span class="hljs-string">"lock"</span>;

<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">method</span>() {
    synchronized (<span class="hljs-keyword">lock</span>) {
        <span class="hljs-keyword">try</span> {
            System.<span class="hljs-keyword">out</span>.println(<span class="hljs-string">"当前线程: "</span> + Thread.currentThread().getName() + <span class="hljs-string">"开始"</span>);
            Thread.sleep(<span class="hljs-number">1000</span>);
            System.<span class="hljs-keyword">out</span>.println(<span class="hljs-string">"当前线程: "</span> + Thread.currentThread().getName() + <span class="hljs-string">"结束"</span>);
        } <span class="hljs-keyword">catch</span> (InterruptedException e) {

        }
    }
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span>(String[] args) {
    final StringLock stringLock = <span class="hljs-keyword">new</span> StringLock();
    <span class="hljs-keyword">new</span> Thread(<span class="hljs-keyword">new</span> Runnable() {
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span>() {
            stringLock.method();
        }
    }, <span class="hljs-string">"t1"</span>).start();

    <span class="hljs-keyword">new</span> Thread(<span class="hljs-keyword">new</span> Runnable() {
        <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span>() {
            stringLock.method();
        }
    }, <span class="hljs-string">"t2"</span>).start();
}

}

  • 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

执行结果:

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

(3)单例模式-双重校验锁:

普通的加锁的单例模式:

public class Singleton {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> Singleton instance = <span class="hljs-keyword">null</span>; <span class="hljs-comment">//懒汉模式</span>
<span class="hljs-comment">//private static Singleton instance = new Singleton(); //饿汉模式</span>

<span class="hljs-keyword">private</span> <span class="hljs-title">Singleton</span>() {

}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> synchronized Singleton <span class="hljs-title">newInstance</span>() {
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">null</span> == instance) { <span class="hljs-comment">//判断实例是否已经被其他线程创建了</span>
        instance = <span class="hljs-keyword">new</span> Singleton();
    }
    <span class="hljs-keyword">return</span> instance;
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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

public class DubbleSingleton {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> DubbleSingleton instance;

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

}

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

(双重校验锁的方式相对于线程安全的懒汉模式来说,从表面上是将锁的粒度缩小为方法内部的同步代码块,而不是线程安全的懒汉模式同步整个方法!是锁优化中:减小锁粒度的一种表现形式)

但是,需要注意的是,上述的代码是错误的写法,这是因为:指令重排优化,可能会导致初始化单例对象和将该对象地址赋值给instance字段的顺序与上面Java代码中书写的顺序不同。

例如:线程A在创建单例对象时,在构造方法被调用之前,就为 该对象分配了内存空间并将对象设置为默认值。此时线程A就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有完成初始化操作。线程B来调用newInstance()方法,得到的 就是未初始化完全的单例对象,这就会导致系统出现异常行为。

为了解决上述的问题,可以使用volatile关键字进行修饰instance字段。volatile关键字在这里的含义就是禁止指令的重排序优化(另一个作用是提供内存可见性),从而保证instance字段被初始化时,单例对象已经被完全初始化。

最终代码如下:

public class DubbleSingleton {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">volatile</span> DubbleSingleton instance;

<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> DubbleSingleton <span class="hljs-title">getInstance</span>(){
    <span class="hljs-keyword">if</span>(instance == <span class="hljs-keyword">null</span>){
        <span class="hljs-keyword">try</span> {
            <span class="hljs-comment">//模拟初始化对象的准备时间...</span>
            Thread.sleep(<span class="hljs-number">3000</span>);
        } <span class="hljs-keyword">catch</span> (InterruptedException e) {
            e.printStackTrace();
        }
        <span class="hljs-comment">//类上加锁,表示当前对象不可以在其他线程的时候创建</span>
        synchronized (DubbleSingleton.class) { 
            <span class="hljs-comment">//如果不加这一层判断的话,这样的话每一个线程会得到一个实例</span>
            <span class="hljs-comment">//而不是所有的线程的到的是一个实例</span>
            <span class="hljs-keyword">if</span>(instance == <span class="hljs-keyword">null</span>){ 
                instance = <span class="hljs-keyword">new</span> DubbleSingleton();
            }
        }
    }
    <span class="hljs-keyword">return</span> instance;
}

}

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

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

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值