为了账号安全,请及时绑定邮箱和手机立即绑定

Android基础进阶之EffectiveJava翻译系列(第九章:并发)

标签:
Android

线程允许多个任务同时执行.并发编程比单线程难,因为很多事情一起处理容易出错,也很难减少错误,但是你不能避免并发.这章帮助你编写简洁的,正确的,良好阅读性的并发编程

Item66 同步共享的可变数据

synchronized 关键字可以保证一次只有一个线程访问代码块,许多开发者认为同步就是一种互斥,防止对象在另一个线程修改时处于不一致的状态.在这种观点中,对象处于一种正确的状态,因为访问它的方法锁住了.这些方法确保对象的状态由一种状态安全的转移到另一种状态.

这种观点只正确了一半,不同步的话,一个线程的改变对其它线程是不可见的.通过相同的锁,同步不仅阻止线程在不一致状态下观察对象,而且确保每个进入同步方法或块的线程都能看到所有一致性的效果.

考虑一下从一个线程停止另一个线程,Java lib提供了Thread.stop方法,但是这个方法被遗弃了,因为它是不安全的---将导致数据损坏.一种建议方法是获取到第一个线程的boolean变量,一些人可能会这么写:

//badpublic class StopThread {private static boolean stopRequested;public static void main(String[] args)
    throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {            public void run() {            int i = 0;            while (!stopRequested)
                i++;
            }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
        }
}

你可能期望这个程序运行大约一秒,然后主线程设置stopRequested为true,从而导致后台线程的循环终止 .然而在我的机器上,程序永远不会停:子线程永远在循环!

问题在于,在没有同步的情况下,无法保证后台线程何时会看到主进程所做的修改. 在没有同步的情况下,虚拟机转换成以下代码:

while (!done)
    i++;//转换if (!done)    while (true)
        i++;

修复方式如下:

//goodpublic class StopThread {    private static boolean stopRequested;    private static synchronized void requestStop() {
        stopRequested = true;
    }    private static synchronized boolean stopRequested() {        return stopRequested;
    }public static void main(String[] args)
    throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {            public void run() {                int i = 0;                while (!stopRequested())
                    i++;
            }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

注意:读和写都是同步的,光对写方法同步,同步会失效

还可以使用volatile关键字修复为:

public class StopThread {    private static volatile boolean stopRequested;    public static void main(String[] args)
        throws InterruptedException {
            Thread backgroundThread = new Thread(new Runnable() {                public void run() {                    int i = 0;                    while (!stopRequested)
                        i++;
                }
            });
            backgroundThread.start();
            TimeUnit.SECONDS.sleep(1);
            stopRequested = true;
    }
}

但使用volatile关键字要小心,考虑如下代码

//badprivate static volatile int nextSerialNumber = 0;public static int generateSerialNumber() {    return nextSerialNumber++;
}

乍看之下没有什么问题,但是"++"操作不是原子性的,包含了两个操作,一个是读旧值,另一个是在旧值的基础上加一,在赋值.修复方式为加上synchronized关键字:

//goodprivate static volatile int nextSerialNumber = 0;public static synchronized int generateSerialNumber() {    return nextSerialNumber++;
}

避免此类问题最好的方式是不要共享可变数据.要么共享不可变的数据,要么就不共享.换句话说,在一个线程中定义可变数据.如果采用此策略,则必须将其文档化,以便程序维护此原则

总之,当多个线程共享数据时,读取或写入数据的每个线程都必须执行同步.没有同步,无法保证一个线程的修改对另一个线程可见.这将会导致程序安全问题,而且很难调试.如果你只需要内部间的线程通信而不考虑互斥, volatile 关键字可以替代synchronized,但是volatile很难被正确使用


Item 67: 避免过度同步

Item66警示了不使用同步的危险性,Item 67讨论完全相反的一面.在某种情况下,过度使用同步会导致性能下降,死锁或者不可预期的行为

为了避免重复和安全故障,千万不要在同步方法或块内控制客户端.换句话说,不要再同步方法中调用需要重写的方法或者从客户端提供的对象方法(这种方法被称为"外星人").因为同步块不知道这个方法是干什么的也不能控制这个客户端,调用它将会导致异常或者数据损坏

考虑如下的"外星人"代码

//bad Broken - invokes alien method from synchronized block!public class ObservableSet<E> extends ForwardingSet<E> {  public ObservableSet(Set<E> set) { super(set); }  private final List<SetObserver<E>> observers = 
                    new ArrayList<SetObserver<E>>();  public void addObserver(SetObserver<E> observer) {      synchronized(observers) {
          observers.add(observer);
      }
  }  public boolean removeObserver(SetObserver<E> observer) {      synchronized(observers) {          return observers.remove(observer);
      }
  }  private void notifyElementAdded(E element) {      synchronized(observers) {        for (SetObserver<E> observer : observers)
            observer.added(this, element);
      }
  }@Override public boolean add(E element) {    boolean added = super.add(element);    if (added)
        notifyElementAdded(element);    return added;
}@Override public boolean addAll(Collection<? extends E> c) {    boolean result = false;    for (E element : c)
        result |= add(element); 
    return result;
    }
}

observers通过addObserver/removeObserver 订阅/取消订阅,但是SetObserver<E> observer被传递进来了:

public interface SetObserver<E> {    // Invoked when an element is added to the observable set
    void added(ObservableSet<E> set, E element);
}

在测试运行时,上述代码似乎运行良好,如打印0~99:

public static void main(String[] args) {
    ObservableSet<Integer> set =        new ObservableSet<Integer>(new HashSet<Integer>());    set.addObserver(new SetObserver<Integer>() {        public void added(ObservableSet<Integer> s, Integer e) {
        System.out.println(e)
    }});    for (int i = 0; i < 100; i++)        set.add(i);
}

现在我们来做一点改变:

set.addObserver(new SetObserver<Integer>() {    public void added(ObservableSet<Integer> s, Integer e) {
        System.out.println(e);        if (e == 23) s.removeObserver(this);
    }
});

我们会期望打印出0-23,实际上会发生什么呢,打印出0~23后接着会报ConcurrentModificationException.因为我们正在移除集合中的元素,此时notifyElementAdded正在遍历集合

解决方法是:

//good 将"外星人"代码移除同步块private void notifyElementAdded(E element) {
    List<SetObserver<E>> snapshot = null;    synchronized(observers) {
        snapshot = new ArrayList<SetObserver<E>>(observers);
    }    for (SetObserver<E> observer : snapshot)
        observer.added(this, element);
}

事实上,有一种更好的方式移除同步代码中的外星人代码,在JDK1.5之后,Java提供了一系列并发集合,如 CopyOnWriteArrayList刚好可以解决上面的问题,很适合于观察者模式:

//good Thread-safe observable set with CopyOnWriteArrayListprivate final List<SetObserver<E>> observers = 
    new CopyOnWriteArrayList<SetObserver<E>>();public void addObserver(SetObserver<E> observer) {
    observers.add(observer);
}public boolean removeObserver(SetObserver<E> observer) {    return observers.remove(observer);
}private void notifyElementAdded(E element) {    for (SetObserver<E> observer : observers)
        observer.added(this, element);
}

第一部分讨论了正确性,现在我们看看性能.在多核时代,过度同步的开销并不在CPU获取锁身上,真正的开销在于平行执行的机会和多个核心CPU保持一致的内存记忆模型.过度同步另一个隐藏的开销是减少了虚拟机优化代码块的执行

并发使用的前提是保证可变对象线程安全,我们仅仅需要在需要同步的地方同步,而不是将整个对象同步.因为这个原因,在1.5的版本,StringBuffer被StringBuild替代了,不要将整个对象同步,而仅仅告知它是非线程安全对象,然后客户端调用的时候在做适当处理

总之,为了避免死锁和数据损坏,不要写"外星人"代码,尽量减少同步块中的工作量.在多核时代,更重要的是不使用同步.当你需要设计一个可变类的时候,好好想想在该同步的地方同步,不要过度


Item 68:使用Java或者Android平台提供好的线程工具类

不要直接使用thread,因为它控制不住

在JDK1.5中,java.util.concurrent提供了基于接口的任务类框架Executor,创建了一种更好的工作流方式:

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(runnable);//执行executor.shutdown();//优雅的通知停止

你可以用ExecutorService做很多事情,比如你可以等待特定任务完成,可以等待任务停止(用awaitTermination方法)等等

如果你想多个线程做任务队列里的工作,可以通过线程池创建不同的ExecutorService.  java.util.concurrent.Executors提供了很多你需要的静态工厂方法,你也可以直接使用 ThreadPoolExecutor.

对特定的任务选择合适的线程池.如果你想写一个小的轻量级的加载服务,用Executors.newCachedThreadPool是一个很好的选择.但是这个线程池不适合做很重的操作,Executors.newFixedThreadPool是一个很好的选择,内部维护了一个固定数量的线程.



作者:青楼爱小生
链接:https://www.jianshu.com/p/a358f3178d22


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消