《Java并发编程实战》2.1笔记发布与逸出、线程封闭、不变性

发布与逸出

发布一个对象的意思是使对象能够在当前作用域外的代码中使用。
当某个不应该被发布的对象被发布了则成为逸出(比如再对象构造之前就发布该对象)。
注意发布一个对象,它的非私有的属性也同样会被发布。
比如一种通过内部类发布对象,这时就会将本类的this引用发布出去(内部类能够调用外部类的属性),所以不要在构造函数中发布某内部对象,这时可能外部对象尚未构造完全。
比如下面的代码就会与预期的不一样。

class Test {//绑定监听器并做一些其他操作
    private int vv;
    Test(Out j) throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+"进入Test构造方法");
        j.register((x)->{
            System.out.println(x);
            doSomething();
        });
        Thread.sleep(100);//模拟其他初始化操作
        vv= 88;
        System.out.println(Thread.currentThread().getName()+"即将退出Test构造方法");
    }
    private void doSomething(){
        System.out.println(vv+"干点什么");
    }
}
class Out {//类似监听器类
    public static List<F> fs = Collections.synchronizedList(new ArrayList());
    public void register(F i){
        fs.add(i);
        System.out.println(Thread.currentThread().getName()+"注册成功");
    }
}
interface F{//实现某些行为的接口
    void move(String s);
}

测试代码:

 public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(2);
            service.execute(()->{
                try {
                    new Test(new Out());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            service.execute(()->{
                while (Out.fs.size()!=1) {
                }
                Out.fs.get(0).move("aaaaaa");
            });
        service.shutdown();
    }

就会发现输出结果是这个亚子的:
在这里插入图片描述
发现vv未被正确初始化。如果之后再开个线程这时就被正确初始化了,也就是说在结果出来之前,处于正确与不正确的叠加态 (薛定谔的初始化)
那么该怎么正确处理呢?很简单,将发布对象调整到某个方法当中,这时,对象肯定已经初始化完全了。

public Test newInstance(Out t){
        t.register((x)->{
            System.out.println(x);
            doSomething();
        });
        return this;
    }

线程封闭

如果不使用共享数据,自然就不会出现线程不安全的情况,这个方法就是这样的原理,将对象封闭到一个线程中。这就需要对象不要从该线程逸出。主要用于读数据再进行一些中间操作。(如Swing、JDBC)

Ad-hoc线程封闭

维护线程封闭性的职责完全由程序实现来承担,特别的脆弱,一般不用。比如使用volatile变量,只要保证了只有单个线程对共享的volatile执行写操作就可避免竞态条件的产生。

栈封闭

栈封闭是线程封闭的一种特例,只能通过局部变量才能访问对象,而局部变量本身就是属于当时执行该方法的线程的。比Ad-hoc线程封闭更易于维护,更健壮。我决定这样的好处就在于可以个线程独立的执行一些内部处理操作。

class StackFB{
    public int addTheList(Collection<String> loginIPs){
        Set<String> ips;
        int numIP = 0;
        ips = new HashSet<>();
        ips.addAll(loginIPs);
        for(String s:ips){
            IP.add(s);//add方法还是得加锁,因为用了共享数据且有竞争
            numIP++;
        }
        return numIP;
    }
}

TreadLocal

该类能使得线程中的某个值和保存值的对象关联起来。它提供了get、set等访问接口或方法,这些方法为每个使用该变量的线程提供一份独立的副本。通常用于防止对可变的单实例变量或全局变量的共享。
当某线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值,ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置,当线程终止后,这些值会被作为垃圾回收。
将一个单线程应用移植到多线程环境时,通过将共享的全局变量转换为ThreadLocal对象,可以维持线程安全性。
大佬的源码级讲解(俺受益匪浅)

不变性

满足同步需求的另一种方法是使用不变性对象。如果某个对象在被创建后其状态不能被修改便被称为不变性对象。不变性对象一定是线程安全的!
不可变对象需要满足:

  1. 对象创建后状态就不被修改
  2. 对象的所有域都是final类型
  3. 对象是正确创建的(创建期间this引用没有逸出)

Final域

除非更高的的可见性需求,那么所有域都应该声明为私有,除非需要某个域可变,否则全申明为final域。
从代码可以看出,这个类将其内部变量全部final化了,而且也没有发布任何内在对象。有点妙啊

public class OneValueCache{
    private final BigInteger num;
    private final BigInteger[] fac;
    public OneValueCache(BigInteger i,BigInteger[] bs){
        num = i;
        fac = bs==null?null: Arrays.copyOf(bs,bs.length);
    }
    public BigInteger[] getFac(BigInteger i){
        if(num==null||!num.equals(i)){
            return null;
        }
        return Arrays.copyOf(fac,fac.length);
    }
}

不过不知道为啥俺用来测试的Servlet加载得很慢,有大佬帮忙看看吗?

public class StatelessFactorizer extends HttpServlet {
    private int num;
    private static final long serialVersionUID = 554L;
    private volatile OneValueCache cache ;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        resp.setContentType("text/html");
        PrintWriter writer = resp.getWriter();
        writer.println("<html><head>Test</head><body><form method = 'post'><table><tr><input name='number'/></tr><tr>" +
                "<input type='submit'/></tr></table>" +
                "</form><br/>"+(num++)+"</body></html>");

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=utf-8");
        BigInteger b = extractFromRequest(req);
        BigInteger[] factor = cache==null?null:cache.getFac(b);
        if(factor==null){
            factor =  factor(b);
            cache = new OneValueCache(b,factor);
        }
        encodeIntoResponse(resp,factor);

    }

    private void encodeIntoResponse(ServletResponse servletResponse, BigInteger[] factors) throws IOException {
        PrintWriter writer = servletResponse.getWriter();
        writer.print("<html><head>jieguo</head><body><table>");
        int count = 1;
        for(BigInteger i:factors){
            writer.println("<tr><td>第"+(count++)+"对<td><td>"+i+"<td></tr>");
        }
        writer.print("</table><a href='/test2/stateless'>返回</a></body></html>");
    }

    private BigInteger[] factor(BigInteger i) {
        Set<BigInteger> bigSet = new HashSet<>();
        for(int j=1;!bigSet.contains(BigInteger.valueOf(j));j++){
            if(i.mod(BigInteger.valueOf(j)).equals(BigInteger.ZERO)){
                bigSet.add(BigInteger.valueOf(j));
                bigSet.add(i.divide(BigInteger.valueOf(j)));
            }
        }
        return bigSet.toArray(new BigInteger[0]);
    }

    private BigInteger extractFromRequest(ServletRequest servletRequest) {
        return new BigInteger(servletRequest.getParameter("number"));
    }
}

正确发布一个对象可以采用以下方式:

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

按对象的可变性可以采用以下发布方式:

  • 不可变对象可以通过任意机制发布
  • 事实不可变对象(就是技术上可变但是创建后并不会改变的对象)必须通过安全方式来发布
  • 可变对象必须通过安全方式来发布,并且是线程安全的或者由某个锁锁起来。
    并发程序中使用和共享变量可采取以下实用策略:
  • 线程封闭:只由一个线程拥有。
  • 只读共享:允许多个线程并发访问,但不可修改它。包括不可变对象和事实不可变对象
  • 线程安全共享:线程安全的对象再其内部实现同步。
  • 保护对象:只有持有特定的锁才能访问。
发布了18 篇原创文章 · 获赞 1 · 访问量 270

猜你喜欢

转载自blog.csdn.net/qq_38732834/article/details/105226069