并发

来源:互联网 发布:linux 内核版本 编辑:程序博客网 时间:2024/06/05 08:41

66.同步访问共享的可变程序

读或写一个变量是原子的,除了long和double,读取一个非long或double类型的变量,可以保证返回的值是某个线程保存在该变量中的,即使多个线程在没有同步的情况下并发地修改这个变量也是如此

关键词synchronized可以保证同一时刻,只有线程可以执行某一种方法,或者一个代码块。

同步不仅可以阻止一个线程看到对象处于不一致的状态之中,还可以保证进入同步方法或者同步代码块的每一个线程,都看到由同一个锁保护的之前所有的修改效果

public class StopThread {private static boolean stopRequested;public static void main(String[] args) {Thread backgroundThread = new Thread(new Runnable() {@Overridepublic void run() {int i = 0;while (!stopRequested) {i++;}}});backgroundThread.start();stopRequested = true;}}
这个程序永远不会停止,因为后台程序永远在循环

由于没有同步,不能保证后台线程何时看到主线程对stopRequested的值所做的改变,没有同步,虚拟机将这个代码转变成这样

while(!done){i++;}
变成

if(!done){while (true ) {i++;}}
这种优化称为提升,结果是个活性失败

修正的一种方法是采用同步访问

public 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 Exception {Thread backgroundThread = new Thread(new Runnable() {@Overridepublic void run() {int i = 0;while (!stopRequested()) {i++;}}});backgroundThread.start();TimeUnit.SECONDS.sleep(1);requestStop();}}
写方法(requestStop)和读方法(stopRequested)都被同步了,读和写没有都被同步,同步不会起作用

stopThread中被同步方法的动作即时没有同步也是原子性的,这些同步只是为了更好的通信,而不是互斥

可以stopRequested声明为volatile。虽然它不执行互斥访问,但可以保证任何一个线程在读取该域的时候都将看到最近被写入的值

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

下面这个有问题,操作符++不是原子的,它先读取值,然后返回一个新值,相当于原来的值加上一,第二个线程在第一个线程读取旧值和写回新值期间读取这个域,第二个线程就会与第一个线程一起看到同一个值,并返回相同序列号,这就是安全性失败

private static volatile int nextSerialNumber = 0;public static int generateSerialNumber(){return nextSerialNumber++;}
一种解决方法是,在它的声明中增加synchronized修饰符,确保多个调用不会较差存取,可以删除volatile,为了更可靠,用long代替int,或者在nextSerialNumber快要重叠时抛出异常

更好的做法是使用AtomicLong

private static final AtomicLong nextSerialNum = new AtomicLong();public static long generateSerialNumber(){return nextSerialNum.getAndIncrement();}
不共享可变的数据,要么共享不可变的数据,将可变数据限制在单个线程中

当多个线程共享可变数据时,每个读或者写数据的线程都必须执行同步,如果只需要线程之间的交互通信,而不需要互斥,volatile就是一个可以接受的同步形式

67.避免过度同步
过度同步可能会导致性能下降,死锁,甚至不确定行为

将外来方法的调用移除同步的代码块

private void notifyElementAdded(E element){List<SetObserver<E>> snapshot = null;synchronized (observers) {snapshot = new ArrayList<>();}for (SetObserver<E> setObserver : snapshot) {observers.added(this,element);}}

还可以使用CopyOnWriteArrayList,这是ArrayList的一种变体,通过重新拷贝整个底层数组,在这里实现所有的写操作,由于内部数组永远不改动,因此迭代不需要锁定,速度也快,大量使用,性能将大受影响

private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<>();public void addObserver(SetObserver<E> observer){observer.add(observer);}
68.executer和task优先于线程
ExecutorService eService =Executors.newSingleThreadExecutor();eService.execute(runnable);eService.shutdown;
就是线程池

69.并发工具优先于wait和notify
70.线程安全性的文档化

71.慎用延迟初始化

延迟初始化降低了初始化类或者创建实例的开销,却增加了访问被延迟初始的域的开销

如果域只在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化;如果俩个或多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的
正常初始化

private final FieldType field = computeFieldValue();

延迟初始化,要用到同步访问方法

private FieldType field;synchronized FieldType getField(){if(field == null){field = computeFieldValue();}return field;}
出于性能的考虑需要对静态域使用延迟初始化,第一次读取时被初始化
private static class FieldHolder{static final FieldType field = computeFieldValue();}static FieldType getField(){return FieldHolder.field;}
出于性能考虑需要对实例域使用延迟初始化,使用双重检查模式,这种模式避免了在域被初始化后访问这个域时的锁定开销,俩次检查域的值,第一次检查时没有锁定,看看这个域是否被初始化了,第二次检查有锁定,只有当第二次检查时表明这个域没有被初始化,才进行初始化,如果域已经被初始化就不会有锁定,result变量为了确保field只在已经被初始化的情况下读取一次,可以提高性能。
private volatile FieldType field;FieldType getField(){FieldType result = field;if(result == null){synchronized (this) {result = field;if(result == null){field = result = computerFieldValue();}}return result;}
延迟初始化一个可以接受重复初始化的实例域,使用单冲检查模式

private volatile FieldType field;private FieldType getField(){FieldType result = field;if(result == null){field = result = cpmputeFieldValue();}return result;}
不在意是否每个线程都重新计算域的值,并且域的类型为基本类型,而不是long或者double,可以从单重检查模式的域中删除volatile,加快了某些域上的域访问,增加了额外的初始化,(直到访问该域的每个线程都进行一次初始化)
72.不要依赖于线程调度器
任何依赖于线程调度器来达到正确性或者性能要求的程序,很可能都是不可移植的

确保可运行线程的平均数量不明显多于处理器的数量

保持可运行线程数量尽可能少的主要办法是,让每个线程做些有意义的工作,然后等待更多有意义的工作

过多的线程处于等待状态,性能会极大的降低

如果一个程序不能工作,是由于某些线程无法获得足够的cpu时间,不要用Thread.yield来修正,因为不可移植,更好的解决办法是重构应用程序,减少可并发运行的线程数量。

调整线程优先级不可移植,用它解决严重的活性问题不合理,在找到并修正底层的真正原因之前,这个问题可能还会出现

73.避免使用线程组