• 并发编程学习笔记(3)----synchronized关键字以及单例模式与线程安全问题


    再说synchronized关键字之前,我们首先先小小的了解一个概念-内置锁。

    什么是内置锁?

    在java中,每个java对象都可以用作synchronized关键字的锁,这些锁就被称为内置锁,每个对象的锁的信息都存在对象头中

    所以synchronized关键字在使用过程中之所以能够保证线程的安全,也是因为使用了锁。下面就说说synchronized具体的几种用法,及使用何种类型的内置锁。

    (一)用synchronized关键字修饰实例方法,示例代码:

    /**
         * synchronized 作用在实力方法上时,此时的内置锁为当前对象,即this
         */
        public synchronized void fun1 () {
    
        }

    当synchronized关键字作用在实例方法上时,此时的内置锁为当前的对象-thsi。

    (二)用synchronized关键字修饰静态方法,示例代码:

     /**
         * synchronized 作用在静态方法上时,此时的内置锁为当前类的字节码(Xxx.class)所对应Class对象
         */
        public static synchronized void fun2 () {
    
        }

    当synchronized关键字作用在静态方法(类方法)上时,此时的内置锁为当前类的字节码对应的Class对象,及Xxx.class所对应的对象。

    (三)用synchronized关键字修饰代码块,示例代码:

     /**
         * synchronized 作用在代码块时,此时的内置锁为括号中的对象
         */
        public static synchronized void fun3 () {
    
            synchronized (Main.class){
            }
        }

    当作用在代码块上时,内置对象为括号中的对象,此时的括号是必须要有的,因为在代码块中,synchronized必须要接收一把锁来保证线程的安全,这里对象可以是任意的java对象,如你可以自己创建一个obj传入也是没有问题的。

    由于每次的锁的获取和释放总是会耗费时间,所以在synchronized中优化中也存在了偏向锁,轻量级锁等。

    偏向锁:很多情况下,竞争锁时不是多个线程,而是单个线程在访问,如果此时依然按照多个线程来获取和释放锁,就会在消耗资源,所以在java的对象头中的Mark Word中就会存储偏向锁的信息,偏向锁会偏向于第一次进来的线程,如果在运行过程中不存在其他线程使用同步锁的情况下,那么就会给当前线程添加偏向锁标记。它不会每次执行都获取释放锁,当遇到其他线程竞争该锁时,该偏向锁会被挂起,jvm将会消除该锁的偏向锁标记,使其变成标准的轻量级锁。偏向锁适用于始终只有一个线程执行任务的情况。可以通过命令 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

    关闭偏向锁:-XX:-UseBiasedLocking

    轻量级锁:轻量级锁是偏向锁失败升级得来的,它会简单的将对象头部作为指针,自旋的指向持有锁的线程堆栈内部,以此来判断一个线程是否持有对象锁,如果线程获取到轻量级锁成功,该线程可以顺利的进入到临界区执行,如果失败,则表示其他线程已经抢夺到了所,那么当前的轻量级锁就会膨胀成为重量级锁。

    单例模式与线程安全问题。

    在单利模式的懒汉式下,由于所执行的都是原子性操作,所以不会产生线程安全问题,但是当在饿汉式的模式下,由于非原子性操作,所以在多线程的环境下就会产生安全问题,产生非单例的对象。

    所以一般在写多线程下的单例模式时,都采用的是双重判断加锁的机制方式来实现单例模式。实例代码:

    package com.wangx.thread.t3;
    
    /**
     * 单利模式与线程安全
     */
    public class Singleton {
    
        /**
         * 使用volatile可以防止指令重排序的情况下产生的线程安全问题
         */
        private volatile static Singleton singleton;
        private Singleton() {
    
        }
    
        /**
         * 使用双重判断加锁的可以避免线程安全问题
         * @return
         */
        public static Singleton getInstance() {
            if (singleton == null ) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

    使用双重判定是因为在第一个if为空的情况下,可能两个线程同时进入了if代码块,但是当前一个线程获取锁并创建了对象之后,后一个语句块由于是在第一个if语句块中,所以会继续往下执行,创建新的对象,产生了安全问题,所以在synchronized代码块中还需要再进行一次判断。使用volatile关键字修饰属性是因为在虚拟机的执行过程中由于性能优化,会进行指令重排序,如第一步申请了内存空间,第二部需要在空间上创建实例,第三步将对象引用指向实例空间,但是由于指令重排序可能会在申请空间之后就将对象引用指向空间,此时的对象还会创建,但是singleton 已经指向了空间,此时如果有其他线程进来刚好执行到第一个if语句时,singleton已经不为空了,就会产出线程安全问题,所以使用volatile可以禁止指令重排序,会对该变量的写操作设置一道屏障,在该变量没有赋值完成之前,不允许其他线程对该变量进行读操作。

    volatile的使用及原理:

    volatile在多线程的环境下保证了变量的修改对每个线程可见,可以轻量级的保证线程安全问题,比如在A线程中修改了a变量的值,那么该修改在线程b中是可见的,a变量一旦发生改变完成,那么b线程就能立刻读取到a改变后的值,这样就实现了线程之间通信的一个概念,保证线程的安全,相比较synchronized而已,volatile只能够保证一些原子性操作的变量的值一致性。在运用可以使用volatile来告诉另一个线程是否可以执行等操作。

    volatile实现的原理是,在多个处理器处理任务时,cpu会将任务缓存到处理器中,这样可以加快任务的执行效率,但是到资源被写回到内存中时,所有处理器上缓存的该资源都将会失效,需要重新从内存中读取资源,为了保证数据一致性。而volatile关键字的作用就是将每个变量经处理器执行之后立刻写回到内存中,此时其他处理器失效,重新重内存中读取该资源,实现了变量在线程中的可见性,但是根据原理也可以看出,使用volatile也会降低处理器的执行效率,进而影响到程序的执行速度。所以在开发中volatile的使用也是需要注意和思考取舍的。

    原文 并发编程学习笔记(3)----synchronized关键字以及单例模式与线程安全问题

  • 相关阅读:
    php中模拟多继承如何实现
    js进阶 12-4 jquery键盘事件如何使用
    php课程 3-12 带默认参数的函数怎么写
    android_线
    Cocos2d-x 文本渲染
    一张地图告诉你,只JavaScript不够!
    python 导入库问题
    Cocos2d-X字体
    R语言做文本挖掘 Part5情感分析
    Java存储区域——JVM札记<一个>
  • 原文地址:https://www.cnblogs.com/xiaoshen666/p/10868765.html
Copyright © 2020-2023  润新知