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

上一篇:

Java多线程编程-(1)-线程安全和锁Synchronized概念

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

一、Synchronized锁重入

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

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

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

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();
    }
}

执行结果:

method1-----
method2-----
method3-----

示例代码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 {

    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)出现异常时,锁自动释放

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

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();
    }
}

执行结果如下:

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)
    //其他输出信息

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

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

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();
    }
}

执行结果:

当前线程: t1开始
当前线程: t1结束
当前线程: t2开始
当前线程: t2结束

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

普通的加锁的单例模式:

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()方法都会进行加锁和解锁操作,也就是说该锁可能会成为系统的瓶颈,为了解决这个问题,有人提出了“双重校验锁”的方式,示例代码如下:

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;
    }
}

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

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

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

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

最终代码如下:

public class DubbleSingleton {

    private static volatile DubbleSingleton instance;

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

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

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

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

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

    原文作者:java锁
    原文地址: https://blog.csdn.net/xlgen157387/article/details/78005352
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞