1.线程的概念:
多线程使得程序中的多个任务可以同时执行。Java的重要功能之一就是内部支持多线程,在一个程序中运行同时运行多个任务。
线程:一个程序可能包含多个同时运行的任务。线程是指一个任务从头至尾的执行流程。
在java中,每个任务都是Runnable接口的一个实例,也称为可运行对象(runnable object).线程本质上讲就是便于任务执行的对象。
2.创建任务和线程:
一个任务类必须实现 Runnable接口。任务必须从线程运行。
实例:
第一个任务打印字符 a 100次;
第二个任务打印字符 b 100次;
第三个任务打印字符 1到100的整数;
这三个线程讲共享CPU,并且在控制台上轮流打印字母和数字。
package TaskThread; public class TaskThreadDemo { public static void main(String[] args) { // TODO Auto-generated method stub Runnable printA = new PrintChar('a', 100); Runnable printB = new PrintChar('b', 100); Runnable print100 = new PrintNum(100); Thread thread1 = new Thread(printA); Thread thread2 = new Thread(printB); Thread thread3 = new Thread(print100); thread1.start(); thread2.start(); thread3.start(); } } class PrintChar implements Runnable{ private char charToPrint; private int times; public PrintChar(char c ,int t) { charToPrint = c; times = t; } @Override public void run() { // TODO Auto-generated method stub for(int i = 0; i < times; i++) { System.out.println("PrintChar" + charToPrint); } } } class PrintNum implements Runnable{ private int lastNum; public PrintNum(int n) { lastNum = n; } @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i <= lastNum; i++) { System.out.println(" " + i); } } }
调用start()方法启动一个线程,它会导致任务中的run()方法被执行。
3.Thread类:
Thread类包含为任务而创建的线程的构造方法,以及控制线程的方法。
方法 sleep(long mills) 可以将改线程设置为休眠以保障其他线程的执行。如果一个循环中调用了sleep方法,那就应该讲这个循环放在try-catch块中。
4.线程池:
可以使用线程池来高效的执行任务。
线程池是管理并发执行任务个数的理想方法。Java提供Executor接口来执线程池中的任务,提供ExecutorService接口来管理和控制任务。
ExecutorService executor = Executors.newFixedThreadPool(3); 创建了一个最大线程数为3的线程池执行器。
package TaskThread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExecutorsDemo { public static void main(String[] args) { // TODO Auto-generated method stub ExecutorService executor = Executors.newFixedThreadPool(3); executor.execute(new PrintCharA('a',150)); executor.execute(new PrintCharA('b',180)); executor.execute(new PrintNumA(190)); executor.shutdown(); } } class PrintCharA implements Runnable{ private char charToPrint; private int times; public PrintCharA(char c ,int t) { charToPrint = c; times = t; } @Override public void run() { // TODO Auto-generated method stub for(int i = 0; i < times; i++) { System.out.println("PrintChar" + charToPrint + "--" + i); } } } class PrintNumA implements Runnable{ private int lastNum; public PrintNumA(int n) { lastNum = n; } @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i <= lastNum; i++) { System.out.println(" " + i); } } }
shutdown()通知执行器关闭。不能接受新的任务,但是现有的任务将继续执行直至完成。
如果仅需要为一个任务创建一个线程,就使用Thread类。如果需要为多个任务创建线程,最好使用线程池。
5.线程同步:
线程同步用于协调相互依赖的线程的执行。
如果一个共享资源被多个线程同时访问,可能遭到破坏。下面的例子将说明这个问题。
假设创建并启动100个线程,每个线程都往同一个账户中添加一元钱。定义一个名为Account的类模拟账户,一个名为AddAPennyTask的类用来向账户里添加一个便是,以及一个用于创建和启动线程的主类。
package TaskThread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class AccountWithoutSync { private static Account account = new Account(); public static void main(String[] args) { // TODO Auto-generated method stub ExecutorService executor = Executors.newCachedThreadPool(); //Create and launch 100 threads for (int i = 0; i < 100; i++) { executor.execute(new AddPennyTask());; } executor.shutdown(); //Wait until all tasks are finished while(!executor.isTerminated()) { } System.out.println("账户余额:" + account.getBalance()); } private static class AddPennyTask implements Runnable{ @Override public void run() { // TODO Auto-generated method stub 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; try { Thread.sleep(5); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } balance = newBalance; } } }
AddAPennyTask和Account都是内部类。
改账户的初始余额为0 , 当所有的线程都完成时,余额应该为100,但是输出的结果不是可预测的。
如果运行了几遍程序还没有看出问题,可以增加休眠的时间,这会显著的增加出数据不一致问题的可能性。
问题是如何发生的:
step Balance Task1 Task2
1 0 newBalance = balance + 1;
2 0 newBalance = balance + 1;
3 1 balance = newBalance;
4 1 balance = newBalance;
任务1 和任务2 都向同一余额里加1
这个情景的效果就是任务1什么也没做,因为在步骤4中,任务2覆盖了任务1的结果。问题是任务1和任务2以一种引起冲突的方式访问一个公共资源,成为竞争状态(race contidiont) .
如果一个类的对象在多线程程序中没有导致竞争状态,该类为线程安全的(thread-safe)。
6.synchronized 关键字
为避免竞争状态,应为防止多个线程同时进入程序的某一个特定部分,程序中的这部分称为临界区(critical region).
事例中的临界区是整个deposit方法。可以使用关键字synchronized来同步方法,以便一次只有一个线程可以访问这个方法。
public synchronized void deposit(double amount)
这样Account类为线程安全的类。
7.利用加锁同步:
可以显式地采用锁和状态来同步线程。
public synchronized void deposit(double amount)
同步的实例方法在执行方法之前都隐式地需要一个加在实例上的锁.
Java 可以显式地加锁,这给协调线程带来了更多的控制功能。一个锁是一个Lock接口的实例,它定义了加锁和释放锁。锁也可以使用newCondition() 方法来创建任意个数的Condition对象,用来进行线程通信。
ReentrantLock 是Lock的一个具体实现,用于创建相互排斥的锁。可以创建具有特定的公平策略的锁。公平策略为真,则确保等待时间最长的线程首先获得锁。
package newThread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class lock { private static Account account = new Account(); public static void main(String[] args) { // TODO Auto-generated method stub ExecutorService executor = Executors.newCachedThreadPool(); for (int i = 0; i < 100; i++) { executor.execute(new AddPennyTask()); } executor.shutdown(); while(!executor.isTerminated()){ } System.out.println("账户余额:" + account.getBalance()); } public static class AddPennyTask implements Runnable{ @Override public void run() { // TODO Auto-generated method stub account.deposit(1); } } //An innner Class for Account public static class Account{ private static Lock lock = new ReentrantLock();//Create a lock private int balance = 0; public int getBalance(){ return balance; } public void deposit(int amount){ lock.lock();//Acquire the lock try { int newBalance = balance + amount; Thread.sleep(5); balance = newBalance; } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { lock.unlock(); } } } }
通常,使用synchronized方法或语句比使用相互排斥的显示锁更简单些。然而,使用显式锁对同步具有状态的线程更加直观和灵活。
8.线程间协作:
锁上的条件可以用于协调线程之间的交互。
通过保证在临界区上多个线程之间的相互排斥,线程同步完全可以避免竞争条件的发生,但是有时候还需要线程之间的相互协作。可以使用条件实现线程间通信。一个线程可以指定在某种条件下该做什么。
条件是通过Lock对象的newCondition()方法而创建的对象。一旦创建了条件,就可以使用await(), signal()和 signalAll()方法来实现进程之间的相互通信。
await() 方法可以让当前线程进入等待,直到条件发生; signal()方法唤醒一个等待的线程,而signalAll() 唤醒所有等待的线程。
事例:
启动两个任务,一个用来向账户中存款,另一个从同一账户中提款。当提款的数额大于账户的当前余额时,提款线程必须等待。不管什么时候,只要向账户新存入一笔资金,存储线程必须通知提款线程重新尝试。
为了同步这些操作,使用一个具有条件的锁 newDeposit (即增加到账户的新存款)。如果余额小于取款数额,提款任务将等待 newDeposit 条件。当存款任务给账户增加资金时,存款任务唤醒等待中的提款任务再次尝试。
package TaskThread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ThreadCooperation { private static Account account = new Account(); public static void main(String[] args) { // Create a thread pool with two threads ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute(new DepositTask()); executor.execute(new WithDrawTask()); executor.shutdown(); System.out.println("Thread 1 Thread 2 Balance"); } public static class DepositTask implements Runnable{ @Override public void run() { // TODO Auto-generated method stub try { while(true) { account.Deposit((int) (Math.random() * 10) + 1); Thread.sleep(1000); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } public static class WithDrawTask implements Runnable{ @Override public void run() { // TODO Auto-generated method stub while(true) { account.withDraw((int) (Math.random() * 10) + 1); } } } 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(" Wait for a deposit"); newDeposit.await(); } balance -= amount; System.out.println(" WithDraw " + amount + " " +getBalance()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { lock.unlock(); } } public void Deposit(int amount) { lock.lock();//Acquire the lock try { balance += amount; System.out.println("Deposit " + amount + " " + getBalance()); //Signal thread waiting on the condition newDeposit.signalAll(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); }finally { lock.unlock();//Release the lock } } } }
示例:生产者和消费者
9.阻塞队列:
Java合集框架提供了ArrayBlockingQueue , LinkedBlockingQueue 和 PriorityBlockingQueue来支持阻塞队列。
阻塞队列(blocking queue) 在试图向一个满队列添加元素或者从空队列中删除元素时会导致线程阻塞。BlockingQueue接口继承了java.util.Queue,并且提供同步的put和take方法向队列尾部添加元素,以及从队列头部删除元素。
ArrayBlockingQueue使用数组实现阻塞队列。必须使用一个容量或者可选的公平性策略来构造ArrayBlockingQueue。
LinkedBlockingQueue使用链表实现阻塞队列。可以创建无边界或有边界的LinkedBlockingQueue.
PriorityBlockingQueue是优先队列。可以创建无边界的或有 边界的优先队列。
10.信号量:
可以使用信号量来限制访问一个共享资源的线程数。
信号量指对共同资源进行访问控制的对象。在访问资源之前,线程必须从信号量获取许可。在访问完资源之后,这个线程必须将许可返回给信号量。
为了创建信号量,必须确定许可的数量,同时可炫耀公平策略。
java.util.concurrent.Semaphore
+Semaphore(numberOfPermits: int) 创建一个具有指定书目的许可信号量。公平性策略参数为假。