【The Java™ Tutorials】【Concurrency】8. High Level Concurrency Objects

Lock Objects

前面我们都是用一个对象的固有锁来实现同步的,对象的固有锁是重入锁。对象的固有锁很方便使用,但是也有很多限制。java.util.concurrent.locks包中提供了一些更加复杂也更加灵活的锁。这一小节我们不打算详细介绍这些锁,我们只关注公共的接口:Lock

Lock objects work very much like the implicit locks used by synchronized code. As with implicit locks, only one thread can own a Lock object at a time.

The biggest advantage of Lock objects over implicit locks is their ability to back out of an attempt to acquire a lock. The tryLock method backs out if the lock is not available immediately or before a timeout expires (if specified). The lockInterruptibly method backs out if another thread sends an interrupt before the lock is acquired.

下面我们使用java.util.concurrent.locks包中提供的ReentrantLock解决Liveness中的死锁问题:

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

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 bower) {
            Boolean myLock = false;
            Boolean yourLock = false;
            try {
                myLock = lock.tryLock();
                yourLock = bower.lock.tryLock();
            } finally {
                if (! (myLock && yourLock)) {
                    if (myLock) {
                        lock.unlock();
                    }
                    if (yourLock) {
                        bower.lock.unlock();
                    }
                }
            }
            return myLock && yourLock;
        }
            
        public void bow(Friend bower) {
            if (impendingBow(bower)) {
                try {
                    System.out.format("%s: %s has"
                        + " bowed to me!%n", 
                        this.name, bower.getName());
                    bower.bowBack(this);
                } finally {
                    lock.unlock();
                    bower.lock.unlock();
                }
            } else {
                System.out.format("%s: %s started"
                    + " to bow to me, but saw that"
                    + " I was already bowing to"
                    + " him.%n",
                    this.name, bower.getName());
            }
        }

        public void bowBack(Friend bower) {
            System.out.format("%s: %s has" +
                " bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    static class BowLoop implements Runnable {
        private Friend bower;
        private Friend bowee;

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

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new BowLoop(alphonse, gaston)).start();
        new Thread(new BowLoop(gaston, alphonse)).start();
    }
}

在鞠躬的之前,首先获得自己的锁,然后尝试获得对方的锁。只有同时获取成功的时候才鞠躬。保证两个人不会同时鞠躬。

Executors

之前的例子中,任务(Runnable Object)和线程(Thread Object)有很紧密的联系。这在小型应用中很适用,但是在大型应用中,最好把线程的创建和管理单独拿出来。封装这些功能的对象被称为executors。下面我们分三部分来介绍executor:

Executor Interfaces

java.util.concurrent包中定义了3个executor接口:

  • Executor
    Executor接口只提供了一个方法: execute。假设r是一个Runnable对象,我们可以把以下写法:
    (new Thread(r)).start();
    
    替换为:
    e.execute(r);
    

但是,相对于第一种写法,execute的定义没有那么明确。第一种写法会马上创建一个新的线程然后执行r。而Executor可能也会马上创建一个新的线程然后执行r,但是Executor更可能使用一个已经存在的worker thread去执行r,或者将r放在一个队列中等待worker thread可用,具体会采用哪一种方式取决于Executor的实现。

  • ExecutorService
    ExecutorService继承了Executor,但是提供了一个比execute更加灵活的方法:submit。submit不仅能接收Runnable对象,还能接收Callable对象。Callable对象允许任务有返回值。submit本身也有返回值,它返回Feature对象,通过Feature对象,我们可以拿到Callable对象任务的返回值,也可以获取Callable和Runnable任务的状态。

    同时ExecutorService还提供了shutdown方法, 可以通过调用该方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。

  • ScheduledExecutorService
    ScheduledExecutorService继承了ExecutorService,不同的是ScheduledExecutorService使用schedule方式来实现线程调度。该接口中定义了两种schedule方式:scheduleAtFixedRate 和 scheduleWithFixedDelay,实现以固定的间隔来执行任务。

Thread Pools

java.util.concurrent包中的executor大部分是使用线程池实现的。

Most of the executor implementations in java.util.concurrent use thread pools, which consist of worker threads. This kind of thread exists separately from the Runnable and Callable tasks it executes and is often used to execute multiple tasks.

Using worker threads minimizes the overhead due to thread creation. Thread objects use a significant amount of memory, and in a large-scale application, allocating and deallocating many thread objects creates a significant memory management overhead.

以下是四种常见的线程池:

  • FixedThreadPool
    最多只能有固定数目的活动线程存在,如果达到最大数目时有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止被移出线程池。
  • CachedThreadPool
    缓存型,先查看池中有没有以前建立的空闲线程,如果有,就重用;如果没有,就建一个新的线程加入池中。能重用的线程,必须是timeout IDLE内的池中线程,缺省timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池(也就是说当一个线程执行完之后会被缓存60s,超过这个时间,线程实例才会被移出线程池)。
  • ScheduledThreadPool
    可以按schedule依次delay执行,或周期执行。
  • SingleThreadExecutor
    任意时间线程池中最多只能有一个线程,可以看成是FixedThreadPool或CachedThreadPool的特例。

我们可以通过调用java.util.concurrent.Executors的工厂方法来创建以上线程池,这些线程池都实现了ExecutorService接口。也就说这些线程池就是我们所说的executor。

Fork/Join

The fork/join framework is an implementation of the ExecutorService interface that helps you take advantage of multiple processors. It is designed for work that can be broken into smaller pieces recursively.

As with any ExecutorService implementation, the fork/join framework distributes tasks to worker threads in a thread pool. The fork/join framework is distinct because it uses a work-stealing algorithm. Worker threads that run out of things to do can steal tasks from other threads that are still busy.

The center of the fork/join framework is the ForkJoinPool class, an extension of the AbstractExecutorService class. ForkJoinPool implements the core work-stealing algorithm and can execute ForkJoinTask processes.

基本用法:

if (my portion of the work is small enough)
  do the work directly
else
  split my work into two pieces
  invoke the two pieces and wait for the results

在ForkJoinTask的子类中实现上面的代码。常用的ForkJoinTask子类有 RecursiveTaskRecursiveAction,RecursiveTask可以带返回值。

当ForkJoinTask的子类写好之后,将它传递给ForkJoinPool实例的invoke方法就可以开始执行任务。

下面是一个将图片模糊化的例子:

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

/**
 * ForkBlur implements a simple horizontal image blur. It averages pixels in the
 * source array and writes them to a destination array. The sThreshold value
 * determines whether the blurring will be performed directly or split into two
 * tasks.
 *
 * This is not the recommended way to blur images; it is only intended to
 * illustrate the use of the Fork/Join framework.
 */
public class ForkBlur extends RecursiveAction {

    private int[] mSource;
    private int mStart;
    private int mLength;
    private int[] mDestination;
    private int mBlurWidth = 15; // Processing window size, should be odd.

    public ForkBlur(int[] src, int start, int length, int[] dst) {
        mSource = src;
        mStart = start;
        mLength = length;
        mDestination = dst;
    }

    // Average pixels from source, write results into destination.
    protected void computeDirectly() {
        int sidePixels = (mBlurWidth - 1) / 2;
        for (int index = mStart; index < mStart + mLength; index++) {
            // Calculate average.
            float rt = 0, gt = 0, bt = 0;
            for (int mi = -sidePixels; mi <= sidePixels; mi++) {
                int mindex = Math.min(Math.max(mi + index, 0), mSource.length - 1);
                int pixel = mSource[mindex];
                rt += (float) ((pixel & 0x00ff0000) >> 16) / mBlurWidth;
                gt += (float) ((pixel & 0x0000ff00) >> 8) / mBlurWidth;
                bt += (float) ((pixel & 0x000000ff) >> 0) / mBlurWidth;
            }

            // Re-assemble destination pixel.
            int dpixel = (0xff000000)
                    | (((int) rt) << 16)
                    | (((int) gt) << 8)
                    | (((int) bt) << 0);
            mDestination[index] = dpixel;
        }
    }
    protected static int sThreshold = 1000000;

    @Override
    protected void compute() {
        if (mLength < sThreshold) {
            computeDirectly();
            return;
        }

        int split = mLength / 2;

        invokeAll(new ForkBlur(mSource, mStart, split, mDestination),
                new ForkBlur(mSource, mStart + split, mLength - split, 
                mDestination));
    }

    // Plumbing follows.
    public static void main(String[] args) throws Exception {
        String srcName = "red-tulips.jpg";
        File srcFile = new File(srcName);
        BufferedImage image = ImageIO.read(srcFile);
        
        System.out.println("Source image: " + srcName);
        
        BufferedImage blurredImage = blur(image);
        
        String dstName = "blurred-tulips.jpg";
        File dstFile = new File(dstName);
        ImageIO.write(blurredImage, "jpg", dstFile);
        
        System.out.println("Output image: " + dstName);
        
    }

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

        System.out.println("Array size is " + src.length);
        System.out.println("Threshold is " + sThreshold);

        int processors = Runtime.getRuntime().availableProcessors();
        System.out.println(Integer.toString(processors) + " processor"
                + (processors != 1 ? "s are " : " is ")
                + "available");

        ForkBlur fb = new ForkBlur(src, 0, src.length, dst);

        ForkJoinPool pool = new ForkJoinPool();

        long startTime = System.currentTimeMillis();
        pool.invoke(fb);
        long endTime = System.currentTimeMillis();

        System.out.println("Image blur took " + (endTime - startTime) + 
                " milliseconds.");

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

        return dstImage;
    }
}

如果像素点个数小于阈值sThreshold,就直接模糊化;否则将图片分为两部分,分两个任务同时进行模糊化。

Java 8中,有些标准库中的类也开始使用Fork/Join框架,比如java.util.Arrays中的parallelSort()方法。java.util.streams包中的一些方法也使用了Fork/Join框架,具体可以去看API文档。

Concurrent Collections

java.util.concurrent包提供了一些集合类,这些集合类能够避免内存一致性错误。比如:

  • BlockingQueue
  • ConcurrentHashMap(HashMap的并发版)
  • ConcurrentSkipListMap(TreeMap的并发版)

Atomic Variables

The java.util.concurrent.atomic package defines classes that support atomic operations on single variables. All classes have get and set methods that work like reads and writes on volatile variables. That is, a set has a happens-before relationship with any subsequent get on the same variable.

我们使用Atomic Variable重写前面的Counter

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }

}

Concurrent Random Numbers

在Java 7中,包含了一个新的类:ThreadLocalRandom,用于在多线程间产生随机数。在并发访问的情况下,它比Math.random()更不容易引起竞争,有更好的性能。

你可能感兴趣的:(【The Java™ Tutorials】【Concurrency】8. High Level Concurrency Objects)