Java的并发

Java的并发

文章目录

一、同步访问共享的可变数据

1.1 关键词synchronized

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

1.2 同步的概念

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

为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的

(1)同步是一种互斥的方式

当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态。

对象被创建的时候出于一致的状态。当有方法访问它的时候,它就被锁定了。这些方法观察到对象的状态,并且可能会引起状态转变,即把对象从一种一致的状态转换成另一种一致的状态。正确地使用同步可以保证没有任何方法会看到对象出于不一致的状态中。

(2)如果没有同步,一个线程的变化就不能被其他线程看到

1.3 Java语言规范保证读或者写一个变量是原子的,除非这个变量的类型为long或者double

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

你可能听说过,为了提高性能,在读或写原子数据的时候,应该避免使用同步。这个建议是非常危险的而错误的。虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数据,但是它并不保证一个线程写入的值对于另一个线程将是可见的

1.4 千万不要使用Thread.stop方法

因为Thread.stop本质上是不安全的——使用它会导致数据遭到破坏。

要阻止一个线程妨碍另一个线程,建议的做法是让第一个线程轮询一个boolean域。这个域一开始为false,但是可以通过第二个线程设置为true,以表示第一个线程将终止自己。

下面的例如,由于没有同步,就不能保证后台线程何时“看到“主线程对stopRequested对值所做的改变:

public class StopThread {

    private static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(()->{
           int i=0;
           while (!stopRequested){
               i++;
           }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

没有同步,虚拟机将把一下代码:

           while (!stopRequested){
               i++;
           }

转变成这样:

if(!stopRequested){
		while(true)
			i++;
}

这种优化称作提升。结果是一个活性失败:程序并没有得到提升。

修正这个问题的一种方式是同步访问stopRequested域。

1.5 除非读和写操作都被同步,否则无法保证同步能起作用

注意读方法和读方法都被同步了。

/**
 * @Date 2020/2/16
 * @Author lifei
 */
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 InterruptedException {
        Thread backgroundThread = new Thread(()->{
            int i=0;
            while (!stopRequested()){
                i++;
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

1.6 volatile修饰符的使用

StopThread中被同步方法的动作即使没有同步也是原子的。换句话说,这些方法的同步只是为了它的通信效果,而不是为了互斥访问。

如果stopRequested被声明为volatile,第二种版本的StopThread中的锁就可以省略。虽然volatile修饰符不执行互斥访问,但它可以保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值。

public class StopThread {

    private static volatile boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(()->{
            int i=0;
            while (!stopRequested){
                i++;
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

1.7 使用volatile时应该小心

因为增量操作符(++)不是原子的。下面的程序会出现**安全性失败**:这个程序会计算出错误的结果。

    // Broken - require synchronization!
    private static volatile int nextSerialNumber = 0;
    
    public static int generateSerialNumber(){
        return nextSerialNumber++;
    }

1.8 了解和使用类库

上面的代码还可以改用AtomicLong类,它是java.util.concurrent.atomic的组成部分。这个包为单个变量上进行免锁定、线程安全的编程提供了基本类型。

虽然volatile提供了同步的通信效果,但这个包还提供了原子性:

    private static final AtomicLong nextSerialNumberAtomic = new AtomicLong();
    
    public static long generateSerialNumberAtomic(){
        return nextSerialNumberAtomic.getAndIncrement();
    }

二、避免过度的同步

为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。

换句话说,在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。

从包含该同步区域的类的角度来看,这样的方法是外来的

这个类不知道该方法会做什么事情,也无法控制它。根据外来方法的作用,从同步区域中调用它会导致异常、死锁或者数据损坏。

2.1 异常和死锁

下面的代码,它实现了一个可以观察到的集合包装。该类允许客户端在将元素添加到集合中时预订通知。这就是观察者模式

ObservableSet这个类在可重用的ForwardingSet上实现的:

/**
 * @Date 2020/2/17
 * @Author lifei
 */
public interface SetObserver<E> {

    // Invoked when an element is added to the observable set
    void added(ObservableSet<E> set, E element);
}
/**
 * @Date 2020/2/17
 * @Author lifei
 */
public class ObservableSet<E> extends ForwardingSet<E> {
    public ObservableSet(Set<E> s) {
        super(s);
    }

    private final List<SetObserver<E>> observers = new ArrayList<>();

    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){
            // Calls notifyElementAdded
            result |= add(element);
        }
        return result;
    }
}

客户端代码,版本一:

    public static void main(String[] args) {
        ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
        set.addObserver((s, e)-> System.out.println(e));

        for (int i=0; i<100; i++){
            set.add(i);
        }
    }

客户端代码,版本二:

同步代码块可以防止并发的修改,但是无法防止迭代线程本身回调到可观察到集合中,也无法防止修改它的observers列表。

    public static void main(String[] args) {
        ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
        set.addObserver(new SetObserver<Integer>() {
            @Override
            public void added(ObservableSet<Integer> s, Integer element) {
                System.out.println(element);
                if (element==23){
                    s.removeObserver(this);
                }
            }
        });

        for (int i=0; i<100; i++){
            set.add(i);
        }
    }

上面的代码,打印出数字0~23,然后抛出ConcurrentModificationException。因为企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的,和下面的代码报错一样:

    private static void iterateDeleteTest(){
        Set<Integer> ia = new HashSet<>();
        ia.add(2);
        ia.add(3);
        ia.add(5);
        for (Integer i: ia){
            if (i==3){
                ia.remove(i);
            }
        }
    }

客户端代码,版本三:启动另一个线程完成删除操作

    private static void client03(){
        ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
        set.addObserver(new SetObserver<Integer>() {
            @Override
            public void added(ObservableSet<Integer> set, Integer element) {
                System.out.println(element);
                if (element == 23){
                    ExecutorService executor = Executors.newSingleThreadExecutor();
                    try {
                        executor.submit(()->set.removeObserver(this)).get();
                    } catch (InterruptedException | ExecutionException e) {
                        throw new AssertionError(e);
                    } finally {
                        executor.shutdown();
                    }
                }
            }
        });


        for (int i=0; i<100; i++){
            set.add(i);
        }
    }

上面的代码会遭遇死锁

运行这个程序时,没有遇到异常,而是遭遇了死锁。后台线程调用s.removeObserver,它企图锁定observers,但它无法获得该锁。因为主线程已经有所锁了。在这期间,主线程一直在等待后台线程来完成对观察者的删除,这正是造成死锁的原因。

由于Java程序设计语言中的锁事可重入的,这种调用不会死锁,就像客户端代码版本二一样,它会产生一个异常。

可重入的锁简化了多线程的面相对象程序的构造,但是它们可能会降活性失败变成安全性失败。

2.2 将外来方法移出同步代码块

尽量将同步区域的工作量限制到最少

将外来方法移出同步代码块,解决客户端版本二和三的异常、死锁问题。

    // 将外来的方法移出同步代码块
    private void notifyElementAdded(E element){
        List<SetObserver<E>> snapshot = null;
        synchronized (observers){
            snapshot = new ArrayList<>(observers);
        }
        for (SetObserver<E> observer : snapshot) {
            observer.added(this, element);
        }
    }

2.3 使用Java类库提供的并发集合

Java类库提供了一个并发集合,称作CopyOnWriteArraySet。下面的代码没有显式的同步:

    private final Set<SetObserver<E>> observers = new CopyOnWriteArraySet<>();
    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);
        }
    }

2.4 永远不要过度同步

在多核的时代,过度同步的实际成本并不是指获取锁所花费的CPU时间;而是指失去了并发的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。

2.5 编写一个可变的类

编写一个可变的类,有两种选择:

(1)省略所有的同步

省略所有的同步,如果想要并发使用,就允许客户端在必要的时候从外部同步,或者通过内部同步,使这个类变成线程安全的。

java.util中的集合(除了已经废弃的VectorHashtable之外)采用了这种方法(省略所有同步)。

(2)内部同步

通过内部同步,使这个类变成是线程安全的,还可以因此获得明显比外部锁定整个对象更高的并发性。

java.util.concurrent中的集合采用了这种方法(内部同步)。

三、executortaskstream优先于线程

尽量不要编写自己的工作队列,也尽量不直接使用线程。

3.1 Executor Framework

Java平台java.util.concurrent包中包含一个的Executor Framework,它是一个很灵活的基于接口的任务执行工具。

(1)能够创建更好的工作队列,只需要一行代码:

ExecutorService exec = Executors.newSingleThreadExecutor();

执行而提交一个runnable的方法:

exec.execute(runnable);

告诉executor如何优雅地终止(如果没有这么做,虚拟机可能不会退出

exec.shutdown();

(2)定时执行

ScheduledExecutorService

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(corePoolSize);

(3)缓存线程池

如果编写的是小程序,或者是轻量负载的服务器,使用缓存线程池是不错的选择。

ExecutorService executorService = Executors.newCachedThreadPool();

(4)固定线程数目的线程池

在大负载的产品服务器中,最好使用包含固定数目的线程池:

ExecutorService executorService = Executors.newFixedThreadPool(nThreads);

3.2 尽量不直接使用线程

Thread是既充当工作单元,又是执行机制。在Executor Framework中,工作单元和执行机制是分开的。现在关键的抽象是工作单元,称作任务(task)。

任务又两种:Runnable及其近亲Callable(它与Runnable类似,但它返回值,并且能够抛出任意的异常)。

执行任务的通用机制是executor service

如果从任务的角度来看问题,并让一个executor service替你执行任务,在选择适当的执行策略方面就获得了极大的灵活性。

从本质上讲,Executor Framework所做的工作是执行,Collections Framework 所做的工作是聚合。

四、并发工具优先于waitnotify

自从Java5发行版本开始,Java平台就提供了更高级的并发工具,它们可以完成以前必须在waitnotify上手写代码来完成的各项工作。

既然正确地使用waitnotify比较困难,就应该用更高级的并发工具来代替。

java.util.concurrent中更高级的工具分成三类:Executor Foramework、并发集合(Concurrent Collection)、同步器(Synchronizer)。

4.1 并发集合

并发集合中不可能排出并发活动;将它锁定没有什么作用,只会使程序的速度变慢。

并发集合接口已经通过依赖状态的修改操作进行了扩展,它将几个基本操作合并到了单个原子操作中。这些操作已经通过缺省的方法加入Java8对应的集合接口中。

例如,Map中的putIfAbsent方法

    private static String intern(String s){
        String result = map.get(s);
        if (result == null){
            result = map.putIfAbsent(s, s);
            if (result == null){
                result = s;
            }
        }
        return result;
    }

并发集合导致同步的集合大多被废弃了。比如,应该优先使用ConcurrentHashMap,而不是Collections.synchronizedMap

4.2 同步器(Synchronizer)

最常用的是CountDownLatchSemaphore,较不常用的是CyclicBarrierExchanger,功能最强大的同步器是Phaser

倒计时锁存器 (Countdown Latch),允许一个或多个线程等待一个或者多个其他线程来做某些事情。

CountDownLatch的唯一构造器带有一个int类型的参数,这个int参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown方法的次数。

下面的代码,是使用CountDownLatch的案例:

还有一个细节值得注意,传递给time方法的executor必须允许创建至少与制定并发级别一样多的线程。

否则这个测试将永远不会结束。这就是线程饥饿死锁。

对于间歇性的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMillis

因为System.nanoTime更准确,也更精确,它不受系统的实时时钟的调整所影响。

    /**
     *
     * @param executor 执行该动作的executor
     * @param concurrency 并发级别
     * @param action  并发执行的动作
     * @return
     */
    public static long time(Executor executor, int concurrency, Runnable action) throws InterruptedException {
        // 倒计时锁存器:这个int参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown方法的次数
        CountDownLatch ready = new CountDownLatch(concurrency);
        CountDownLatch start = new CountDownLatch(1);
        CountDownLatch done = new CountDownLatch(concurrency);

        for (int i=0; i< concurrency; i++){
            final int temp = i;
            executor.execute(()->{
                System.out.println("begin: " + temp);
                ready.countDown();
                try {
                    start.await();
                    System.out.println("end: " + temp);
                    action.run();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }finally {
                    done.countDown();
                }
            });
        }
        
        ready.await();
        long statNanos = System.nanoTime();
        start.countDown();
        done.await();
        return System.nanoTime() - statNanos;
    }
    public static void main(String[] args) {
        Runnable runnable = ()->{
            long start = System.nanoTime();
            int i=0;
            while (System.nanoTime() - start <2*1000){
                i++;
            }
            System.out.println("i: " + i);
        };
        ExecutorService exec = Executors.newFixedThreadPool(5);
        try {
            long time = time(exec, 5, runnable);
            System.out.println("time: " + time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        exec.shutdown();
    }

4.3 wait和notifyAll

没有理由在新代码中使用wait方法和notify方法,即使有,也是极少的。

wait

wait方法被用来使线程等待某个条件。它必须在同步区域内部被调用。

始终应该使用wait循环模式来调用wait方法;永远不要在循环之外调用wait方法。

synchronized(obj){
	while(<condition does not hold>){
    obj.wait(); // Releases lock, and reacquires on wakeup
  }
  ...// Perform action approgriate to condition
}

notifyAll

一般情况下,应该优先使用notifyAll方法,而不是使用notify方法。

如果使用notify方法,请一定要小心,以确保程序的活性。

五、线程安全性的文档

一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它锁支持的线程安全性级别。

5.1 线程安全性地级别

  • 不可变的(immutable)

    这个类的实例时不可变的,所以,不需要外部的同步。

  • 无条件的线程安全(unconditionally thread-safe)

    这个类的实例是可变的,但是这个类有着足够的内部同步,所以它的实例可以被并发使用,无须外部同步。

  • 有条件的线程安全(conditionally thread-safe)

    除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件的线程安全级别相同。

    例如:Collections.synchronized包装返回的集合,它们的迭代器需要外部同步。

        /**
         * It is imperative that the user manually synchronize on the returned
         * collection when traversing it via {@link Iterator}, {@link Spliterator}
         * or {@link Stream}:
         * <pre>
         *  Collection c = Collections.synchronizedCollection(myCollection);
         *     ...
         *  synchronized (c) {
         *      Iterator i = c.iterator(); // Must be in the synchronized block
         *      while (i.hasNext())
         *         foo(i.next());
         *  }
         * </pre>
         * Failure to follow this advice may result in non-deterministic behavior.
         *
         */
        public static <T> Collection<T> synchronizedCollection(Collection<T> c) {
            return new SynchronizedCollection<>(c);
        }
    
  • 非线程安全(not thread-safe)

    这个类的实例时可变的,为了并发地使用它们,客户端必须利用自己选择的外部同步包围每个方法调用(或者调用序列)。

    例如:ArrayList和HashMap

  • 线程对立的(thread-hostile)

    这种类不能被多个线程并发使用,即使所有的方法调用都被外部同步包围。

六、慎用延迟初始化

延迟初始化是指延迟到需要域的值时才将它初始化的行为。

如果永远不需要这个值,这个域就永远不会被初始化。

“延迟初始化”这种方法既适用于静态域,也适用于实例域。

5.1 延迟初始化是一把双刃剑

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

延迟初始化(就像其他的许多优化一样)实际上降低了性能。

要确定是否用延迟初始化,唯一的办法就是测量类在用和不用延迟初始化时的性能差别。

在大多数情况下,正常的初始化要优先于延迟初始化。

5.2 正常初始化和使用了同步访问的延迟初始化

  • 正常初始化

        // Normal initialization of an instance field
        private final FieldType field = computeFieldValue();
    
  • 使用了同步访问的延迟初始化

        // lazy initialization of instance field - synchronized accessor
        private FieldType field;
        private synchronized FieldType getField(){
            if (field == null)
                field = computeFieldValue();
            return field;
        }
    

5.3 (出于性能考虑)对静态域延迟初始化,就使用 Lazy initialization holder class模式

这种模式的魅力在于,getField方法没有被同步,并且只执行了一个域访问,因此延迟初始化实际上并没有增加任何访问成本。

    // lazy initialization holder class idiom for static fields
    private static class FieldHolder{
        static final FieldType field = computeFieldValue();
    }
    
    public static FieldType getField(){
        return FieldHolder.field;
    }

回顾单件模式(Singleton Partten)的一种设计方案:

public class Singleton(){
	
	private Singleton(){}
	
	private static class SingletonHolder(){
		private static final Singleton INSTANCE = new Singleton();
	}
	
	public static final Singleton getInstance(){
		return SingletonHolder.INSTANCE;
	}
}

5.4 (出于性能考虑)对实例域进行延迟初始化,就使用双重检查模式(double-check idiom)

下面代码中的局部变量result的作用时确保field只在被初始化的情况下读取一次。

    // Double-check idiom for lazy initialization of instance fields
    private volatile FieldType field;
    private FieldType getField(){
        FieldType result = field;
        if (result==null){
            synchronized (this){
                if (field==null){
                    field = result = computeFieldValue();
                }
            }
        }
        return result;
    }

5.5 单检查模式(single-check idiom)

有时候kennel需要延迟初始化一个可以接受重复初始化的实例域,就可以使用单检查模式(single-check idiom)

    // Single-check idiom - can cause repeated initialization
    private volatile FieldType field;
    
    private FieldType getField(){
        FieldType result = field;
        if (result == null){
            field = result = computeFieldValue();
        }
        return result;
    }

如果不在意是否每个线程都重新计算域的值,并且域的类型为基本类型,而不是long或者double类型,就可以选择从单检查模式的域声明中删除volatile 修饰符。这种变体称为racy single-check idiom。这显然是一种特殊的方法,不适合于日常的使用。

七、不要依赖于线程调度器

任何依赖于线程调度器来达到正确性或者性能要求的程序,都有可能是不可移植的。

如果线程没有在做有意义的工作,就不应该运行。

不要企图通过调用Thread.yield来“修正”该程序。

Thread.yield没有可测试的语义。

线程优先级是Java平台上最不可移植的特性了。

线程优先级可以用来提供一个已经能够正常工作的程序的服务质量,但永远不应该用来“修正”一个原本并不能工作的程序。

原创文章 161 获赞 19 访问量 6万+

猜你喜欢

转载自blog.csdn.net/hefrankeleyn/article/details/104455925