java并发

java支持并发编程,语言本身提供基础了并发支持,而java.util.concurrent类库提供了一些高层的API。

进程和线程

并发编程有两个基本的执行单元:进程和线程。对于java,并发编程大部分时候和线程相关,但是进程也很重要。

进程拥有独立的执行环境和内存空间。进程通常被视为程序或应用的同义词,但是,一个应用实际上很可能是许多相互合作进程的集合。为了进程间的高效沟通,大部分操作系统支持IPC(Inter Process Communicatin),比如管道和套接字。IPC不仅用于同系统间的进程通信,也可以用于不同系统间的进程。

JVM的大部分实现以单进程运行。一个java应用可以使用ProcessBuilder对象创建额外的进程。这里我们暂不讨论多进程应用。

线程有时被称为轻量级进程,进程和线程都提供一个独立的执行环境,但是创建线程的资源开销比进程少。线程在进程内存在,每个进程至少有一个线程。为了高效的通信,线程共享进程的资源,包括内存和文件,但也会带来一些潜在的问题。

多线程执行是java的基本特性,从程序员的角度看,你先以一个线程开始,叫main thread主线程,该线程可以创建额外的线程。

线程对象

每个线程都是Thread的实例,使用Tread对象创建并发应用有两种基本策略:

  • 直接控制线程的创建和管理,每次应用需要开始一个异步任务时就实例化线程对象。
  • 将线程的管理从应用中抽象出来,将应用的任务传给执行器。

定义并运行一个线程

创建线程实例的应用必须提供线程要运行的代码,有两种方式:

  • 提供一个Runnable对象。实现Runnable接口中的run方法,将该对象传给Thread的构造器:

    public class HelloRunnable implements Runnable {
    
        public void run() {
            System.out.println("hell from a thread");
        }
    
        public static void main(String[] args) {
            Thread thread = new Thread(new HelloRunnable());
            thread.start();
        }
    }
    
  • 子类化ThreadThread类本身实现了Runnable,但是其run方法啥也不做,应用可以子类化Thread,并提供run实现:

    public class HelloThread extends Thread{
    
        public void run() {
            System.out.println("hello from a thread");
        }
    
        public static void main(String[] args) {
            (new HelloThread()).start();
        }
    }
    

这两种方式都调用Thread.start来启动一个新线程。但是通常更推荐使用Runnable对象。子类化Thread局限比较大,只适合简单的应用。

睡眠

Thread.sleep方法可以暂停当前线程的执行,将处理器时间让给其他线程或应用。但是睡眠是可以被打断的,因此任何情况下,都不要假定调用sleep就可以让某线程暂停特定的时长。

睡眠示例:

public class SleepMessage {

    public static void main(String[] args) throws InterruptedException {
        String[] message = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
        };

        for (String s : message) {
            System.out.println(s);
            Thread.sleep(2000);
        }
    }
}

注意,如果另一个线程中断了当前线程的睡眠状态,会抛出InterruptedException异常。因为这里没有其他进程中断当前线程的运行,因此我们不必捕获该错误。

中断

中断(interrupt)通知线程它该停止正在进行的事情,来做些其他事情。至于线程如何响应中断,取决于程序员。一个线程通过调用目标线程的interrupt方法来发送中断信号。

要想中断机制正确的运作,被中断的线程必须支持中断。比如收到中断信号后,立即返回:

for (String s : message) {
    System.out.println(s);
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        return;
    }
}

很多方法会抛出InterruptedException异常,比如sleep,被设计为一旦收到中断信号,立即取消当前操作并返回。但是如果线程一直不调用这些抛出InterruptedException异常的方法怎么办?那么它必须周期性地调用Thread.interrupted方法,来检测是否收到了中断信号,比如:

for (String s : message) {
    System.out.println(s);
    if (Thread.interrupted()) {
        return;
    }
}

这里是检测到中断信号后,直接返回。在复杂的应用中,收到中断信号后,抛出InterruptedException更合理:

if (Thread.interrupted()) {
    throw new InterruptedException();
}

这样就可以将处理中断的逻辑都放在catch代码块中。

中断机制的实现是通过一个内部的标志位——中断状态。调用Thread.interrupt可以设置这个标志。调用静态方法Thread.interrupted可以检测中断。非静态方法isInterrupted,用于一个线程查询另一个线程的中断状态,但是不会改变中断状态标志位。

任何抛出中断异常的方法都会清除中断状态。但是,只要另一个线程再次调用中断,就可以重新设置中断状态。

等待 join

t.join(),令当前线程等待另一个线程(t)完成。它也可以接收参数,指定等待多久。就像sleepjoin方法可以抛出中断异常来响应一个中断。

示例

public class SimpleThreads {

    static void threadMessage(String msg) {
        String threadName = Thread.currentThread().getName();
        System.out.format("%s: %s%n", threadName, msg);
    }

    private static class MsgLoop implements Runnable {
        @Override
        public void run() {
            String[] msg = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };
            try {
                for (String s : msg) {
                    Thread.sleep(4000);
                    threadMessage(s);
                }
            } catch (InterruptedException e) {
                threadMessage("i wasn't done");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        long patience = 1000 * 10;
        threadMessage("starting msgLoop thread");
        long startTime = System.currentTimeMillis();
        Thread t = new Thread(new MsgLoop());
        t.start();

        threadMessage("waiting for msgLoop thread to finish");
        while (t.isAlive()) {
            threadMessage("still waiting...");
            t.join(1000);
            if ((System.currentTimeMillis() - startTime) > patience && t.isAlive()) {
                threadMessage("Tired of waiting");
                t.interrupt();
                t.join();  // 等t响应中断,否则会继续往下执行,打印 main: still waiting..., 然后才打印t线程中的中断信息
            }
        }
        threadMessage("Finally");
    }
}

打印结果如下:

main: starting msgLoop thread
main: waiting for msgLoop thread to finish
main: still waiting...
main: still waiting...
main: still waiting...
main: still waiting...
Thread-0: Mares eat oats
main: still waiting...
main: still waiting...
main: still waiting...
main: still waiting...
Thread-0: Does eat oats
main: still waiting...
main: still waiting...
main: Tired of waiting
Thread-0: i wasn't done
main: Finally

同步(Synchronization)

线程通信主要是通过共享对字段以及字段引用的对象的访问。这种通信及其高效,但是可能造成两种可能的错误:线程干扰和内存一致性错误。同步可以防止这些错误。

但是,同步会引入线程争抢,当多个线程同时访问同样的资源时会导致java运行时执行其中一些线程慢一些,甚至暂停执行。饥饿和活锁就是线程争抢的形式。

线程干扰 Thread Interference

下面看一个简单的计数器:

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }
}

调用increment,c加1, 调用decrement,c减1。假如多个线程引用同一个计数器对象,线程干扰可能导致计数异常。

当运行在不同线程,但作用于同样数据的两个操作交错时,就会发生干扰。这意味着两个操作都包含多个步骤,且步骤的顺序发生了重叠。

这里似乎不可能发生交错,因为对c的操作都是单个简单语句。但是,即便是最简单的语句,虚拟机也要翻译为多个步骤。我们没必要测试虚拟机执行的具体步骤,只要知道表达式c++可以分解为以下3步即可:

  1. 获取c的当前值
  2. 当前值加1
  3. 当前值赋值再赋值给c

c--也是类似。假设线程A调用increment,同时线程B调用decrement,c的初始值是0,AB交错可能是下面的顺序:

  1. A:获取c的值
  2. B:获取c的值
  3. A:对返回值加1,结果是1
  4. B:对返回值减1,结果是-1
  5. A:结果赋值给c,c现在是1
  6. B:结果赋值给c,c现在是-1

线程A的结果丢了,被线程B的覆写了。这种交错只是一种可能,也有可能是B的结果丢失,或者完全不发生错误。正是因为不可预测,线程干扰的bug很难定位和修复。

内存一致性错误 Memery Consistency Errors

先看例子,首先声明一个如下的变量:

int counter = 0;

并且线程A和B共享这个counter变量。假设A自增counter:

counter++;

紧接着B打印counter:

System.out.println(counter);

如果以上两个语句在同一线程中执行,可以很安全地打印出“1”,但是如果在不同的线程中执行,那结果很可能是“0”,因为不能保证A的操作对B可见,除非程序员已经明确了这两个语句的事前关联。

有些行为可以用来确定事前关联(其中之一就是同步,这个稍后讨论),我们目前已经见过了两种:

  • 当某个语句调用Thread.start时,每个与该语句有事前关联的语句,也会与新线程中执行的语句有事前关联。
  • 当一个线程终止并导致另一个线程Thread.join时,终止线程的所有语句与join后的所有语句有事前关联。该线程代码的效果对于执行join的线程是可见的。

同步方法 Synchronized Methods

java提供了两种基本的同步用法:

  • 同步方法 synchronized methods
  • 同步语句 synchronized statements

后者要复杂一下,稍后再讨论。这里我们先看同步方法,要想使一个方法变成同步方法,只需在声明处添加synchronized关键字即可:

public class SynchronizedCounter {
    private int c = 0;

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

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

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

同步方法有以下作用:

  • 首先,不可能交错调用同一对象的同步方法。当一个线程在执行一个对象的某个同步方法时,所有调用该对象任何同步方法的其他线程都会阻塞,直到第一个线程处理完毕。
  • 其次,只要存在同步方法,就会与随后对该对象同步方法的调用自动确定事前关联。这样可以保证对该对象的改变对其他线程是可见的。

记住,构造器不能被同步化,在构造器上使用synchronized关键字是句法错误,并且没有任何意义,因为对象被创建时,只有创建该对象的线程才会访问它。

同步方法提供了一种简单的策略来防止线程干扰和内存一致性错误。如果一个对象对多个线程可见,对该对象变量的所有读写都通过同步方法完成(除了final字段,对象创建后便不可修改,因此可以安全地通过非同步方法读写)。但是同步方法也会导致活跃度(liveness),这个稍后讨论。

内在锁和同步 Intrinsic Locks and Synchronization

同步基于一个内部实体,称为内在锁(或监视锁)。内在锁强制对对象状态的互斥访问,并确定事前关联。每个对象都有一个和自身关联的内在锁。通常,一个线程如果需要对某对象字段的互斥和一致性访问,必须先获得该对象的内在锁,并在访问完成后释放锁。在获得锁和释放锁之间,我们说该线程拥有内在锁,这时其他线程无法获得同样的锁,要想获得需要阻塞等待。当一个线程释放锁时,该行为和后续获得锁的行为之间的事前关联就确定了。

同步方法中的锁

当线程调用某个同步方法时,它自动获得该方法对象的内在锁,并在方法返回(或抛出异常)后释放锁。另外,如果调用一个静态的同步方法,又会如何呢?由于静态方法和类关联,而不是对象,因此线程将获得类的内在锁。

同步语句

同步语句(synchronized statements)是另一种创建同步代码的方式。和同步方法不同的是,同步语句必须指定提供内在锁的对象。比如:

public void addName(String name) {
  synchronized(this) {
    lastName = name;
    nameCount++;
  }
  nameList.add(name);  // 不希望同步调用
}

在方法addName中,对lastNamenameCount的修改需要保证同步,但是要避免对其他对象方法的同步调用。如果没有同步语句,就需要在一个单独的,非同步的方法中调用nameList.add

同步语句有助于在更细的同步粒度上提高并发。看下面的示例:

public class Foo {
  private long c1 = 0;
  private long c2 = 0;
  private Object lock1 = new Object();
  private Object lock2 = new Object();
  
  public void inc1() {
    synchronized(lock1) {
      c1++;
    }
  }
  
  public void inc2() {
    synchronized(lock2) {
      c2++;
    }
  }
}

在这个示例中,类Foo有两个实例字段,c1和c2,二者绝不会一起使用,且单独对任一字段的更新必须保证同步,但是不必在更新c2时阻止对c1的更新,否则不必要的阻塞会降低并发。这里我们既不使用同步方法,也通过this使用锁,而是单独创建两个对象来提供锁。这样使用时必须十分小心,你必须百分百确定,交错访问这些字段是安全的。

重入同步

一个线程不能获得另一个线程拥有的锁,但是,它可以再次获得它已经拥有的锁,这叫做重入同步(reentrant synchronization)。它描述了一种情形,同步代码直接或间接地调用一个也包含同步代码的方法,并且两套代码使用同一把锁。如果没有重入同步,同步代码必须采取额外的预防措施,以避免线程自身阻塞。

原子访问

原子行为要么完全发生,要么完全不发生。

即便是很简单的表达式也可以由多个行为构成,比如c++,并不是一个原子行为。但是有些行为可以指定为原子性:

  • 对引用变量和大部分基本变量(除了longdouble)的读写是原子性的
  • 对所有声明为volatile的变量的读写是原子性的(包括longdouble变量)

原子行为不能被交错,所以使用时不用担心线程干扰。但是,这无法排除对同步原子行为的需要,因为内存一致性错误仍有可能发生。使用volatile变量可以降低内存一致性错误的风险,因为任何对volatile变量的写操作都会与后续的读操作确定事前联系。这意味对volatile变量的修改对其他线程是可见的。另外,这也意味着,如果一个线程读取一个volatile变量,它不仅看到的是最新的修改,而且也包含导致这个修改的代码的副作用。

使用简单的原子变量访问比通过同步代码访问更高效,但是需要开发人员注意避免内存一致性错误。

java.util.concurrent包中提供了不依赖于同步的原子方法,这个我们稍后在高层并发中讨论。

活跃度 Liveness

一个并发应用程序及时执行的能力称之为活跃度。这部分讨论一种最常见的活跃度问题,死锁(deadlock),并简要地描述另外两种活跃度问题,饥饿(starvation)和活锁(livelock)。

死锁

死锁是指两个或多个线程永远阻塞,互相等待。下面我们看一个示例,Jerry和Tom都是非常礼貌的孩子,打招呼时行鞠躬礼,别人行礼时,他们也会回礼。并且只有当对方回礼时,另一个人才能起身。不过他们没有考虑到同时行礼的情况,于是就尴尬了:

public class DeadLock {
    static class Friend {
        private final String name;

        public Friend(String name) {
            this.name = name;
        }

        public String getName() {
            return this.name;
        }

        public synchronized void bow(Friend friend) {
            System.out.format("%s: 向%s鞠躬!%n", this.name, friend.getName());
            friend.bowBack(this);
          
        }

        public synchronized void bowBack(Friend friend) {
            System.out.format("%s: 向%s回礼!%n", this.name, friend.getName());
        }
    }

    public static void main(String[] args) {
        final Friend jerry = new Friend("Jerry");
        final Friend tom = new Friend("Tom");
				
      	// 第一次见面jerry先向tom打招呼
        jerry.bow(tom);
        // 第二次见面,tom先向jerry打招呼
        tom.bow(jerry);
    }
}

以上代码顺序执行时,输出结果如下:

Jerry: 向Tom鞠躬!
Tom: 向Jerry回礼!
Tom: 向Jerry鞠躬!
Jerry: 向Tom回礼!

假如两人见面时同时鞠躬:

// A线程
new Thread(new Runnable() {
    @Override
    public void run() {
        jerry.bow(tom);
    }
}).start();

// B线程
new Thread(new Runnable() {
    @Override
    public void run() {
        tom.bow(jerry);
    }
}).start();

那么输出如下,然后程序卡永远卡住,无法继续执行下去:

Jerry: 向Tom鞠躬!
Tom: 向Jerry鞠躬!

谁都不能回礼,这是因为,线程A调用jerry的bow方法时,B同时调用tom的bow方法。二者都要互相调用对方的bowBack方法才能返回,但bow和bowBack都是同步方法,一个线程在调用时,另一个线程不得调用。对于A线程来说,线程A要执行tom的bowBack,但是B线程正在调用tom的bow方法,所以要等B先执行完,反之亦然。

饥饿和活锁

这两种情况比死锁要少见,但是也是开发者要考虑的问题。

饥饿(starvation)指的是一个线程无法获得对共享资源的正常访问而无法继续下去。当”贪婪“线程长时间使共享资源不可用时,就会发生这种情况。比如,一个对象有个同步方法,需要较长时间才能返回。如果某个线程频繁地调用该方法,其他需要同步访问该对象的线程就会经常被阻塞。

一个线程的行动是对另一个线程的响应,如果另一个线程的行动也是对其他线程的响应,就可能导致活锁(livelock)。就像死锁一样,活锁的的线程无法继续进行下去。但是,这些线程并没有被阻塞,它们只是忙于互相响应以至于无法恢复工作。就好比相对而行的两个人在走廊中要互相通过一样:A往自身的左边走,让B通过,同时呢,B往自己的右边走,让A通过,结果就是两人还是互相堵着。然后他们决定换个方向,A往自身的右边走,同时B往自身的左边走,还是互相堵着,然后。。。

守卫块 Guarded Blocks

线程之间经常需要互相合作,最常见的方式是使用guarded block,它循环检查一个条件,直到为真才继续往下执行。

示例:

public void guardedJoy() {
    while(!joy) {}
    System.out.println("Joy has been achieved!");
}

guardedJoy方法如果要执行下去,需要检测到另一个线程设置共享变量joy值为真。理论上,简单的while循环可以实现这一需求,但是,循环太浪费了,当前线程在等待期间也要不断地执行。

更高效的方式是调Object.wait挂起当前线程,直到另一个线程发出事件通知, wait抛出中断异常:

public synchronized void guardedJoy() {
    while(!joy) {
        try {
            wait(); // 当前线程在此处挂起,等待wait返回
        } catch (InterruptedException e) {}
    }
    System.out.println("Joy and efficiency have been achieved!");
}

记住,一定要在循环体内调用wait方法,这样可以检测条件是否确实满足,因为通知的事件不一定是当前线程等待的。这样,比起简单循环,守卫块只有每次事件通知时才会循环一次。

和其他中断执行的方法一样(比如sleep, join),wait也会抛出InterruptedException。因为我们只是想在收到事件中断后判断joy的值,所以这里只捕获异常,但是不做任务处理。

一个线程要调用object.wait方法,必须先获得该对象的内部锁,而获得内部锁的最简单方法就是在同步方法中调用,这也就是为什么guardedJoy是同步的。调用wait后,当前线程释放锁并暂停执行。在未来某个时间,另一个线程会获得该锁,然后调用Object.notifyAll,通知所有等待该锁的线程,有大事发生了,如下:

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

重新获得该锁的线程从wait的调用处返回,恢复运行。

另外,除了notifyAll,还有一个notify方法,只会唤起一个线程,但是又不能指定唤醒哪个线程。对于有大量线程并行执行相似的任务,而你又不关心唤醒哪个线程时,可以使用notify。

了解完守卫块后,我们来写一个生产者消费者模型的应用:生产线程生成数据,消费线程消费数据,二者通过共享对象通信,并且消费线程在生产线程生产未就绪时,不得去取数据,如果消费线程还有旧的数据未消费,生产线程也不得生产新的数据。

假设数据是一串文本消息,并且通过Drop的对象共享:

package producer.demo;

public class Drop {
    private String message;
    private boolean empty = true;  // 生产和消费的判断条件

    public synchronized String take() {
        while(empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        empty = true;
        notifyAll();
        return this.message;
    }

    public synchronized void put(String message) {
        while (!empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        empty = false;
        this.message = message;
        notifyAll();
    }
}

生产者:

package producer.demo;

import java.util.Random;

public class Producer implements Runnable{
    private Drop drop;

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

    public void run() {
        String[] messages = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
        };
        Random random = new Random();
        for (String msg : messages) {
            this.drop.put(msg);
            // 模拟生产间隔时间
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
        // DONE 告诉消费者所有消息已发送完成
        this.drop.put("DONE");
    }
}

消费者:

package producer.demo;

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 msg = this.drop.take(); !msg.equals("DONE"); msg = this.drop.take()) {
            System.out.println(msg);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
    }
}

主程序:

package producer.demo;

public class App {

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

注意,这里使用自定义的Drop对象只是为了演示守卫块。在实际使用时,可以用java集合框架中提供的数据结构。

不可变对象

一个对象如果创建后状态无法修改,就是不可变对象(immutable)。

不可变对象对于构建简单,可靠的代码非常有帮助。在并发应用中,它们的状态无法被改变,不会被线程干扰,不会出现一致性错误。

开发人员通常不情愿使用不可变对象,他们担心创建新对象的开销要大于更新现有对象。其实这种开销通常被高估了,不可变对象带来的效率会抵消这种开销:它可以减少了垃圾回收的开销,并且省去为了确保可变对象不要发生错误的代码。

下面我们看看如何将一个可变对象的类转化为不可变对象的类,并演示使用不可变对象的优势。

代表颜色的一个同步类:

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;
    }
}

使用SynchronizedRGB必须十分小心,多线程下可能会发生一致性错误。比如一个线程执行如下代码:

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

如果另一个线程在语句1和语句2之间调用了color.set方法,那么第一个线程获取的色值和色名就会匹配不上。为了避免这种情况出现,两个语句需要绑定在一起执行:

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

这种不一致只有可变对象才可能出现,而不可变对象无此问题。

以下规则适用于创建不可变对象:

  • 不要提供可以修改字段或其引用对象的“setter”方法
  • 将所有的字段声明为finalprivate
  • 不要允许子类化来重写方法,最简单的方式是将类声明为final,更老到的做法是,将构造方法声明为private,在工厂方法中构造实例。
  • 如果实例字段包含对可变对象的引用,不要允许对这些对象的修改:
    • 不提供修改可变对象的方法
    • 不要共享对可变对象的引用。不要存储外部可变的对象的引用,如果构造方法必须要接受外部可变对象,创建这些可变对象的副本,然后存储对副本的引用。类似地,避免直接在方法中返回内部可变对象,必要时返回创建的副本。

应用以上规则,将之前的SynchronizedRGB变为不可变的ImmutableRGB:

// 声明类为final,不可被子类化
final public class ImmutableRGB {

    // 所有字段fianl private
    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;
    }
		
   // set 方法被移除

    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);
    }
}

高层并发对象

目前所讲这些都是java早期的低层API,它们只适合基本的任务。对于更高级的任务,需要更高级的API,尤其是现在的高并发应用,需要充分利用多核处理器及操作系统。

接下来我们看一些java5引入的高层并发特性,这些特性大部分都在java.util.concurrent包中有实现。java的集合框架中也定义了新的并发数据结构。

  • Lock 锁对象简化锁的使用
  • Executors 为加载和管理线程定义的高层API,其实现提供了线程池
  • 并发集合使得管理大型集合更容易,极大地减少对同步的需要
  • 原子变量可以最小化同步,并且避免内存一致性错误
  • ThreadLocalRandom (JDK7)可以在多线程下高效地生成随机数

锁对象 Lock

同步代码依赖某种简单的可重入锁,它很易用,但是也有很多限制。java.util.concurrent.locks包中提供了更好的锁用法,其中最基本的接口就是Lock。Lock对象和之前同步代码中使用的内部锁很相似,就像内部锁一样,一次只能一个线程获得锁对象。通过关联的Condition对象,Lock对象也支持wait/notify机制。

比起内部锁,Lock对象的最大优势是在尝试获取锁时可以撤回。tryLock方法在锁不可用或超时后,可以立即撤回。lockInterruptibly方法在获得锁前如果收到另一个线程发送的中断也会撤回。

现在让我们利用锁对象解决死锁问题。jerry和tom学会了观察对方是否要鞠躬。我们来模拟这个改善,要求Friend对象在执行bow时必须获得双方的锁。这里使用的是Lock接口的重入锁实现ReentrantLock:

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SafeLock {
    static class Friend {
        private final String name;
        private final Lock lock = new ReentrantLock();

        public Friend(String name) {
            this.name = name;
        }

        public String getName() {
            return this.name;
        }

        // 判断是否可以鞠躬
        public boolean impendingBow(Friend backBower) {
            boolean myLock = false;
            boolean yourLock = false;
            try {
                // 获取自己的锁和对方的锁
                myLock = this.lock.tryLock();
                yourLock = backBower.lock.tryLock();
            } finally {
                // 只要有任一一方的锁未获得,便释放已获得的锁
                if (! (myLock && yourLock)) {
                    if (myLock) {
                        lock.unlock();
                    }
                    if (yourLock) {
                        backBower.lock.unlock();
                    }
                }
            }
            return myLock && yourLock;
        }

        public void bow(Friend backBower) {
            // 只要同时获得自己和对方的锁,才能鞠躬;鞠躬完后释放锁
            if (impendingBow(backBower)) {
                try {
                    System.out.format("%s: 向%s鞠躬!%n", this.name, backBower.getName());
                    backBower.bowBack(this);
                } finally {
                    this.lock.unlock();
                    backBower.lock.unlock();
                }
            } else {
                System.out.format("%s: 我刚要对%s鞠躬,但是看到他已经向我鞠躬了%n", this.name, backBower.getName());
            }
        }

        public void bowBack(Friend bower) {
            System.out.format("%s: 向%s回礼%n", this.name, bower.getName());
        }
    }
    static class BowLoop implements Runnable {
        private Friend bower;
        private Friend backBower;

        public BowLoop(Friend bower, Friend bowee) {
            this.bower = bower;
            this.backBower = bowee;
        }

        public void run() {
            Random random = new Random();
            for(;;) {
                try {
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {}
                bower.bow(backBower);
            }
        }
    }

    public static void main(String[] args) {
        final Friend jerry = new Friend("Jerry");
        final Friend tom = new Friend("tom");
        (new Thread(new BowLoop(jerry, tom))).start();
        (new Thread(new BowLoop(tom, jerry))).start();
    }
}

执行器 Executors

在之前的例子中,任务(Runnable对象)和线程(Thread对象)紧密耦合。在小型应用中,这样没问题,但是在大型应用中,必须将线程的管理和创建与应用解藕,而封装这种功能的对象,就是执行器。接下来看看几种具体的执行器:

  • Executor Interfaces 定义了三种执行器对象类型
  • Thread Pools 最通用的执行器实现
  • Fork/Join JDK7中新引用的框架,以充分利用多核处理器

####Executor Interfaces

java.util.concurrent包中定义了三种执行器接口:

  • Executor, 支持启动任务的简单接口
  • ExecutorServiceExecutor的子接口,增加了对任务和执行器的生命周期管理
  • ScheduleExecutorServiceExecutorService的子接口,支持任务的延迟或周期性之行
Executor Interface

Executor 接口只提供了一个方法execute,用来替代线程创建,比如:

(new Thread(runnableObj)).start();

可以替换为:

e.execute(runnableObj);

execute方法在不同的Executor实现中可能有所不同,低层的实现直接为任务创建新的线程,但更可能是利用现有的工作线程来运行任务,或者将任务放入队列等待可用的工作线程(我们在稍后的线程池中会再说工作线程)。

ExecutorService Interface

提供了更强大的submit方法,除了可以接受Runnable对象,还可以接受Callable对象作为任务,后者可以有返回值。submit方法返回Future对象,用于获取Callable对象的返回值,也可以用于管理任务的状态。ExecutorService还提供了一些方法来管理执行器的关闭,如果要支持立即关闭,任务需要正确处理中断。

ScheduledExecutorService Interface

作为ExecutorService的子接口,其实现了父类中所有方法的schedule版,支持延时执行。而且,接口定义了scheduleAtFixedRatescheduleWithFixedDelay,支持以指定的间隔重复执行任务。

线程池 Thread Pools

java.util.concurrent包中的大部分执行器实现都使用线程池,线程池由工作线程组成,这些线程和它要执行的任务是解藕的。任务通过一个内部队列提交到线程池,并且多余的任务可以暂存在队列中。这样,系统就可以有条不紊的处理大量任务,避免了线程数量过载造成的宕机。

要创建使用线程池的执行器,可以调用java.util.concurrent.Executors的工厂方法:

  • newFixedThreadPool 创建使用固定线程池大小的执行器
  • newCachedThreadPool 适合处理大量短时任务的应用
  • newSingleThreadExecutor 一次执行一个任务
  • 上述执行器的schedule版

如果上述执行器没有你需要的,还可以使用java.util.concurrent.ThreadPoolExecutor或者java.util.concurrent.ScheduleThreadPoolExecutor

Fork/Join

fork/join框架是ExecutorService接口的一种实现,旨在充分利用多核处理器的优势。它适合那些可以递归地分割为更小片段的工作。

和ExecutorService的其他实现一样,fork/join框架也是分发任务给线程池中的工作线程。但是fork/join的独特之处在于,它使用work-stealing工作窃取算法,完成任务的线程可以从其他繁忙的线程那里“偷取”任务。它的核心是ForkJoinPool这个类(是AbstractExecutorService的子类),它实现了work-stealing算法,可以执行ForkJoinTask任务。

基本使用

使用fork/join框架的第一步是写执行单个片段的代码,伪代码如下:

if (工作足够小)
  直接执行
else
  切分工作为两个片段
  调用两个片段,等待执行结果

将这段代码包装在ForkJoinTask子类中,一般是RecursiveTask(可以返回一个结果)或者RecursiveAction,代表工作任务,将它传给ForkJoinPool实例的invoke方法。

模糊图片的例子

假如图片由一个整数数组表示,每个整数代表一个像素的色值。如果要对图片做模糊,需要处理每个像素,这个处理过程相当耗时,通过fork/join框架,就可以并行处理。

package join.demo;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;

public class ForkBlur extends RecursiveAction {
    private int[] source;  // 原图片像素数组
    private int start;  // 起始像素位置(数组索引),一开始是0
    private int end;  // 结束像素位置,一开始是数组长度
    private int[] destination;  // 模糊后的像素数组
    private static int max = 100000;  // 最大像素处理数量

    public ForkBlur(int[] src, int start, int length, int[] dst) {
        this.source = src;
        this.start = start;
        this.end = length;
        this.destination = dst;
    }

    protected void computeDirectly() {
        // do something to calculate new pixel value and write to mDestination
        ;
    }

    // 实现compute()方法,它根据工作的大小来决定是直接执行,还是分解为更小的任务,直至任务分解到足够小,能够被直接执行
    @Override
    protected void compute() {
        if (end < max) {
            computeDirectly();
            return;
        }

        int split = end / 2;
        invokeAll(new ForkBlur(source, start, split, destination),
                new ForkBlur(source, start + split, end  - split, mDestination));
    }

    public static BufferedImage blur(BufferedImage srcImage) {
        int w = srcImage.getWidth();
        int h = srcImage.getHeight();

        int[] src = srcImage.getRGB(0, 0, w, h, null, 0, w);
        int[] dst = new int[src.length];
				
        ForkBlur fb = new ForkBlur(src, 0, src.length, dst);
        ForkJoinPool pool = new ForkJoinPool();
        pool.invoke(fb);

        BufferedImage dstImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        dstImage.setRGB(0, 0, w, h, dst, 0, w);

        return dstImage;
    }

    public static void main(String[] args) throws Exception {
        String srcName = "demo.jpg";
        File srcFile = new File(srcName);
        BufferedImage image = ImageIO.read(srcFile);
        BufferedImage blurredImage = blur(image);

        String dstName = "blurred.jpg";
        File dstFile = new File(dstName);
        ImageIO.write(blurredImage, "jpg", dstFile);
    }
}
标准实现中的并行计算

除了直接使用fork/join框架利用多核处理器作并行计算,java中一些常用特性的实现也利用了fork/join框架。比如java se 8中,java.util.ArraysparallelSort()方法

另外一些利用fork/join框架的实现在java.util.streams包中。

并发集合

java.util.concurrent包中还有许多额外的集合框架:

  • BlockingQueue 先进先出数据结构,队列满时或者空时,添加和获取操作会阻塞或超时。
  • ConcurrentMap 接口定义了一些有用的原子性操作。只有键存在时,移除或者替换操作才会进行,只有键不存在时,添加键值对的操作才会进行。使这些操作原子化,避免了同步。它的通用实现是ConcurrentHashMap,也就是HashMap的并发模式。
  • ConcurrentNavigableMapConcurrentMap的子接口,支持近似匹配。它的通用实现是ConcurrentSkipListMap,也就是TreeMap的并发模式。

所有这些集合都有助于避免内存一致性错误,它会在添加操作与之后的访问或删除操作之间,定义清楚事发前关系。

下面看看利用BlockingQueue代替自定义的Drop类,简化生产者消费者模型:

生产者:

package producer2.demo;

import java.util.Random;
import java.util.concurrent.BlockingQueue;

public class Producer implements Runnable {
    private BlockingQueue<String> drop;

    public Producer(BlockingQueue<String> drop) {
        this.drop = drop;
    }

    @Override
    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();

        try{
            for (String msg: importantInfo) {
                drop.put(msg);
                Thread.sleep(random.nextInt(5000));
            }
            drop.put("DONE");
        } catch (InterruptedException e) {}
    }
}

消费者:

package producer2.demo;

import java.util.Random;
import java.util.concurrent.BlockingQueue;

public class Consumer implements Runnable{
    private BlockingQueue<String> drop;

    public Consumer(BlockingQueue<String> drop) {
        this.drop = drop;
    }

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

入口:

package producer2.demo;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;

public class App {
    public static void main(String[] args) {
        BlockingQueue<String> drop = new SynchronousQueue<>();
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}

原子变量

java.util.concurrent.atomic包中定义了一些类,支持对单个变量的原子性操作。所有的类都有getset方法,类似于volatile变量的读和写方法。就是说,对同一变量,set和后续的get之间有事发前关系。之前演示线程干扰的计数器示例,除了将计数器各方法变为同步方法外,更可取的方式是使用原子变量,这样可以避免不必要同步造成的活跃度影响。

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCount {
  private AtomicInteger c = new AtomicInteger(0);
  
  public void increment() {
    c.incrementAndGet();
  }
  
  public void decrement() {
    c.decrementAndGet();
  }
  
  public int value() {
    return c.get();
  }
}

并发随机数

JDK 7中,java.util.concurrent包中提供了ThreadLocalRandom类,以便在多线程或ForkJoinTasks中使用随机数。在并发访问中,使用它代替Math.random()可以获得更好的性能:

int r = ThreadLocalRandom.current().nextInt(4, 77);
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值