• Java


    保证并发安全性的方式有三:

    不共享、不可变、同步 

    前两种方式相对第三种要简单一些。

    这一篇不说语言特性和API提供的相关同步机制,主要记录一下关于共享的一些思考。

    共享(shared),可以简单地认为多个线程可以同时访问某个对象。

    如果仅仅在单线程内进行访问则不存在同步的问题。

    保证数据的单线程访问称为线程封闭(thread confinement)。

    线程封闭有三种方式:

    ·Ad-hoc线程封闭

    ·栈封闭

    ·ThreadLocal

      

    Ad-hoc线程封闭

    通过程序实现来进行线程封闭,也就是说我们无法利用语言特性将对象封闭到特定的线程上,这一点导致这种方式显得不那么可靠。

    举个例子,假设我们保证只有一个线程可以对某个共享的对象进行写入操作,那么这个对象的"读取-修改-写入"(比如自增操作)在任何情况下都不会出现竟态条件。

    如果我们为这个对象加上volatile修饰则可以保证该对象的可见性,任何线程都可以读取该对象,但只有一个线程可以对其进行写入。

    这样,仅仅通过线程封闭+volatile修饰就适当地保证了其安全性,相比直接使用synchoronized修饰,虽然更适合,但实现起来稍微复杂。

    而对于线程封闭方式的选择,这种方式是最不被推荐的。

    栈封闭

    这个方式理解起来比较简单,封闭在执行线程是局部变量本身固有的特性,封闭在执行线程的栈里,其他线程无法访问是理所当然的。

    对于基本类型的局部变量,我们不用考虑任何事情,因为Java语言特性本身就保证了任何方法都无法获得基本类型的引用。

    而对于引用类型的局部变量,我们需要稍微注意一些问题来保证其栈封闭。

    参考下面的装载方舟的代码,现在我们要保护animals,则需要保证该方法的参数、调用的外来方法、返回值都不会引用到animals:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
        public int loadTheArk(Collection<Animal> candidates) {
            SortedSet<Animal> animals;
            int numPairs = 0;
            Animal candidate = null;
     
            animals = new TreeSet<Animal>(new SpeciesGenderComparator());
            animals.addAll(candidates);
            for (Animal a : animals) {
                if (candidate == null || !candidate.isPotentialMate(a))
                    candidate = a;
                else {
                    ark.load(new AnimalPair(candidate, a));
                    ++numPairs;
                    candidate = null;
                }
            }
            return numPairs;
        }

    先说说loadTheArk的参数candidates,我们将它的元素进行筛选后装载到了方舟中,方法结束后无法通过该参数影响方舟中的动物夫妇。

    其次是外来方法,我们使用了"种类性别比较器"对animals进行排序,但它是一个concrete,不会有不确定的行为对animals的状态产生影响。

    最后是返回值,显然我们是想报告装载了多少对动物夫妇,返回类型是个基本类型,无法引用animals。

    好了,这就是个成功的栈封闭。

    ThreadLocal

    给人一种亲切感,这几乎是很常见的方式,而且也是最规范的方式。

    我们通常用ThreadLocal保证可变的单例变量和全局变量不被多线程共享。

    先让我们想想单线程场景中使用Connection对象连接数据库,鉴于Connection对象的初始化开销,整个应用中会维护一个全局的Connection对象。

    如果我们想将这个应用改为多线程的,鉴于Connection对象本身不是线程安全的,我们需要对其进行线程封闭,此时我们可以使用ThreadLocal:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class ConnectionDispenser {
        static String DB_URL = "jdbc:mysql://localhost/mydatabase";
     
        private ThreadLocal<Connection> connectionHolder
                new ThreadLocal<Connection>() {
                    public Connection initialValue() {
                        try {
                            return DriverManager.getConnection(DB_URL);
                        catch (SQLException e) {
                            throw new RuntimeException("Unable to acquire Connection, e");
                        }
                    };
                };
     
        public Connection getConnection() {
            return connectionHolder.get();
        }
    }

    不仅是Connection这种场景,如果我们的很多操作频繁地用到某个对象,而我们又需要考虑它的线程封闭又需要考虑它的初始化开销,ThreadLocal几乎是最好的选择。

    虽然这看起来有点像一个全局的Map<Thread,T>,事实上也可以这样理解,但其实现并不是这样你懂的。

    当然,这种方式很方便,但这并不代表ThreadLocal可以滥用, 比如仅仅是考虑到应用的并发安全性就把全局变量一律变成ThreadLocal。

    而这种做法会导致全局变量难以抽象,并降低其可重用性,而且也增加了耦合。

  • 相关阅读:
    PHP 学习轨迹
    beego 遇到的一些问题
    Fiddler 502问题
    SourceTree
    Trait
    PHP PSR 标准
    解决MySQL联表时出现字符集不一样
    Git 代码管理命令
    PHP 运行相关概念
    CentOS 7
  • 原文地址:https://www.cnblogs.com/kavlez/p/4041388.html
Copyright © 2020-2023  润新知