Java并发

Posted 水獭同学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发相关的知识,希望对你有一定的参考价值。

并发编程的第二部分,先来谈谈发布(Publish)逸出(Escape);

 

发布是指:对象能够在当前作用域之外的代码中使用,例如:将对象的引用传递到其他类的方法中,对象的引用保存在其他类可以访问的地方,或在某个非私有的方法中返回对象的引用;

逸出是指:发布内部状态可能会破坏封装性,如果在对象构造完成之前就发布该对象,就会破坏线程安全性;

下面结合一个例子来理解:

class UnsafeStates {
        private String[] states = new String[] {
                "AA" , "BB", .....
        };
        public String[] getStates() { return states; }
}

通过返回对象引用来发布states数组,任何调用者都能修改这个数组的内容,故此数组对象逸出了,有可能引起线程安全性问题;

 

隐式this引用逸出:

public class ThisEscape {
        public ThisEscape (EventSource source) {
                source.registerListrner(new EventListner() {
                        public void onEvent() {
                            doing(e);
                        });
                }
         //一些初始化操作 }
    public void doing(){
      //doing
    }   }

在构造ThisEscape对象时,匿名内部类被发布,内部的this.doing()的this也被发布,但构造过程并没结束,在线程执行到内部类中,发布了ThisEscape对象之后,此时ThisEscape对象已经对于其它线程可见了,但还有一些初始化操作没做,故产生了逸出;

 

避免逸出的最好的方法是:私有化构造器,通过静态方法(工厂方法)返回构造完毕的对象;

public class SafeListner {
        privat final EventListner listner;
        
        private SafeListner() {
                listner = new EventListner(){
                        public void onEvent(Event e) {
                                doing(e):
                        }
                }
        }
        
        public static SafeListner newInstance(){
                SafeListner safe = new SafeListner();
                source.registerListner(safe.listner);
                return safe;
        }
}

 

 

 

接下来了解一下线程封闭(Thread Confinement)

 

实现线程安全性的最简单的方式之一就是线程封闭,也就是单线程内访问数据,就不需要同步;

例如,JDBC中的Connection对象并不要求时线程安全的,线程从连接池中获取一个Connection对象,并用该对象处理请求,使用完返还给线程池;

 

 

1.栈封闭:利用局部变量实现,例如,封闭参数引用,不返回引用类型数据等工作;

public int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Animal> animals;
        int numPairs = 0;
        Animal candidate = null;

        //animal被封闭,不要发布到外界!
        animals = new TreeSet<Animal>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for(Animal a : animals){
                //...
        }
        return numPairs;
}

新建一个局部集合animals,用于封闭方法接收的参数引用,保证它不会逸出,返回值必须是基本类型(只返回值),而不是引用类型(返回引用,对象逸出);

 

 

2.ThreadLocal类:能使线程中的某个值与保存值的对象相关联,提供get与set访问接口与方法,为每个使用该变量的线程都存有一份独立的副本,通常用于防止对可变的单实例变量或全局变量进行分享;

  例如:JDBC的连接对象不一定使线程安全的,因此,将连接保存在ThreadLocal对象中,没给西安城会拥有自己的连接

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

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

每次调用get方法时都会调用initialValue()来获取连接对象;ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性.---Java并发编程实战

 

 

再来谈谈不变性:

  不可变对象一定是线程安全的;


需要满足以下条件:

  1.对象创建后不能修改;

  2.所有域都是final;

  3.对象使正确创建的(this没有逸出).

例如:

public final class ThreeStooges {
        private final Set<String> stooges =  new HashSet<String>();
        
        public ThreeStooges(){
                stooges.add("Joey");
                stooges.add("Moe");
                stooges.add("Curly");
        }

        public boolean isStooge(String name) {
                return stooges.contains(name);
        }
}

以上代码中,Set对象构造完成后无法对其进行修改,所有域为final,也没有this逸出,因此ThreeStooges是不可变对象;

 

结合Volatile和不可变对象来实现线程安全:

不可变对象

class OneValueCache {
        private final BigInteger lastNumber;
        private final BigInteger[] lastFactors;

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

用Volatile发布不可变对象:

public class VolatileCachedFactorizer implements Servlet {
        private volatile OneValueCache cache = new OneValueCache(null, null);
        
        public void service(ServletRequest req, ServletResponse resp) {
                BigInteger i = extractFromRequest(req);
                BigInteger[] factors = cache.getFactors(i);
                if(factors == null){
                        factors = factor(i);
                        cache = new OneValueCache(i, factors);
                }
                recodeIntoResponse(resp, factors);
           }
}

 volatile提供了线程可见性,当设置引用时,其它线程能够看到新缓存的数据,且与cache相关的操作不会相互干扰,因为OneValueCache是一个不可变对象,使得没有使用锁的情况下也能够拥有线程安全性和可见性;

 

 

之前工厂模式解决的是如何让对象不被发布出去,现在我们谈谈如何正确发布对象:

先来看看不正确的发布是怎样的:

public class Holder {
        private int n;
        public Holder(){ this.n = n; }
        
        public void asertSanity() {
                if(n != n) {
                        throw new AssertExceptionError("This statement is false!");
               }
        }
}

由于没有确保Holder对象的状态被其它线程可见,会有线程安全的问题,例如:一个线程A,一个线程B,首先Holder被初始化,n默认值为0,赋值后为1,但是因为没有保证可见性,1这个时候还在当前线程的缓存中,并没有更新到主存中去,所以A可能看到的是缓存中的值1,但是B线程的缓存和A是独立的,所以他看到的还是主存中的值0,这就发现了线程不安全的问题,A看到的是最新值,但B看到的是过期值。因此这是线程不安全的.

 

如何正确发布呢?

  1.在静态初始化函数中初始化一个对象的引用;

  2.将对象的引用保存到volatile类型的域或者AtomicReferance对象中;

  3.将对象的引用保存到某个正确构造对象的final类型域中;

  4.将对象的引用保存到一个由锁保护的域中;

 

 

并发程序中使用或者共享对象时,实用策略小结:

  1.线程封闭:临界资源只被一个线程拥有,对象被封闭在该线程中,并只有本线程才有修改权;

  2.只读共享:共享的对象可以由多个线程并发访问,但任何线程都不能修改它,共享的只读对象包括不可变对象和事实不可变对象;

  3.线程安全共享:线程安全对象在内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步;

  4.保护对象:只能通过锁来访问对象,包括封装在其它线程安全对象中的对象,以及已发布的并且某个特定锁保护的对象.






以上是关于Java并发的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程实战 04死锁了怎么办?

Java并发编程实战 04死锁了怎么办?

Java编程思想之二十 并发

java并发线程锁技术的使用

如何从设置中获取数据并发送到此片段

Swift新async/await并发中利用Task防止指定代码片段执行的数据竞争(Data Race)问题