[Java] 多线程和并行编程

1  线程概念

Java为创建和运行线程以及锁定资源以防止冲突提供了非常好的支持。你可以在程序中创建额外的线程以执行并发任务。在Java中,每个任务是Runnable接口的实例,也称为一个runnable对象。一个线程实质上是一个对象,它为任务的执行提供便利。

2  创建任务和线程

task类必须实现 Runnable接口,task必须从线程执行。
task
是对象,为了创建task,必须首先为task定义一个实现了Runnable接口的类。Runnable接口相当简单,只包含了一个Run方法。
public class TaskClass implements Runnable {
    public TaskClass(...) {
    }
    // 实现Runnable中的run方法
    public void run() {
        // 告诉系统如何运行自定义线程
    }
}

public class Client {
    public void someMethod() {
        // 创建TaskClass的实例
        TaskClass task = new TaskClass(...);
        // 创建线程
        Thread thread = new Thread(task);
        // 启动线程
        thread.start();
    }
}

task必须在线程中执行,Thread类含有用于创建线程的构造函数,以及很多用于控制线程的方法,创建task的线程:
                                      Thread thread = new Thread(task);
然后调用start()方法告诉JVM,线程已经可以运行:
                                         thread.start();
JVM
通过调用taskrun()方法执行task. 下面的例子,创建3个线程,分别打印'a' 100 次,打印'b' 100 次, 以及打印 0 ~ 100 之间的整数:

public class TaskThreadDemo {
    public static void main(String[] args) {
        // 创建task
        Runnable printA = new PrintChar('a', 100);  // Runnable 改成 PrintChar 也可以
        Runnable printB = new PrintChar('b', 100);  // Runnable 改成 PrintChar 也可以
        Runnable print100 = new PrintNum(100);  // Runnable 改成 PrintNum 也可以
        // 创建线程
        Thread thread1 = new Thread(printA);
        Thread thread2 = new Thread(printB);
        Thread thread3 = new Thread(print100);
        // 启动线程
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

// 打印指定次数字符的 task
 class PrintChar implements Runnable {
    private char charToPrint; // The character to print
    private int times; // The number of times to repeat

    /** Construct a task with a specified character and number of
    * times to print the character
    */
    public PrintChar(char c, int t) {
        charToPrint = c;
        times = t;
    }

    @Override /** Override the run() method to tell the system
    * what task to perform
    */
    public void run() {
        for (int i = 0; i < times; i++) {
            System.out.print(charToPrint);
        }
    }
}

// The task class for printing numbers from 1 to n for a given n
class PrintNum implements Runnable {
    private int lastNum;

    /** Construct a task for printing 1, 2, ..., n */
    public PrintNum(int n) {
        lastNum = n;
    }

    @Override /** Tell the thread how to run */
    public void run() {
        for (int i = 1; i <= lastNum; i++) {
            System.out.print(" " + i);
        }
    }
}

输出结果:

run:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaababbbbbb 1 2 3aaabaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 4 5 6b 7b 8b 9 10 11
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
35 36bbbbbbbbbbbbbbbbbbbbb 37b 38b 39b 40b 41b 42b 43b 44b 45bb 46b 
47b 48b 49b 50b 51b 52 53 54 55 56 57 58 59 60 61 62b 63b 64 65 66 67 
68 69 70 71 72 73 74 75b 76b 77b 78b 79b 80b 81b 82b 83b 84b 85b 86b 
87b 88b 89b 90b 91b 92b 93b 94b 95b 96b 97b 98b 99b 100bbbbbbbbbbbbbb
bbbbbbbbbbb成功生成(总时间:0 秒)

Thread

Thread 类包含了创建线程的构造函数以及控制线程的方法
Thread
类实现接口 Runnable:

                          << interface >>
   java.lang.Thread  ->  java.lang.Runnable   
  
 

因为Thread实现Runnable,因此可以定义类继承Thread并实现Run方法:

// 自定义thread类
public class CustomThread extends Thread {
    public CustomThread(...) {
    }
    // 重写Runnable里的run方法
    public void run() {
        // 告诉系统如何执行这个task
    }
}

// 自定义类
public class Client {
    public void someMethod() {
    // 创建一个线程
    CustomThread thread1 = new CustomThread(...);
    // 启动线程
    thread1.start();
    // 创建另一个线程
    CustomThread thread2 = new CustomThread(...);
    // 启动线程
    thread2.start();
    }
}

但是这个方法不推荐,原因是它将task和运行task的机制混合起来,将task同线程分开是一种更好的设计。

Thread类也含有stop(), suspend(),以及resume()方法, 对于Java 2, 这些方法废弃不用了,因为它们本质上不安全,与其使用stop() 方法,应该将线程变量赋值为 null 以标识线程已终止

可以用yield() 方法暂时地将时间释放给其他线程,sleep(long millis) 方法使线程睡眠指定的时间,时间以毫秒为单位。例如下面的代码:
public void run() {
    try {
        for (int i = 1; i <= lastNum; i++) {
            System.out.print(" " + i);
            if (i >= 50) Thread.sleep(1);  // 如果 number>=50, 等待1毫秒
        }
    }
    catch (InterruptedException ex) {
    }
}


sleep方法可能抛出异常 InterruptedException,这是一种检查异常(checked exception),  如果睡眠线程的interrupt() 方法被调用,就会产生这种异常。但由于对线程极少会调用interrupt() 方法,所以InterruptedException不太可能会发生。但因为 Java 会强迫别人捕捉检查异常,所以仍然必须将代码放在try-catch块内。如果在循环内部调用了sleep方法,就必须将循环放在try-catch块内部,即使线程被中断,它也可能继续执行。

join()方法用于强迫一个thread等待另一个thread执行完成。如下的代码:

public void run() {
    Thread thread4 = new Thread(new PrintChar('c', 40));
    thread4.start();
    try {
        for (int i = 1; i <= lastNum; i++) {
            System.out.print (" " + i);
            if (i == 50) thread4.join();
        }
    }
    catch (InterruptedException ex) {
    }
}

50之后的整数,要等到 thread4 结束之后才打印。Java 给每一个线程赋一个优先级,默认情况下,线程继承父线程的优先级,可以通过setPriority方法增加或降低任意线程的优先级,而且还可以通过使用getPriority方法获得线程的优先级。优先级用 1~10范围内的数字表示,Thread类使用int常量MIN_PRIORITY, NORM_PRIORITY, MAX_PRIORITY分别表示1, 5, 和 10, main thread的优先级是Thread.NORM_PRIORITY.

JVM总是选择当前具有最高优先级的可运行线程, 优先级较低的线程只有在没有更高优先级的线程正在运行时才能运行。 如果所有可运行的线程具有相同优先级,则分配给在循环队列中的每个线程以同等比例的CPU时间,称为循环调度(round-robin scheduling)。例如:下列代码设置thread3为最高优先级,thread3将最先结束。

thread3.setPriority(Thread.MAX_PRIORITY);

4  例子学习:闪烁的文字

Thread可用于控制动画
一个JavaFX的例子:

package flashtext;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.scene.control.Label;

public class FlashText extends Application { 
    private String text = "";
    @Override
    public void start(Stage primaryStage) {
        StackPane pane = new StackPane();
        Label lblText = new Label("Programming is fun");
        pane.getChildren().add(lblText);
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        if (lblText.getText().trim().length() == 0)
                            text = "Welcome";
                        else
                            text = "";
                        Platform.runLater(new Runnable() { // Run from JavaFX GUI
                            @Override
                            public void run() {
                                lblText.setText(text);
                            }
                        });
                        Thread.sleep(1000);
                    }
                }
                catch (InterruptedException ex) {
                }
            }
        }).start();
        
    // Create a scene and place it in the stage
    Scene scene = new Scene(pane, 200, 50);
    primaryStage.setTitle("FlashText"); // Set the stage title
    primaryStage.setScene(scene); // Place the scene in the stage
    primaryStage.show(); // Display the stage    
    }
}

运行结果:可看到"welcome"交替出现并消失:


5  线程池

thread pool 用于高效地执行task

如果需要为一个task创建一个thread,那么用Thread类,如果有多个task,最好用thread pool,否则就要逐个task创建thread,这种做法会导致低吞吐量低性能。

Executor 接口用于在thread pool 中执行thread,ExecutorService 子接口用于管理和控制thread。
( 下图中应该为 java.util.concurrent.Executors? )

isTerminated() : 如果线程池中所有的task都已终止,则返回true。


1. 使用 Executor 类中的静态方法创建 Executor 对象。
2. newFixedThreadPool(int) 方法在池中创建固定数目的thread。如果一个线程结束执行task,则可以重用来执行另一个task。3. 如果一个thread在shutdown前失败,并且池中所有thread非idle状态,还有新的task等待执行,那么新的thread会被创建以取代出错的thread。
4. 如果池中所有thread非idle状态,并且还有新的task等待执行,newCachedThreadPool() 方法将创建新的thread。
5. 如果缓冲池中的线程超过60秒未被使用,则会被终止。缓冲池用来执行数目众多的短任务十分高效。


打印字母和数字的code可以修改为使用线程池的方式,Executor并发执行3个线程:

package testpackage;
import java.util.concurrent.*;
public class TaskThreadDemo {
    public static void main(String[] args) {
        // Create a fixed thread pool with maximum three threads
        ExecutorService executor = Executors.newFixedThreadPool(3);
        // Submit runnable tasks to the executor
        executor.execute(new PrintChar('a', 100));
        executor.execute(new PrintChar('b', 100));
        executor.execute(new PrintNum(100));
        // Shut down the executor
        executor.shutdown();
    }
}

如果将固定线程数由3改为1:
ExecutorService executor = Executors.newFixedThreadPool(1);
3个task将顺序执行。如果改为数目不固定,3个task将并发执行。
ExecutorService executor = Executors.newCachedThreadPool();
shutdown()则命令 executor关闭,之后不再接受新的task,但如果有存在的线程,那么这些线程将继续执行直到结束。

6  线程同步

线程同步是为了协调互相依赖的线程的执行。

下面的例子,创建100个线程,每个线程各往同一个银行账户里存1分钱,理论上全部线程执行完毕,账户结余应为100分,运行结果只有3分或其他不正确的结果等等。

package testpackage;
import java.util.concurrent.*;

public class TaskThreadDemo {
    
    private static Account account = new Account();
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();

        // Create and launch 100 threads
        for (int i = 0; i < 100; i++) {
            executor.execute(new AddAPennyTask());
        }
        executor.shutdown();
        // Wait until all tasks are finished
        while (!executor.isTerminated()) {
        }
        System.out.println("What is balance? " + account.getBalance());
    }
    // A thread for adding a penny to the account
    private static class AddAPennyTask implements Runnable {
        public void run() {
            account.deposit(1);
        }
    }

    // An inner class for account
    private static class Account {
        private int balance = 0;

        public int getBalance() {
            return balance;
        }

        public void deposit(int amount) {
            int newBalance = balance + amount;
            // data-corruption problem and make it easy to see.
            try {
                Thread.sleep(5);
            }
            catch (InterruptedException ex) {
            }
            balance = newBalance;
        }
    }
}

出问题的是以下两条语句,这两条语句各个线程叠加访问,造成访问冲突,变量值没有同步.  这两条语句之间等待的时间越长,执行结果越不正确:

newBalance = balance + 1;
balance = newBalance;

6.1  使用Synchronized关键字

为了解决上面的问题,使用Synchronized关键字, 对临界区加锁,执行完成后释放锁。

public synchronized void deposit(double amount)

同步的方法在执行前请求锁,如果是实例方法,对对象加锁。如果是静态方法,对类加锁, 方法执行结束后释放锁。

6.2  Synchronizing Statement (同步语句)

也可以只对语句块进行同步,语法:

synchronized (expr) {
   
 statements;
}

expr 必须为对象的引用,上例中,相应语句修改如下,运行结果就是100.

synchronized (account) {
       account.deposit(1);
}

比起对整个method加Synchronized, 这种改法能提高并发性。
以下两种写法等价:

1.
public synchronized void xMethod() {
    // method body
}

2.
public void xMethod() {
    synchronized (this) {
        // method body
    }
}

7  使用锁进行同步

使用Synchronized关键字,是隐式地对类或对象加锁。也可以使用Lock接口的实例,显式地获取锁。ReentrantLock 类是Lock的具体实现,可用于创建互斥锁。如果ReentrantLock构造函数的fair参数为true,那么等待时间最长的线程将获得锁,如果为false, 那么优先级随机。


往银行账号存钱的例子,可以修改为使用lock实现互斥访问:

public synchronized void deposit(int amount) {
    lock.lock();  // Acquire the lock           
    try {
        int newBalance = balance + amount;
        // This delay is deliberately added to magnify the
        // data-corruption problem and make it easy to see.
        Thread.sleep(5);
        balance = newBalance;
    }
    catch (InterruptedException ex) {
    }
    finally {
        lock.unlock(); // release the lock
    }
}

lock() 之后加一个 try-catch 块,并在 finally 语句中释放 lock,是一种好的做法,这样能保证lock一定会被释放。
一般而言,使用 Synchronized 关键字比使用显式的 lock 更简单,显式使用lock则更加灵活。

8  线程之间的合作

对锁加条件可用于协调线程交互。

线程同步已经足以通过保证临界区的线程互斥以避免竞争情况出现,但有时也需要实现线程合作的方法。condition 可用于实现线程间通信,thread可指定特定condition下的行为,condition 是通过对Lock对象调用 newContidition() 方法创建的对象。一旦condition创建,你可以使用它的await(), signal(), 和signalAll()方法用于thread通信。

«interface»
java.util.concurrent.Condition

---------------------------------
+await(): void                             使当前线程等待,直到条件信号发出。
+signal(): void                            唤醒一个等待的线程。
+signalAll(): Condition                唤醒所有等待的线程。


举一个例子来说明thread通信:假定你创建并启动了两个task: 一个往银行账户里存钱,一个从相同的账号里取钱。如果待提取的金额超出当前账户余额,那么取款task必须等待。一旦有新资金被存入账户,存款task将通知取款线程恢复执行(resume), 如果账户余额依然低于取款金额,那么取款线程继续等待新的存款。

为了同步操作,要使用带condition的lock:在account中增加一个newDeposit,假若余额低于取款金额,取款task将等待newDeposit 的 condition. 当存款task向账户价钱时,这个task给等待的取款task发信号,让取款task重试。两个task之间的交互如下图所示:


你可以从Lock对象创建一个 condition,为了使用condition,你必须首先获得一个lock。await()方法导致线程等待并按condition自动释放lock。一旦 condition正确,thread 获取 lock 并继续执行。假定初始余额为0,存款和取款金额随机生成。运行结果:

Deposit        Withdraw                                  Balance
==================================
Deposit 6                                                        6
                    Withdraw   3                               3
                    Want 4: wait for a deposit
Deposit 1                                                        4
                    Withdraw   4                               0
                    Want 4: wait for a deposit
Deposit 4                                                        4
                    Withdraw   4                               0
                    Want 8: wait for a deposit

==================================

如下是程序:

package testpackage;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

public class TaskThreadDemo {
    private static Account account = new Account();
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new DepositTask());
        executor.execute(new WithdrawTask());
    }
       
    public static class DepositTask implements Runnable {
        @Override // Keep adding an amount to the account
        public void run() {
            try { // Purposely delay it to let the withdraw method proceed
                while (true) {
                    account.deposit((int)(Math.random() * 10) + 1);
                    Thread.sleep(1000);
                }
            }
            catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }

    public static class WithdrawTask implements Runnable {
        @Override // Keep subtracting an amount from the account
        public void run() {
            while (true) {
                account.withdraw((int)(Math.random() * 10) + 1);
            }
        }
    }    

    // An inner class for account
    private static class Account {
        // Create a new lock
        private static Lock lock = new ReentrantLock();
        // Create a condition
        private static Condition newDeposit = lock.newCondition();

        private int balance = 0;
        public int getBalance() {
            return balance;
        }
        public void withdraw(int amount) {
            lock.lock(); // Acquire the lock
            try {
                while (balance < amount) {
                    System.out.println("\t\t\twant " + amount + " :wait for a deposit");
                    newDeposit.await();
                }
                balance -= amount;
                System.out.println("\t\t\tWithdraw " + amount + "\t\t" + getBalance());
            }
            catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            finally {
                lock.unlock(); // Release the lock
            }
        }

        public void deposit(int amount) {
            lock.lock(); // Acquire the lock
            try {
                balance += amount;
                System.out.println("Deposit " + amount + "\t\t\t\t\t" + getBalance());

                // Signal thread waiting on the condition
                newDeposit.signalAll();
            }
            finally {
                lock.unlock(); // Release the lock
            }
        }
    }          
}

这个例子包含:

1. 一个新的名为Account的inner class,以模拟有deposit(int)withdraw(int)两个方法的账号。
2. 一个名为 WithdrawTask的类,将从余额中取款。
3. 一个创建并启动两个线程的main

程序创建并提交两个task:depositwithdrawdeposit 故意睡眠,让withdraw先跑,当账号余额不足时,withdraw等待desposit发出的有关余额变化的通知。

lock创建后,名为 newDeposit的condition随后被创建。condition同lock绑定,在等待condition信号或发出condition信号之前,线程必须首先获得condition的lock, withdraw 任务获得了lock,如果余额不足则等待 newDeposit条件并释放lock. deposit任务获得lock并在新的存款动作完成后发信号给所有等待的线程以通知他们 newDeposit 条件。

如果将上例中的一个 while 循环改成下列的 if 语句会发生什么情况呢?

if (balance < amount) {
    System.out.println("\t\t\twant " + amount + " :wait for a deposit");
    newDeposit.await();
}

一旦账号结余改变,deposit 任务将通知withdraw 任务。(balance < amount) 在withdraw被唤醒之后可能仍然为true。使用 if 语句可能会导致不正确的取款。但如果使用循环语句,withdraw任务会有机会在取款重新检查condition是否满足。

Java 5 中新加来 lock 和 condition, Java 5 之前,线程通信是用对象的内建监视器实现的。lock 和 condition 比起内建监视器强大而灵活,所以你不需要再使用监视器。然而,如果你遇到的是Java遗产代码,就有可能遇到Java的内建监视器。

监视器是一个有互斥和同步能力的对象,每次只能有一个线程在监视器中执行它的方法,线程进入监视器前获取锁并对监视器加锁,退出时释放锁。任何对象都可以是一个监视器,一旦线程对对象加锁,该对象就成为监视器。加锁是通过对方法或代码库使用synchronized关键字实现的。在线程执行一个synchronized方法或代码块之前必须获得锁。如果条件不满足不能继续执行时,线程将会在监视器中等待。你可以对监视器对象调用wait() 方法以释放锁,以便其他的线程好进入监视器,这些线程或许能改变监视器的状态。如果condition 正确,其他线程能够调用notify() 或 notifyAll() 方法,以发出信号给一个或所有等待的线程,让它们收回锁并恢复执行。

wait(), notify(), 和 notifyAll()方法必须在synchronized方法内或synchronized块内由接收这些方法的对象调用  (The wait(), notify(), and notifyAll() methods must be called in a synchronized method or a synchronized block on the receiving object of these methods). 否则将产生IllegalMonitorStateException异常.

wait() 被调用时,它将暂停线程并同时释放加在对象上的锁,当线程收到通知后重新启动时,将自动获得锁。用于对象的wait(), notify(), notifyAll() 方法和用于conditionawait(), signal(), 以及signalAll()类似。

9 例子:生产者/消费者

这是一个演示线程合作的经典例子

有一个具有有限size的名称为Buffer的类,它有两个method:write(int) 和 read(),一个往Buffer里放整数(生产者),一个从Buffer里删除整数(消费者),为了实现同步,绑定两个条件 notEmpty 和 notFull到 lock,只有在Buffer非满时才写入,Buffer非空时才读取。


完整程序如下:

package consumerproducer;
import java.util.concurrent.locks.*;
import java.util.concurrent.*;

public class ConsumerProducer {
    private static Buffer buffer = new Buffer(); 

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        // TODO code application logic here
        // Create a thread pool with two threads
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new ProducerTask());
        executor.execute(new ConsumerTask());
        executor.shutdown();   
    }
    
    // A task for adding an int to the buffer
    private static class ProducerTask implements Runnable {
        public void run() {
            try {
                int i = 1;
                while (true) {
                    System.out.println("Producer writes " + i);
                    buffer.write(i++); // Add a value to the buffer
                    // Put the thread into sleep
                    Thread.sleep((int)(Math.random() * 10000));
                }
            }
            catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }

    // A task for reading and deleting an int from the buffer
    private static class ConsumerTask implements Runnable {
        public void run() {
            try {
                while (true) {
                    System.out.println("\t\t\tConsumer reads " + buffer.read());
                    // Put the thread into sleep
                    Thread.sleep((int)(Math.random() * 10000));
                }
            }
            catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }

    // An inner class for buffer
    private static class Buffer {
        private static final int CAPACITY = 1; // buffer size
        private java.util.LinkedList<Integer> queue =
        //new java.util.LinkedList<>();  // 由于Java 6 不支持钻石操作符,我改成了下面的一行
        new java.util.LinkedList();

        // Create a new lock
        private static Lock lock = new ReentrantLock();

        // Create two conditions
        private static Condition notEmpty = lock.newCondition();
        private static Condition notFull = lock.newCondition();

        public void write(int value) {
            lock.lock(); // Acquire the lock
            try {
                while (queue.size() == CAPACITY) {
                    System.out.println("Wait for notFull condition");
                    notFull.await();
                }
                queue.offer(value);
                notEmpty.signal(); // Signal notEmpty condition
            }
            catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            finally {
                lock.unlock(); // Release the lock
            }
        }

        public int read() {
            int value = 0;
            lock.lock(); // Acquire the lock
            try {
                while (queue.isEmpty()) {
                    System.out.println("\t\t\tWait for notEmpty condition");
                    notEmpty.await();
                }

                value = queue.remove();
                notFull.signal(); // Signal notFull condition
            }
            catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            finally {
                lock.unlock(); // Release the lock
                return value;
            }
        }  // end read
    } //end class Buffer   
} // end class CosumerProducer

运行结果:

Producer writes 1
                                Consumer reads 1
                                Wait for notEmpty condition
Producer writes 2
                                Consumer reads 2
Producer writes 3
                                Consumer reads 3
                                Wait for notEmpty condition
Producer writes 4
                                Consumer reads 4
                                Wait for notEmpty condition


10 阻塞队列

为了支持阻塞队列,Java 集合框架提供了ArrayBlockingQueue, LinkedBlockingQueue 以及 PriorityBlockingQueue

如果你试图往已经满掉的阻塞队列里添加元素,或者从空的阻塞队列中删除元素,都将导致thread阻塞。BlockingQueue接口继承了java.util.Queue, 并提供了同步的put和take方法,分别用于将元素添加到队列尾部,以及从队列头部移除元素。


Java中提供了3个具体的阻塞队列:ArrayBlockingQueue,LinkedBlockingQueue, 以及PriorityBlockingQueue,它们都包含在包java.util.concurrent里. ArrayBlockingQueue使用数组实现阻塞队列。如果要创建一个ArrayBlockingQueue,必须指定大小(capacity) 以及 fairness(可选)。LinkedBlockingQueue 是用链表实现的,分两种:bounded LinkedBlockingQueue 以及 unbounded LinkedBlockingQueue. PriorityBlockingQueue 是一种优先级队列,也分两种 : bounded PriorityBlockingQueue 和 unbounded PriorityBlockingQueue.


可以用ArrayBlockingQueue 简化生产者/消费者程序:在上例的生产者/消费者程序中,使用了lock和condition同步生产者和消费者线程,这个程序不需要,因为同步已经在ArrayBlockingQueue中实现。


11 信号量

信号量用于限制访问共享资源的线程数

在计算机科学中,信号量是一个对象,它控制着对公共的访问。访问资源之前,线程必须从信号量获得许可,访问结束后,线程必须返还许可给信号量,如下图所示:


为了创建信号量,你必须指定许可数目,另fairness可选, 如下图所示。一个task分别通过调用信号量的acquire()和release() 方法获得许可和释放许可。一旦许可被获取,信号量的可用许可数减 1,被释放则加 1,


许可数仅为1 的信号量可用于模拟互斥锁,下面的例子使用信号量以保证一个时间段内仅有一个线程可访问deposit方法。一个线程在执行deposit方法时首先获得许可,余额更新后,线程释放许可。永远将release() 方法放在finally语句中是一种很好的做法,这种做法可保证即使出现了异常,许可最终可被释放。


12 避免死锁

通过恰当的资源排序可避免死锁。

有时候两个或两个以上的线程需要在一些共享的对象上获取锁,这就可能导致死锁:两个线程互相将对方需要访问的对象加了锁,并等待被对方加锁的对象。通过资源排序可以轻松避免死锁,即为所有带有锁的对象指定顺序,每一个线程都按这种顺序获得锁。

13 线程状态

task 以线程方式执行,线程可以为5种状态之一,即New,Ready,Running,Blocked Finished.

线程新建时,状态为 New,调用它的start() 方法后,进入 Ready 状态,ready状态的线程可运行,但不是处于运行状态,操作系统必须为它分配CPU时间。

当一个ready状态的线程开始执行时,它就进入 Running 状态。一个running状态的线程当它的CPU时间到期或当它的yield()方法被调用时,可以进入Ready状态。


一个线程可以因为好几种原因进入Blocked 状态,Blocked 状态也就是inactive状态。例如调用了 join(), sleep()wait()方法,再如等待I/O操作完成等等。当阻塞线程的动作反转时,被阻塞的线程可能会被重新激活。例如一个睡眠状态的线程,睡眠时间用完,线程被重新激活,进入Ready状态。

最后,一个线程如果完成了 run() 方法的操作,状态就变为 Finished.  如果要知道线程的状态,用 isAlive()方法,如果是 Ready,Blocked,Running状态,方法返回true,否则如果是New且尚未启动或Finished,方法返回false

intrerrupt() 方法以下列方式中断线程:如果线程当前处于ReadyRunning状态,就设置它的interrupted flag; 如果线程当前被阻塞,会被唤醒并进入Ready状态,并抛出 java.lang.InterruptedException 异常。

14 同步的集合 Synchronized Collections

Java 的集合框架为list,set和map提供了同步的集合

Java Collections Framework 中的类是非 thread-safe的,也就是说,如果它们被多个线程并发访问和更新,它们的内容可能被破坏。你可以通过给collection上锁,或者使用synchronized collection保护数据。Collection类提供了6个静态方法,用于将collection包装为synchronized版本,使用这些方法创建的collection称为synchronized wrapper.


调用synchronizedCollection(Collection c)返回一个新的Collection对象,这个对象所有的访问和更新原有的collection c的方法都被同步,这些方法使用synchronized关键字实现,例如,add 方法是像下面这样实现的:

public boolean add(E o) {
    synchronized (this) {
        return c.add(o);
    }
}

Synchronized collection可以安全地被多个线程并发访问和修改,java.util.Vector, java.util.Stack, java.util.Hashtable 中的方法已经被同步, 这些都是JDK 1.0中的老的类。从JDK 1.5 开始,你应该使用java.util.ArrayList替换Vector,用java.util.LinkedList替换Stack,用java.util.Map替换Hashtable,如果需要synchronization, 使用synchronization wrapper.

synchronization wrapper 类是thread-safe的,但是iterator却是 fail-fast. 这意味着,如果你正在使用iterator遍历一个collection,而同时该collection正在被另一个thread修改,那么iterator会立刻fail,抛出异常               java.util.ConcurrentModificationException,此异常为RuntimeException的子类。为了避免这种错误,你需要创建一个synchronized collection对象并且在遍历时获取锁。例如,遍历一个set时,code必须像下面这样写:

Set hashSet = Collections.synchronizedSet(new HashSet());
synchronized (hashSet) {  // Must synchronize it
    Iterator iterator = hashSet.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

假如不这样写可能导致不确定的行为,比如 ConcurrentModificationExceptionFailure 异常

15 并行编程

Java 中的 Fork/Join 框架用于并行编程。

多核系统的广泛使用创造了软件的变革,为了利用多核处理器的优势,软件需要以并行的方式运行。JDK 7 为并行编程引入了新的 Fork/Join 框架,这种框架利用了多核处理器。Fork/Join 框架如下图所示(该图类似于一个叉子/fork, Fork的名称就是这么来的),待解决的问题被划分为若干非重叠的子问题,这些子问题可以以独立并行的方式解决。所有子问题的解决方案联合起来以获得初始问题的总体解决方案, 这就是分而治之(divide-and-conquer)方法的并行实现。在JDK 7 的Fork/Join 框架中,一个fork可被视为通过线程运行的独立task。






ForkJoinTask 是task的抽象基类, 一个ForkJoinTask是一个thread-like 实体,但是它远比一般的线程轻(lighter than),因为大量的任务及子任务可以被ForkJoinPool中的少数实际线程执行。多个task主要使用fork() 和 join() 进行协调。在task上调用fork() 会安排异步执行, 在task完成之前 join()必须等待. invoke()invokeAll(task) 方法隐式地调用fork()去执行task,隐式调用 join() 以等待任务完成,假如有结果的话,会返回结果。注意静态方法 invokeAll 使用 ... 语法选取可变数目的ForkJoinTask为参数(?? )。

Fork/Join 框架设计用来并行化分而治之解决方案,这是一种自然递归的解决方案。RecursiveAction RecursiveTaskForkJoinTask 的两个子类,为了定义具体的task类,你的类必须继承 RecursiveAction RecursiveTaskRecursiveAction 用于不会返回值的task, RecursiveTask用于有返回值的task。你设计的task类必须重写 compute() 方法以指定task如何执行。

我们现在使用 merge 排序来演示如何使用Fork/Join 框架开发并行程序,merge 排序算法将数组划分为两半,对每一半递归使用merge 排序。当两半都排好序后,算法再将二者合并。下面的程序merge排序算法的并行实现,并将它的执行时间和顺序排序进行比较。

因为排序算法不会返回值,所以我们可以通过继承 RecursiveAction 定义具体的 ForkJoinTask类,compute 方法被重写以实现递归的merge排序。如果列表较小,顺序实现排序效率更高。大的列表则被分为两半,这两半并行排序然后合并。

程序创建一个主 ForkJoinTask(main ForkJoinTask) , 一个ForkJoinPool, 并将主task放入forkJoinPool中执行,invoke 方法将会在main task完成后返回。

当执行main task时,task被划分为subtask,subtask被 invokeAll方法调用。invokeAll 方法将在所有的subtask完成后返回。注意每一个subtask会被进一步递归地划分为更小的task。数目巨大的subtask可以被创建并在池中执行。Fork/Join框架高效地自动执行并协调所有task。MergeSort 定义在 23.5列表中,程序调用 MergeSort.mergeSort 以使用merge排序顺序地对list排序。你可以看到并行排序比顺序排序快得多。

注意,用于初始化list的循环也可以并行化。然而你应该避免在代码中使用Math.random(), 因为它已经被同步化,不能被并行执行。parallelMergeSort 方法仅仅对整型数组进行排序,实际上它也是可以改成泛型方法的。

一般而言,一个问题可以使用如下的模式并行解决:

if (是个小程序)
    顺序解决;
else {
    将问题划分为不重叠的子问题;
    并发解决子问题;
    合并子问题的结果,以得到整个问题的解决方案;
}



 Introduction to Java Programming Chapter 30 (10th Edition)

猜你喜欢

转载自blog.csdn.net/ftell/article/details/79353571