Java程序设计17——多线程-Part-C
11 使用管道流
前面介绍的两种方式与其称为线程之间的通信,还不如称为线程之间协调运行的控制策略。如果需要在两条线程之间进行更多的信息交互,则可以考虑使用管道流进行通信。
管道流有3中存在形式:PipedInputStream和PipedOutputStream,PipedReader和PipedWriter以及Pipe.SinkChannel和Pipe.SourceChannel,它们分别是管道字节流、管道字符流和新IO的管道Channel
使用管道流实现多线程通信可按如下步骤进行:
1.使用new操作符分别创建管道输入流和管道输出流
2.使用管道输入流或管道输出流的connect方法把两个输入流和输出流连接起来
3.将管道输入流、管道输出流分别传入两个线程。
4.两个线程可以分别依赖个子的管道输入流、管道输出流进行通信
下面程序示范了如何使用管道流进行线程通信,程序实现遵守上面的步骤,程序如下:
1 package chapter16; 2 3 import java.io.*; 4 5 class ReaderThread extends Thread{ 6 private PipedReader pr; 7 //用于包装管道流的BufferedReader对象 8 private BufferedReader br; 9 public ReaderThread(){ 10 11 }; 12 public ReaderThread(PipedReader pr){ 13 this.pr = pr; 14 this.br = new BufferedReader(pr); 15 }; 16 public void run(){ 17 String buf = null; 18 try{ 19 //逐行读取管道输入流的内容 20 while((buf = br.readLine()) != null){ 21 System.out.println(buf); 22 } 23 }catch(IOException ex){ 24 ex.printStackTrace(); 25 }finally{ 26 try{ 27 if(br != null){ 28 br.close(); 29 } 30 }catch(IOException ex){ 31 ex.printStackTrace(); 32 } 33 } 34 } 35 }; 36 class WriterThread extends Thread{ 37 String[] books = new String[]{ 38 "English", 39 "Deutsch", 40 "Castellano", 41 "French" 42 }; 43 private PipedWriter pw; 44 private BufferedWriter bw; 45 public WriterThread(){ 46 47 }; 48 public WriterThread(PipedWriter pw){ 49 this.pw = pw; 50 this.bw = new BufferedWriter(pw); 51 }; 52 public void run(){ 53 try{ 54 //循环100次,向管道输出流输出100个字符串 55 for(int i = 0; i < 100; i++){ 56 pw.write(books[i%4] + "\n"); 57 } 58 }catch(IOException ex){ 59 ex.printStackTrace(); 60 }finally{ 61 try{ 62 if(pw != null){ 63 pw.close(); 64 } 65 }catch(IOException ex){ 66 ex.printStackTrace(); 67 } 68 } 69 } 70 } 71 72 public class PipedCommunicationTest { 73 public static void main(String[] args){ 74 PipedWriter pw = null; 75 PipedReader pr = null; 76 try{ 77 //分别创建两个独立的输入流、输出流 78 pw = new PipedWriter(); 79 pr = new PipedReader(); 80 //连接管道输入输出流 81 pw.connect(pr); 82 //将连接好的管道分别传入两个线程 83 //就可以让两个线程通过管道通信 84 new WriterThread(pw).start(); 85 new ReaderThread(pr).start(); 86 }catch(IOException ex){ 87 ex.printStackTrace(); 88 } 89 } 90 } 91 输出结果: 92 English 93 Deutsch 94 Castellano 95 French 96 English 97 Deutsch 98 Castellano 99 French 100 English 101 Deutsch 102 Castellano 103 French
上面代码创建了管道输入流和管道输出流,并将这两个管道流连接起来,至于程序其他地方使用管道流进行IO通信的代码与普通Writer、Reader进行IO通信没有区别。
通常没有必要使用管道流来控制两个线程之间的通信,因为两个线程属于同一个进程,它们可以非常方便地共享数据,这种方式才是线程之间进行信息交换的最好方式,而不是使用管道流。
12 线程组和未处理的异常
Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。对线程组的控制相当于同时控制这批线程。用户创建的所有线程都属于指定线程组,如果程序没有显式指定线程属于哪个线程组,则该线程属于默认线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内:例如A线程创建了线程B,并且没有指定线程B的线程组,则线程A和线程B属于同一个线程组。
一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。
Thread类提供了如下几个构造器来设置新创建的线程属于哪个线程组:
1.Thread(ThreadGroup group,Runnable target):以target的run方法作为线程执行体创建新线程,属于group线程组。
2.Thread(ThreadGroup group,Runnable target,String name):以target的run方法作为线程执行体创建新线程,该线程属于group线程组,且线程名为name。
3.Thread(ThreadGroup group,String name):创建新线程,新线程名为name,属于group线程组。
因为中途不可改变线程所属的线程组,所以Thread类没有提供setThreadGroup的方法来改变线程所属的线程组,但提供了一个getThreadGroup()方法来返回该线程所属的线程组,getThreadGroup()方法的返回值是ThreadGroup,表示一个线程组,ThreadGroup类有如下两个简单的构造器来创建实例:
1.ThreadGroup(String name):以指定线程组名字来创建新的线程组
2.ThreadGroup(ThreadGroup parent,String name):以指定的名字、指定的父线程组创建一个新线程组。
上面两个构造器创建线程组实例时都必须为其指定一个名字,也就是线程组总是具有一个字符串名字,该名称可调用ThreadGroup的getName()方法得到,但不允许改变线程组的名字。
在ThreadGroup里提供了如下几个常用的方法来操作整个线程组里的所有线程:
1.int activeCount():返回此线程组中活动线程的数目
2.interrupt():中断此线程组中的所有线程
3.isDaemon():判断该线程组是否是后台线程组
4.setDaemon(boolean daemon):把该线程组设置成后台线程组。后台线程组具有一个特征,当后台线程组的最后一个线程执行结束或最后一个线程被销毁,后台线程组将自动销毁。
5.setMaxPriority(int pri):设置线程组的最高优先级
下面程序创建了几条线程,它们分别属于不同的线程组,程序还将一个线程组设置成后台线程组。
1 package chapter16; 2 3 class TestThread extends Thread{ 4 //线程组的构造器 5 public TestThread(String name){ 6 7 }; 8 public TestThread(ThreadGroup group, String name){ 9 super(group,name); 10 }; 11 public void run(){ 12 for(int i = 0; i < 20; i++){ 13 System.out.println(getName() + "线程的i变量" + i); 14 } 15 }; 16 }; 17 public class ThreadGroupTest{ 18 public static void main(String[] args) throws InterruptedException{ 19 //获取主线程的线程组,这是所有线程默认的线程组 20 ThreadGroup mainGroup = Thread.currentThread().getThreadGroup(); 21 System.out.println("主线程组的名称是: " + mainGroup.getName()); 22 System.out.println("主线程是否是后台线程组:" + mainGroup.isDaemon()); 23 // Thread.sleep(1000); 24 new TestThread("主线程组的线程").start(); 25 ThreadGroup tg = new ThreadGroup("新线程组"); 26 tg.setDaemon(true); 27 System.out.println("主线程是否是后台线程组:" + tg.isDaemon()); 28 TestThread tt = new TestThread(tg, "新线程组甲"); 29 tt.start(); 30 new TestThread(tg,"tg组的线程乙").start(); 31 } 32 }
运行结果:
主线程组的名称是: main 主线程是否是后台线程组:false 主线程是否是后台线程组:true Thread-0线程的i变量0 Thread-0线程的i变量1 Thread-0线程的i变量2 Thread-0线程的i变量3 .............. Thread-0线程的i变量16 Thread-0线程的i变量17 Thread-0线程的i变量18 Thread-0线程的i变量19 新线程组甲线程的i变量0 新线程组甲线程的i变量1 新线程组甲线程的i变量2 .............. 新线程组甲线程的i变量17 新线程组甲线程的i变量18 新线程组甲线程的i变量19 tg组的线程乙线程的i变量0 tg组的线程乙线程的i变量1 tg组的线程乙线程的i变量2 tg组的线程乙线程的i变量3 tg组的线程乙线程的i变量4 tg组的线程乙线程的i变量5 tg组的线程乙线程的i变量6 tg组的线程乙线程的i变量7 ............... tg组的线程乙线程的i变量17 tg组的线程乙线程的i变量18 tg组的线程乙线程的i变量19 此外ThreadGroup内还定义了一个比较有用的方法:void uncaughtException(Thread t,Throwable e),该方法可以处理该线程组内的线程所抛出的未处理异常。
对于线程的异常处理,如果线程执行过程中抛出了一个未处理的异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,将会调用该对象的uncaughtException(Thread t,Throwable e)方法来处理该异常。
Thread.UncaughtExceptionHandler是Thread类的一个内部公共静态接口,该接口内只有一个方法:void uncauhtException(Thread t,Throwable e),该方法中的t代表出现异常的线程,而e表示该线程抛出的异常
13 Callable和Future
可能受C#(C#可以把任意方法包装成线程执行体,包括有返回值的方法)的启发,从JDK1.5开始,Java提供了Callable接口,该接口怎么看都像是Runnable接口的增强版,Callable接口也提供了一个call()方法可以作为线程执行体,但call方法比run()方法功能更强大:
1.call()方法可以有返回值
2.call()可以声明抛出异常。
因此我们完全可以提供一个Callable对象作为Thread的target,而该线程的线程执行体就是该Callable对象的call方法。问题是:Callable接口并非Runnable接口的子接口,所以Callable对象不能直接作为Thread的target。而且call()方法还有一个返回值————call()方法并不是直接调用,它是作为线程执行体被调用的。那么如何获取call()方法的返回值呢?
JDK1.5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口————可以作为Thread类的target
在Future接口里定义了如下几个公共方法来控制它关联的Callable任务:
1 boolean cancel(boolean mayInterruptIfRunning):试图取消对此任务的执行。 2 V get():如有必要,等待计算完成,然后获取其结果。 3 V get(long timeout, TimeUnit unit):如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。 4 boolean isCancelled():如果在任务正常完成前将其取消,则返回 true。 5 boolean isDone():如果任务已完成,则返回 true。
创建、并启动有返回值的线程的步骤如下:
1.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call方法又返回值
注意:Callable接口有泛型限制,Callable接口里的泛型形参类型与call方法返回值类型相同。
2.创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象包装了该Callable对象call()方法的返回值
3.使用FutureTask对象作为Thread对象的target创建,并启动新线程
4.调用FutureTask对象的方法来获得子线程执行结束后的返回值。
下面程序通过实现Callable接口来实现线程类,并启动该线程。
1 package chapter16; 2 3 import java.util.concurrent.*; 4 5 class RtnThread implements Callable<Integer>{ 6 //实现call方法,作为线程执行体 7 public Integer call(){ 8 int i = 0; 9 for(; i < 100; i++){ 10 System.out.println(Thread.currentThread().getName() + "的循环变量i的值 " + i); 11 } 12 //call方法可以有返回值 13 return i; 14 } 15 }; 16 17 public class CallableTest { 18 public static void main(String[] args){ 19 //创建Callable对象 20 RtnThread rt = new RtnThread(); 21 //使用FutureTask来包装Callable对象 22 FutureTask<Integer> task = new FutureTask<Integer>(rt); 23 for(int i = 0; i < 100; i++){ 24 System.out.println(Thread.currentThread().getName() + "的循环变量的i值:" + i); 25 if(i == 20){ 26 //实质还是以Callable对象来创建并启动线程 27 new Thread(task, "有返回值的线程").start(); 28 } 29 } 30 try{ 31 //获取线程返回值 32 System.out.println("子线程的返回值: " + task.get()); 33 }catch(Exception ex){ 34 ex.printStackTrace(); 35 } 36 } 37 }
输出结果: main的循环变量的i值:0 main的循环变量的i值:1 main的循环变量的i值:2 .................... main的循环变量的i值:22 main的循环变量的i值:23 main的循环变量的i值:24 有返回值的线程的循环变量i的值 0 main的循环变量的i值:25 有返回值的线程的循环变量i的值 1 main的循环变量的i值:26 有返回值的线程的循环变量i的值 2 main的循环变量的i值:27 有返回值的线程的循环变量i的值 3 main的循环变量的i值:28 有返回值的线程的循环变量i的值 4 main的循环变量的i值:29 有返回值的线程的循环变量i的值 5 main的循环变量的i值:30 有返回值的线程的循环变量i的值 6 main的循环变量的i值:31 有返回值的线程的循环变量i的值 7 ................................. 有返回值的线程的循环变量i的值 77 有返回值的线程的循环变量i的值 78 有返回值的线程的循环变量i的值 79 有返回值的线程的循环变量i的值 80 有返回值的线程的循环变量i的值 81 有返回值的线程的循环变量i的值 82 有返回值的线程的循环变量i的值 83 main的循环变量的i值:32 有返回值的线程的循环变量i的值 84 .......................... 有返回值的线程的循环变量i的值 97 有返回值的线程的循环变量i的值 98 有返回值的线程的循环变量i的值 99 main的循环变量的i值:33 main的循环变量的i值:34 main的循环变量的i值:35 ........................ main的循环变量的i值:97 main的循环变量的i值:98 main的循环变量的i值:99 子线程的返回值: 100
上面程序创建的Callable实现类与创建Runnable实现类并没有太大的差别,只是Callable的call方法允许声明抛出异常,而且允许带返回值。 上面的程序先创建一个Callable实现类的实例,然后将该实例包装成一个FutureTask对象。主线程中当循环变量i等于20时,程序启动以FutureTask对象为target的线程。程序最后调用FutureTask对象的get方法来返回call()方法的返回值————该方法将导致主线程被阻塞,直到call()方法结束并返回为止。
14 线程池
系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互。在这种情况下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行对象的run方法,当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中称为空闲状态,等待执行下一个Runnable对象的run方法。
除此之外,使用线程池可以有效地控制系统中并发线程的数量,但系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程数目不超过此数目。
JDK1.5提供了一个Executors工厂类来产生连接池,该工厂类里包含如下几个静态工厂方法来创建连接池:
1.newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。 2.newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池 3.newSingleThreadExecutor():创建一个只有单线程的线程池,它相当于newFixedThreadPool方法时传入参数为 4.newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。 5.newSingleThreadScheduledExecutor():创建只有一条线程的线程池,它可以在指定延迟后执行线程任务。
上面5个方法中前3个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runnable对象或Callable对象所代表的线程。而后两个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以在指定延迟后执行线程任务。
ExecutorService代表尽快执行线程的线程池(只要线程池中有空闲线程立即执行线程任务),程序只要将一个Runnable对象或Callable对象(代表线程任务)提交给该线程池即可,该线程池就会尽快执行该任务。ExecutorService里提供了如下三个方法:
当用完一个线程池后,应该调用线程池的shutdown()方法,该方法将启动线程池的关闭序列,调用了shutdown()方法后的线程池不再接受新任务,但会将以前所有已提交任务执行完成。当线程池中的所有任务都执行完成后,池中所有线程都会死亡;另外也可以调用线程池的shutdownNow()方法来关闭线程池,该方法视图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
使用线程池来执行线程任务的步骤如下:
1.调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。
2.创建Runnable实现类或Callable实现类的实例,作为线程执行任务。
3.调用ExecutorService对象的submit方法来提交Runnable实例或Callable实例。
4.当不想提交任何任务时调用ExecutorService对象的shutdown方法来关闭线程池。
1 package chapter16; 2 3 import java.util.concurrent.Executor; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 7 class TestThread1 implements Runnable{ 8 public void run(){ 9 for(int i = 0; i < 100; i++){ 10 System.out.println(Thread.currentThread().getName() + "的循环变量i的值" + i); 11 } 12 }; 13 }; 14 public class ThreadPoolTest{ 15 public static void main(String[] args){ 16 //创建一个具有固定数目线程数的线程池 17 ExecutorService pool = Executors.newFixedThreadPool(6); 18 //向线程池提交两个线程 19 pool.submit(new TestThread1()); 20 pool.submit(new TestThread1()); 21 //关闭线程池 22 pool.shutdown(); 23 } 24 }
上面程序中创建Runnable实现类与最开始创建线程池并没有太大差别,创建了Runnable实现类之后程序没有直接创建线程、启动线程来执行该Runnable任务,而是通过线程池来执行该任务,使用线程池来执行Runnable任务的代码如下所示。运行程序,结果如下:
pool-1-thread-2的循环变量i的值0 pool-1-thread-1的循环变量i的值0 ............................... pool-1-thread-1的循环变量i的值15 pool-1-thread-1的循环变量i的值16 pool-1-thread-1的循环变量i的值17 pool-1-thread-2的循环变量i的值1 pool-1-thread-2的循环变量i的值2 pool-1-thread-2的循环变量i的值3 ........................... pool-1-thread-2的循环变量i的值97 pool-1-thread-2的循环变量i的值98 pool-1-thread-2的循环变量i的值99 pool-1-thread-1的循环变量i的值18 pool-1-thread-1的循环变量i的值19 pool-1-thread-1的循环变量i的值20 pool-1-thread-1的循环变量i的值21 pool-1-thread-1的循环变量i的值22 ........................... pool-1-thread-1的循环变量i的值93 pool-1-thread-1的循环变量i的值94 pool-1-thread-1的循环变量i的值95 pool-1-thread-1的循环变量i的值96 pool-1-thread-1的循环变量i的值97 pool-1-thread-1的循环变量i的值98 pool-1-thread-1的循环变量i的值99
14 线程相关类
java还为线程安全提供了一些工具类,如ThreadLocal类,它代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每条线程创建一个该变量的副本,从而避免并发访问的线程安全问题。
14.1 ThreadLocal类
该类支出泛型,这个工具类可以很简洁地编写出优美的多线程程序。ThreadLocal是Thread Local Variable(是线程局部变量)的意思。线程局部变量ThreadLocal的功用其实非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。 ThreadLocal类的用法非常简单,只提供了三个public方法:
T get():返回此线程局部变量的当前线程副本中的值。
void remove():移除此线程局部变量当前线程的值。
void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。
1 package chapter8; 2 3 class Account{ 4 //定义一个ThreadLocal变量,该变量是每一个线程的局部变量 5 //每个线程都将保存一个副本 6 private ThreadLocal<String> name = new ThreadLocal<String>(); 7 //定义一个初始化name属性的构造器 8 public Account(String str){ 9 this.name.set(str); 10 //下面代码看到输出初始名 11 System.out.println("-------" + this.name.get()); 12 }; 13 //定义了name属性的getter和setter方法 14 public void setName(String str){ 15 this.name.set(str); 16 }; 17 public String getName(){ 18 return this.name.get(); 19 } 20 }; 21 class MyTest extends Thread{ 22 //定义一个Account属性 23 private Account account; 24 public MyTest(Account account, String name){ 25 super(name); 26 this.account = account; 27 }; 28 public void run(){ 29 //循环10次 30 for(int i = 0; i < 20; i++){ 31 //当i = 6时输出将账户名,替换成线程名 32 if(i == 6){ 33 account.setName(getName()); 34 } 35 //输出同一个账户的用户名和循环变量 36 System.out.println(account.getName() + "账户的i的值" + i); 37 } 38 } 39 } 40 41 public class ThreadLocalTest { 42 public static void main(String[] args){ 43 //启动两条线程,两条线程共享一个Account 44 Account at = new Account("初始名"); 45 /* 46 虽然两条线程共享同一个账户,即只有一个账户名 47 但由于账户名是ThreadLocal类型的,所以两条线程将 48 导致有同一个Account,但有两个账户名的副本,每条线程 49 都完全拥有各自的账户名副本,所以从i == 6之后,将看到两条 50 线程访问同一个账户时看到不同的账户名。 51 */ 52 new MyTest(at, "线程甲").start(); 53 new MyTest(at, "线程乙").start(); 54 } 55 }
输出结果: -------初始名 null账户的i的值0 null账户的i的值1 null账户的i的值2 null账户的i的值3 null账户的i的值4 null账户的i的值5 线程甲账户的i的值6 线程甲账户的i的值7 线程甲账户的i的值8 .................... 线程甲账户的i的值18 线程甲账户的i的值19 null账户的i的值0 null账户的i的值1 null账户的i的值2 null账户的i的值3 null账户的i的值4 null账户的i的值5 线程乙账户的i的值6 线程乙账户的i的值7 ................... 线程乙账户的i的值18 线程乙账户的i的值19
说明:上面程序中的账户名是一个ThreadLocal变量,所以虽然程序中只有一个Account对象,两条线程将会导致产生两个账户名。两条线程进行循环时都会在i == 6时将账户名修改为与线程名相同,这样就可以看得两个线程拥有2个账户名清晰。
ThreadLocal和其他所有的同步机制都是为了解决多线程中对同一变量的访问冲突,在普通的同步机制中,是通过对象加锁来实现多个线程对同一变量的安全访问的。在这种情形下,该变量是多个线程共享的,所以要使用这种同步机制需要很细致地分析在什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放该对象的锁等。在这种情形下,系统并没有将这份资源复制多份,只是采用了安全机制来控制对这份资源的访问而已。
ThreadLocal就从另一个角度来解决多线程的并发访问,ThreadLocal将需要并发访问的资源复制出多份来,每个线程都拥有一份资源,每一个线程都拥有自己的资源副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的整个变量封装进ThreadLocal,或者把该对象与线程相关的状态使用ThreadLocal保存。
ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上避免了多个线程之间共享资源(变量),也就不需要对多个线程进行同步了。 通常我们认为:如果需要进行多个线程之间共享资源,以达到线程之间的通信功能,就使用同步机制,如果仅仅需要隔离多个线程之间的共享冲突,可以使用ThreadLocal。
15 包装线程不安全的集合
前面介绍Java集合时所介绍的ArrayList、LinkedList、HashSet、TreeSet、HashMap等都是线程不安全的,也就是有可能当多个线程向这些集合中放入一个元素时,可能会破坏这些集合数据的完整性。
如果程序有多条线程可能访问以上这些集合,我们可以使用Collections提供的静态方法来把这些集合包装成线程安全的集合。Collections提供了如下几个静态方法:
static <T> Collection<T> synchronizedCollection(Collection<T> c):返回指定 collection 支持的同步(线程安全的)collection。 static <T> List<T> synchronizedList(List<T> list):返回指定列表支持的同步(线程安全的)列表。 static <K,V> Map<K,V> synchronizedMap(Map<K,V> m):返回由指定映射支持的同步(线程安全的)映射。 static <T> Set<T> synchronizedSet(Set<T> s):返回指定 set 支持的同步(线程安全的)set。 static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m):返回指定有序映射支持的同步(线程安全的)有序映射。 static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s):返回指定有序 set 支持的同步(线程安全的)有序 set。
例如我们需要在多线程中使用线程安全的HashMap对象,程序可以采用如下代码
//使用Collections的synchronizedMap方法将一个普通的HashMap包装成线程安全的类 HashMap m = Collections.synchronizedMap(new HashMap());
如果需要把某个集合包装成线程安全的集合,则应该在创建之后立即包装,如上程序所示:当HashMap对象创建后立即被包装线程安全的HashMap对象。
15.1 线程安全的集合类
实际上从JDK1.5开始,在java.util.concurrent包下提供了ConcurrentHashMap、ConcurrentLinkedQueue两个支撑并发访问的集合,它们分别代表了支撑并发访问的HashMap和支持并发访问的Queue
在默认情况下,它们都可以支持多条线程并发写访,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。ConcurrentHashMap和ConcurrentLinkedQueue集合都采用了非常复杂的算法,所以永远不会锁住整个集合。
当多个线程共享访问一个公共集合时,ConcurrentLinkedQueue是一个恰当的选择。ConcurrentLinkedQueue不允许使用null元素。ConcurrentLinkedQueue实现了多线程的高效访问,多条线程访问ConcurrentLinkedQueue集合时无须瞪大。
默认情况下ConcurrentHashMap支持16条线程并发写入,当有超过16条线程并发向该Map中写入数据时,可能有一些线程需要等待。实际上,程序通过设置concurrencyLevel构造方法参数(默认值为16)来支持更多的并发写入线程。
当我们使用java.util包下的Collection作为集合对象时,如果该集合对象创建迭代器后集合元素发生改变,将会引发ConcurrentModificationException
16 本章小结
本章主要介绍了Java的多线程编程支持。介绍了线程的基本概念并讲解了线程和线程之间的区别和联系。本章详细介绍了如何创建、启动多线程,并对比了两种创建多线程方式之间的优势和劣势,也详细介绍了线程的生命周期,本章还通过示例程序示范了控制线程的几个方法,还详细讲解了线程同步的意义和必要性,并介绍了两种不同的线程同步方法。另外也结束了三种实现线程通信的方式。
本章最后还介绍了JDK1.5新增的Callable和Future,使用Callable可以以第三种方式来创建线程,而Future则代表线程执行接收后的返回值,使用Callable和Future增强了Java的线程功能,最后介绍了池的相关类。