并发基础(一)

image

前言

跟着 The Java Tutorials 把并发的一些基础过了一遍,发现仍然还是有很多不清楚的地方,主要是因为平常没有机会实际应用吧,理论知识要有,实践也很重要,哪怕是写些小 demo 也可以的。

虽然主要是跟着 tutorials 的 concurrency 章节整理的,但这并不是官方文档的一个翻译哈,看到一个地方有前置技能不足的时候,就会穿插着一些对 API 的学习之类的。

文章有点长,分为两篇,此篇为 并发基础(一),主要是一些简单的并发基础知识。

Concurrency Foundation

1. Synchronized

Java 提供了两种基础的同步语法:1. synchronized 方法 2. synchronized 语句块

同步是在内置锁(intrinsic lock)或者叫做监视锁(monitor lock)的基础上建立的(API 中通常将其称为 monitor),这个 monitor 在同步的两个方面发挥作用: 1. 强制性的单独访问对象 2. 建立 happens-before 关系

每个对象都有自己的内置锁,如果有线程想要访问这个对象,需要先获取这个对象的内置锁,那么此时其他线程是无法获取这个锁的,也就是无法访问这个对象,直至先前的线程释放锁。

synchronized 就是 Java 对内置锁的支持。

  1. synchronized 方法

当线程调用 synchronized 方法时,就会自动获取该方法对象的 synchronized 锁,返回时才会释放,即使是由没被 catch 的异常返回的,也会释放锁。

那么,对于 static synchronized method 呢?这个让人有些迷惑,因为 static method 是和类关联的,而不是对象。

Intrinsic Locks and Synchronization (The Java™ Tutorials > Essential Classes > Concurrency)

In this case, the thread acquires the intrinsic lock for the Class object associated with the class. Thus access to class’s static fields is controlled by a lock that’s distinct from the lock for any instance of the class.

根据文档的描述,这种情况下,对类的静态域的访问和对类的普通实例的访问所获取的锁是不同的。

其实就是类锁和对象锁的区别,类锁也就是 static synchronized method 和 synchronized(xx.class){} 所使用的锁,对象锁就是 non-static synchronized method 和 synchronized(xxx){} 所使用的锁。

因为这两个锁是不同的,所以如果对同一个类 A,线程 1 想获取类 A 对象实例的锁,和线程 2 想获取类 A 的类锁,这两个线程是不存在竞争关系的。

下面看看 synchronized method 的具体作用:

  • 作用:
    • 同一个对象上的两个 synchronized 方法的调用不会交替进行,只有一个线程释放后其他线程才能获取这个锁
    • 当一个 synchronized 方法退出后,它会自动和后面再对这个对象的 synchrnoized 方法的调用建立一个 happens-before 关系,这可以保证这个对象的状态的改变对其他线程都可见。
  • 需要注意的是,构造器是不能被同步的,这是语法上面的要求,这好理解,构造器本来就是只有创建对象的线程才能访问,所以不需要同步。

这个 warning 值得注意

Synchronized Methods (The Java™ Tutorials > Essential Classes > Concurrency)

Warning: When constructing an object that will be shared between threads, be very careful that a reference to the object does not "leak" prematurely. For example, suppose you want to maintain a List called instances containing every instance of class. You might be tempted to add the following line to your constructor:

instances.add(this);

But then other threads can use instances to access the object before construction of the object is complete.

这是在多线程中很容易忽略的一个问题,就是在还没有构建对象完成时,其他线程就已经访问了这个对象。

  • synchronized 算是一种简单直接的方式去解决多线程之间的干扰和内存一致性错误,但是可能会带来活跃性的问题。
  1. synchronized 语句和可重入的同步

这部分以及活跃性问题在之前的博客里有详细写了 线程安全性

2. Atomic Access

原子性的动作是一次完成的

Atomic Access (The Java™ Tutorials > Essential Classes > Concurrency) >

  • Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).
  • Reads and writes are atomic for all variables declared volatile (including long and double variables).

翻译过来就是:

  • 对引用变量和除了 long 和 double 的基本数据类型的读、写都是原子操作
  • 对 volatile 修饰的变量的读、写也是原子操作。

这段我看了之后有点迷,因为之前的概念中就是 volatile 只能保证被修饰变量的可见性而不能保证原子性,为何文档中说 “Reads and writes are atomic for all variables declared volatile”?还有就是,对引用变量的读写是原子操作?

想了想,我觉得他想表达的意思是这样的:

首先,文档中说的读写,肯定指的是单独的读、单独的写,比如 int a = 1; 这句肯定是一个原子操作,就是向 a 写入值 1,这很容易看出来。

可如果是 b = a; 呢?这样就很容易让人感到迷惑了,猛然一看,这就是对引用变量的写入,但是,它应该是分为几步操作的,1. 读取 a 2. 写入 b,如果说单独对 a 的读取和单独对 b 的写入,这些都是原子操作,文档中说的是没有错的,可是实际上是对于 a 这个变量引用,它是随时有可能被更新的,也就是说 b = a; 可能被分解为 1. 读取 a 2. 写入 b 3. 写入 a,这个时候 b = a; 这个语句就会带来错误。

因为这种情况其实是很容易遇到的,所以给我的印象就是 b = a; 这种对引用变量的读写通常并不是原子操作,所以看到文档中这段话感到很迷惑,文档的描述是正确的,只不过他所界定的原子操作和读写的概念,指的是一种最窄的概念,并且,对于 b = a; 这种对引用变量的赋值,之所以出现问题,实际上是因为对变量 a 可见性没有保证,这就不能让原子操作背这个锅了。

文档中后面也说到了,虽然原子操作是完整的操作,使用它们可以不用担心线程干扰,但是有时仍然需要对原子操作进行同步,因为即使是原子操作也避免不了可能出现的内存一致性错误。

Atomic actions cannot be interleaved, so they can be used without fear of thread interference. However, this does not eliminate all need to synchronize atomic actions, because memory consistency errors are still possible.

所以说,使用简单的原子操作访问会比用 synchronized 访问变量会更有效,但是这就需要我们考虑到内存一致性的问题,因此需要使用哪种方式访问变量,就要看应用的规模和复杂程度了。

3. Guarded blocks

多线程之间肯定少不了协同工作,最常见的方式就是使用 Guarded block:

public void guardedJoy() {
    // Simple loop guard. Wastes
    // processor time. Don't do this!
    while(!joy) {}
    System.out.println("Joy has been achieved!");
}

这就是一个 Guarded block,它会不断地检查 joy 这个值,而 joy 是由另一个线程所设置的。

不过,像上面这样的循环等待对资源也太浪费了,可以采用另一种方式:

public synchronized void guardedJoy() {
    // This guard only loops once for each special event, which may not
    // be the event we're waiting for.
    while(!joy) {
        try {
            wait();
        } catch (InterruptedException e) {}
    }
    System.out.println("Joy and efficiency have been achieved!");
}

这里使用到了 Object.wait()

Object (Java Platform SE 8 )

Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object. In other words, this method behaves exactly as if it simply performs the call wait(0).

它的作用就是让调用该方法的线程进入 WAITING 状态,直到有其他线程调用该对象的 notify() or notifyAll() 方法

The current thread must own this object’s monitor. The thread releases ownership of this monitor and waits until another thread notifies threads waiting on this object’s monitor to wake up either through a call to the notify method or the notifyAll method. The thread then waits until it can re-obtain ownership of the monitor and resumes execution.

调用 wait() 之前需要先获取这个对象的锁,一旦调用之后就会释放对象的锁,然后等待,直到其他线程调用 notify() or notifyAll(),但也不是能让等待的线程立马从 wait() 返回,而是要等到调用 notify() or notifyAll() 的线程释放锁之后,等待线程才有机会从 wait() 返回,这个机会就是:当前线程可以重新获取锁的持有权限,然后才能继续执行。

还是回到 guardedJoy 那个例子,这个版本的 guardedJoy 变成了 synchronized 的了,为什么呢?刚刚介绍 wait() 方法时说到了,一个线程想要调用一个对象的 wait() 方法首先要获取到这个对象的锁,而获取锁的最简单的方式就是在 synchronized 方法里调用 wait() 啦

那么,当 Thread1 调用 wait() 时,就会释放锁然后挂起。当其他线程 Thread2 请求获取这个锁并成功后调用 notifyAll() 通知等待的所有线程,在 Thread2 释放这个锁后,等待的线程 Thread1 再次申请就可以重新获得这个锁,之后就可以从 wait() 方法返回继续执行了。

public synchronized notifyJoy() {
    joy = true;
    notifyAll();
}

关于 notify(),这个方法只会唤醒一个线程,并且不允许指定唤醒哪个线程,这是可能会发生死锁的。以生产者消费者问题为例,假设分别有 2 个consumer 和 producer,缓冲区大小为 1,有可能你唤醒的那个线程可能恰好是相同角色的线程,也就是说现在可能是一个 consumer 唤醒了另一个 consumer,本来 consumer 想要唤醒的是 producer,缓存区仍然为空的,但是 producer 却还在 wait,因为错过了被 consumer 唤醒的机会,从而就会产生死锁。

那么什么时候可以使用 notify() 呢?文档中是这样给的建议:

Guarded Blocks (The Java™ Tutorials >Essential Classes > Concurrency)

Note: There is a second notification method, notify, which wakes up a single thread. Because notify doesn’t allow you to specify the thread that is woken up, it is useful only in massively parallel applications — that is, programs with a large number of threads, all doing similar chores. In such an application, you don’t care which thread gets woken up.

所以 notify() 一般只在大规模并发应用(即系统有大量相似任务的线程)中使用。因为对于大规模并发应用,我们其实并不关心哪一个线程被唤醒。

看一下官网给的生产者消费者的示例程序:

数据通过 Drop 对象共享信息:

public class Drop {
    // Message sent from producer
    // to consumer.
    private String message;
    // true 表示为空,需要等待生产数据
    // false 表示可以取数据了
    private boolean empty = true; 

    public synchronized String take() {
        // Wait until message is
        // available.
        while (empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = true;
        // Notify producer that
        // status has changed.
        notifyAll();
        return message;
    }

    public synchronized void put(String message) {
        // Wait until message has
        // been retrieved.
        while (!empty) {
            try { 
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = false;
        // Store message.
        this.message = message;
        // Notify consumer that status
        // has changed.
        notifyAll();
    }
}

生产者:

import java.util.Random;

public class Producer implements Runnable {
    private Drop drop;

    public Producer(Drop drop) {
        this.drop = drop;
    }

    public void run() {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };
        Random random = new Random();

        for (int i = 0;
             i < importantInfo.length;
             i++) {
            drop.put(importantInfo[i]);
            try {
                // 随机的暂停一段时间,接近实际中情况
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
        drop.put("DONE");
    }
}

消费者:

import java.util.Random;

public class Consumer implements Runnable {
    private Drop drop;

    public Consumer(Drop drop) {
        this.drop = drop;
    }

    public void run() {
        Random random = new Random();
        for (String message = drop.take();
             ! message.equals("DONE");
             message = drop.take()) {
            System.out.format("MESSAGE RECEIVED: %s%n", message);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
    }
}

主线程

public class ProducerConsumerExample {
    public static void main(String[] args) {
        Drop drop = new Drop();
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}

4. Immutable Objects

在并发编程中,一种被普遍认可的原则就是:尽可能的使用不可变对象来创建简单、可靠的代码。 因为 immutable objects 创建后不能被修改,所以不会出现由于线程干扰产生的错误 or 内存一致性错误。

但有时候会有这种担忧,就是使用 immutable objects 每次创建新对象的开销会不会太大,如果不使用 immutable objects,就可以避免创建过多新对象,只需要 update 就可以。

而文档中说到,这种创建对象的开销常常被过分高估,因为使用不可变对象所带来的一些效率提升可以抵消这种开销。

e.g. 使用不可变对象降低了垃圾回收所产生的额外开销,同时也可以减少一些为了维护在并发中的 mutable objects 的代码开销。

看一下举得这个例子:

public class SynchronizedRGB {

    // Values must be between 0 and 255.
    private int red;
    private int green;
    private int blue;
    private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public SynchronizedRGB(int red,
                           int green,
                           int blue,
                           String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public void set(int red,
                    int green,
                    int blue,
                    String name) {
        check(red, green, blue);
        synchronized (this) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }
    }

    public synchronized int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public synchronized String getName() {
        return name;
    }

    public synchronized void invert() {
        red = 255 - red;
        green = 255 - green;
        blue = 255 - blue;
        name = "Inverse of " + name;
    }
}

假如现在 线程1 在执行下列代码,已经执行到 Statement 1,然后 线程2 恰好也在执行,但是 线程2 偏偏是执行到 Statement 1 和 2 之间,此时 线程1 如果再继续执行 Statement 2,就会出现 getRGB() 和 getName() 结果不匹配的情况,这就是在并发中很容易出现的问题。

SynchronizedRGB color =
    new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB();      //Statement 1
String myColorName = color.getName(); //Statement 2

此时加上了 synchronized,将 Statement 1 和 2 绑定到了一起,这样并发就不会出问题了。

synchronized (color) {
    int myColorInt = color.getRGB();
    String myColorName = color.getName();
}

会出现上面这种不匹配的情况,是因为 color 是一个 mutable object,如果它变成 immutable object,就不会出现这种问题了。

对于如何定义 immutable objects,文档给出了一个 strategy

A Strategy for Defining Immutable Objects (The Java™ Tutorials >Essential Classes > Concurrency)

  1. Don't provide "setter" methods — methods that modify fields or objects referred to by fields.
  2. Make all fields final and private.
  3. Don't allow subclasses to override methods. The simplest way to do this is to declare the class as final. A more sophisticated approach is to make the constructor private and construct instances in factory methods.
  4. If the instance fields include references to mutable objects, don't allow those objects to be changed:
    • Don't provide methods that modify the mutable objects.
    • Don't share references to the mutable objects. Never store references to external, mutable objects passed to the constructor; if necessary, create copies, and store references to the copies. Similarly, create copies of your internal mutable objects when necessary to avoid returning the originals in your methods.

最后一点,关于对 mutable objects 的引用:如果一个对外部可变对象的引用需要被传到构造函数中,一定不要保存这个引用,如果必须要保存,就做一个该对象的 copy,然后保存这个 copy。类似的,如果是引用内部的可变对象,必要时也要创建内部可变对象的 copy,以避免在方法中返回原对象引用。

这一点是很容易被忽略的,在 Core Java 中也提到了类似的情况,作者的建议也是做 copy,不要直接引用原对象,因为很难知道这个对象引用在其他地方是不是会被改变。

根据上面的 strategy,重新定义 RGB:

final public class ImmutableRGB {

    // Values must be between 0 and 255.
    final private int red;
    final private int green;
    final private int blue;
    final private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public ImmutableRGB(int red,
                        int green,
                        int blue,
                        String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }


    public int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public String getName() {
        return name;
    }

    public ImmutableRGB invert() {
        return new ImmutableRGB(255 - red,
                       255 - green,
                       255 - blue,
                       "Inverse of " + name);
    }
}

参考资料

The Java™ Tutorials

 
comments powered by Disqus