• Java多线程系列八——volatile和ThreadLocal


    参考资料:

    http://ifeve.com/java-memory-model-4/

    http://www.infoq.com/cn/articles/java-memory-model-1

    http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/

    https://en.wikipedia.org/wiki/Singleton_pattern#Java_5_solution

    https://www.ibm.com/developerworks/java/library/j-jtp06197/

    1. volatile

    final class Singleton {
        private static Singleton instance = null;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }

     以上代码尝试实现单例模式,但存在严重的线程安全风险。Java Memory Model定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。假设Thread1/Thread2并发,instance为它们的共享变量,Thread1与Thread2之间通信必须要经历下面2个步骤:

    • Thread1把本地内存更新过的instance刷新到主内存中去
    • Thread2到主内存中去读取Thread1之前已更新过的instance

    那么可能的场景之一——Thread1执行完instance = new Singleton(),但刷新到主内存前Thread2的instance == null仍然成立,于是再次执行instance = new Singleton(),这时两个线程得到了两个不同的对象,与预期不符。

    final class Singleton {
        private static Singleton instance = null;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

     加入锁和双重校验后,仍然存在风险,因为为了提高性能,编译器和处理器常常会对指令做重排序,以Singleton instance = new Singleton()为例,它包含了三个指令:

    • ①为instance分配内存
    • ②调用Singleton构造方法
    • ③把instance指向分配的内存地址

    三个指令执行顺序可能是①②③或①③②,在③执行之后,instance==null将不再成立。可能的场景——假设Thread1/Thread2并发,Thread1执行了除②以外的指令,Thread2的instance==null不成立,虽然得到了内存地址,但由于未调用构造方法而报错。

    final class Singleton {
        private static volatile Singleton instance = null;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

    为instance变量加上volatile关键字彻底解决问题。volatile的特性:

    • volatile的变量修改后将立即刷新到主内存,其他线程即可读取到新值
    • 编译器利用内存屏障的概念禁止上述三条指令的重排序,只允许①②③的执行顺序

    由于以上特性使volatile极适用于修饰多线程环境下的状态标识。

    2. ThreadLocal

    当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

    以非线程安全的SimpleDateFormat类为例,在并发运行时会出错,但使用ThreadLocal维护则可以完美避免此问题

    import java.text.DateFormat;
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @Description: 测试ThreadLocal
     */
    public class ThreadLocalTest {
        private static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        private static final ThreadLocal<DateFormat> DATE_FORMAT = new ThreadLocal<DateFormat>() {
            public DateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd");
            }
        };
    
        public static void main(String[] args) throws InterruptedException {
            String date = "2017-07-06";
            testDateFormat(date);
            testThreadLocal(date);
        }
    
        private static void testDateFormat(String date) throws InterruptedException {
            multilpleThreadExecute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(df.parse(date));
                    } catch (ParseException e) {
                    }
                }
            });
        }
    
        private static void testThreadLocal(String date) throws InterruptedException {
            multilpleThreadExecute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(DATE_FORMAT.get().parse(date));
                    } catch (ParseException e) {
                    }
                }
            });
        }
    
        private static void multilpleThreadExecute(Runnable runnable) throws InterruptedException {
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < 10; i++) {
                executorService.execute(runnable);
            }
            executorService.shutdown();
            executorService.awaitTermination(Integer.MAX_VALUE, TimeUnit.DAYS);
        }
    }
  • 相关阅读:
    spring boot集成redis和mongodb实现计步排名
    spring boot + redis 实现网站限流和接口防刷功能
    基于spring-boot-data-jdbc的RowMapper实例的初始化配置,配置内容和实体类的代码由测试类生成
    自定义注解并封装成jar包,实现一般方法和Controller类的入参、出参和执行时间的日志打印
    封装自己的spring-boot-starter实现初始化字典数据和脱敏(windows10+jdk8+idea+spring-boot2.1.5)
    spring boot 集成mybatis的多数据源
    常用工具说明--jsdoc 前端文档输出工具
    常用工具说明--mysql数据库安装
    web前端--实现前后端分离的心得
    资料汇总--Ajax中Put和Delete请求传递参数无效的解决方法(Restful风格)【转】
  • 原文地址:https://www.cnblogs.com/hiver/p/7128024.html
Copyright © 2020-2023  润新知