java并发程序和共享对象实用策略

Wesley13
• 阅读 346

java并发程序和共享对象实用策略

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:

  1. 线程封闭
  2. 只读共享。共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象
  3. 线程安全共享。线程安全地对象在器内部实现同步。
  4. 保护对象。被保护的对象只能通过持有特定的锁来方访问。

线程封闭

当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭,它是实现线程安全性的最简单方式之一。

Ad-hoc线程封闭指的是,维护线程封闭性的职责完全由程序实现来承担。

当决定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为一个单线程子系统。在volatile变量上存在一种特殊的线程封闭,只要你能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享的volatile变量上执行“读取-修改-写入”操作。在这种情况下,相当于将修改操作封闭在单个线程中以防止发生竞态条件,并且volatile变量的可见性保证还确保了其他线程能看到的最新值。

栈封闭是一种线程封闭的特例,在栈封闭中,只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。在下面的例子中,由于任何方法都无法获得对基本类型的引用,因此Java语言的这种语义就确保了基本类型的局部变量始终封闭在线程内。

public int loadTheArk(Collection<Animal> candidates){
    SortedSet<Animal> animals;
    int numPirs = 0;
    Animal candidate = null;
    
    //animals被封闭在方法中,不要使它们逸出!
    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;
}

在维持对象引用的栈封闭时,程序员需要多做一些工作以确保引用的对象不会逸出。

ThreadLocal类通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。例如通过将JDBC的连接保存到ThreaLocal对象中,每个线程都会拥有属于自己的连接

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

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

当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以用这个技术。但是,ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。

不变性

满足同步需求的另一种方法是使用不可变对象。线程安全性是不可变对象的固有属性之一,它们的不变性条件是由构造函数创建的,只要它们的状态不改变,那么这些不变条件就能得以维持。另一方面,不可变对象不会像这样被恶意代码或者有问题的代码破坏,因此可以安全地共享和发布这些对象,而无须创建保护性的副本。

当满以下条件时,对象才是不可变的:

  1. 对象创建以后其状态就不能修改。

  2. 对象的所有域都是final类型。

  3. 对象是正确创建的(在对象的创建期间,this引用没有逸出)。

    @Immutable public final class ThreeStooges{ private final Set stooges = new HashSet(); public ThreeStooges(){ stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); } public boolean isStooge(String name){ return stooges.contains(name); } }

使用Volatile类型来发布不可变对象

@Immutable
public class OneValueCache {

    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger lastNumber, BigInteger[] lastFactors) {
        this.lastNumber = lastNumber;
        this.lastFactors = Arrays.copyOf(lastFactors, lastFactors.length);
    }

    public BigInteger[] getFactors(BigInteger integer) {
        if (lastNumber == null || !lastNumber.equals(integer)) {
            return null;
        } else {
            return Arrays.copyOf(lastFactors, lastFactors.length);
        }
    }
}

使用指向不可变容器对象的volatile类型引用以缓存最新的结果

@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {

    private volatile OneValueCache cache = new OneValueCache(null, null);

    /**
     * 通过使用包含多个状态变量的容器对象来维持不变性的条件,并使用一个volatile类型的引用来确保可见性,
     * 使得Volatile Cache Factorizer 在没有显式地使用锁的情况下仍然是线程安全的
     * @param servletRequest
     * @param servletResponse
     * @throws ServletException
     * @throws IOException
     */
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse)
            throws ServletException, IOException {
        BigInteger i = extractFromRequest(servletRequest);
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(servletRequest, servletResponse);
    }
}

安全发布

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:

  • 在静态初始化函数中初始化一个对象引用。
  • 将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
  • 将对象的引用保存到某个正确构造对象的final类型域中。
  • 将对象的引用保存到一个由锁保护的域中

尽管javadoc在这个主题上没有给出很清晰的说明,但线程安全库中的容器类提供了以下的安全发布保证:

  • 通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程
  • 通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程。
  • 通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程

通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器:

public static Holder hodler= new Holder(42);

静态初始化器由JVM在类的初始化阶段执行。由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布。

事实不可变对象指的是如果对象从技术上看是可变的,但其状态在发布后不会再改变,那么把这种对象称为事实不可变对象。在这些对象发布后,程序只需要将它们视为不可变对象即可。

本文分享 CNBlog - luozhiyun。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
九路 九路
3年前
3 Java对象的内存布局以及对象的访问定位
先来看看Java对象在内存中的布局一Java对象的内存布局在HotSpot虚拟机中,对象在内存中的布局分为3个区域对象头(Header)MarkWord(在32bit和64bit虚拟机上长度分别为32bit和64bit)存储对象自身的运行时数据,包括哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等类型指
Wesley13 Wesley13
2年前
java 面试知识点笔记(十)多线程与并发
问:线程安全问题的主要诱因?1.存在共享数据(也称临界资源)2.存在多条线程共同操作这些共享数据解决方法:同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作互斥锁的特征:1.互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程协调机制,这样在同一时间只有一
Wesley13 Wesley13
2年前
Java线程知识深入解析(2)
多线程程序对于多线程的好处这就不多说了。但是,它同样也带来了某些新的麻烦。只要在设计程序时特别小心留意,克服这些麻烦并不算太困难。(1)同步线程许多线程在执行中必须考虑与其他线程之间共享数据或协调执行状态。这就需要同步机制。在Java中每个对象都有一把锁与之对应。但Java不提供单独的lock和unlock操作。它由高层的结构隐
Wesley13 Wesley13
2年前
Java中的锁原理、锁优化、CAS、AQS,看这篇就对了!
01为什么要用锁?锁是为了解决并发操作引起的脏读、数据不一致的问题。02 锁实现的基本原理2.1、volatileJava编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些
Wesley13 Wesley13
2年前
Java分布式锁看这篇就够了
\什么是锁?在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到
Wesley13 Wesley13
2年前
Java并发概述之安全
Java并发的学习内容主要来自《Java并发编程实战》一书,本文为一概述。并发最简单的解释应该是不同任务的执行时间区间存在交集。由于时间上的交集共享变量,并发会带来安全问题。从任务的角度而言,任务的执行需要得到正确的效果;从对象的角度而言,对象需要被正确的访问。所谓正确,或常说的线程安全,包括了一个对象操作,或者一个任务执行的三个方面:前置条件
Wesley13 Wesley13
2年前
C#之线程同步
参考:线程之线程同步(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fnufangrensheng%2Fp%2F3521654.html)多个线程同时使用共享对象会造成很多问题,同步这些线程使得对共享对象的操作能够以正确的顺序执行是非常重要的。如果
Wesley13 Wesley13
2年前
Java多线程——线程封闭
线程封闭:当访问共享的可变数据时,通常需要同步。一种避免同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步,这种技术称为线程封闭(thread confinement)  线程封闭技术一个常见的应用就是JDBC的Connection对象,JDBC规范并没有要求Connection对象必须是线程安全的,在服务器应用程序中,线程从连接
Wesley13 Wesley13
2年前
Java并发编程总结(一)Syncronized解析
Syncronized解析作用:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。用法:(1)修饰普通方法(锁是当前实例对象)(2)修饰静态方法(锁是当前对象的Class对象)(3)修饰代码块(锁是Synchonized括号里配置的