Java 并发 Concurrency

8.17'21

Concurrency

计算机用户通常期望应用程序能同时执行多项任务。Java 平台 支持并发(concurrency)编程来实现这一效果。

进程和线程 Processes and Threads

并发编程中,有两个基本执行单元:进程和线程。(basic units of execution: processes and threads。主要关心的是线程, 但进程也很重要。

进程

进程是一个自包含的执行环境,具有完整私有的运行时资源和独立的内存空间。

大部分 Java virtual machine 以单进程的方式运行。

线程

线程有时也叫做轻量级的进程。两者都提供了执行环境,但新建线程占用更少的资源。

线程存在于进程中,共享进程的资源,如内存和文件等。这样交互更有效率,但也更容易出问题。

多线程运行是 Java 的核心特性。每个应用从一个主线程(main thread)开始,主线程可以创建更多的附加线程(additional threads)。

线程对象 Thread Objects

每个线程和类 Thread 的一个实例关联。当需要执行异步的任务时,可以实例化 Thread,通过该对象直接创建和管理线程。

定义和启动线程

  • implements Runnable / run method
  • new Thread(runnableObject)
  • t.start()
public class HelloRunnable implements Runnable {

    public void run() {
        System.out.println("Hello from a thread!");
    }

    public static void main(String args[]) {
        (new Thread(new HelloRunnable())).start();
    }

}

Thread.sleep 暂停当前的线程

Thread.sleep 可以暂停当前的线程,持续指定时间

// Pause for 4 seconds
Thread.sleep(4000);

join 暂停当前线程,等待另一线程完成

可以指定时间,超时后继续当前线程

t.join(1000);

interrupt 提前中止线程

会触发被中止线程的 InterruptedException

t.interrupt();

// in t run method
try {
} catch (InterruptedException e) {
}

The SimpleThreads Example

public class SimpleThread {
  // Display a message, preceded by the name the current thread
  static void threadMessage(String message) {
    String threadName = Thread.currentThread().getName();
    System.out.format("%s: %s%n", threadName, message);
  }

  private static class MessageLoop
      implements Runnable {
      public void run() {
        String importantInfo[] = {
          "Mares eat oats", 
          "Does eat oats", 
          "Mares eat lettuce", 
          "Does eat lettuce", 
        };
        try {
          for (int i = 0; i < importantInfo.length; i++) {
            Thread.sleep(4000);
            threadMessage(importantInfo[i]);
          }
        } catch (InterruptedException e) {
          threadMessage("I wasn't done!");
        }
      }
  }

  public static void main(String args[])
    throws InterruptedException {
    // Delay, in milliseconds before we interrupt MessageLoop thread (default one hour)
    long patience = 1000 * 60 * 60;

    // If command line argument present, gives patience in seconds
    if (args.length > 0) {
      try {
        patience = Long.parseLong(args[0]) * 1000;
      } catch (NumberFormatException e) {
        System.err.println("Argument must be an integer.");
        System.exit(1);
      }
    }

    threadMessage("Starting MessageLoop thread");
    long startTime = System.currentTimeMillis();
    Thread t = new Thread(new MessageLoop());
    t.start();

    threadMessage("Waiting for MessageLoop thread to finish");
    // loop until MessageLoop thread exits
    while (t.isAlive()) {
      threadMessage("Still waiting...");
      // Wait maximum of 1 second
      // for MessageLoop thread
      // to finish
      t.join(1000);
      if (((System.currentTimeMillis()  - startTime) > patience) && t.isAlive()) {
        threadMessage("Tired of waiting!");
        t.interrupt();
        // Shouldn't be long now
        // -- wait indefinitely
        t.join();
      }
    }
    threadMessage("Finally!");
  }
}

Synchronization (同步机制)

线程主要通过共享对象和对象的字段来通信。这回导致两种错误:thread interference and memory consistency errors。预防这两种的错误的工具就是 synchronization

Thread Interference (线程干扰)

两个线程在同时修改同一对象。修改所包含的步骤的存在覆盖。这会导致修改的结果不可靠。

public class Counter {
  private int c = 0;

  public void increment() {
    c++;
  }

  public void decrement() {
    c--;
  }

  public int value() {
    return c;
  }
}

c++ 实际包含了三个步骤

  • retrieve the value of c
  • increment the value by 1
  • store the value back in c

Memory Consistency Errors (内存一致性错误)

指不同的线程看到的数据不一致。避免这一错误的关键是理解 happens-before 关系。两个语句存在 happens-before,表示 特定语句对内存的写入操作对后续的语句可见(生效)。

对于线程内的语句, 以下方法可以保证 happens-before 关系

  • Thread.start 前的语句

  • Thread.join 后的语句

Synchronized Methods (同步方法)

在方法前添加 synchronized 关键字,有以下效果

同一个对象的同步方法

  • 只能依次执行
  • 存在 happends-before 关系
public class SynchronizeCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}

Intrinsic Locks and Synchronization(内部锁和同步)

同步机制建立在一个内部实体的基础上,称为 intrinsic lock or monitor lock or simply monitor。它既能保证排他性的访问对象状态,也能确保 happens-before 关系。

Locks In Synchronized Methods (同步方法的锁)

当线程调用同步方法时,会自动获得对象的内部锁,并在结束时释放。

Synchronized Statements (同步化语句)

同步化语句需要指定提供内部锁的对象。 在语句级别进行同步,可以避免同步同一方法的其他语句,从而避免一些可能的错误。

public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}

Reentrant Synchronization

一个线程可以再次获取已经拥有的锁。也就是可以多次获取同一锁。

Atomic Access (原子化读写)

原子化表示一次性完成的动作。要么完成,要么就不发生。等完成后才产生副作用。

以下行为是原子化的

  • 读、写引用型变量和基础变量 (除了 long and double)
  • 读、写所有声明为 volatile 的变量 (包括 long and double)

原子化行为不能被拆分和覆盖,可以避免线程之间的干扰。但依然存在内存一致性错误的可能,需要同步原子化行为。

++ 是非原子化的,因为可以拆分成多个独立的步骤。

📖