-->

多线程中的共享对象

2020-11-28 13:53发布

转自:https://www.cnblogs.com/lntea/p/4686394.html

可见性

上一章中我们讨论过synchronized块可以阻塞执行以及确保操作执行中的原子化。因而往往存在这样一个误区,synchronized仅仅用来对操作进行原子化,设置操作执行的边界点。然而synchronized块还有一个重要的作用,内存可见性。简单的理解,即一个线程修改了对象的状态,其他线程能够真正地看到状态的改变

过期数据

当多个线程同时访问共享数据时,如果对共享数据的操作没有同步,可能会出现一个线程获取共享数据的同时另一个线程在修改共享数据,这样第一个线程拿到的共享数据可能就会是过期的数据,也就不是最新鲜的数据。过期数据导致的错误可能只是输出错误,但也可能严重到导致程序终止。

锁和可见性

内置锁可以确保一个线程以某种可预见的方式看到另一个线程对于共享状态的影响。
线程A执行某一同步块时,线程B也进入被同一锁监视的同步块,这是可以保证在A释放锁前对A可见的变量,B获得锁后同样也是可见的。

但是如果没有同步,就没有这样的内存可见性的保证。因此锁不仅仅是用于同步和互斥的,也是用来保证内存可见性的。为了保证所有线程都能看到共享数据的最新值,读取和写入操作都需要使用同一个锁进行同步

volatile

java提供了另一种保证内存可见性的方式,即volatile变量。当一个变量被volatile修饰后,编译器和进行时会一直监视变量的变化,且该变量不会被进行重排序。
但需要强调的是volatile不能保证原子性,即使是自增操作也保证不了。因而正确使用volatile变量的方式是用来标示状态或生命周期事件。下面是标准的使用volatile的一些前提:

  • 写入变量并不依赖变量的当前值,或者确保只有单一线程修改变量的值
  • 变量不需要和其他变量参与不变约束
  • 访问变量时没有其他原因需要加锁

记住:加锁可以保证可见性和原子性,但volatile只能保证可见性。

发布和逸出

发布是指将让一个对象在当前范围之外被访问。当前范围可大可小,可以是一个模块,也可以是一个jar包。有时我们需要将一个对象发布出去,以供外部使用,但如果对象在未准备好时就被发布出去,是相当危险的,我们称之为逸出

最常见的对象发布方式是将对象的引用存储到公共静态域中。任何类和线程都能看到它。

public static Set<String> set;

public void initialize(){
  set = new HashSet<String>();
}

还可以通过非私有的方法发布对象。

private String[] states = new String[]{
    "A","B"
};

public String[] getStates() {return states;}

这样的方式发布states会让任何一个调用者都能修改它的内容,states域本身应该是不允许改变的。

要知道发布一个对象,意味着也发布了该对象中所有非私有域所引用的对象,甚至那些非私有域的引用链以及方法调用链中可获得的对象都会被发布。

一旦将不该发布的对象逸出了,就会存在被误用的风险。就像你在某个网站的用户名密码被黑客窃取公布到网上了,无论是否有人使用你的帐号,你的帐号已经存在风险了。

最后一种发布对象的方式是发布一个内部类实例。

public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener(){
                public void onEvent(Event e) {
                    //ThisEscape.this.toString();
                    //doSomething(e);
                }
            }
        );
    }
}

当ThisEscape发布EventListener时,它也无条件地发布了封装ThisEscape的实例。因为内引类(inner class instances)的实例包含了对封装实例隐含的引用,即this的引用。

上面的ThisEscape也演示了一种重要的逸出特例,this引用在构造时逸出。对象只用通过构造函数返回后才是稳定的状态,因此在构造函数内部发布的对象,只是一个未完成的、不稳定对象。

不要让this引用在构造期间逸出

一个导致this引用在构造期间逸出的常见错误就是在构造函数中启动一个线程。当对象在构造函数中创建了一个线程时,无论显式的(通过参数传给构造函数)还是隐式的(Thead或Runnable是所属对象的内部类),this引用几乎总是被新线程共享。

解决办法是在构造函数中创建线程,但不立即启动它。而是通过发布一个start方法来启动线程。

线程封闭

我们知道访问共享的可变的数据需要同步。如果数据仅在单线程中被访问,就不需要任何同步。因而我们可以将对象封闭在一个线程中,即可实现线程安全的目的。我们称之为线程封闭

Ad-hoc线程限制

Ad-hoc(非正式的)线程限制是指维护线程限制性的任务全部落在实现上的情况。这种方式是非常容易出错的。

其中一个特例是通过单一线程写入共享的volatile变量,在执行“读-改-写”操作时是安全的。

如果可能的话,尽量用另一种线程限制的强形式(栈限制或ThreadLocal)。

栈限制

将对象设置为本地变量,只存在于执行线程栈,其他线程无法访问这个栈,称之为栈限制。就算使用非线程安全的对象仍可以保证线程安全性。但一旦对象在某种情况下发布,就会导致对象逸出。

ThreadLocal

ThreadLocal是维护线程限制的更规范的方式。它为每个使用它的线程维护一份单独的copy,通过它提供的get和set方法对当前线程中维护的变量进行读取和更新。

比如我们想维护一个全局的数据库连接,这个Connection在启动时已经初始化。因为JDBC规范并未要求Connection一定是线程安全的,在没有额外的辅助下,使用全局的Connection不是线程安全的。此时可以利用ThreadLocal存储Connection,从而使每个线程都有自己的Connection。

private static ThreadLocal<Connection>     connectionHolder = 
        new ThreadLocal<Connection>(){
    protected Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    };
};

public static Connection getConnection(){
    return connectionHolder.get();
}

线程首次调用ThreadLocal.get()方法时,会请求initialValue()方法提供一个初始值。

ThreadLocal很容易被滥用。比如将它们所封闭的属性作为使用全局变量的许可证。ThreadLocal变量会降低重入性,但会引入隐晦的类之间的耦合。因此要谨慎地使用。

不可变性

创建后不能被修改的对象称为不可变对象。不可变对象永远是线程安全的

不可变性不能简单地等于将对象中的所有域都声明为final类型。因为final域可以获得一个到可变对象的引用。只有满足如下状态,一个对象才是不可变的:

  • 它的状态不能在创建后再被修改
  • 所有域都是final类型
  • 它被正确地创建(没有发生this引用)

由于程序的状态自始至终都是变化着的。你会觉得使用不可变对象会有很多限制。但存储在不可变对象中的状态可以通过替换一个新的状态的不可变对象进行更新。

final域

final域是不可修改的(其指向的对象是可变的),final域限制了对象的可变性,使得安全性能够提高。

Effective Java中说道

将所有的域声明为私有的,除非它们需要更高的可见性。
将所有的域声明为final型,除非它们是可变的。

安全发布

在并发程序中,使用共享对象的一些最有效的策略:

  • 线程限制:一个线程限制的对象,被线程独占,且只能被占有它的线程修改。
  • 共享只读:一个共享的只读对象,可以被多个线程并发访问,但任何线程都不能修改它。
  • 共享线程安全:一个线程安全的对象在内部进行同步,其他线程就无需额外同步,可以通过公共接口随意访问。
  • 被守护的:一个被守护的对象只能通过特定的锁来访问。

下一章我们来学习jdk提供的一些基础的并发容器和工具。

转自:https://www.cnblogs.com/lntea/p/4686394.html

可见性

上一章中我们讨论过synchronized块可以阻塞执行以及确保操作执行中的原子化。因而往往存在这样一个误区,synchronized仅仅用来对操作进行原子化,设置操作执行的边界点。然而synchronized块还有一个重要的作用,内存可见性。简单的理解,即一个线程修改了对象的状态,其他线程能够真正地看到状态的改变

过期数据

当多个线程同时访问共享数据时,如果对共享数据的操作没有同步,可能会出现一个线程获取共享数据的同时另一个线程在修改共享数据,这样第一个线程拿到的共享数据可能就会是过期的数据,也就不是最新鲜的数据。过期数据导致的错误可能只是输出错误,但也可能严重到导致程序终止。

锁和可见性

内置锁可以确保一个线程以某种可预见的方式看到另一个线程对于共享状态的影响。
线程A执行某一同步块时,线程B也进入被同一锁监视的同步块,这是可以保证在A释放锁前对A可见的变量,B获得锁后同样也是可见的。

但是如果没有同步,就没有这样的内存可见性的保证。因此锁不仅仅是用于同步和互斥的,也是用来保证内存可见性的。为了保证所有线程都能看到共享数据的最新值,读取和写入操作都需要使用同一个锁进行同步

volatile

java提供了另一种保证内存可见性的方式,即volatile变量。当一个变量被volatile修饰后,编译器和进行时会一直监视变量的变化,且该变量不会被进行重排序。
但需要强调的是volatile不能保证原子性,即使是自增操作也保证不了。因而正确使用volatile变量的方式是用来标示状态或生命周期事件。下面是标准的使用volatile的一些前提:

  • 写入变量并不依赖变量的当前值,或者确保只有单一线程修改变量的值
  • 变量不需要和其他变量参与不变约束
  • 访问变量时没有其他原因需要加锁

记住:加锁可以保证可见性和原子性,但volatile只能保证可见性。

发布和逸出

发布是指将让一个对象在当前范围之外被访问。当前范围可大可小,可以是一个模块,也可以是一个jar包。有时我们需要将一个对象发布出去,以供外部使用,但如果对象在未准备好时就被发布出去,是相当危险的,我们称之为逸出

最常见的对象发布方式是将对象的引用存储到公共静态域中。任何类和线程都能看到它。

public static Set<String> set;

public void initialize(){
  set = new HashSet<String>();
}

还可以通过非私有的方法发布对象。

private String[] states = new String[]{
    "A","B"
};

public String[] getStates() {return states;}

这样的方式发布states会让任何一个调用者都能修改它的内容,states域本身应该是不允许改变的。

要知道发布一个对象,意味着也发布了该对象中所有非私有域所引用的对象,甚至那些非私有域的引用链以及方法调用链中可获得的对象都会被发布。

一旦将不该发布的对象逸出了,就会存在被误用的风险。就像你在某个网站的用户名密码被黑客窃取公布到网上了,无论是否有人使用你的帐号,你的帐号已经存在风险了。

最后一种发布对象的方式是发布一个内部类实例。

public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener(){
                public void onEvent(Event e) {
                    //ThisEscape.this.toString();
                    //doSomething(e);
                }
            }
        );
    }
}

当ThisEscape发布EventListener时,它也无条件地发布了封装ThisEscape的实例。因为内引类(inner class instances)的实例包含了对封装实例隐含的引用,即this的引用。

上面的ThisEscape也演示了一种重要的逸出特例,this引用在构造时逸出。对象只用通过构造函数返回后才是稳定的状态,因此在构造函数内部发布的对象,只是一个未完成的、不稳定对象。

不要让this引用在构造期间逸出

一个导致this引用在构造期间逸出的常见错误就是在构造函数中启动一个线程。当对象在构造函数中创建了一个线程时,无论显式的(通过参数传给构造函数)还是隐式的(Thead或Runnable是所属对象的内部类),this引用几乎总是被新线程共享。

解决办法是在构造函数中创建线程,但不立即启动它。而是通过发布一个start方法来启动线程。

线程封闭

我们知道访问共享的可变的数据需要同步。如果数据仅在单线程中被访问,就不需要任何同步。因而我们可以将对象封闭在一个线程中,即可实现线程安全的目的。我们称之为线程封闭

Ad-hoc线程限制

Ad-hoc(非正式的)线程限制是指维护线程限制性的任务全部落在实现上的情况。这种方式是非常容易出错的。

其中一个特例是通过单一线程写入共享的volatile变量,在执行“读-改-写”操作时是安全的。

如果可能的话,尽量用另一种线程限制的强形式(栈限制或ThreadLocal)。

栈限制

将对象设置为本地变量,只存在于执行线程栈,其他线程无法访问这个栈,称之为栈限制。就算使用非线程安全的对象仍可以保证线程安全性。但一旦对象在某种情况下发布,就会导致对象逸出。

ThreadLocal

ThreadLocal是维护线程限制的更规范的方式。它为每个使用它的线程维护一份单独的copy,通过它提供的get和set方法对当前线程中维护的变量进行读取和更新。

比如我们想维护一个全局的数据库连接,这个Connection在启动时已经初始化。因为JDBC规范并未要求Connection一定是线程安全的,在没有额外的辅助下,使用全局的Connection不是线程安全的。此时可以利用ThreadLocal存储Connection,从而使每个线程都有自己的Connection。

private static ThreadLocal<Connection>     connectionHolder = 
        new ThreadLocal<Connection>(){
    protected Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    };
};

public static Connection getConnection(){
    return connectionHolder.get();
}

线程首次调用ThreadLocal.get()方法时,会请求initialValue()方法提供一个初始值。

ThreadLocal很容易被滥用。比如将它们所封闭的属性作为使用全局变量的许可证。ThreadLocal变量会降低重入性,但会引入隐晦的类之间的耦合。因此要谨慎地使用。

不可变性

创建后不能被修改的对象称为不可变对象。不可变对象永远是线程安全的

不可变性不能简单地等于将对象中的所有域都声明为final类型。因为final域可以获得一个到可变对象的引用。只有满足如下状态,一个对象才是不可变的:

  • 它的状态不能在创建后再被修改
  • 所有域都是final类型
  • 它被正确地创建(没有发生this引用)

由于程序的状态自始至终都是变化着的。你会觉得使用不可变对象会有很多限制。但存储在不可变对象中的状态可以通过替换一个新的状态的不可变对象进行更新。

final域

final域是不可修改的(其指向的对象是可变的),final域限制了对象的可变性,使得安全性能够提高。

Effective Java中说道

将所有的域声明为私有的,除非它们需要更高的可见性。
将所有的域声明为final型,除非它们是可变的。

安全发布

在并发程序中,使用共享对象的一些最有效的策略:

  • 线程限制:一个线程限制的对象,被线程独占,且只能被占有它的线程修改。
  • 共享只读:一个共享的只读对象,可以被多个线程并发访问,但任何线程都不能修改它。
  • 共享线程安全:一个线程安全的对象在内部进行同步,其他线程就无需额外同步,可以通过公共接口随意访问。
  • 被守护的:一个被守护的对象只能通过特定的锁来访问。

下一章我们来学习jdk提供的一些基础的并发容器和工具。

标签: