Java并发编程指南(三):线程同步工具

1. 控制并发访问资源Semaphore

Semaphore是一个控制访问多个共享资源的计数器。
当一个线程想要访问某个共享资源,首先,它必须获得semaphore。如果semaphore的内部计数器的值大于0,那么semaphore减少计数器的值并允许访问共享的资源。计数器的值大于0表示,有可以自由使用的资源,所以线程可以访问并使用它们。
另一种情况,如果semaphore的计数器的值等于0,那么semaphore让线程进入休眠状态一直到计数器大于0。计数器的值等于0表示全部的共享资源都正被线程们使用,所以此线程想要访问就必须等到某个资源成为自由的。
当线程使用完共享资源时,他必须放出semaphore为了让其他线程可以访问共享资源。这个操作会增加semaphore的内部计数器的值。

//1.   创建一个会实现print queue的类名为 PrintQueue。
class PrintQueue {

    //2.   声明一个对象为Semaphore,称它为semaphore。
    private final Semaphore semaphore;

    //3.   实现类的构造函数并初始能保护print quere的访问的semaphore对象的值。
    public PrintQueue() {
        semaphore = new Semaphore(1);
    }

    //4.   实现Implement the printJob()方法,此方法可以模拟打印文档,并接收document对象作为参数。
    public void printJob(Object document) {

        //5.   在这方法内,首先,你必须调用acquire()方法获得demaphore。这个方法会抛出 InterruptedException异常,使用必须包含处理这个异常的代码。
        try {
            semaphore.acquire();

            //6.   然后,实现能随机等待一段时间的模拟打印文档的行。
            long duration = (long) (Math.random() * 10);
            System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n", Thread.currentThread().getName(), duration);
            Thread.sleep(duration);

        //7.	最后,释放semaphore通过调用semaphore的relaser()方法。
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
        }
    }
}

//8.   创建一个名为Job的类并一定实现Runnable 接口。这个类实现把文档传送到打印机的任务。
class Job implements Runnable {

    //9.   声明一个对象为PrintQueue,名为printQueue。
    private PrintQueue printQueue;

    //10. 实现类的构造函数,初始化这个类里的PrintQueue对象。
    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }

    //11. 实现方法run()。
    @Override
    public void run() {

        //12. 首先, 此方法写信息到操控台表明任务已经开始执行了。
        System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());

        //13. 然后,调用PrintQueue 对象的printJob()方法。
        printQueue.printJob(new Object());

        //14. 最后, 此方法写信息到操控台表明它已经结束运行了。
        System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
    }
}

//15. 实现例子的main类,创建名为 Main的类并实现main()方法。
class MainClient {
    public static void main(String args[]) {

        //16. 创建PrintQueue对象名为printQueue。
        PrintQueue printQueue = new PrintQueue();

        //17. 创建10个threads。每个线程会执行一个发送文档到print queue的Job对象。
        Thread thread[] = new Thread[10];
        for (int i = 0; i < 10; i++) {
            thread[i] = new Thread(new Job(printQueue), "Thread" + i);
        }

        //18. 最后,开始这10个线程们。
        for (int i = 0; i < 10; i++) {
            thread[i].start();
        }
    }
}

它是怎么工作的…

这个例子的关键是PrintQueue类的printJob()方法。此方法展示了3个你必须遵守的步骤当你使用semaphore来实现critical section时,并保护共享资源的访问:

  1. 首先, 你要调用acquire()方法获得semaphore。
  2. 然后, 对共享资源做出必要的操作。
  3. 最后, 调用release()方法来释放semaphore。
另一个重点是PrintQueue类的构造方法和初始化Semaphore对象。你传递值1作为此构造方法的参数,那么你就创建了一个binary semaphore。初始值为1,就保护了访问一个共享资源,在例子中是print queue。

当你开始10个threads,当你开始10个threads时,那么第一个获得semaphore的得到critical section的访问权。剩下的线程都会被semaphore阻塞直到那个获得semaphore的线程释放它。当这情况发生,semaphore在等待的线程中选择一个并给予它访问critical section的访问权。全部的任务都会打印文档,只是一个接一个的执行。

更多…
Semaphore类有另2个版本的 acquire() 方法:
  • acquireUninterruptibly():acquire()方法是当semaphore的内部计数器的值为0时,阻塞线程直到semaphore被释放。在阻塞期间,线程可能会被中断,然后此方法抛出InterruptedException异常。而此版本的acquire方法会忽略线程的中断而且不会抛出任何异常。
  • tryAcquire():此方法会尝试获取semaphore。如果成功,返回true。如果不成功,返回false值,并不会被阻塞和等待semaphore的释放。接下来是你的任务用返回的值执行正确的行动。
Semaphores的公平性

fairness的内容是指全java语言的所有类中,那些可以阻塞多个线程并等待同步资源释放的类(例如,semaphore)。默认情况下是非公平模式。在这个模式中,当同步资源释放,就会从等待的线程中任意选择一个获得资源,但是这种选择没有任何标准。而公平模式可以改变这个行为并强制选择等待最久时间的线程。

随着其他类的出现,Semaphore类的构造函数容许第二个参数。这个参数必需是 Boolean 值。如果你给的是 false 值,那么创建的semaphore就会在非公平模式下运行。如果你不使用这个参数,是跟给false值一样的结果。如果你给的是true值,那么你创建的semaphore就会在公平模式下运行。

2. 控制并发访问多个资源:

semaphore=new Semaphore(3);

Semaphore对象创建的构造方法是使用3作为参数的。前3个调用acquire() 方法的线程会获得临界区的访问权,其余的都会被阻塞 。当一个线程结束临界区的访问并解放semaphore时,另外的线程才可能获得访问权。


3. 等待多个并发事件完成CountDownLatch

Java并发API提供这样的类,它允许1个或者多个线程一直等待,直到一组操作执行完成。 这个类就是CountDownLatch类。它初始一个整数值,此值是线程将要等待的操作数。当某个线程为了想要执行这些操作而等待时, 它要使用 await()方法。此方法让线程进入休眠直到操作完成。 当某个操作结束,它使用countDown() 方法来减少CountDownLatch类的内部计数器。当计数器到达0时,这个类会唤醒全部使用await() 方法休眠的线程们。

//1.   创建一个类名为 Videoconference 并特别实现 Runnable 接口。这个类将实现 video-conference 系统。
class Videoconference implements Runnable {
    //2.   声明 CountDownLatch 对象名为 controller。
    private final CountDownLatch controller;
    //3.   实现类的构造函数,初始 CountDownLatch 属性。Videoconference 类接收将要等待的参与者的量为参数。
    public Videoconference(int number) {
        controller = new CountDownLatch(number);
    }
    //4.   实现 arrive() 方法。每次有参与者到达都会调用此方法。它接收String类型的参数名为 name。
    public void arrive(String name) {
        //5.   首先,它输出某某参数已经到达。
        System.out.printf("%s has arrived.", name);
        //6.   然后,调用CountDownLatch对象的 countDown() 方法。
        controller.countDown();
        //7.	最后,使用CountDownLatch对象的 getCount() 方法输出另一条关于还未确定到达的参与者数。
        System.out.printf("VideoConference: Waiting for %d participants.\n", controller.getCount());
    }
    //8.   实现video-conference 系统的主方法。它是每个Runnable都必须有的 run() 方法。
    @Override
    public void run() {
        //9.   首先,使用 getCount() 方法来输出这次video conference的参与值的数量信息。
        System.out.printf("VideoConference: Initialization: %d participants.\n", controller.getCount());
        //10. 然后, 使用 await() 方法来等待全部的参与者。由于此法会抛出 InterruptedException 异常,所以要包含处理代码。
        try {
            controller.await();
            //11. 最后,输出信息表明全部参与者已经到达。
            System.out.printf("VideoConference: All the participants have come\n");
            System.out.printf("VideoConference: Let's start...\n");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//12. 创建 Participant 类并实现 Runnable 接口。这个类表示每个video conference的参与者。
class Participant implements Runnable {
    //13. 声明一个私有 Videoconference 属性名为 conference.
    private Videoconference conference;
    //14. 声明一个私有 String 属性名为 name。
    private String name;
    //15. 实现类的构造函数,初始化那2个属性。
    public Participant(Videoconference conference, String name) {
        this.conference = conference;
        this.name = name;
    }
    //16. 实现参与者的run() 方法。
    @Override
    public void run() {
        //17.  首先,让线程随机休眠一段时间。
        long duration = (long) (Math.random() * 10);
        try {
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //18. 然后,使用Videoconference 对象的arrive() 方法来表明参与者的到达。
        conference.arrive(name);
    }
}
//19. 最后,实现例子的 main 类通过创建一个名为 Main 的类并为其添加 main() 方法。
class Client1 {
    public static void main(String[] args) {
        //20. 创建 Videoconference 对象名为 conference,将等待10个参与者。
        Videoconference conference = new Videoconference(10);
        //21. 创建 Thread 来运行这个 Videoconference 对象并开始运行。
        Thread threadConference = new Thread(conference);
        threadConference.start();
        //22. 创建 10个 Participant 对象,为每个对象各创建一个 Thread 对象来运行他们,开始运行全部的线程。
        for (int i = 0; i < 10; i++) {
            Participant p = new Participant(conference, "Participant " + i);
            Thread t = new Thread(p);
            t.start();
        }
    }
}
CountDownLatch类有3个基本元素:
  1. 初始值决定CountDownLatch类需要等待的事件的数量。
  2. await() 方法, 被等待全部事件终结的线程调用。
  3. countDown() 方法,事件在结束执行后调用。
当创建 CountDownLatch 对象时,对象使用构造函数的参数来初始化内部计数器。每次调用 countDown() 方法, CountDownLatch 对象内部计数器减一。当内部计数器达到0时, CountDownLatch 对象唤醒全部使用 await() 方法睡眠的线程们。
不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。一旦计数器的值初始后,唯一可以修改它的方法就是之前用的 countDown() 方法。当计数器到达0时, 全部调用 await() 方法会立刻返回,接下来任何countDown() 方法的调用都将不会造成任何影响。
此方法与其他同步方法有这些不同:

CountDownLatch 机制不是用来保护共享资源或者临界区。它是用来同步一个或者多个执行多个任务的线程。它只能使用一次。像之前解说的,一旦CountDownLatch的计数器到达0,任何对它的方法的调用都是无效的。如果你想再次同步,你必须创建新的对象。

CountDownLatch 类有另一种版本的 await() 方法,它是:
await(long time, TimeUnit unit): 此方法会休眠直到被中断; CountDownLatch 内部计数器到达0或者特定的时间过去了。


4.在同一个点同步任务CyclicBarrier:

Java 并发 API 提供了可以允许多个线程在在一个确定点进行同步。它是 CyclicBarrier 类。此类与在此章节的等待多个并发事件完成指南中的 CountDownLatch 类相似,但是它有一些特殊性让它成为更强大的类。
CyclicBarrier 类有一个整数初始值,此值表示将在同一点同步的线程数量。当其中一个线程到达确定点,它会调用await() 方法来等待其他线程。当线程调用这个方法,CyclicBarrier阻塞线程进入休眠直到其他线程到达。当最后一个线程调用CyclicBarrier 类的await() 方法,它唤醒所有等待的线程并继续执行它们的任务。
CyclicBarrier 类有个有趣的优势是,你可以传递一个外加的 Runnable 对象作为初始参数,并且当全部线程都到达同一个点时,CyclicBarrier类 会把这个对象当做线程来执行。此特点让这个类在使用 divide 和 conquer 编程技术时,可以充分发挥任务的并行性。

//1.  我们从实现2个辅助类开始。首先,创建一个类名为 MatrixMock。此类随机生成一个在1-10之间的 数字矩阵,我们将从中查找数字。
class MatrixMock {
    //2.   声明私有 int matrix,名为 data。
    private int data[][];
    //3.   实现类的构造函数。此构造函数将接收矩阵的行数,行的长度,和我们将要查找的数字作为参数。3个参数全部int 类型。
    public MatrixMock(int size, int length, int number) {
        //4.   初始化构造函数将使用的变量和对象。
        int counter = 0;
        data = new int[size][length];
        Random random = new Random();
        //5.   用随机数字填充矩阵。每生成一个数字就与要查找的数字对比,如果相等,就增加counter值。
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < length; j++) {
                data[i][j] = random.nextInt(10);
                if (data[i][j] == number) {
                    counter++;
                }
            }
        }
        //6.   最后,在操控台打印一条信息,表示查找的数字在生成的矩阵里的出现次数。此信息是用来检查线程们获得的正确结果的。
        System.out.printf("Mock: There are %d ocurrences of number %d in generated data.\n", counter, number);
    }
    //7.	实现 getRow() 方法。此方法接收一个 int为参数,是矩阵的行数。返回行数如果存在,否则返回null。
    public int[] getRow(int row) {
        if ((row >= 0) && (row < data.length)) {
            return data[row];
        }
        return null;
    }
}

//8.   现在,实现一个类名为 Results。此类会在array内保存被查找的数字在矩阵的每行里出现的次数。
class Results {
    //9.   声明私有 int array 名为 data。
    private int data[];
    //10. 实现类的构造函数。此构造函数接收一个表明array元素量的整数作为参数。
    public Results(int size) {
        data = new int[size];
    }
    //11. 实现 setData() 方法。此方法接收array的某个位置和一个值作为参数,然后把array的那个位置设定为那个值。
    public void setData(int position, int value) {
        data[position] = value;
    }
    //12. 实现 getData() 方法。此方法返回结果 array。
    public int[] getData() {
        return data;
    }
}
//13. 现在你有了辅助类,是时候来实现线程了。首先,实现 Searcher 类。这个类会在随机数字的矩阵中的特定的行里查找数字。
// 创建一个类名为Searcher 并一定实现  Runnable 接口.
class Searcher implements Runnable {
    //14. 声明2个私有int属性名为 firstRow 和 lastRow。这2个属性是用来确定将要用的子集的行。
    private int firstRow;
    private int lastRow;
    //15. 声明一个私有 MatrixMock 属性,名为 mock。
    private MatrixMock mock;
    //16. 声明一个私有 Results 属性,名为 results。
    private Results results;
    //17.  声明一个私有 int 属性名为 number,用来储存我们要查找的数字。
    private int number;
    //18. 声明一个 CyclicBarrier 对象,名为 barrier。
    private final CyclicBarrier barrier;

    //19. 实现类的构造函数,并初始化之前声明的全部属性。
    public Searcher(int firstRow, int lastRow, MatrixMock mock, Results results, int number, CyclicBarrier barrier) {
        this.firstRow = firstRow;
        this.lastRow = lastRow;
        this.mock = mock;
        this.results = results;
        this.number = number;
        this.barrier = barrier;
    }
    //20. 实现 run() 方法,用来查找数字。它使用内部变量,名为counter,用来储存数字在每行出现的次数。
    @Override
    public void run() {
        int counter;
        //21. 在操控台打印一条信息表明被分配到这个对象的行。
        System.out.printf("%s: Processing lines from %d to %d.\n", Thread.currentThread().getName(), firstRow, lastRow);
        //22. 处理分配给这个线程的全部行。对于每行,记录正在查找的数字出现的次数,并在相对于的 Results 对象中保存此数据。
        for (int i = firstRow; i < lastRow; i++) {
            int row[] = mock.getRow(i);
            counter = 0;
            for (int j = 0; j < row.length; j++) {
                if (row[j] == number) {
                    counter++;
                }
            }
            results.setData(i, counter);
        }
        //23. 打印信息到操控台表明此对象已经结束搜索。
        System.out.printf("%s: Lines processed.\n", Thread.currentThread().getName());
        //24. 调用 CyclicBarrier 对象的 await() 方法 ,由于可能抛出的异常,要加入处理 InterruptedException and BrokenBarrierException 异常的必需代码。
        try {
            barrier.await();
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}
//25. 现在,实现一个类来计算数字在这个矩阵里出现的总数。它使用储存了矩阵中每行里数字出现次数的 Results 对象来进行运算。创建一个类,名为 Grouper 并一定实现 Runnable 接口.
class Grouper implements Runnable {
    //26. 声明一个私有 Results 属性,名为 results。
    private Results results;
    //27.  实现类的构造函数,并初始化 Results 属性。
    public Grouper(Results results) {
        this.results = results;
    }
    //28.实现 run() 方法,用来计算结果array里数字出现次数的总和。
    @Override
    public void run() {
        //29. 声明一个 int 变量并写在操控台写一条信息表明开始处理了。
        int finalResult = 0;
        System.out.printf("Grouper: Processing results...\n");
        //30. 使用 results 对象的 getData() 方法来获得每行数字出现的次数。然后,处理array的全部元素,把每个元素的值加给 finalResult 变量。
        int data[] = results.getData();
        for (int number : data) {
            finalResult += number;
        }
        //31. 在操控台打印结果。
        System.out.printf("Grouper: Total result: %d.\n", finalResult);
    }
}
//32. 最后, 实现例子的 main 类,通过创建一个类,名为 Main 并为其添加 main() 方法。
class Main2 {
    public static void main(String[] args) {
        //33. 声明并初始5个常熟来储存应用的参数。
        final int ROWS = 10000;
        final int NUMBERS = 1000;
        final int SEARCH = 5;
        final int PARTICIPANTS = 5;
        final int LINES_PARTICIPANT = 2000;
        //34. Create a MatrixMock 对象,名为 mock. 它将有 10,000 行,每行1000个元素。现在,你要查找的数字是5。
        MatrixMock mock = new MatrixMock(ROWS, NUMBERS, SEARCH);
        //35. 创建 Results 对象,名为 results。它将有 10,000 元素。
        Results results = new Results(ROWS);
        //36. 创建 Grouper 对象,名为 grouper。
        Grouper grouper = new Grouper(results);
        //37.  创建 CyclicBarrier 对象,名为 barrier。此对象会等待5个线程。当此线程结束后,它会执行前面创建的 Grouper 对象。
        CyclicBarrier barrier = new CyclicBarrier(PARTICIPANTS, grouper);
        //38. 创建5个 Searcher 对象,5个执行他们的线程,并开始这5个线程。
        Searcher searchers[] = new Searcher[PARTICIPANTS];
        for (int i = 0; i < PARTICIPANTS; i++) {
            searchers[i] = new Searcher(i * LINES_PARTICIPANT, (i * LINES_PARTICIPANT) + LINES_PARTICIPANT, mock, results, 5, barrier);
            Thread thread = new Thread(searchers[i]);
            thread.start();
        }
        System.out.printf("Main: The main thread has finished.\n");
    }
}
例子中解决的问题比较简单。我们有一个很大的随机的整数矩阵,然后你想知道这矩阵里面某个数字出现的次数。为了更好的执行,我们使用了 divide 和 conquer 技术。我们 divide 矩阵成5个子集,然后在每个子集里使用一个线程来查找数字。这些线程是 Searcher 类的对象。
我们使用 CyclicBarrier 对象来同步5个线程的完成,并执行 Grouper 任务处理个别结果,最后计算最终结果。
如我们之前提到的,CyclicBarrier 类有一个内部计数器控制到达同步点的线程数量。每次线程到达同步点,它调用 await() 方法告知 CyclicBarrier 对象到达同步点了。CyclicBarrier 把线程放入睡眠状态直到全部的线程都到达他们的同步点。
当全部的线程都到达他们的同步点,CyclicBarrier 对象叫醒全部正在 await() 方法中等待的线程们,然后,选择性的,为CyclicBarrier的构造函数 传递的 Runnable 对象(例子里,是 Grouper 对象)创建新的线程执行外加任务。


CyclicBarrier 类有另一个版本的 await() 方法:
await(long time, TimeUnit unit): 线程会一直休眠直到被中断;内部计数器到达0,或者特定的时间过去了。
此类也提供了 getNumberWaiting() 方法,返回被 await() 方法阻塞的线程数,还有 getParties() 方法,返回将与CyclicBarrier同步的任务数。

重置 CyclicBarrier 对象
CyclicBarrier 类与CountDownLatch有一些共同点,但是也有一些不同。最主要的不同是,CyclicBarrier对象可以重置到它的初始状态,重新分配新的值给内部计数器,即使它已经被初始过了。
可以使用 CyclicBarrier的reset() 方法来进行重置操作。当这个方法被调用后,全部的正在await() 方法里等待的线程接收到一个 BrokenBarrierException 异常。此异常在例子中已经用打印stack trace处理了,但是在一个更复制的应用,它可以执行一些其他操作,例如重新开始执行或者在中断点恢复操作。

破坏 CyclicBarrier 对象 
CyclicBarrier 对象可能处于一个特殊的状态,称为 broken。当多个线程正在 await() 方法中等待时,其中一个被中断了,此线程会收到 InterruptedException 异常,但是其他正在等待的线程将收到 BrokenBarrierException 异常,并且 CyclicBarrier 会被置于broken 状态中。

CyclicBarrier 类提供了isBroken() 方法,如果对象在 broken 状态,返回true,否则返回false。

5. 运行阶段性并发任务Phaser

Java 并发 API 提供的一个非常复杂且强大的功能是,能够使用Phaser类运行阶段性的并发任务。当某些并发任务是分成多个步骤来执行时,那么此机制是非常有用的。Phaser类提供的机制是在每个步骤的结尾同步线程,所以除非全部线程完成第一个步骤,否则线程不能开始进行第二步。
相对于其他同步应用,我们必须初始化Phaser类与这次同步操作有关的任务数,我们可以通过增加或者减少来不断的改变这个数。


6. 控制并发阶段性任务的改变:

Phaser 类提供每次phaser改变阶段都会执行的方法。它是 onAdvance() 方法。它接收2个参数:当前阶段数和注册的参与者数;它返回 Boolean 值,如果返回false, phaser继续执行;如果返回true,即phaser结束运行并进入 termination 状态。

如果注册参与者为0,此方法的默认的实现值为真,要不然就是false。如果你扩展Phaser类并覆盖此方法,那么你可以修改它的行为。通常,当你要从一个phase到另一个,来执行一些行动时,你会对这么做感兴趣的。


7. 在并发任务间交换数据Exchanger:

Java 并发 API 提供了一种允许2个并发任务间相互交换数据的同步应用。更具体的说,Exchanger 类允许在2个线程间定义同步点,当2个线程到达这个点,他们相互交换数据,使用第一个线程的数据变成第二个的,然后第二个线程的数据变成第一个的。


参考资料:《Java 7 Concurrency Cookbook》

                 《Java 9 Concurrency Cookbook Second Edition》


猜你喜欢

转载自blog.csdn.net/sunjin9418/article/details/79523758