Java线程池

Java线程安全 专栏收录该内容
15 篇文章 0 订阅

线程池编程

java.util.concurrent多线程框架---线程池编程(一)

一般的服务器都需要线程池,比如Web、FTP等服务器,不过它们一般都自己实现了线程池,比如以前介绍过的Tomcat、Resin和Jetty等,现在有了JDK5,我们就没有必要重复造车轮了,直接使用就可以,何况使用也很方便,性能也非常高。

 1 package concurrent;
 2 import java.util.concurrent.ExecutorService; 
 3 import java.util.concurrent.Executors; 
 4 public class TestThreadPool 
 5 { 
 6     public static void main(String args[]) throws InterruptedException
 7     { 
 8         // only two threads 
 9         ExecutorService exec = Executors.newFixedThreadPool(2); 
10         for(int index = 0; index < 100; index++)
11         { 
12             Runnable run = new Runnable()
13             { 
14                 public void run() 
15                 { 
16                     long time = (long) (Math.random() * 1000); 
17                     System.out.println("Sleeping " + time + "ms");
18                     try
19                     {
20                         Thread.sleep(time); 
21                         
22                     } 
23                     catch (InterruptedException e) 
24                     { 
25                         
26                     } 
27                     
28                 }
29                 
30             };
31                     
32             exec.execute(run); 
33                     
34         } // must shutdown
35                     
36         exec.shutdown(); 
37         
38     } 
39     //}
40     }
41 //}
42 

 

上面是一个简单的例子,使用了2个大小的线程池来处理100个线程。但有一个问题:在for循环的过程中,会等待线程池有空闲的线程,所以主线程会阻塞的。为了解决这个问题,一般启动一个线程来做for循环,就是为了避免由于线程池满了造成主线程阻塞。不过在这里我没有这样处理。[重要修正:经过测试,即使线程池大小小于实际线程数大小,线程池也不会阻塞的,这与Tomcat的线程池不同,它将Runnable实例放到一个“无限”的BlockingQueue中,所以就不用一个线程启动for循环,Doug Lea果然厉害]

另外它使用了Executors的静态函数生成一个固定的线程池,顾名思义,线程池的线程是不会释放的,即使它是Idle。这就会产生性能问题,比如如果线程池的大小为200,当全部使用完毕后,所有的线程会继续留在池中,相应的内存和线程切换(while(true)+sleep循环)都会增加。如果要避免这个问题,就必须直接使用ThreadPoolExecutor()来构造。可以像Tomcat的线程池一样设置“最大线程数”、“最小线程数”和“空闲线程keepAlive的时间”。通过这些可以基本上替换Tomcat的线程池实现方案。

需要注意的是线程池必须使用shutdown来显式关闭,否则主线程就无法退出。shutdown也不会阻塞主线程。

许多长时间运行的应用有时候需要定时运行任务完成一些诸如统计、优化等工作,比如在电信行业中处理用户话单时,需要每隔1分钟处理话单;网站每天凌晨统计用户访问量、用户数;大型超时凌晨3点统计当天销售额、以及最热卖的商品;每周日进行数据库备份;公司每个月的10号计算工资并进行转帐等,这些都是定时任务。通过 java的并发库concurrent可以轻松的完成这些任务,而且非常的简单。

 

 1 package concurrent;
 2 import static java.util.concurrent.TimeUnit.SECONDS;
 3 import java.util.Date; 
 4 import java.util.concurrent.Executors; 
 5 import java.util.concurrent.ScheduledExecutorService;
 6 import java.util.concurrent.ScheduledFuture; 
 7 
 8 public class TestScheduledThread 
 9 { 
10     public static void main(String[] args) 
11     { 
12         final ScheduledExecutorService scheduler = Executors .newScheduledThreadPool(2); 
13         final Runnable beeper = new Runnable() 
14         { 
15             int count = 0; 
16             public void run() 
17             { 
18                 System.out.println(new Date() + "beep "+ (++count));
19             } 
20         }; // 1秒钟后运行,并每隔2秒运行一次
21         final ScheduledFuture beeperHandle = scheduler.scheduleAtFixedRate( beeper, 1, 2, SECONDS);
22         // 2秒钟后运行,并每次在上次任务运行完后等待5秒后重新运行 
23         final ScheduledFuture beeperHandle2 = scheduler .scheduleWithFixedDelay(beeper, 2, 5, SECONDS); 
24         // 30秒后结束关闭任务,并且关闭Scheduler 
25         scheduler.schedule(
26             new Runnable() 
27             {
28                 public void run() 
29                 { 
30                     beeperHandle.cancel(true);
31                     beeperHandle2.cancel(true);
32                     scheduler.shutdown(); 
33                 } 
34             }, 30, SECONDS); 
35     } 
36 }
37 
38 
39 

 

为了退出进程,上面的代码中加入了关闭Scheduler的操作。而对于24小时运行的应用而言,是没有必要关闭Scheduler的。

在实际应用中,有时候需要多个线程同时工作以完成同一件事情,而且在完成过程中,往往会等待其他线程都完成某一阶段后再执行,等所有线程都到达某一个阶段后再统一执行。

比如有几个旅行团需要途经深圳、广州、韶关、长沙最后到达武汉。旅行团中有自驾游的,有徒步的,有乘坐旅游大巴的;这些旅行团同时出发,并且每到一个目的地,都要等待其他旅行团到达此地后再同时出发,直到都到达终点站武汉。

这时候CyclicBarrier就可以派上用场。CyclicBarrier最重要的属性就是参与者个数,另外最要方法是await()。当所有线程都调用了await()后,就表示这些线程都可以继续执行,否则就会等待。

 

package concurrent; 
import java.text.SimpleDateFormat;
import java.util.Date; 
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
class TestCyclicBarrier 

    // 徒步需要的时间: Shenzhen, Guangzhou, Shaoguan, Changsha, Wuhan 
    public static int[] timeWalk = { 5, 8, 15, 15, 10 }; 
    // 自驾游 
    public static int[] timeSelf = { 1, 3, 4, 4, 5 };
    // 旅游大巴 
    public static int[] timeBus = { 2, 4, 6, 6, 7 }; 
    public static String now() 
    { 
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); 
        return sdf.format(new Date()) + ": "; 
    } 
}

public class Tour implements Runnable 

    private int[] times; 
    private CyclicBarrier barrier;
    private String tourName; 
    public Tour(CyclicBarrier barrier, String tourName, int[] times) 
    { 
        this.times = times; 
        this.tourName = tourName; 
        this.barrier = barrier;
    } 
    public void run() 
    { 
        try 
        { 
            Thread.sleep(times[0] * 1000); 
            System.out.println(TestCyclicBarrier.now() + tourName + "Reached Shenzhen");
            barrier.await();
            Thread.sleep(times[1] * 1000);
            System.out.println(TestCyclicBarrier.now() + tourName + " Reached Guangzhou"); 
            barrier.await(); Thread.sleep(times[2] * 1000);
            System.out.println(TestCyclicBarrier.now() + tourName + " Reached Shaoguan");
            barrier.await();
            Thread.sleep(times[3] * 1000); 
            System.out.println(TestCyclicBarrier.now() + tourName + " Reached Changsha"); 
            barrier.await(); 
            Thread.sleep(times[4] * 1000);
            System.out.println(TestCyclicBarrier.now() + tourName + " Reached Wuhan"); 
            barrier.await(); 
        } 
        catch (InterruptedException e) 
        { } 
        catch (BrokenBarrierException e) 
        { } 
    } 

    public static void main(String[] args) 
    { // 三个旅行团 
        CyclicBarrier barrier = new CyclicBarrier(3);
        ExecutorService exec = Executors.newFixedThreadPool(3);
        exec.submit(new Tour(barrier, "WalkTour", TestCyclicBarrier.timeWalk));
        exec.submit(new Tour(barrier, "SelfTour", TestCyclicBarrier.timeSelf));
        exec.submit(new Tour(barrier, "BusTour", TestCyclicBarrier.timeBus));
        exec.shutdown();
    }
}
    

运行结果: 00:02:25: SelfTour Reached Shenzhen00:02:25: BusTour Reached Shenzhen 00:02:27: WalkTour Reached Shenzhen00:02:30: SelfTour Reached Guangzhou 00:02:31:BusTour Reached Guangzhou 00:02:35: WalkTourReached Guangzhou00:02:39: SelfTour Reached Shaoguan 00:02:41: BusTour Reached Shaoguan

并发库中的BlockingQueue是一个比较好玩的类,顾名思义,就是阻塞队列。该类主要提供了两个方法put()和take(),前者将一个对象放到队列中,如果队列已经满了,就等待直到有空闲节点;后者从head取一个对象,如果没有对象,就等待直到有可取的对象。

下面的例子比较简单,一个读线程,用于将要处理的文件对象添加到阻塞队列中,另外四个写线程用于取出文件对象,为了模拟写操作耗时长的特点,特让线程睡眠一段随机长度的时间。另外,该Demo也使用到了线程池和原子整型(AtomicInteger),AtomicInteger可以在并发情况下达到原子化更新,避免使用了synchronized,而且性能非常高。由于阻塞队列的put和take操作会阻塞,为了使线程退出,特在队列中添加了一个“标识”,算法中也叫“哨兵”,当发现这个哨兵后,写线程就退出。

当然线程池也要显式退出了。

线程池--java.util.concurrent多线程框架(二)

 

当然线程池也要显式退出了。

package concurrent;
import java.io.File;
import java.io.FileFilter;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

public class TestBlockingQueue {
static long randomTime() {
return (long) (Math.random() * 1000);
}

public staticvoid main(String[] args) {
// 能容纳100个文件
final BlockingQueue queue = new LinkedBlockingQueue(100);
// 线程池
final ExecutorService exec = Executors.newFixedThreadPool(5);
final File root = new File(“F:\\JavaLib”);
// 完成标志
final File exitFile = new File(“”);
// 读个数
final AtomicInteger rc = new AtomicInteger();
// 写个数
final AtomicInteger wc = new AtomicInteger();
// 读线程
Runnable read = new Runnable() {
public void run() {
scanFile(root);
scanFile(exitFile);
}

public void scanFile(File file) {
if (file.isDirectory()) {
File[] files = file.listFiles(new FileFilter() {
public boolean accept(File pathname) {
return pathname.isDirectory()
|| pathname.getPath().endsWith(“.java”);
}
});
for (File one : files)
scanFile(one);
} else {
try {
int index = rc.incrementAndGet();
System.out.println(“Read0: ” + index + ”“
+ file.getPath());
queue.put(file);
} catch (InterruptedException e) {
}
}
}
};
exec.submit(read);
// 四个写线程
for (int index = 0; index < 4; index++) {
// write thread
final int NO = index;
Runnable write = new Runnable() {
String threadName = “Write” + NO;
public void run() {
while (true) {
try {
Thread.sleep(randomTime());
int index = wc.incrementAndGet();
File file = queue.take();
// 队列已经无对象
if (file == exitFile) {
// 再次添加”标志”,以让其他线程正常退出
queue.put(exitFile);
break;
}
System.out.println(threadName + “: ” + index + ”“
+ file.getPath());
} catch (InterruptedException e) {
}
}
}
};
exec.submit(write);
}
exec.shutdown();
}
}

从名字可以看出,CountDownLatch是一个倒数计数的锁,当倒数到0时触发事件,也就是开锁,其他人就可以进入了。在一些应用场合中,需要等待某个条件达到要求后才能做后面的事情;同时当线程都完成后也会触发事件,以便进行后面的操作。

 

CountDownLatch最重要的方法是countDown()和await(),前者主要是倒数一次,后者是等待倒数到0,如果没有到达0,就只有阻塞等待了。

一个CountDouwnLatch实例是不能重复使用的,也就是说它是一次性的,锁一经被打开就不能再关闭使用了,如果想重复使用,请考虑使用CyclicBarrier

下面的例子简单的说明了CountDownLatch的使用方法,模拟了100米赛跑,10名选手已经准备就绪,只等裁判一声令下。当所有人都到达终点时,比赛结束。

同样,线程池需要显式shutdown。

package concurrent;

 

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestCountDownLatch {
public static void main(String[] args) throws InterruptedException{
// 开始的倒数锁
final CountDownLatch begin = new CountDownLatch(1);
// 结束的倒数锁
final CountDownLatch end = new CountDownLatch(10);
// 十名选手
final ExecutorService exec = Executors.newFixedThreadPool(10);
for(int index = 0; index < 10; index++) {
final int NO = index + 1;
Runnable run = new Runnable(){
public void run() {
try {
begin.await();
Thread.sleep((long) (Math.random() * 10000));
System.out.println(“No.” + NO + ” arrived”);
} catch (InterruptedException e) {
} finally {
end.countDown();
}
}
};
exec.submit(run);
}
System.out.println(“Game Start”);
begin.countDown();
end.await();
System.out.println(“Game Over”);
exec.shutdown();
}
}

运行结果:
Game Start
No.4 arrived
No.1 arrived
No.7 arrived
No.9 arrived
No.3 arrived
No.2 arrived
No.8 arrived
No.10 arrived
No.6 arrived
No.5 arrived
Game Over

有时候在实际应用中,某些操作很耗时,但又不是不可或缺的步骤。比如用网页浏览器浏览新闻时,最重要的是要显示文字内容,至于与新闻相匹配的图片就没有那么重要的,所以此时首先保证文字信息先显示,而图片信息会后显示,但又不能不显示,由于下载图片是一个耗时的操作,所以必须一开始就得下载。

 

Java的并发库Future类就可以满足这个要求。Future的重要方法包括get()和cancel(),get()获取数据对象,如果数据没有加载,就会阻塞直到取到数据,而 cancel()是取消数据加载。另外一个get(timeout)操作,表示如果在timeout时间内没有取到就失败返回,而不再阻塞。

下面的Demo简单的说明了Future的使用方法:一个非常耗时的操作必须一开始启动,但又不能一直等待;其他重要的事情又必须做,等完成后,就可以做不重要的事情。

package concurrent;

 

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class TestFutureTask {
public static void main(String[] args)throws InterruptedException,
ExecutionException {
final ExecutorService exec = Executors.newFixedThreadPool(5);
Callable call = new Callable() {
public String call() throws Exception {
Thread.sleep(1000 * 5);
return “Other less important but longtime things.”;
}
};
Future task = exec.submit(call);
// 重要的事情
Thread.sleep(1000 * 3);
System.out.println(“Let’s doimportant things.”);
// 其他不重要的事情
String obj = task.get();
System.out.println(obj);
// 关闭线程池
exec.shutdown();
}
}

运行结果:
Let’s do important things.
Other less important but longtime things.

考虑以下场景:浏览网页时,浏览器了5个线程下载网页中的图片文件,由于图片大小、网站访问速度等诸多因素的影响,完成图片下载的时间就会有很大的不同。如果先下载完成的图片就会被先显示到界面上,反之,后下载的图片就后显示。

 

Java的并发库CompletionService可以满足这种场景要求。该接口有两个重要方法:submit()和take()。submit用于提交一个runnable或者callable,一般会提交给一个线程池处理;而take就是取出已经执行完毕runnable或者callable实例的Future对象,如果没有满足要求的,就等待了。 CompletionService还有一个对应的方法poll,该方法与take类似,只是不会等待,如果没有满足要求,就返回null对象。

package concurrent;

 

import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class TestCompletionService {
public static void main(String[] args) throws InterruptedException,
ExecutionException {
ExecutorService exec = Executors.newFixedThreadPool(10);
CompletionService serv =
new ExecutorCompletionService(exec);

for (int index = 0; index < 5;index++) {
final int NO = index;
Callable downImg = new Callable() {
public String call() throws Exception {
Thread.sleep((long) (Math.random() * 10000));
return “Downloaded Image ” + NO;
}
};
serv.submit(downImg);
}

Thread.sleep(1000 *2);
System.out.println(“Show web content”);
for (int index = 0; index < 5; index++) {
Future task = serv.take();
String img = task.get();
System.out.println(img);
}
System.out.println(“End”);
// 关闭线程池
exec.shutdown();
}
}

运行结果:
Show web content
Downloaded Image 1
Downloaded Image 2
Downloaded Image 4
Downloaded Image 0
Downloaded Image 3
End

操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java并发库Semaphore可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,acquire()获取一个许可,如果没有就等待,而release()释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。

Semaphore维护了当前访问的个数,提供同步机制,控制同时访问的个数。在数据结构中链表可以保存“无限”的节点,用Semaphore可以实现有限大小的链表。另外重入锁ReentrantLock也可以实现该功能,但实现上要负责些,代码也要复杂些。

下面的Demo中申明了一个只有5个许可的Semaphore,而有20个线程要访问这个资源,通过acquire()和release()获取和释放访问许可。

package concurrent;

 

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class TestSemaphore {
public static void main(String[] args) {
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
// 只能5个线程同时访问
final Semaphore semp = new Semaphore(5);
// 模拟20个客户端访问
for (int index = 0; index < 20; index++) {
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
// 获取许可
semp.acquire();
System.out.println(“Accessing: ” + NO);
Thread.sleep((long) (Math.random() * 10000));
// 访问完后,释放
semp.release();
} catch (InterruptedException e) {
}
}
};
exec.execute(run);
}
// 退出线程池
exec.shutdown();
}
}

运行结果:
Accessing: 0
Accessing: 1
Accessing: 2
Accessing: 3
Accessing: 4
Accessing: 5
Accessing: 6
Accessing: 7
Accessing: 8
Accessing: 9
Accessing: 10
Accessing: 11
Accessing: 12
Accessing: 13
Accessing: 14
Accessing: 15
Accessing: 16
Accessing: 17
Accessing: 18
Accessing: 19

java.util.concurrent多线程框架---线程池编程(三)

1 引言
在软件项目开发中,许多后台服务程序的处理动作流程都具有一个相同点,就是:接受客户端发来的请求,对请求进行一些相关的处理,最后将处理结果返回给客户 端。这些请求的来源和方式可能会各不相同,但是它们常常都有一个共同点:数量巨大,处理时间短。这类服务器在实际应用中具有较大的普遍性,如web服务 器,短信服务器,DNS服务器等等。因此,研究如何提高此类后台程序的性能,如何保证服务器的稳定性以及安全性都具有重要的实用价值。
2 后台服务程序设计
2.1 关于设计原型
构建服务器应用程序的一个简单的模型是:启动一个无限循环,循环里放一个监听线程监听某个地址端口。每当一个请求到达就创建一个新线程,然后新线程为请求服务,监听线程返回继续监听。

 

//简单举例如下:
import java.net.*;
public class MyServer extends Thread{
    public void run(){
    try{
ServerSocket server=null;
Socket clientconnection=null;
server = new ServerSocket(8008);//监听某地址端口对
while(true){进入无限循环
clientconnection =server.accept();//收取请求
new ServeRequest(clientconnection).start();//启动一个新服务线程进行服务
……
}
}catch(Exception e){
System.err.println("Unable to start serve listen:"+e.getMessage());
e.printStackTrace();
}
}
}

实际上,这只是个简单的原型,如果试图部署以这种方式运行的服务器应用程序,那么这种方法的严重不足就很明显。
首先,为每个请求创建一个新线程的开销很大,为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源, 往往有时候要比花在处理实际的用户请求的时间和资源更多。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提 高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数。这样综合看来,系统的性能瓶颈就在于线程的创建开销。
其次,除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。在一个 JVM 里创建太多的线程可能会导致系统由于过度消耗内存而用完内存或“切换过度”。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻运行的处理 线程数目,以防止服务器被“压死”的情况发生。所以在设计后台程序的时候,一般需要提前根据服务器的内存、CPU等硬件情况设定一个线程数量的上限值。
如果创建和销毁线程的时间相对于服务时间占用的比例较大,那末假设在一个较短的时间内有成千上万的请求到达,想象一下,服务器的时间和资源将会大量的花在 创建和销毁线程上,而真正用于处理请求的时间却相对较少,这种情况下,服务器性能瓶颈就在于创建和销毁线程的时间。按照这个模型写一个简单的程序测试一下 即可看出,由于篇幅关系,此处略。如果把(服务时间/创建和销毁线程的时间)作为衡量服务器性能的一个参数,那末这个比值越大,服务器的性能就越高。
应此,解决此类问题的实质就是尽量减少创建和销毁线程的时间,把服务器的资源尽可能多地用到处理请求上来,从而发挥多线程的优点(并发),避免多线程的缺点(创建和销毁的时空开销)。
线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时 线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也 就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。

3 JAVA线程池原理
3.1 原理以及实现
在实践中,关于线程池的实现常常有不同的方法,但是它们的基本思路大都是相似的:服务器预先存放一定数目的“热”的线程,并发程序需要使用线程的时候,从 服务器取用一条已经创建好的线程(如果线程池为空则等待),使用该线程对请求服务,使用结束后,该线程并不删除,而是返回线程池中,以备复用,这样可以避 免对每一个请求都生成和删除线程的昂贵操作。
一个比较简单的线程池至少应包含线程池管理器、工作线程、任务队列、任务接口等部分。其中线程池管理器(ThreadPool Manager)的作用是创建、销毁并管理线程池,将工作线程放入线程池中;工作线程是一个可以循环执行任务的线程,在没有任务时进行等待;任务队列的作 用是提供一种缓冲机制,将没有处理的任务放在任务队列中;任务接口是每个任务必须实现的接口,主要用来规定任务的入口、任务执行完后的收尾工作、任务的执 行状态等,工作线程通过该接口调度任务的执行。下面的代码实现了创建一个线程池:

public class ThreadPool

private Stack threadpool = new Stack();
private int poolSize;
private int currSize=0;
public void setSize(int n)

poolSize = n;
}
public void run()
{
for(int i=0;i

 

4.2    框架与结构
下面让我们来看看util.concurrent的框架结构。关于这个工具包概述的e文原版链接地址是http://gee.cs.oswego.edu/dl/cpjslides/util.pdf。该工具包主要包括三大部分:同步、通道和线程池执行器。第一部分 主要是用来定制锁,资源管理,其他的同步用途;通道则主要是为缓冲和队列服务的;线程池执行器则提供了一组完善的复杂的线程池实现。
--主要的结构如下图所示

4.2.1 Sync
acquire/release协议的主要接口
- 用来定制锁,资源管理,其他的同步用途
- 高层抽象接口
- 没有区分不同的加锁用法

实现
-Mutex, ReentrantLock, Latch, CountDown,Semaphore, WaiterPreferenceSemaphore,FIFOSemaphore, PrioritySemaphore
还有,有几个简单的实现,例如ObservableSync, LayeredSync

举例:如果我们要在程序中获得一独占锁,可以用如下简单方式:
try {
lock.acquire();
try {
action();
}
finally {
lock.release();
}
}catch(Exception e){
}

程序中,使用lock对象的acquire()方法获得一独占锁,然后执行您的操作,锁用完后,使用release()方法释放之即可。呵呵,简单吧,想 想看,如果您亲自撰写独占锁,大概会考虑到哪些问题?如果关键的锁得不到怎末办?用起来是不是会复杂很多?而现在,以往的很多细节和特殊异常情况在这里都 无需多考虑,您尽可以把精力花在解决您的应用问题上去。

4.2.2 通道(Channel)
为缓冲,队列等服务的主接口

具体实现
LinkedQueue, BoundedLinkedQueue,BoundedBuffer, BoundedPriorityQueue,SynchronousChannel, Slot

通道例子

在后台服务器中,缓冲和队列都是最常用到的。试想,如果对所有远端的请求不排个队列,让它们一拥而上的去争夺cpu、内存、资源,那服务器瞬间不当掉才怪。而在这里,成熟的队列和缓冲实现已经提供,您只需要对其进行正确初始化并使用即可,大大缩短了开发时间。

4.2.3执行器(Executor)
Executor是这里最重要、也是我们往往最终写程序要用到的,下面重点对其进行介绍。
类似线程的类的主接口
- 线程池
- 轻量级运行框架
- 可以定制调度算法

只需要支持execute(Runnable r)
- 同Thread.start类似

实现
- PooledExecutor, ThreadedExecutor, QueuedExecutor, FJTaskRunnerGroup

PooledExecutor(线程池执行器)是个最常用到的类,以它为例:
可修改得属性如下:
- 任务队列的类型
- 最大线程数
- 最小线程数
- 预热(预分配)和立即(分配)线程
- 保持活跃直到工作线程结束
-- 以后如果需要可能被一个新的代替
- 饱和(Saturation)协议
-- 阻塞,丢弃,生产者运行,等等

可不要小看上面这数条属性,对这些属性的设置完全可以等同于您自己撰写的线程池的成百上千行代码。下面以笔者撰写过得一个GIS服务器为例:
该GIS服务器是一个典型的“请求-服务”类型的服务器,遵循后端程序设计的一般框架。首先对所有的请求按照先来先服务排入一个请求队列,如果瞬间到达的 请求超过了请求队列的容量,则将溢出的请求转移至一个临时队列。如果临时队列也排满了,则对以后达到的请求给予一个“服务器忙”的提示后将其简单抛弃。这 个就够忙活一阵的了。
然后,结合链表结构实现一个线程池,给池一个初始容量。如果该池满,以x2的策略将池的容量动态增加一倍,依此类推,直到总线程数服务达到系统能力上限, 之后线程池容量不在增加,所有请求将等待一个空余的返回线程。每从池中得到一个线程,该线程就开始最请求进行GIS信息的服务,如取坐标、取地图,等等。 服务完成后,该线程返回线程池继续为请求队列离地后续请求服务,周而复始。当时用矢量链表来暂存请求,用wait()、 notify() 和 synchronized等原语结合矢量链表实现线程池,总共约600行程序,而且在运行时间较长的情况下服务器不稳定,线程池被取用的线程有异常消失的 情况发生。而使用util.concurrent相关类之后,仅用了几十行程序就完成了相同的工作而且服务器运行稳定,线程池没有丢失线程的情况发生。由 此可见util.concurrent包极大的提高了开发效率,为项目节省了大量的时间。

 

Code
使用PooledExecutor例子
import java.net.*;
/**


Title: 



Description: 负责初始化线程池以及启动服务器



Copyright: Copyright (c) 2003



Company: 


* @author not attributable
* @version 1.0
*/
public class MainServer {
//初始化常量
public static final int MAX_CLIENT=100; //系统最大同时服务客户数
//初始化线程池
public static final PooledExecutor pool =
new PooledExecutor(new BoundedBuffer(10), MAX_CLIENT); //chanel容量为10,
//在这里为线程池初始化了一个
//长度为10的任务缓冲队列。

public MainServer() {
//设置线程池运行参数
pool.setMinimumPoolSize(5); //设置线程池初始容量为5个线程
pool.discardOldestWhenBlocked();//对于超出队列的请求,使用了抛弃策略。
pool.createThreads(2); //在线程池启动的时候,初始化了具有一定生命周期的2个“热”线程
}

public static void main(String[] args) {
MainServer MainServer1 = new MainServer();
new HTTPListener().start();//启动服务器监听和处理线程
new manageServer().start();//启动管理线程
}
}

类HTTPListener
import java.net.*;
/**

Title: 



Description: 负责监听端口以及将任务交给线程池处理



Copyright: Copyright (c) 2003



Company: 


* @author not attributable
* @version 1.0
*/

public class HTTPListener extends Thread{
public HTTPListener() {
}
public void run(){
try{
ServerSocket server=null;
Socket clientconnection=null;
server = new ServerSocket(8008);//服务套接字监听某地址端口对
while(true){//无限循环
clientconnection =server.accept();
System.out.println("Client connected in!");
//使用线程池启动服务
MainServer.pool.execute(new HTTPRequest(clientconnection));//如果收到一个请求,则从线程池中取一个线程进行服务,任务完成后,该线程自动返还线程池
}
}catch(Exception e){
System.err.println("Unable to start serve listen:"+e.getMessage());
e.printStackTrace();
}
}
}

class Service { // 
final Channel msgQ = new LinkedQueue();
public void serve() throws InterruptedException {
String status = doService();
msgQ.put(status);
}
public Service() { // start background thread
Runnable logger = new Runnable() {
public void run() {
try {
for(;;)
System.out.println(msqQ.take());
}
catch(InterruptedException ie) {} }
};
new Thread(logger).start();
}
}

 

关于util.concurrent工具包就有选择的介绍到这,更详细的信息可以阅读这些java源代码的API文档。Doug Lea是个很具有“open”精神的作者,他将util.concurrent工具包的java源代码全部公布出来,有兴趣的读者可以下载这些源代码并细 细品味。

5    结束语
以上内容介绍了线程池基本原理以及设计后台服务程序应考虑到的问题,并结合实例详细介绍了重要的多线程开发工具包util.concurrent的构架和使用。结合使用已有完善的开发包,后端服务程序的开发周期将大大缩短,同时程序性能也有了保障。

 

java.util.concurrent多线程框架---线程池编程(四)

java.util.concurrent结构

 

Sync:获得/释放(acquire/release) 协议。同步(定制锁、资源管理、其他同步)

Channel:放置/取走(put/take) 协议。通信(缓冲队列服务)

Executor:执行Runnable任务。线程池执行器(线程池的实现一些实现了Executor接口的)

 

 

Sync

-- acquire/release协议的主要接口

-用来定制锁,资源管理,其他的同步用途

- 高层抽象接口

- 没有区分不同的加锁用法

--实现

-Mutex,ReentrantLock, Latch, CountDown,Semaphore,WaiterPreferenceSemaphore,FIFOSemaphore, PrioritySemaphore

   还有,有几个简单的实现,例如ObservableSync, LayeredSync

 

 

独占锁

try {

lock.acquire();

try {

action();

}

finally {

lock.release();

}

}

catch(InterruptedException ie) { ... }

-- Java同步块不适用的时候使用它

- 超时,回退(back-off)

- 确保可中断

- 大量迅速锁定

- 创建Posix风格应用(condvar)

 

 

独占例子

classParticleUsingMutex {

int x; int y;

final Random rng= new Random();

final Mutexmutex = new Mutex();

public voidmove() {

try {

mutex.acquire();

try { x +=rng.nextInt(2)-1; y += rng.nextInt(2)-1; }

finally {mutex.release(); }

}

catch(InterruptedException ie) {

Thread.currentThread().interrupt();}

}

public voiddraw(Graphics g) {

int lx, ly;

try {

mutex.acquire();

try { lx = x; ly= y; }

finally {mutex.release(); }

}

catch (InterruptedExceptionie) {

Thread.currentThread().interrupt();return; }

g.drawRect(lx,ly, 10, 10);

}

}

 

 

回退(Backoff)例子

classCellUsingBackoff {

private longval;

private finalMutex mutex = new Mutex();

voidswapVal(CellUsingBackoff other)

throwsInterruptedException {

if (this ==other) return; // alias check

for (;;) {

mutex.acquire();

try {

I f (other.mutex.attempt(0)){

try {

long t = val;

val = other.val;

other.val = t;

return;

}

finally {other.mutex.release(); }

}

}

finally {mutex.release(); };

Thread.sleep(100);// heuristic retry interval

}

}

}

 

 

读写锁

interfaceReadWriteLock {

Sync readLock();

Sync writeLock();

}

-- 管理一对锁

- 和普通的锁一样的使用习惯

-- 对集合类很有用

-半自动的方式实现SyncSet, SyncMap, ...

-- 实现者使用不同的锁策略

- WriterPreference,ReentrantWriterPreference,

ReaderPreference,FIFO

 

 

ReadWriteLock例子

-- 示范在读写锁中执行任何Runnable的包装类

class WithRWLock{

finalReadWriteLock rw;

publicWithRWLock(ReadWriteLock l) { rw = l; }

public voidperformRead(Runnable readCommand)

throwsInterruptedException {

rw.readLock().acquire();

try {readCommand.run(); }

finally {rw.readlock().release(); }

}

public voidperformWrite(...) // similar

}

 

 

闭锁(Latch)

-- 闭锁是开始时设置为false,但一旦被设置为true,他将永远保持true状态

- 初始化标志

- 流结束定位

- 线程中断

- 事件出发指示器

-- CountDown和他有点类似,不同的是,CountDown需要一定数量的触发设置,而不是一次

-- 非常简单,但是广泛使用的类

- 替换容易犯错的开发代码

 

 

Latch Example 闭锁例子

class Workerimplements Runnable {

LatchstartSignal;

Worker(Latch l){ startSignal = l; }

public voidrun() {

startSignal.acquire();

// ... doWork();

}

}

class Driver {// ...

void main() {

Latch ss = newLatch();

for (int i = 0;i < N; ++i) // make threads

new Thread(newWorker(ss)).start();

doSomethingElse();// don’t let run yet

ss.release(); //now let all threads proceed

}

}

 

 

信号(Semaphores)

-- 服务于数量有限的占有者

- 使用许可数量构造对象(通常是0)

- 如果需要一个许可才能获取,等待,然后取走一个许可

- 释放的时候将许可添加回来

-- 但是真正的许可并没有转移(But no actual permits change hands.)

- 信号量仅仅保留当前的计数值

-- 应用程序

- 锁:一个信号量可以被用作互斥体(mutex)

- 一个独立的等待缓存或者资源控制的操作

- 设计系统是想忽略底层的系统信号

-- (phores‘remember’ past signals)记住已经消失的信号量

 

 

信号量例子

class Pool {

ArrayList items= new ArrayList();

HashSet busy =new HashSet();

final Semaphoreavailable;

public Pool(intn) {

available = newSemaphore(n);

// ... somehowinitialize n items ...;

}

public ObjectgetItem() throws InterruptedException {

available.acquire();

return doGet();

}

public voidreturnItem(Object x) {

if (doReturn(x))available.release();

}

synchronizedObject doGet() {

Object x =items.remove(items.size()-1);

busy.add(x); //put in set to check returns

return x;

}

synchronizedboolean doReturn(Object x) {

returnbusy.remove(x); // true if was present

}

}

 

 

屏障(Barrier)

-- 多部分同步接口

- 每一部分都必须等待其他的分不撞倒屏障

-- CyclicBarrier类

- CountDown的一个可以重新设置的版本

- 对于反复划分算法很有用(iterative partitioning algorithms)

-- Rendezvous类

- 一个每部分都能够和其他部分交换信息的屏障

- 行为类似同时的在一个同步通道上put和take

- 对于资源交换协议很有用(resource-exchange protocols)

 

 

 

通道(Channel)

--为缓冲,队列等服务的主接口

-- 具体实现

- LinkedQueue,BoundedLinkedQueue,BoundedBuffer, BoundedPriorityQueue,SynchronousChannel, Slot

 

 

通道属性

-- 被定义为Puttable和Takable的子接口

- 允许安装生产者/消费者模式执行

-- 支持可超时的操作offer和poll

- 当超时值是0时,可能会被阻塞

- 所有的方法能够抛出InterruptedException异常

-- 没有接口需要size方法

- 但是一些实现定义了这个方法

- BoundedChannelcapacity方法

 

 

通道例子

class Service {// ...

final ChannelmsgQ = new LinkedQueue();

public voidserve() throws InterruptedException {

String status =doService();

msgQ.put(status);

}

public Service(){ // start background thread

Runnable logger= new Runnable() {

public voidrun() {

try {

for(;;)

System.out.println(msqQ.take());

}

catch(InterruptedExceptionie) {} }

};

newThread(logger).start();

}

}

 

运行器(Executor)

-- 类似线程的类的主接口

- 线程池

- 轻量级运行框架

- 可以定制调度算法

-- 只需要支持execute(Runnable r)

- 同Thread.start类似

-- 实现

- PooledExecutor,ThreadedExecutor,QueuedExecutor, FJTaskRunnerGroup

- 相关的ThreadFactory类允许大多数的运行器通过定制属性使用线程

 

PooledExecutor

-- 一个可调的工作者线程池,可修改得属性如下:

- 任务队列的类型

- 最大线程数

- 最小线程数

- 预热(预分配)和立即(分配)线程

- 保持活跃直到工作线程结束

-- 以后如果需要可能被一个新的代替

- 饱和(Saturation)协议

-- 阻塞,丢弃,生产者运行,等等

 

PooledExecutor例子

class WebService{

public staticvoid main(String[] args) {

PooledExecutorpool =

newPooledExecutor(new BoundedBuffer(10), 20);

pool.createThreads(4);

try {

ServerSocketsocket = new ServerSocket(9999);

for (;;) {

final Socketconnection = socket.accept();

pool.execute(newRunnable() {

public voidrun() {

newHandler().process(connection);

}});

}

}

catch(Exceptione) { } // die

}

}

class Handler {void process(Socket s); }

 

前景(Future)和可调用(Callable)

-- Callabe是类似于Runnable的接口,用来作为参数和传递结果

interfaceCallable {

Objectcall(Object arg) throws Exception;

}

-- FutureResult管理Callable的异步执行

classFutureResult { // ...

// block calleruntil result is ready

public Objectget()

throws InterruptedException,InvocationTargetException;

public voidset(Object result); // unblocks get

// createRunnable that can be used with an Executor

public Runnablesetter(Callable function);

}

 

FutureResult例子

classImageRenderer { Image render(byte[] raw); }

class App { //...

Executorexecutor = ...; // any executor

ImageRendererrenderer = new ImageRenderer();

public voiddisplay(byte[] rawimage) {

try {

FutureResultfutureImage = new FutureResult();

Runnable cmd =futureImage.setter(new Callable(){

public Objectcall() {

returnrenderer.render(rawImage);

}});

executor.execute(cmd);

drawBorders();// do other things while executing

drawCaption();

drawImage((Image)(futureImage.get()));// use future

}

catch (Exceptionex) {

cleanup();

return;

}

}

} 

 

其他的类

--CopyOnWriteArrayList

- 支持整个集合复制时每一个修改的无锁访问

- 适合大多数的多路广播应用程序

-- 工具包还包括了一个java.beans多路广播类的COW版本

--SynchronizedDouble, SynchronizedInt,SynchronizedRef, etc

- 类似于java.lang.Double,提供可变操作的同步版本.例如,addTo,inc

- 添加了一些象swap,commit这样的实用操作

 

未来计划

-- 并发数据构架

- 一组繁重线程连接环境下有用的工具集合

--支持侧重I/O的程序

- 事件机制的IO系统

-- 小版本的实现

- 例如SingleSourceQueue

--小幅度的改善

- 使运行器更容易使用

-- 替换

- JDK1.3java.util.Timer 被ClockDaemon取代

java.util.concurrent多线程框架---线程池编程(五)

 

jdk5.0多线程学习笔记

一、简介

线程池类为 java.util.concurrent.ThreadPoolExecutor,常用构造方法为:

ThreadPoolExecutor(intcorePoolSize, int maximumPoolSize,

long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandlerhandler)

corePoolSize:线程池维护线程的最少数量

maximumPoolSize:线程池维护线程的最大数量

keepAliveTime:线程池维护线程所允许的空闲时间

unit:线程池维护线程所允许的空闲时间的单位

workQueue:线程池所使用的缓冲队列

handler:线程池对拒绝任务的处理策略

一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法。

当一个任务通过execute(Runnable)方法欲添加到线程池时:

如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

如果此时线程池中的数量等于 corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任务。

也就是:处理任务的优先级为:

核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

unit可选的参数为java.util.concurrent.TimeUnit中的几个静态属性:

NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。

workQueue我常用的是:java.util.concurrent.ArrayBlockingQueue

handler有四个选择:

ThreadPoolExecutor.AbortPolicy()

抛出java.util.concurrent.RejectedExecutionException异常

ThreadPoolExecutor.CallerRunsPolicy()

重试添加当前的任务,他会自动重复调用execute()方法

ThreadPoolExecutor.DiscardOldestPolicy()

抛弃旧的任务

ThreadPoolExecutor.DiscardPolicy()

抛弃当前的任务

二、一般用法举例

//------------------------------------------------------------

//TestThreadPool.java

//packagecn.simplelife.exercise;

importjava.io.Serializable;

importjava.util.concurrent.ArrayBlockingQueue;

importjava.util.concurrent.ThreadPoolExecutor;

importjava.util.concurrent.TimeUnit;

public classTestThreadPool {

private static intproduceTaskSleepTime = 2;

private static intconsumeTaskSleepTime = 2000;

private static intproduceTaskMaxNumber = 10;

public static voidmain(String[] args) {

//构造一个线程池

ThreadPoolExecutorthreadPool = new ThreadPoolExecutor(2, 4, 3,

TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(3),

newThreadPoolExecutor.DiscardOldestPolicy());

for(inti=1;i<=produceTaskMaxNumber;i++){

try {

//产生一个任务,并将其加入到线程池

String task ="task@ " + i;

System.out.println("put" + task);

threadPool.execute(newThreadPoolTask(task));

//便于观察,等待一段时间

Thread.sleep(produceTaskSleepTime);

} catch (Exceptione) {

e.printStackTrace();

}

}

}

/**

* 线程池执行的任务

* @author hdpan

*/

public static classThreadPoolTask implements Runnable,Serializable{

private staticfinal long serialVersionUID = 0;

//保存任务所需要的数据

private ObjectthreadPoolTaskData;

ThreadPoolTask(Objecttasks){

this.threadPoolTaskData= tasks;

}

public void run(){

//处理一个任务,这里的处理方式太简单了,仅仅是一个打印语句

System.out.println("start.."+threadPoolTaskData);

try {

便于观察,等待一段时间

Thread.sleep(consumeTaskSleepTime);

} catch (Exceptione) {

e.printStackTrace();

}

threadPoolTaskData= null;

}

public ObjectgetTask(){

returnthis.threadPoolTaskData;

}

}

}

//------------------------------------------------------------

说明:

1、在这段程序中,一个任务就是一个Runnable类型的对象,也就是一个ThreadPoolTask类型的对象。

2、一般来说任务除了处理方式外,还需要处理的数据,处理的数据通过构造方法传给任务。

3、在这段程序中,main()方法相当于一个残忍的领导,他派发出许多任务,丢给一个叫 threadPool的任劳任怨的小组来做。

这个小组里面队员至少有两个,如果他们两个忙不过来,任务就被放到任务列表里面。

如果积压的任务过多,多到任务列表都装不下(超过3个)的时候,就雇佣新的队员来帮忙。但是基于成本的考虑,不能雇佣太多的队员,至多只能雇佣 4个。

如果四个队员都在忙时,再有新的任务,这个小组就处理不了了,任务就会被通过一种策略来处理,我们的处理方式是不停的派发,直到接受这个任务为止(更残忍!呵呵)。

因为队员工作是需要成本的,如果工作很闲,闲到 3SECONDS都没有新的任务了,那么有的队员就会被解雇了,但是,为了小组的正常运转,即使工作再闲,小组的队员也不能少于两个。

4、通过调整 produceTaskSleepTime和consumeTaskSleepTime的大小来实现对派发任务和处理任务的速度的控制,改变这两个值就可以观察不同速率下程序的工作情况。

5、通过调整4中所指的数据,再加上调整任务丢弃策略,换上其他三种策略,就可以看出不同策略下的不同处理方式。

6、对于其他的使用方法,参看jdk的帮助,很容易理解和使用。

 

JDK1.5新特性 (三) - 线程池(2)

3.2 JDK1.5中的线程池

3.2.1 简单介绍

在J2SE(TM)5.0 中,Doug Lea 编写了一个优秀的并发实用程序开放源码库 util.concurrent,它包括互斥、信号量、诸如在并发访问下执行得很好的队列和散列表之类集合类以及几个工作队列实现。该包中的 PooledExecutor 类是一种有效的、广泛使用的以工作队列为基础的线程池的正确实现。Util.concurrent 定义一个 Executor 接口,以异步地执行Runnable,另外还定义了 Executor 的几个实现,它们具有不同的调度特征。将一个任务排入 executor 的队列非常简单:

Executor executor =new QueuedExecutor();

Runnable runnable =… ;

executor.execute(runnable);

PooledExecutor 是一个复杂的线程池实现,它不但提供工作线程(worker thread)池中任务的调度,而且还可灵活地调整池的大小,同时还提供了线程生命周期管理,这个实现可以限制工作队列中任务的数目,以防止队列中的任务耗尽所有可用内存,另外还提供了多种可用的关闭和饱和度策略(阻塞、废弃、抛出、废弃最老的、在调用者中运行等)。所有的 Executor 实现为您管理线程的创建和销毁,包括当关闭 executor 时,关闭所有线程,

3.2.2 线程池的使用
线程池类为 java.util.concurrent.ThreadPoolExecutor,常用构造方法为:

ThreadPoolExecutor(intcorePoolSize,
                  int maximumPoolSize,
                  long keepAliveTime, TimeUnit unit,
                  BlockingQueue<Runnable> workQueue,
                  RejectedExecutionHandler handler)

·      corePoolSize
线程池维护线程的最少数量

·      maximumPoolSiz
线程池维护线程的最大数量

·      keepAliveTime
线程池维护线程所允许的空闲时间

·      unit
线程池维护线程所允许的空闲时间的单位

·      workQueue
线程池所使用的缓冲队列

·      handler
线程池对拒绝任务的处理策略

一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法。

当一个任务通过execute(Runnable)方法欲添加到线程池时:

 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

  如果此时线程池中的数量等于 corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。

  如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。

 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。

也就是:处理任务的优先级为:
核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。

当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

unit可选的参数为java.util.concurrent.TimeUnit中的几个静态属性:
NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。

workQueue我常用的是:java.util.concurrent.ArrayBlockingQueue

handler有四个选择:

·      ThreadPoolExecutor.AbortPolicy()
抛出java.util.concurrent.RejectedExecutionException异常

·      ThreadPoolExecutor.CallerRunsPolicy()
重试添加当前的任务,他会自动重复调用execute()方法

·      ThreadPoolExecutor.DiscardOldestPolicy()
抛弃旧的任务

·      ThreadPoolExecutor.DiscardPolicy()
抛弃当前的任务

用法举例

packagecn.simplelife.exercise;

importjava.util.concurrent.ArrayBlockingQueue;

importjava.util.concurrent.ThreadPoolExecutor;

importjava.util.concurrent.TimeUnit;

public classTestThreadPool {

     private static int produceTaskSleepTime = 2;

     public static void main(String[] args) {

         //构造一个线程池

         ThreadPoolExecutor producerPool = newThreadPoolExecutor(1, 1, 0,

                 TimeUnit.SECONDS, new ArrayBlockingQueue(3),

                 newThreadPoolExecutor.DiscardOldestPolicy());

         //每隔produceTaskSleepTime的时间向线程池派送一个任务。

         int i=1;

         while(true){

             try {

                 Thread.sleep(produceTaskSleepTime);

                 String task = “task@ ” + i;

                 System.out.println(”put ” + task);

                 producerPool.execute(newThreadPoolTask(task));

                 i++;

             } catch (Exception e) {

                 e.printStackTrace();

             }

         }

     }

}            

packagecn.simplelife.exercise;

importjava.io.Serializable;

/**

 * 线程池执行的任务

 * @author hdpan

 */

public classThreadPoolTask implements Runnable,Serializable{

     //JDK1.5中,每个实现Serializable接口的类都推荐声明这样的一个ID

     private static final long serialVersionUID =0;

     private static int consumeTaskSleepTime =2000;

     private Object threadPoolTaskData;

    ThreadPoolTask(Objecttasks){

         this.threadPoolTaskData = tasks;

     }

     //每个任务的执行过程,现在是什么都没做,除了print和sleep,:)

     public void run(){

         System.out.println(”start..”+threadPoolTaskData);

         try {

             //便于观察现象,等待一段时间

             Thread.sleep(consumeTaskSleepTime);

         } catch (Exception e) {

             e.printStackTrace();

         }

         threadPoolTaskData = null;

     }

}    

对这两段程序的说明:

1. 在这段程序中,一个任务就是一个Runnable类型的对象,也就是一个ThreadPoolTask类型的对象。

2. 一般来说任务除了处理方式外,还需要处理的数据,处理的数据通过构造方法传给任务。

3. 在这段程序中,main()方法相当于一个残忍的领导,他派发出许多任务,丢给一个叫 threadPool的任劳任怨的小组来做。

o     这个小组里面队员至少有两个,如果他们两个忙不过来, 任务就被放到任务列表里面。

o     如果积压的任务过多,多到任务列表都装不下(超过3个)的时候,就雇佣新的队员来帮忙。但是基于成本的考虑,不能雇佣太多的队员, 至多只能雇佣 4个。

o     如果四个队员都在忙时,再有新的任务, 这个小组就处理不了了,任务就会被通过一种策略来处理,我们的处理方式是不停的派发, 直到接受这个任务为止(更残忍!呵呵)。

o     因为队员工作是需要成本的,如果工作很闲,闲到 3SECONDS都没有新的任务了,那么有的队员就会被解雇了,但是,为了小组的正常运转,即使工作再闲,小组的队员也不能少于两个。

4. 通过调整 produceTaskSleepTime和consumeTaskSleepTime的大小来实现对派发任务和处理任务的速度的控制, 改变这两个值就可以观察不同速率下程序的工作情况。

5. 通过调整4中所指的数据,再加上调整任务丢弃策略, 换上其他三种策略,就可以看出不同策略下的不同处理方式。

6. 对于其他的使用方法,参看jdk的帮助,很容易理解和使用。

 

JDK1.5新特性 (3) - 线程池

 

3. 线程池

3.1简单的线程池实现

我们通常想要的是同一组固定的工作线程相结合的工作队列,它使用 wait() 和 notify() 来通知等待线程新的工作已经到达了。该工作队列通常被实现成具有相关监视器对象的某种链表。以下代码实现了具有线程池的工作队列。

public class WorkQueue

{

    private final int nThreads;

    private final PoolWorker[] threads;

    private final LinkedList queue;

    public WorkQueue(int nThreads)

    {

        this.nThreads = nThreads;

        queue = new LinkedList();

        threads = new PoolWorker[nThreads];

        for (int i=0; i<nThreads; i++) {

            threads[i] = new PoolWorker();

            threads[i].start();

        }

    }

    public void execute(Runnable r) {

        synchronized(queue) {

            queue.addLast(r);

            queue.notify();

        }

    }

    private class PoolWorker extends Thread {

        public void run() {

            Runnable r;

            while (true) {

                synchronized(queue) {

                    while (queue.isEmpty()) {

                        try

                        {

                            queue.wait();

                        }

                        catch (InterruptedException ignored)

                        {

                        }

                    }

                    r = (Runnable) queue.removeFirst();

                }

                // If we don’t catch RuntimeException,

                // the pool could leak threads

                try {

                    r.run();

                }

                catch (RuntimeException e) {

                    // You might want to log something here

                }

            }

        }

    }

}

虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。

用线程池执行任务

如果你开发项目的时候用到很多的short-lived任务,这里推荐使用“线程池”这项技术。你可以创建一个线程池来执行池中的的任务,来取代每次执行任务是都要为新的任务来new和discard。如果一个线程在池中是可用状态,那么任务将立即执行。执行完成之后线程返回池中,否则,任务将一直等待直到有线程处在可用状态。

J2SE 5.0为大家提供了一个新的java.util.concurrent package,并且在这个报中提供了一个pre-built 的线程池架构。在java.util.concurrent中提供了一个Executor 接口,里面有一个execute的方法,参数是Runnable 类型

   public interface Executor {

     public void execute(Runnable command);

   }
使用线程池架构,你就必须创建一个Executor实例,然后你给他分配一些runnable任务,例如:

Java代码

Executor executor = ...;  

executor.execute(aRunnable1);  

executor.execute(aRunnable2);  

   Executor executor = ...;

   executor.execute(aRunnable1);

   executor.execute(aRunnable2);

然后你创建或者找到Executor的实现类,实现类可以立即(或者连续)执行分配的任务,例如:

Java代码

class MyExecutor implements Executor {  

    public void execute(Runnable r) {  

        new Thread(r).start();  

    }  

}  

   class MyExecutor implements Executor {

       public void execute(Runnable r) {

           new Thread(r).start();

       }

   }

concurrencyutilities也包括了一个ThreadPoolExecutor类,它提供了很多对线程的一般性操作,提供了四个构造函数,每个都可以指定如:线程池大小,持续时间,一个线程factory,和拒绝线程的handler。

Java代码

public ThreadPoolExecutor(int corePoolSize,  

                          int maximumPoolSize,  

                          long keepAliveTime,  

                          TimeUnit unit,  

                          BlockingQueue<Runnable> workQueue)  

public ThreadPoolExecutor(int corePoolSize,  

                          int maximumPoolSize,  

                          long keepAliveTime,  

                          TimeUnit unit,  

                          BlockingQueue<Runnable> workQueue,  

                          ThreadFactory threadFactory)  

public ThreadPoolExecutor(int corePoolSize,  

                          int maximumPoolSize,  

                          long keepAliveTime,  

                          TimeUnit unit,  

                          BlockingQueue<Runnable> workQueue,  

                          RejectedExecutionHandler handler)  

public ThreadPoolExecutor(int corePoolSize,  

                          int maximumPoolSize,  

                          long keepAliveTime,  

                          TimeUnit unit,  

                          BlockingQueue<Runnable> workQueue,  

                          ThreadFactory threadFactory,  

                          RejectedExecutionHandler handler)  

   public ThreadPoolExecutor(int corePoolSize,

                             intmaximumPoolSize,

                             longkeepAliveTime,

                             TimeUnit unit,

                            BlockingQueue<Runnable> workQueue)

   public ThreadPoolExecutor(int corePoolSize,

                             intmaximumPoolSize,

                             longkeepAliveTime,

                             TimeUnit unit,

                            BlockingQueue<Runnable> workQueue,

                             ThreadFactorythreadFactory)

   public ThreadPoolExecutor(int corePoolSize,

                             intmaximumPoolSize,

                             longkeepAliveTime,

                             TimeUnit unit,

                             BlockingQueue<Runnable>workQueue,

                            RejectedExecutionHandler handler)

   public ThreadPoolExecutor(int corePoolSize,

                             intmaximumPoolSize,

                             long keepAliveTime,

                             TimeUnit unit,

                            BlockingQueue<Runnable> workQueue,

                             ThreadFactorythreadFactory,

                            RejectedExecutionHandler handler)
但是你不必声明构造函数,Executors类会为你创建一个线程池。在一种最简单的情况下,你在Executors类中声明了newFixedThreadPool方法,并且在池中分配了许多线程。你可以使用ExecutorService(继承Executor的一个接口),去execute和submit 那些Runnable任务,使用ExecutorService中的submit方法可以得到一个返回结果,当然submit也可以返回一个Future对象用来检查任务是否执行。
让我们来先做一个Runnable类,名字为NamePrinter,它通知你运行、暂停、和耗费的时间。

Java代码

public class NamePrinter implements Runnable {  

  private final String name;  

  private final int delay;  

  public NamePrinter(String name, int delay) {  

    this.name = name;  

    this.delay = delay;  

  }  

  public void run() {  

    System.out.println("Starting: " + name);  

    try {  

      Thread.sleep(delay);  

    } catch (InterruptedException ignored) {  

    }  

    System.out.println("Done with: " + name);  

  }  

}  

   public class NamePrinter implements Runnable{

     private final String name;

     private final int delay;

     public NamePrinter(String name, int delay){

       this.name = name;

       this.delay = delay;

     }

     public void run() {

       System.out.println("Starting:" + name);

       try {

         Thread.sleep(delay);

       } catch (InterruptedException ignored) {

       }

       System.out.println("Done with:" + name);

     }

   }

然后下面是我们测试的项目UsePool,它创建一个有三个线程的线程池,分配了10个任务给它(运行10次NamePrinter),UsePool在被shutdown 和awaitTermination之前将等待并执行分配的任务。一个ExecutorService必须要在terminated之前执行shutdown,shutdownNow方法是立即尝试shutdown操作。shutdownNow 方法将返回没有被执行的任务。

Java代码

import java.util.concurrent.*;  

import java.util.Random;  

public class UsePool {  

  public static void main(String args[]) {  

    Random random = new Random();  

    ExecutorService executor =   

            Executors.newFixedThreadPool(3);  

    // Sum up wait times to know when to shutdown  

    int waitTime = 500;  

    for (int i=0; i<10; i++) {  

      String name = "NamePrinter " + i;  

      int time = random.nextInt(1000);  

      waitTime += time;  

      Runnable runner = new NamePrinter(name, time);  

      System.out.println("Adding: " + name + " / " + time);  

      executor.execute(runner);  

    }  

    try {  

      Thread.sleep(waitTime);  

      executor.shutdown();  

      executor.awaitTermination  

              (waitTime, TimeUnit.MILLISECONDS);  

    } catch (InterruptedException ignored) {  

    }  

    System.exit(0);  

  }  

 }  

   import java.util.concurrent.*;

   import java.util.Random;

   public class UsePool {

     public static void main(String args[]) {

       Random random = new Random();

       ExecutorService executor =

               Executors.newFixedThreadPool(3);

       // Sum up wait times to know when toshutdown

       int waitTime = 500;

       for (int i=0; i<10; i++) {

         String name = "NamePrinter "+ i;

         int time = random.nextInt(1000);

         waitTime += time;

         Runnable runner = new NamePrinter(name,time);

         System.out.println("Adding:" + name + " / " + time);

         executor.execute(runner);

       }

       try {

         Thread.sleep(waitTime);

         executor.shutdown();

         executor.awaitTermination

                 (waitTime,TimeUnit.MILLISECONDS);

       } catch (InterruptedException ignored) {

       }

       System.exit(0);

     }

    }

输出的结果是:
Adding: NamePrinter 0 / 30
Adding: NamePrinter 1 / 727
Adding: NamePrinter 2 / 980
Starting: NamePrinter 0
Starting: NamePrinter 1
Starting: NamePrinter 2
Adding: NamePrinter 3 / 409
Adding: NamePrinter 4 / 49
Adding: NamePrinter 5 / 802
Adding: NamePrinter 6 / 211
Adding: NamePrinter 7 / 459
Adding: NamePrinter 8 / 994
Adding: NamePrinter 9 / 459
Done with: NamePrinter 0
Starting: NamePrinter 3
Done with: NamePrinter 3
Starting: NamePrinter 4
Done with: NamePrinter 4
Starting: NamePrinter 5
Done with: NamePrinter 1
Starting: NamePrinter 6
Done with: NamePrinter 6
Starting: NamePrinter 7
Done with: NamePrinter 2
Starting: NamePrinter 8
Done with: NamePrinter 5
Starting: NamePrinter 9
Done with: NamePrinter 7
Done with: NamePrinter 9
Done with: NamePrinter 8
注意前三个NamePrinter对象启动的非查的快,之后的NamePrinter对象每次启动都要等待前面的执行完成。
在J2SE 5.0有非常多的pooling framework可以用,例如,你可以创建一个scheduled线程池……
更多信息还是看官方的concurrency utilities,地址:http://java.sun.com/j2se/1.5.0/docs/guide/concurrency/

public classPoolAsynService extends BaseService implements Runnable {

         private Thread thread = newThread(this);

         private List waitToList = (List)Collections.synchronizedList(new LinkedList());

         // 线程池参数/

         private int corePoolSize = 5;// :线程池维护线程的最少数量

         private int maximumPoolSize = 10;// :线程池维护线程的最大数量

         private long keepAliveTime = 60;// :线程池维护线程所允许的空闲时间

         private TimeUnit unit =TimeUnit.SECONDS;// : 线程池维护线程所允许的空闲时间的单位

         private BlockingQueue<Runnable>workQueue = new ArrayBlockingQueue<Runnable>(10);// :

         // 线程池所使用的缓冲队列

         private RejectedExecutionHandlerhandler = new ThreadPoolExecutor.CallerRunsPolicy();// :

         // 线程池对拒绝任务的处理策略

         // //线程池参数/

         private ThreadPoolExecutor threadPool =new ThreadPoolExecutor(corePoolSize,

                            maximumPoolSize,keepAliveTime, unit, workQueue, handler);

         public void run() {

                   while(!thread.isInterrupted()) {

                            if(!waitToList.isEmpty()) {

                                     try {

                                               threadPool.execute(newExecutor());

                                     } catch(Exception e) {

                                               logger.error("pool  execute error!!!", e);

                                     }

                            }

                           

                            Tools.block(25);

                  }

         }

         public void doAsync(Object executor,Object... objects) {

                   Throwable t = newThrowable();

                   StackTraceElement[] elements= t.getStackTrace();

                   StackTraceElement element =elements[1];

                   String method =element.getMethodName();

                   AsyncContext ctx = newAsyncContext();

                   ctx.args = objects;

                   ctx.executor = executor;

                   ctx.method = method;

                   if(method.endsWith("PA")) {

                            waitToList.add(ctx);

                   } else {

                            logger.warn("asyncmethod name is not good!");

                   }

         }

         private class AsyncContext {

                   String method;

                   Object executor;

                   Object[] args;

         }

         private class Executor implementsRunnable {

                   public void run() {

                            if(!waitToList.isEmpty()) {

                                     try {

                                               Objecttask = waitToList.remove(0);

                                               AsyncContextctx = (AsyncContext) task;

                                               doTaskByCtx(ctx);

                                     } catch(Exception e) {

                                               logger.error("asyncerror!!!", e);

                                     }

                            }

                   }

                   private voiddoTaskByCtx(AsyncContext ctx) {

                            StringtargetMethodName = ctx.method.substring(0, ctx.method

                                               .length()- 2);

                            Method targetMethod= null;

                            Classclazz = null;

                            try {

                                     clazz =ctx.executor.getClass();

                                     Method[]methods = clazz.getDeclaredMethods();

                                     if (methods!= null) {

                                               for(int i = 0; i < methods.length; i++) {

                                                        Stringname = methods[i].getName();

                                                        if(name.equals(targetMethodName)) {

                                                                 targetMethod= methods[i];

                                                                 break;

                                                        }

                                               }

                                               if(targetMethod != null) {

                                                        targetMethod.invoke(ctx.executor,ctx.args);

                                               }

                                     }

                            } catch (Exceptione) {

                                     logger.error(

                                                        "doasync fail! " + clazz + ":" + targetMethodName, e);

                            }

                   }

         }

         @Override

         public void destroy() {

                   thread.interrupt();

                   threadPool.shutdown();

                   logger.info("thread poolasynService shut down");

         }

         @Override

         public void init() {

                   thread.start();

                   logger.info("thread poolasynService start");

         }

}

jdk5.0多线程学习笔记(一)

先来复习一下什么是线程:

线程有时称为 轻量级进程。与进程一样,它们拥有通过程序运行的独立的并发路径,并且每个线程都有自己的程序计数器,称为堆栈和本地变量。然而,线程存在于进程中,它们与同一进程内的其他线程共享内存、文件句柄以及每进程状态。

一个进程中的线程是在同一个地址空间中执行的,所以多个线程可以同时访问相同对象,并且它们从同一堆栈中分配对象。

在 JDK 5.0 之前,确保线程安全的主要机制是 synchronized 原语。访问共享变量(那些可以由多个线程访问的变量)的线程必须使用同步来协调对共享变量的读写访问。

创建线程的方法:

可以用两种方法创建线程,通过扩展 Thread 和覆盖 run() 方法,或者通过实现 Runnable 接口和使用Thread(Runnable) 构造函数:

Java代码

class WorkerThread extends Thread {   

  public void run() { /* do work */ }  

}   

Thread t = new WorkerThread();  

t.start();  

class WorkerThreadextends Thread {

  public void run() { /* do work */ }

}

Thread t = newWorkerThread();

t.start();

或是

Java代码

Thread t = new Thread(new Runnable() {   

  public void run() { /* do work */ }  

}   

t.start();  

Thread t = newThread(new Runnable() {

  public void run() { /* do work */ }

}

t.start();

 创建线程会使用相当一部分内存,其中包括有两个堆栈(Java 和 C),以及每线程数据结构。如果创建过多线程,其中每个线程都将占用一些 CPU 时间,结果将使用许多内存来支持大量线程,每个线程都运行得很慢。这样就无法很好地使用计算资源。

下面的代码就是一段不好的利用线程的代码:

Java代码

class UnreliableWebServer {   

  public static void main(String[] args) {  

    ServerSocket socket = new ServerSocket(80);  

      while (true) {  

      final Socket connection = socket.accept();  

      Runnable r = new Runnable() {  

        public void run() {  

          handleRequest(connection);  

        }  

      };  

      // Don't do this!  

      new Thread(r).start();  

    }  

  }  

}  

classUnreliableWebServer {

  public static void main(String[] args) {

    ServerSocket socket = new ServerSocket(80);

      while (true) {

      final Socket connection =socket.accept();

      Runnable r = new Runnable() {

        public void run() {

          handleRequest(connection);

        }

      };

      // Don't do this!

      new Thread(r).start();

    }

  }

}

 当服务器被请求吞没时,UnreliableWebServer 类不能很好地处理这种情况。每次有请求时,就会创建新的类。根据操作系统和可用内存,可以创建的线程数是有限的。不幸的是,您通常不知道限制是多少 —— 只有当应用程序因为OutOfMemoryError 而崩溃时才发现。如果足够快地向这台服务器上抛出请求的话,最终其中一个线程创建将失败,生成的 Error 会关闭整个应用程序。

为任务创建新的线程并不一定不好,但是如果创建任务的频率高,而平均任务持续时间低,我们可以看到每项任务创建一个新的线程将产生性能(如果负载不可预知,还有稳定性)问题.

使用线程池解决问题

管理一大组小任务的标准机制是组合工作队列和线程池。工作队列就是要处理的任务的队列,线程池是线程的集合,每个线程都提取公用工作队列。当一个工作线程完成任务处理后,它会返回队列,查看是否有其他任务需要处理。如果有,它会转移到下一个任务,并开始处理。作为一种额外好处,因为请求到达时,线程已经存在,从而可以消除由创建线程引起的延迟。因此,可以立即处理请求,使应用程序更易响应。而且,通过正确调整线程池中的线程数,可以强制超出特定限制的任何请求等待,直到有线程可以处理它,它们等待时所消耗的资源要少于使用额外线程所消耗的资源,这样可以防止资源崩溃。

说了半天,上段代码

Java代码

class ReliableWebServer {   

  Executor pool =  

    Executors.newFixedThreadPool(7);  

    public static void main(String[] args) {  

    ServerSocket socket = new ServerSocket(80);  

      while (true) {  

      final Socket connection = socket.accept();  

      Runnable r = new Runnable() {  

        public void run() {  

          handleRequest(connection);  

        }  

      };  

      pool.execute(r);  

    }  

  }  

}  

classReliableWebServer {

  Executor pool =

    Executors.newFixedThreadPool(7);

    public static void main(String[] args) {

    ServerSocket socket = new ServerSocket(80);

      while (true) {

      final Socket connection =socket.accept();

      Runnable r = new Runnable() {

        public void run() {

          handleRequest(connection);

        }

      };

      pool.execute(r);

    }

  }

}

 java.util.concurrent包中包含灵活的线程池实现,Executor就是这个包中的。(以后会对其进行详细的介绍,但不在本文内)

创建 Executor 时,人们普遍会问的一个问题是“线程池应该有多大?”

用 WT 表示每项任务的平均等待时间,ST表示每项任务的平均服务时间(计算时间)。则 WT/ST 是每项任务等待所用时间的百分比。对于 N 处理器系统,池中可以近似有 N*(1+WT/ST) 个线程。

 本文只是列举部分jdk5中有关线程的东西,大部分是概念上的引导。

jdk5.0多线程学习笔记(二)

关键字: jdk5 线程 并发

在学习jdk5的新特性之前,先看一个多线程的模式:Future Pattern

      去蛋糕店买蛋糕,不需要等蛋糕做出来(假设现做要很长时间),只需要领个提货单就可以了(去干别的事情),等到蛋糕做好了,再拿提货单取蛋糕就可以了。future模式与这个场景类似。

      假设有一个需要执行一段时间的方法,我们可以不必等待结果出来,而是获取一个替代的“提货单”。因为获取“提货单”不需要花时间,这时这个“提货单”就是future参与者。

     获取future参与者的线程会在事后再去获取执行结果,就好像拿提货单去取蛋糕一样。如果有执行结果了,就可以马上拿到数据。如果没有结果,就等到有结果。

    下面看一段代码:

Java代码

public class Main {  

    public static void main(String[] args) {  

        System.out.println("main BEGIN");  

        Host host = new Host();  

        Data data1 = host.request(10, 'A');  

        Data data2 = host.request(20, 'B');  

        Data data3 = host.request(30, 'C');  

        System.out.println("main otherJob BEGIN");  

        try {  

            Thread.sleep(200);  

        } catch (InterruptedException e) {  

        }  

        System.out.println("main otherJob END");  

        System.out.println("data1 = " + data1.getContent());  

        System.out.println("data2 = " + data2.getContent());  

        System.out.println("data3 = " + data3.getContent());  

        System.out.println("main END");  

    }  

}  

public class Main {

    public static void main(String[] args) {

        System.out.println("mainBEGIN");

        Host host = new Host();

        Data data1 = host.request(10, 'A');

        Data data2 = host.request(20, 'B');

        Data data3 = host.request(30, 'C');

        System.out.println("main otherJobBEGIN");

        try {

            Thread.sleep(200);

        } catch (InterruptedException e) {

        }

        System.out.println("main otherJobEND");

        System.out.println("data1 = "+ data1.getContent());

        System.out.println("data2 = "+ data2.getContent());

        System.out.println("data3 = "+ data3.getContent());

        System.out.println("mainEND");

    }

}

 这里的main类就相当于“顾客”,host就相当于“蛋糕店”,顾客向“蛋糕店”定蛋糕就相当于“发请求request”,返回的数据data就相当于“提货单”而不是真正的“蛋糕”。在过一段时间后(sleep一段时间后),再去凭“提货单”取蛋糕“data1.getContent()”。

下面来看一下,顾客定蛋糕后,蛋糕店做了什么:

public class Host {  

    public Data request(final int count, final char c) {  

        System.out.println("    request(" + count + ", " + c + ") BEGIN");  

        // (1) 建立FutureData的实体  

        final FutureData future = new FutureData();  

        // (2) 为了建立RealData的实体,启动新的线程   

        new Thread() {                                        

            public void run() {                               

                RealData realdata = new RealData(count, c);  

                future.setRealData(realdata);  

            }                                                 

        }.start();                                            

        System.out.println("    request(" + count + ", " + c + ") END");  

        // (3) 取回FutureData实体,作为传回值   

        return future;  

    }  

}  

public class Host {

    public Data request(final int count, finalchar c) {

        System.out.println("    request(" + count + ", " + c+ ") BEGIN");

        // (1) 建立FutureData的实体

        final FutureData future = newFutureData();

        // (2) 为了建立RealData的实体,启动新的线程

        new Thread() {                                     

            public void run() {                            

                RealData realdata = newRealData(count, c);

                future.setRealData(realdata);

            }                                              

        }.start();                                         

        System.out.println("    request(" + count + ", " + c+ ") END");

        // (3) 取回FutureData实体,作为传回值

        return future;

    }

}

  host("蛋糕店")在接到请求后,先生成了“提货单”FutureData的实例future,然后命令“蛋糕师傅”RealData去做蛋糕,realdata相当于起个线程去做蛋糕了。然后host返回给顾客的仅仅是“提货单”future,而不是蛋糕。当蛋糕做好后,蛋糕师傅才能给对应的“提货单”蛋糕,也就是future.setRealData(realdata)。

   下面来看看蛋糕师傅是怎么做蛋糕的:

 

 public class RealData implements Data {  

    private final String content;  

    public RealData(int count, char c) {  

        System.out.println("        making RealData(" + count + ", " + c + ") BEGIN");  

        char[] buffer = new char[count];  

        for (int i = 0; i < count; i++) {  

            buffer[i] = c;  

            try {  

                Thread.sleep(1000);  

            } catch (InterruptedException e) {  

            }  

        }  

        System.out.println("        making RealData(" + count + ", " + c + ") END");  

        this.content = new String(buffer);  

    }  

    public String getContent() {  

        return content;  

    }  

}  

 public class RealData implements Data {

    private final String content;

    public RealData(int count, char c) {

        System.out.println("        making RealData(" + count +", " + c + ") BEGIN");

        char[] buffer = new char[count];

        for (int i = 0; i < count; i++) {

            buffer[i] = c;

            try {

                Thread.sleep(1000);

            } catch (InterruptedException e) {

            }

        }

        System.out.println("        making RealData(" + count +", " + c + ") END");

        this.content = new String(buffer);

    }

    public String getContent() {

        return content;

    }

}

   现在来看看“提货单”future是怎么与蛋糕"content"对应的:

Java代码

public class FutureData implements Data {  

    private RealData realdata = null;  

    private boolean ready = false;  

  //将提货单与蛋糕师傅对应也就是与蛋糕对应,一个蛋糕师傅做一个订单   

    public synchronized void setRealData(RealData realdata) {  

        if (ready) {                          

            return;     // balk  

        }  

        this.realdata = realdata;  

        this.ready = true;  

        notifyAll();  

    }  

    public synchronized String getContent() {  

        while (!ready) {  

            try {  

                wait();  

            } catch (InterruptedException e) {  

            }  

        }  

        return realdata.getContent();  

    }  

}  

public classFutureData implements Data {

    private RealData realdata = null;

    private boolean ready = false;

  //将提货单与蛋糕师傅对应也就是与蛋糕对应,一个蛋糕师傅做一个订单

    public synchronized voidsetRealData(RealData realdata) {

        if (ready) {                       

            return;     // balk

        }

        this.realdata = realdata;

        this.ready = true;

        notifyAll();

    }

    public synchronized String getContent() {

        while (!ready) {

            try {

                wait();

            } catch (InterruptedException e) {

            }

        }

        return realdata.getContent();

    }

}

    顾客做完自己的事情后,会拿着自己的“提货单”来取蛋糕:

 

System.out.println("data1 = " + data1.getContent());  

 System.out.println("data1 = " +data1.getContent());

 这时候如果蛋糕没做好,就只好等了:

 

while (!ready) {  

            try {  

                wait();  

            } catch (InterruptedException e) {  

            }  

//等做好后才能取到     

return realdata.getContent();  

while (!ready) {

            try {

                wait();

            } catch (InterruptedException e) {

            }

//等做好后才能取到 

returnrealdata.getContent();

    本文只是简单介绍一下future pattern,本人也是初学,如果要深入了解,还需要研究研究,本文代码并不优,只是做个说明性的例子。在以后将继续学习多线程。

jdk5.0 多线程学习笔记(三)

关键字: jdk5.0 多线程 producerconsumer

在进一步学习jdk5.0的多线程编程以前,先介绍一下生产者--消费者模式(producer-consumer)

生产者是指:生产数据的线程

消费者是指:使用数据的线程

生产者和消费者是不同的线程,他们处理数据的速度是不一样的,一般在二者之间还要加个“桥梁参与者”,用于缓冲二者之间处理数据的速度差。

下面用代码来说明:

Java代码

//生产者   

public class MakerThread extends Thread {  

    private final Random random;  

    private final Table table;  

    private static int id = 0;   

    public MakerThread(String name, Table table, long seed) {  

        super(name);  

        this.table = table;//table就是桥梁参与者   

        this.random = new Random(seed);  

    }  

    public void run() {  

        try {  

            while (true) {  

                Thread.sleep(random.nextInt(1000));//生产数据要耗费时间   

                String cake = "[ Cake No." + nextId() + " by " + getName() + " ]";//生产数据   

                table.put(cake);//将数据存入桥梁参与者   

            }  

        } catch (InterruptedException e) {  

        }  

    }  

    private static synchronized int nextId() {  

        return id++;  

    }  

}  

//生产者

public classMakerThread extends Thread {

    private final Random random;

    private final Table table;

    private static int id = 0;

    public MakerThread(String name, Tabletable, long seed) {

        super(name);

        this.table = table;//table就是桥梁参与者

        this.random = new Random(seed);

    }

    public void run() {

        try {

            while (true) {

               Thread.sleep(random.nextInt(1000));//生产数据要耗费时间

                String cake = "[ CakeNo." + nextId() + " by " + getName() + " ]";//生产数据

                table.put(cake);//将数据存入桥梁参与者

            }

        } catch (InterruptedException e) {

        }

    }

    private static synchronized int nextId() {

        return id++;

    }

}

 再来看看消费者:

Java代码

//消费者线程   

public class EaterThread extends Thread {  

    private final Random random;  

    private final Table table;  

    public EaterThread(String name, Table table, long seed) {  

        super(name);  

        this.table = table;  

        this.random = new Random(seed);  

    }  

    public void run() {  

        try {  

            while (true) {  

                String cake = table.take();//从桥梁参与者中取数据   

                Thread.sleep(random.nextInt(1000));//消费者消费数据要花时间   

            }  

        } catch (InterruptedException e) {  

        }  

    }  

}  

//消费者线程

public classEaterThread extends Thread {

    private final Random random;

    private final Table table;

    public EaterThread(String name, Tabletable, long seed) {

        super(name);

        this.table = table;

        this.random = new Random(seed);

    }

    public void run() {

       try {

            while (true) {

                String cake = table.take();//从桥梁参与者中取数据

               Thread.sleep(random.nextInt(1000));//消费者消费数据要花时间

            }

        } catch (InterruptedException e) {

        }

    }

}

看来在这个模式里table是个很重要的角色啊,让我们来看看他吧(这里只给出个简单的):

Java代码

public class Table {  

    private final String[] buffer;  

    private int tail;  /下一个放put(数据)的地方    

    private int head;  //下一个那曲take(数据)的地方   

    private int count; // buffer内的数据数量   

    public Table(int count) {  

        this.buffer = new String[count];//总量是确定的   

        this.head = 0;  

        this.tail = 0;  

        this.count = 0;  

    }  

    // 放置数据   

    public synchronized void put(String cake) throws InterruptedException {  

        System.out.println(Thread.currentThread().getName() + " puts " + cake);  

        while (count >= buffer.length) {//数据放满了就只能等待   

            wait();  

        }  

        buffer[tail] = cake;  

        tail = (tail + 1) % buffer.length;  

        count++;  

        notifyAll();//有数据了,唤醒线程去取数据   

    }  

    // 取得数据   

    public synchronized String take() throws InterruptedException {  

        while (count <= 0) {//没有数据就只能等待   

            wait();  

        }  

        String cake = buffer[head];  

        head = (head + 1) % buffer.length;  

        count--;  

        notifyAll();//有位置可以放数据了,唤醒线程,不等了   

        System.out.println(Thread.currentThread().getName() + " takes " + cake);  

        return cake;  

    }  

}i  

public class Table{

    private final String[] buffer;

    private int tail;  /下一个放put(数据)的地方

    private int head;  //下一个那曲take(数据)的地方

    private int count; // buffer内的数据数量

    public Table(int count) {

        this.buffer = new String[count];//总量是确定的

        this.head = 0;

        this.tail = 0;

        this.count = 0;

    }

    // 放置数据

    public synchronized void put(String cake)throws InterruptedException {

        System.out.println(Thread.currentThread().getName()+ " puts " + cake);

        while (count >= buffer.length) {//数据放满了就只能等待

            wait();

        }

        buffer[tail] = cake;

        tail = (tail + 1) % buffer.length;

        count++;

        notifyAll();//有数据了,唤醒线程去取数据

    }

    // 取得数据

    public synchronized String take() throwsInterruptedException {

        while (count <= 0) {//没有数据就只能等待

            wait();

        }

        String cake = buffer[head];

        head = (head + 1) % buffer.length;

        count--;

        notifyAll();//有位置可以放数据了,唤醒线程,不等了

       System.out.println(Thread.currentThread().getName() + " takes" + cake);

        return cake;

    }

}i

 好了我们来实验吧:

Java代码

public class Main {  

    public static void main(String[] args) {  

        Table table = new Table(3);     // 建立可以放置数据的桥梁参与者,3是他所能放置的最大数量的数据。  

        new MakerThread("MakerThread-1", table, 31415).start();//生产数据   

        new MakerThread("MakerThread-2", table, 92653).start();  

        new MakerThread("MakerThread-3", table, 58979).start();  

        new EaterThread("EaterThread-1", table, 32384).start();//消费数据   

        new EaterThread("EaterThread-2", table, 62643).start();  

        new EaterThread("EaterThread-3", table, 38327).start();  

    }  

}  

public class Main {

    public static void main(String[] args) {

        Table table = new Table(3);     // 建立可以放置数据的桥梁参与者,3是他所能放置的最大数量的数据。

        newMakerThread("MakerThread-1", table, 31415).start();//生产数据

        newMakerThread("MakerThread-2", table, 92653).start();

        newMakerThread("MakerThread-3", table, 58979).start();

        new EaterThread("EaterThread-1",table, 32384).start();//消费数据

        newEaterThread("EaterThread-2", table, 62643).start();

        newEaterThread("EaterThread-3", table, 38327).start();

    }

}

 之所以在这里要介绍这个模式,是为了更好的理解jdk5的线程编程。

jdk5.0 多线程学习笔记(四)

关键字: jdk5.0 多线程 线程池

学了这么久,终于进入jdk5.0的线程编程了。

先来看一段代码:

Java代码

public class ThreadPoolTest {  

    public static void main(String[] args) {  

        int numWorkers = 10;//工作线程数   

        int threadPoolSize = 2;//线程池大小   

        ExecutorService tpes =  

            Executors.newFixedThreadPool(threadPoolSize);//初始化线程池   

        WorkerThread[] workers = new WorkerThread[numWorkers];  

        for (int i = 0; i < numWorkers; i++) {  

            workers[i] = new WorkerThread(i);//初始一个任务   

            tpes.execute(workers[i]);//执行任务   

        }  

        tpes.shutdown();//所有线程执行完毕后才关闭。   

//         tpes.shutdownNow();//立即关闭   

    }  

}  

public classThreadPoolTest {

         public static void main(String[] args){

        int numWorkers = 10;//工作线程数

        int threadPoolSize = 2;//线程池大小

        ExecutorService tpes =

            Executors.newFixedThreadPool(threadPoolSize);//初始化线程池

        WorkerThread[] workers = newWorkerThread[numWorkers];

        for (int i = 0; i < numWorkers; i++){

            workers[i] = new WorkerThread(i);//初始一个任务

            tpes.execute(workers[i]);//执行任务

        }

        tpes.shutdown();//所有线程执行完毕后才关闭。

//         tpes.shutdownNow();//立即关闭

    }

 

}

 看看工作线程:

Java代码

public class WorkerThread implements Runnable {  

      private int workerNumber;  

      WorkerThread(int number) {  

        workerNumber = number;  

    }  

      public void run() {  

        for (int i=0;i<=100;i+=20) {  

        //Perform some work...  

            System.out.format("Worker number: %d, percent complete: %d%n",  

                workerNumber, i);  

            try {  

                Thread.sleep((int)(Math.random() * 1000));  

            } catch (InterruptedException e) { }  

        }  

    }  

  }  

public classWorkerThread implements Runnable {

         private int workerNumber;

    WorkerThread(int number) {

        workerNumber = number;

    }

    public void run() {

        for (int i=0;i<=100;i+=20) {

        //Perform some work...

            System.out.format("Workernumber: %d, percent complete: %d%n",

                workerNumber, i);

            try {

               Thread.sleep((int)(Math.random() * 1000));

            } catch (InterruptedException e) {}

        }

    }

}

 从执行的结果可以看出:有两个线程在执行操作,因为我们的线程池中就只有两个线程。

这里要注意一下:

tpes.execute(workers[i]);

这里不是启动一个新线程,而是在仅仅是调用了run方法,并没有新建线程。这一点可以参看如下代码(节选自jdk5):

Java代码

**             * Run a single task between before/after methods.    

         */    

        private void runTask(Runnable task) {     

            final ReentrantLock runLock = this.runLock;     

            runLock.lock();     

            try {     

                // Abort now if immediate cancel.  Otherwise, we have     

                // committed to run this task.     

                if (runState == STOP)     

                    return;     

               Thread.interrupted(); // clear interrupt status on entry     

                boolean ran = false;     

                beforeExecute(thread, task);     

                try {     

                    task.run();  //调用的是run()方法 而不是start()     

                    ran = true;     

                    afterExecute(task, null);     

                    ++completedTasks;     

                } catch(RuntimeException ex) {     

                    if (!ran)     

                        afterExecute(task, ex);     

                    // Else the exception occurred within     

                    // afterExecute itself in which case we don't     

                    // want to call it again.     

                    throw ex;     

                }     

            } finally {     

                runLock.unlock();     

            }     

        }    

** 

         * Run a single task betweenbefore/after methods. 

         */ 

        private void runTask(Runnable task) {  

            final ReentrantLock runLock =this.runLock;  

            runLock.lock();  

            try {  

                // Abort now if immediatecancel.  Otherwise, we have  

                // committed to run thistask.  

                if (runState == STOP)  

                    return;  

                  Thread.interrupted(); //clear interrupt status on entry  

                boolean ran = false;  

                beforeExecute(thread,task);  

                try {  

                    task.run();  //调用的是run()方法而不是start()  

                    ran = true;  

                    afterExecute(task,null);  

                    ++completedTasks;  

                } catch(RuntimeException ex){  

                    if (!ran)   

                        afterExecute(task,ex);  

                    // Else the exceptionoccurred within  

                    // afterExecute itself inwhich case we don't  

                    // want to call itagain.  

                    throw ex;  

                }  

            } finally {  

                runLock.unlock();  

            }  

        } 

 请注意task.run(); 这句, 这儿并没有启动线程 而是简单的调用了一个普通对象的一个方法

从多线程设计的角度来讲,jdk5中的线程池应该是基于worker模式的。下一节将对worker模式进行介绍,以加深对jdk5中多线程编程的理解。

jdk5.0 多线程学习笔记(五)

关键字: jdk5.0 多线程 模式

今天,我们来学学worker模式,大家也好对jdk5.0的线程池有一个更好的理解。

先来看看代码:

Java代码

public class Main {  

    public static void main(String[] args) {  

        Channel channel = new Channel(5);   // 工人线程的數量,即线程池内的线程数目   

        channel.startWorkers();//启动线程池内的线程   

        new ClientThread("Alice", channel).start();//发送请求的线程,相当于向队列加入请求   

        new ClientThread("Bobby", channel).start();  

        new ClientThread("Chris", channel).start();  

    }  

}  

public class Main {

    public static void main(String[] args) {

        Channel channel = new Channel(5);   // 工人线程的數量,即线程池内的线程数目

        channel.startWorkers();//启动线程池内的线程

        new ClientThread("Alice",channel).start();//发送请求的线程,相当于向队列加入请求

        new ClientThread("Bobby",channel).start();

        new ClientThread("Chris",channel).start();

    }

}

 再来看看发送请求的client代码:

Java代码

public class ClientThread extends Thread {  

    private final Channel channel;//相当于线程池   

     private static final Random random = new Random();  

      public ClientThread(String name, Channel channel) {  

        super(name);  

        this.channel = channel;  

    }  

      public void run() {  

        try {  

            int i = 0;  

            Request request = new Request(getName(), i);//生成请求   

            channel.putRequest(request);//向队列中放入请求,也即把请求传给线程池   

            Thread.sleep(random.nextInt(1000));  

        } catch (InterruptedException e) {  

        }  

    }  

}  

public classClientThread extends Thread {

         private final Channel channel;//相当于线程池

         private static final Random random =new Random();

         public ClientThread(String name,Channel channel) {

                   super(name);

                   this.channel = channel;

         }

         public void run() {

                   try {

                            int i = 0;

                            Request request =new Request(getName(), i);//生成请求

                            channel.putRequest(request);//向队列中放入请求,也即把请求传给线程池

                            Thread.sleep(random.nextInt(1000));

                   } catch (InterruptedExceptione) {

                   }

         }

}

 clientthread建立请求,并把请求传给了channel,下面来看看channel类(相当于线程池类)

Java代码

public class Channel {  

    private static final int MAX_REQUEST = 100;  

    private final Request[] requestQueue;//存放请求的队列   

    private int tail;  // 下一个putRequest的地方  

    private int head;  // 下一个takeRequest的地方  

    private int count; // Request的数量   

      private final WorkerThread[] threadPool;  

      public Channel(int threads) {  

        this.requestQueue = new Request[MAX_REQUEST];  

        this.head = 0;  

        this.tail = 0;  

        this.count = 0;  

          threadPool = new WorkerThread[threads];  

        for (int i = 0; i < threadPool.length; i++) {  

            threadPool[i] = new WorkerThread("Worker-" + i, this);//生成线程池中的线程   

        }  

    }  

    public void startWorkers() {  

        for (int i = 0; i < threadPool.length; i++) {  

            threadPool[i].start();//启动线程池中的线程   

        }  

    }  

    public synchronized void putRequest(Request request) {//向队列中存入请求   

        while (count >= requestQueue.length) {  

            try {  

                wait();  

            } catch (InterruptedException e) {  

            }  

        }  

        requestQueue[tail] = request;  

        tail = (tail + 1) % requestQueue.length;  

        count++;  

        notifyAll();  

    }  

    public synchronized Request takeRequest() {//从队列取出请求   

        while (count <= 0) {  

            try {  

                wait();  

            } catch (InterruptedException e) {  

            }  

        }  

        Request request = requestQueue[head];  

        head = (head + 1) % requestQueue.length;  

        count--;  

        notifyAll();  

        return request;  

    }  

}  

public classChannel {

    private static final int MAX_REQUEST = 100;

    private final Request[] requestQueue;//存放请求的队列

    private int tail;  // 下一个putRequest的地方

    private int head;  // 下一个takeRequest的地方

    private int count; // Request的数量

    private final WorkerThread[] threadPool;

    public Channel(int threads) {

        this.requestQueue = new Request[MAX_REQUEST];

        this.head = 0;

        this.tail = 0;

        this.count = 0;

        threadPool = new WorkerThread[threads];

        for (int i = 0; i <threadPool.length; i++) {

            threadPool[i] = newWorkerThread("Worker-" + i, this);//生成线程池中的线程

        }

    }

    public void startWorkers() {

        for (int i = 0; i <threadPool.length; i++) {

            threadPool[i].start();//启动线程池中的线程

        }

    }

    public synchronized void putRequest(Requestrequest) {//向队列中存入请求

        while (count >= requestQueue.length){

            try {

                wait();

            } catch (InterruptedException e) {

            }

        }

        requestQueue[tail] = request;

        tail = (tail + 1) %requestQueue.length;

        count++;

        notifyAll();

    }

    public synchronized Request takeRequest(){//从队列取出请求

        while (count <= 0) {

            try {

                wait();

            } catch (InterruptedException e) {

            }

        }

        Request request = requestQueue[head];

        head = (head + 1) %requestQueue.length;

        count--;

        notifyAll();

        return request;

    }

}

 channel类把传给他的请求放入队列中,等待worker去取请求,下面看看worker(即工作线程,线程池中已经初始话好的线程)

Java代码

public class WorkerThread extends Thread {  

    private final Channel channel;  

    public WorkerThread(String name, Channel channel) {  

        super(name);  

        this.channel = channel;  

    }  

    public void run() {  

        while (true) {  

            Request request = channel.takeRequest();//取出请求   

            request.execute();//处理请求   

        }  

    }  

}  

public classWorkerThread extends Thread {

    private final Channel channel;

    public WorkerThread(String name, Channelchannel) {

        super(name);

        this.channel = channel;

    }

    public void run() {

        while (true) {

            Request request =channel.takeRequest();//取出请求

            request.execute();//处理请求

        }

    }

}

 在工作线程中会从线程池的队列里取出请求,并对请求进行处理。这里的workerthread相当于背景线程,他一直都在运行,当有请求的时候,他就会进行处理,这里处理请求的线程是已经存在在channel(线程池里的线程),他不会因为请求的增加而增加(这是本例中的情况),不会来一个请求就新建立一个线程,节省了资源。

再看看请求的代码:

Java代码

public class Request {  

    private final String name; //  委托者   

    private final int number;  // 请求编号   

    private static final Random random = new Random();  

    public Request(String name, int number) {  

        this.name = name;  

        this.number = number;  

    }  

    public void execute() {//执行请求   

        System.out.println(Thread.currentThread().getName() + " executes " + this);  

        try {  

            Thread.sleep(random.nextInt(1000));  

        } catch (InterruptedException e) {  

        }  

    }  

    public String toString() {  

        return "[ Request from " + name + " No." + number + " ]";  

    }  

}  

public classRequest {

    private final String name; //  委托者

    private final int number;  // 请求编号

    private static final Random random = newRandom();

    public Request(String name, int number) {

        this.name = name;

        this.number = number;

    }

    public void execute() {//执行请求

        System.out.println(Thread.currentThread().getName()+ " executes " + this);

        try {

            Thread.sleep(random.nextInt(1000));

        } catch (InterruptedException e) {

        }

    }

    public String toString() {

        return "[ Request from " +name + " No." + number + " ]";

    }

}

 参考(多线程四)中所写的 ExecutorService,其就相当于channel,即线程池。至于其实现当然要比channel复杂多了,channel只是举个例子。而WorkerThread可不是工作线程,他相当于发送到channel的请求,也就是request,当执行代码:tpes.execute(workers[i]);时,相当于向线程池加入一个请求,而WorkerThread中的run则相当于request中的execute,这也是当执行tpes.execute(workers[i]);时,并不会产生新的线程的原因。(多线程四)中的写法是让人有些迷糊的。ExecutorService中产生的背景线程(相当于本篇的WorkerThread )我们是看不到的。

 jdk5中的多线程还有很多需要进一步学习,其实现也反应了多线程的设计模式。本篇的worker模式只是其中的一种。

jdk5.0 多线程学习笔记(六)

关键字: jdk1.5 多线程 线程池

从前面的文章可以看出,jdk1.5为我们提供了很多线程池

这里做一下简要的说明:

类Executors,提供了一些创建线程池的方法

 newFixedThreadPool(int nThreads)

创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。

如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。

 newCachedThreadPool()

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。

对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。

调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。

终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

注意,可以使用 ThreadPoolExecutor类构造方法创建具有类似属性但细节不同,(例如超时参数)的线程池。

其实executors的线程池也是基于ThreadPoolExecutor扩展的。

 newSingleThreadExecutor()

创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。

(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,

一个新线程将代替它执行后续的任务)。

可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

(与其他等效的 newFixedThreadPool(1) 不同,

可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。)

 类ThreadPoolExecutor

Java代码

public ThreadPoolExecutor(int corePoolSize,  

                          int maximumPoolSize,  

                          long keepAliveTime,  

                          TimeUnit unit,  

                          BlockingQueue<Runnable> workQueue,  

                          ThreadFactory threadFactory,  

                          RejectedExecutionHandler handler)  

public ThreadPoolExecutor(int corePoolSize,

                         int maximumPoolSize,

                         long keepAliveTime,

                         TimeUnit unit,

                         BlockingQueue<Runnable> workQueue,

                         ThreadFactory threadFactory,

                         RejectedExecutionHandler handler)

  核心和最大池大小

corePoolSize - 池中所保存的线程数,包括空闲线程。

maximumPoolSize - 池中允许的最大线程数。

ThreadPoolExecutor 将根据 corePoolSize和maximumPoolSize设置的边界自动调整池大小。

当新任务在方法 execute(java.lang.Runnable)中提交时,

如果运行的线程少于corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。

如果运行的线程多于corePoolSize 而少于maximumPoolSize,则仅当队列满时才创建新线程。

如果设置的corePoolSize 和maximumPoolSize相同,则创建了固定大小的线程池。

如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),

则允许池适应任意数量的并发任务。

在大多数情况下,核心和最大池大小仅基于构造来设置,不过也可以使用 setCorePoolSize(int)和

setMaximumPoolSize(int)进行动态更改。

默认情况下,即使核心线程也只是在新任务需要时才创建和启动的。

 threadFactory- 执行程序创建新线程时使用的工厂 

使用 ThreadFactory创建新线程。

如果没有另外说明,则在同一个ThreadGroup中一律使用Executors.defaultThreadFactory()创建线程,

并且这些线程具有相同的 NORM_PRIORITY 优先级和非守护进程状态。

通过提供不同的ThreadFactory,可以改变线程的名称、线程组、优先级、守护进程状态,等等。

如果从 newThread 返回 null 时 ThreadFactory 未能创建线程,则执行程序将继续运行,但不能执行任何任务。

 keepAliveTime- 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。 

(保持活动时间)

unit -keepAliveTime 参数的时间单位。

如果池中当前有多于corePoolSize的线程,则这些多出的线程在空闲时间超过keepAliveTime时将会终止。

这提供了当池处于非活动状态时减少资源消耗的方法。

如果池后来变得更为活动,则可以创建新的线程。

也可以使用方法 setKeepAliveTime(long,java.util.concurrent.TimeUnit)动态地更改此参数。

使用 Long.MAX_VALUE TimeUnit.NANOSECONDS的值在关闭前有效地从以前的终止状态禁用空闲线程。

 workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的Runnable 任务。 

 所有BlockingQueue都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互:

1.如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。

2.如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。

3.如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,

在这种情况下,任务将被拒绝。

 排队有三种通用策略:

1.直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。

在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。

此策略可以避免在处理可能具有内部依赖性的请求集合时出现锁定。

直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。

当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

 2.无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)

将导致在所有 corePoolSize 线程都忙的情况下将新任务加入队列。

这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)

当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。

这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

 3.有界队列。当使用有限的 maximumPoolSizes 时,

有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。

队列大小和最大池大小可能需要相互折衷:

使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,

但是可能导致人工降低吞吐量。

如果任务频繁阻塞(例如,如果它们是 I/O 边界),

则系统可能为超过您许可的更多线程安排时间。

使用小型队列通常要求较大的池大小,CPU 使用率较高,

但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

 handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序 (被拒绝的任务 )

当 Executor 已经关闭,并且Executor 将有限边界用于最大线程和工作队列容量,且已经饱和时,

在方法 execute(java.lang.Runnable)中提交的新任务将被拒绝。

在以上两种情况下,execute 方法都将调用其RejectedExecutionHandler的

RejectedExecutionHandler.rejectedExecution(java.lang.Runnable,  

java.util.concurrent.ThreadPoolExecutor)  

RejectedExecutionHandler.rejectedExecution(java.lang.Runnable,

java.util.concurrent.ThreadPoolExecutor)

  方法。

下面提供了四种预定义的处理程序策略:

 1.在默认的 ThreadPoolExecutor.AbortPolicy中,处理程序遭到拒绝将抛出运行时 RejectedExecutionException。

2.在 ThreadPoolExecutor.CallerRunsPolicy中,线程调用运行该任务的 execute 本身。

此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。

3. 在 ThreadPoolExecutor.DiscardPolicy中,不能执行的任务将被删除。

4.在 ThreadPoolExecutor.DiscardOldestPolicy中,如果执行程序尚未关闭,

则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。

  定义和使用其他种类的 RejectedExecutionHandler类也是可能的,但这样做需要非常小心,

尤其是当策略仅用于特定容量或排队策略时。

这些只是jdk中的介绍,要想更好的使用线程池,就需要对这些参数有所了解。

 

JDK1.5线程池

在多线程大师Doug Lea 的贡献下,在JDK1.5中加入了许多对并发特性的支持,例如:线程池。

一、简介

线程池类为java.util.concurrent.ThreadPoolExecutor,常用构造方法为:

ThreadPoolExecutor (

int corePoolSize,                        //线程池维护线程的最少数量

intmaximumPoolSize,                   //线程池维护线程的最大数量

long keepAliveTime,                   //线程池维护线程所允许的空闲时间

TimeUnit unit,                         //线程池维护线程所允许的空闲时间的单位

BlockingQueue<Runnable>workQueue,    //线程池所使用的缓冲队列

RejectedExecutionHandlerhandler         //线程池对拒绝任务的处理策略

) ;

corePoolSize:线程池维护线程的最少数量

maximumPoolSize:线程池维护线程的最大数量

keepAliveTime:线程池维护线程所允许的空闲时间

unit:线程池维护线程所允许的空闲时间的单位

workQueue:线程池所使用的缓冲队列

handler:线程池对拒绝任务的处理策略

 

一个任务通过execute(Runnable)方法被添加到线程池,任务就是一个Runnable 类型的对

象,任务的执行方法就是Runnable 类型对象的run()方法。

 

当一个任务通过execute(Runnable)方法欲添加到线程池时:

如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要

创建新的线程来处理被添加的任务。

如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue 未满,那么任务被

放入缓冲队列。

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue 满,并且线程池中的数

量小于maximumPoolSize,建新的线程来处理被添加的任务。

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue 满,并且线程池中的数

量等于maximumPoolSize,那么通过handler 所指定的策略来处理此任务。

 

也就是:处理任务的优先级为:

核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满

了,使用handler 处理被拒绝的任务。

 

当线程池中的线程数量大于corePoolSize 时,如果某线程空闲时间超过keepAliveTime,

线程将被终止。这样,线程池可以动态的调整池中的线程数。

 

unit 可选的参数为java.util.concurrent.TimeUnit 中的几个静态属性:

NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。

 

workQueue 我常用的是:java.util.concurrent.ArrayBlockingQueue

handler 有四个选择:

1、ThreadPoolExecutor.AbortPolicy()

抛出java.util.concurrent.RejectedExecutionException异常

2、ThreadPoolExecutor.CallerRunsPolicy()

重试添加当前的任务,他会自动重复调用execute()方法

3、ThreadPoolExecutor.DiscardOldestPolicy()

抛弃旧的任务

4、ThreadPoolExecutor.DiscardPolicy()

抛弃当前的任务

 

 

二、一般用法举例

//------------------------------------------------------------

//TestThreadPool.java

//packagecn.simplelife.exercise;

import java.io.Serializable;

import java.util.concurrent.ArrayBlockingQueue;

import java.util.concurrent.ThreadPoolExecutor;

import java.util.concurrent.TimeUnit;

 

public class TestThreadPool {

         private static intproduceTaskSleepTime = 2;

         private static intconsumeTaskSleepTime = 2000;

         private static intproduceTaskMaxNumber = 10;

 

         public static voidmain(String[] args) {

                   // 构造一个线程池

                   ThreadPoolExecutor threadPool= new ThreadPoolExecutor(2,4, 3,

                                     TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(3),

                                     newThreadPoolExecutor.DiscardOldestPolicy());

                   for (int i = 1;i <= produceTaskMaxNumber; i++) {

                            try {

                                     // 产生一个任务,并将其加入到线程池

                                     String task= "task@ " + i;

                                     System.out.println("put" + task);

                                     threadPool.execute(newThreadPoolTask(task));

                                     // 便于观察,等待一段时间

                                     Thread.sleep(produceTaskSleepTime);

                            } catch(Exception e) {

                                     e.printStackTrace();

                            }

                   }

         }

 

         /**

          * 线程池执行的任务

          * @author hdpan

          */

         public static classThreadPoolTask implements Runnable, Serializable {

                   private static finallong serialVersionUID = 0;

                   // 保存任务所需要的数据

                   private ObjectthreadPoolTaskData;

 

                   ThreadPoolTask(Object tasks){

                            this.threadPoolTaskData= tasks;

                   }

 

                   public voidrun() {

                            // 处理一个任务,这里的处理方式太简单了,仅仅是一个打印语句

                            System.out.println("start.." + threadPoolTaskData);

                            try {

                                     // //便于观察,等待一段时间

                                     Thread.sleep(consumeTaskSleepTime);

                            } catch(Exception e) {

                                     e.printStackTrace();

                            }

                            threadPoolTaskData =null;

                   }

 

                   public ObjectgetTask() {

                            return this.threadPoolTaskData;

                   }

         }

}

//------------------------------------------------------------

说明:

1、在这段程序中,一个任务就是一个Runnable 类型的对象,也就是一个ThreadPoolTask

类型的对象。(ThreadPoolTask implements Runnable)

2、一般来说任务除了处理方式外,还需要处理的数据,处理的数据通过构造方法传给任务。

3、在这段程序中,main()方法相当于一个残忍的领导,他派发出许多任务,丢给一个叫

threadPool 的任劳任怨的小组来做。

这个小组里面队员至少有两个,如果他们两个忙不过来,任务就被放到任务列表里面。

如果积压的任务过多,多到任务列表都装不下(超过3 个)的时候,就雇佣新的队员来帮忙。

但是基于成本的考虑,不能雇佣太多的队员,至多只能雇佣4 个。

如果四个队员都在忙时,再有新的任务,这个小组就处理不了了,任务就会被通过一种策略

来处理,我们的处理方式是不停的派发,

直到接受这个任务为止(更残忍!呵呵)。

因为队员工作是需要成本的,如果工作很闲,闲到3SECONDS 都没有新的任务了,那么有的队员就会被解雇了,但是,为了小组的正常运转,即使工作再闲,小组的队员也不能少于两个。

4、通过调整produceTaskSleepTime 和consumeTaskSleepTime的大小来实现对派发任务和

处理任务的速度的控制,改变这两个值就可以观察不同速率下程序的工作情况。

5、通过调整4 中所指的数据,再加上调整任务丢弃策略,换上其他三种策略,就可以看出

不同策略下的不同处理方式。

6、对于其他的使用方法,参看jdk 的帮助,很容易理解和使用。

 

假设要对一套10 个节点组成的环境进行检查,这个环境有两个入口点,通过节点间的依赖

关系可以遍历到整个环境。依赖关系可以构成一张有向图,可能存在环。为了提高检查的效

率,考虑使用多线程。

1、Executors

通过这个类能够获得多种线程池的实例,例如可以调用newSingleThreadExecutor()获得单

线程的ExecutorService ,调用newFixedThreadPool() 获得固定大小线程池的

ExecutorService。拿到ExecutorService 可以做的事情就比较多了,最简单的是用它来执

行Runnable 对象,也可以执行一些实现了Callable<T>的对象。用Thread 的start()方法

没有返回值,如果该线程执行的方法有返回值那用ExecutorService 就再好不过了,可以选

择submit()、invokeAll()或者invokeAny(),根据具体情况选择合适的方法即可。

Java 代码

package service;

importjava.util.ArrayList;

importjava.util.List;

importjava.util.concurrent.ExecutionException;

importjava.util.concurrent.ExecutorService;

importjava.util.concurrent.Executors;

importjava.util.concurrent.Future;

importjava.util.concurrent.TimeUnit;

/**

* 线程池服务类

*

* @authorDigitalSonic

*/

public classThreadPoolService {

/**

* 默认线程池大小

*/

public static finalint DEFAULT_POOL_SIZE = 5;

/**

* 默认一个任务的超时时间,单位为毫秒

*/

public static finallong DEFAULT_TASK_TIMEOUT = 1000;

private intpoolSize = DEFAULT_POOL_SIZE;

privateExecutorService executorService;

/**

* 根据给定大小创建线程池

*/

publicThreadPoolService(int poolSize) {

setPoolSize(poolSize);

}

/**

* 使用线程池中的线程来执行任务

*/

public voidexecute(Runnable task) {

executorService.execute(task);

}

/**

* 在线程池中执行所有给定的任务并取回运行结果,使用默认超时时间

*

* @see#invokeAll(List, long)

*/

publicList<Node> invokeAll(List<ValidationTask> tasks) {

returninvokeAll(tasks, DEFAULT_TASK_TIMEOUT * tasks.size());

}

/**

* 在线程池中执行所有给定的任务并取回运行结果

*

* @param timeout 以毫秒为单位的超时时间,小于0 表示不设定超时

* @seejava.util.concurrent.ExecutorService#invokeAll(java.util.Collection)

*/

publicList<Node> invokeAll(List<ValidationTask> tasks, long timeout) {

List<Node>nodes = new ArrayList<Node>(tasks.size());

try {

List<Future<Node>>futures = null;

if (timeout < 0){

futures =executorService.invokeAll(tasks);

} else {

futures =executorService.invokeAll(tasks, timeout, TimeUnit.MI

LLISECONDS);

}

for(Future<Node> future : futures) {

try {

nodes.add(future.get());

} catch(ExecutionException e) {

e.printStackTrace();

}

}

} catch(InterruptedException e) {

e.printStackTrace();

}

return nodes;

}

/**

* 关闭当前ExecutorService

*

* @param timeout 以毫秒为单位的超时时间

*/

public voiddestoryExecutorService(long timeout) {

if (executorService!= null && !executorService.isShutdown()) {

try {

executorService.awaitTermination(timeout,TimeUnit.MILLISECONDS)

;

} catch (InterruptedExceptione) {

e.printStackTrace();

}

executorService.shutdown();

}

}

/**

* 关闭当前ExecutorService,随后根据poolSize创建新的ExecutorService

*/

public voidcreateExecutorService() {

destoryExecutorService(1000);

executorService =Executors.newFixedThreadPool(poolSize);

}

/**

* 调整线程池大小

* @see#createExecutorService()

*/

public voidsetPoolSize(int poolSize) {

this.poolSize =poolSize;

createExecutorService();

}

}

package service;

importjava.util.ArrayList;

importjava.util.List;

importjava.util.concurrent.ExecutionException;

importjava.util.concurrent.ExecutorService;

importjava.util.concurrent.Executors;

importjava.util.concurrent.Future;

importjava.util.concurrent.TimeUnit;

/**

* 线程池服务类

*

* @authorDigitalSonic

*/

public classThreadPoolService {

/**

* 默认线程池大小

*/

public static finalint DEFAULT_POOL_SIZE = 5;

/**

* 默认一个任务的超时时间,单位为毫秒

*/

public static finallong DEFAULT_TASK_TIMEOUT = 1000;

private intpoolSize = DEFAULT_POOL_SIZE;

privateExecutorService executorService;

/**

* 根据给定大小创建线程池

*/

public ThreadPoolService(intpoolSize) {

setPoolSize(poolSize);

}

/**

* 使用线程池中的线程来执行任务

*/

public voidexecute(Runnable task) {

executorService.execute(task);

}

/**

* 在线程池中执行所有给定的任务并取回运行结果,使用默认超时时间

*

* @see#invokeAll(List, long)

*/

publicList<Node> invokeAll(List<ValidationTask> tasks) {

returninvokeAll(tasks, DEFAULT_TASK_TIMEOUT * tasks.size());

}

/**

* 在线程池中执行所有给定的任务并取回运行结果

*

* @param timeout 以毫秒为单位的超时时间,小于0 表示不设定超时

* @seejava.util.concurrent.ExecutorService#invokeAll(java.util.Collection)

*/

publicList<Node> invokeAll(List<ValidationTask> tasks, long timeout) {

List<Node>nodes = new ArrayList<Node>(tasks.size());

try {

List<Future<Node>>futures = null;

if (timeout < 0){

futures =executorService.invokeAll(tasks);

} else {

futures =executorService.invokeAll(tasks, timeout,

TimeUnit.MILLISECONDS);

}

for(Future<Node> future : futures) {

try {

nodes.add(future.get());

} catch(ExecutionException e) {

e.printStackTrace();

}

}

} catch(InterruptedException e) {

e.printStackTrace();

}

return nodes;

}

/**

* 关闭当前ExecutorService

*

* @param timeout 以毫秒为单位的超时时间

*/

public voiddestoryExecutorService(long timeout) {

if (executorService!= null && !executorService.isShutdown()) {

try {

executorService.awaitTermination(timeout,

TimeUnit.MILLISECONDS);

} catch (InterruptedExceptione) {

e.printStackTrace();

}

executorService.shutdown();

}

}

/**

* 关闭当前ExecutorService,随后根据poolSize创建新的ExecutorService

*/

public voidcreateExecutorService() {

destoryExecutorService(1000);

executorService =Executors.newFixedThreadPool(poolSize);

}

/**

* 调整线程池大小

* @see#createExecutorService()

*/

public voidsetPoolSize(int poolSize) {

this.poolSize =poolSize;

createExecutorService();

}

}

这里要额外说明一下invokeAll()和invokeAny()方法。前者会执行给定的所有Callable<T>

对象, 等所有任务完成后返回一个包含了执行结果的List<Future<T>> ,每个

Future.isDone()都是true,可以用Future.get()拿到结果;后者只要完成了列表中的任意

一个任务就立刻返回,返回值就是执行结果。

还有一个比较诡异的地方

本代码是在JDK 1.6 下编译测试的,如果在JDK1.5 下测试,很可能在invokeAll 和invokeAny

的地方出错。明明ValidationTask 实现了Callable<Node>,可是它死活不认,类型不匹配,

这时可以将参数声明由List<ValidationTask>改为List<Callable<Node>>。

造成这个问题的主要原因是两个版本中invokeAll 和invokeAny 的方法签名不同,1.6 里是

invokeAll(Collection<?extends Callable<T>> tasks) , 而1.5 里是

invokeAll(Collection<Callable<T>>tasks)。网上也有人遇到类似的问题(invokeAll() is

not willing toacept a Collection<Callable<T>> )。

和其他资源一样,线程池在使用完毕后也需要释放,用shutdown()方法可以关闭线程池,

如果当时池里还有没有被执行的任务,它会等待任务执行完毕,在等待期间试图进入线程池

的任务将被拒绝。也可以用shutdownNow()来关闭线程池,它会立刻关闭线程池,没有执行

的任务作为返回值返回。

2、Lock

多线程编程中常常要锁定某个对象,之前会用synchronized 来实现,现在又多了另一种选

择,那就是java.util.concurrent.locks。通过Lock 能够实现更灵活的锁定机制,它还提   

供了很多synchronized 所没有的功能,例如尝试获得锁(tryLock())。

使用Lock 时需要自己获得锁并在使用后手动释放,这一点与synchronized 有所不同,所以

通常Lock 的使用方式是这样的:

Java 代码

Lock l = ...;

l.lock();

try {

// 执行操作

} finally {

l.unlock();

}

java.util.concurrent.locks中提供了几个Lock 接口的实现类, 比较常用的应该是

ReentrantLock。以下范例中使用了ReentrantLock 进行节点锁定:

Java 代码

package service;

importjava.util.concurrent.locks.Lock;

importjava.util.concurrent.locks.ReentrantLock;

/**

* 节点类

*

* @authorDigitalSonic

*/

public class Node {

private Stringname;

private Stringwsdl;

private Stringresult = "PASS";

private String[]dependencies = new String[] {};

private Lock lock =new ReentrantLock();

/**

* 默认构造方法

*/

public Node() {

}

/**

* 构造节点对象,设置名称及WSDL

*/

public Node(Stringname, String wsdl) {

this.name = name;

this.wsdl = wsdl;

}

/**

* 返回包含节点名称、WSDL 以及验证结果的字符串

*/

@Override

public StringtoString() {

String toString ="Node: " + name + " WSDL: " + wsdl + " Result: "+ re

sult;

return toString;

}

// Getter &Setter

public StringgetName() {

return name;

}

public voidsetName(String name) {

this.name = name;

}

public StringgetWsdl() {

return wsdl;

}

public voidsetWsdl(String wsdl) {

this.wsdl = wsdl;

}

public StringgetResult() {

return result;

}

public voidsetResult(String result) {

this.result =result;

}

public String[]getDependencies() {

return dependencies;

}

public voidsetDependencies(String[] dependencies) {

this.dependencies =dependencies;

}

public LockgetLock() {

return lock;

}

}

package service;

importjava.util.concurrent.locks.Lock;

importjava.util.concurrent.locks.ReentrantLock;

/**

* 节点类

*

* @authorDigitalSonic

*/

public class Node {

private Stringname;

private Stringwsdl;

private Stringresult = "PASS";

private String[]dependencies = new String[] {};

private Lock lock =new ReentrantLock();

/**

* 默认构造方法

*/

public Node() {

}

/**

* 构造节点对象,设置名称及WSDL

*/

public Node(Stringname, String wsdl) {

this.name = name;

this.wsdl = wsdl;

}

/**

* 返回包含节点名称、WSDL 以及验证结果的字符串

*/

@Override

public StringtoString() {

String toString ="Node: " + name + " WSDL: " + wsdl + " Result: "+ result;

return toString;

}

// Getter &Setter

public StringgetName() {

return name;

}

public voidsetName(String name) {

this.name = name;

}

public StringgetWsdl() {

return wsdl;

}

public voidsetWsdl(String wsdl) {

this.wsdl = wsdl;

}

public StringgetResult() {

return result;

}

public voidsetResult(String result) {

this.result =result;

}

public String[]getDependencies() {

returndependencies;

}

public voidsetDependencies(String[] dependencies) {

this.dependencies =dependencies;

}

public LockgetLock() {

return lock;

}

}

Java 代码

package service;

importjava.util.concurrent.Callable;

importjava.util.concurrent.locks.Lock;

importjava.util.logging.Logger;

importservice.mock.MockNodeValidator;

/**

* 执行验证的任务类

*

* @authorDigitalSonic

*/

public classValidationTask implements Callable<Node> {

private staticLogger logger = Logger.getLogger("ValidationTask");

private Stringwsdl;

/**

* 构造方法,传入节点的WSDL

*/

publicValidationTask(String wsdl) {

this.wsdl = wsdl;

}

/**

* 执行针对某个节点的验证<br/>

* 如果正有别的线程在执行同一节点的验证则等待其结果,不重复执行验证

*/

@Override

public Node call()throws Exception {

Node node =ValidationService.NODE_MAP.get(wsdl);

Lock lock = null;

logger.info("开始验证节点:" + wsdl);

if (node != null) {

lock =node.getLock();

if (lock.tryLock()){

// 当前没有其他线程验证该节点

logger.info(" 当前没有其他线程验证节点

" +node.getName() + "[" + wsdl + "]");

try {

Node result =MockNodeValidator.validateNode(wsdl);

mergeNode(result,node);

} finally {

lock.unlock();

}

} else {

// 当前有别的线程正在验证该节点,等待结果

logger.info(" 当前有别的线程正在验证节点

" +node.getName() + "[" + wsdl + "],等待结果");

lock.lock();

lock.unlock();

}

} else {

// 从未进行过验证,这种情况应该只出现在系统启动初期

// 这时是在做初始化,不应该有冲突发生

logger.info("首次验证节点:" + wsdl);

node =MockNodeValidator.validateNode(wsdl);

ValidationService.NODE_MAP.put(wsdl,node);

}

logger.info("节点" + node.getName() + "[" + wsdl +"]验证结束,验证结果:

" +node.getResult());

return node;

}

/**

* 将src 的内容合并进dest 节点中,不进行深度拷贝

*/

private NodemergeNode(Node src, Node dest) {

dest.setName(src.getName());

dest.setWsdl(src.getWsdl());

dest.setDependencies(src.getDependencies());

dest.setResult(src.getResult());

return dest;

}

}

package service;

importjava.util.concurrent.Callable;

importjava.util.concurrent.locks.Lock;

importjava.util.logging.Logger;

importservice.mock.MockNodeValidator;

/**

* 执行验证的任务类

*

* @authorDigitalSonic

*/

public class ValidationTaskimplements Callable<Node> {

private staticLogger logger = Logger.getLogger("ValidationTask");

private Stringwsdl;

/**

* 构造方法,传入节点的WSDL

*/

publicValidationTask(String wsdl) {

this.wsdl = wsdl;

}

/**

* 执行针对某个节点的验证<br/>

* 如果正有别的线程在执行同一节点的验证则等待其结果,不重复执行验证

*/

@Override

public Node call()throws Exception {

Node node =ValidationService.NODE_MAP.get(wsdl);

Lock lock = null;

logger.info("开始验证节点:" + wsdl);

if (node != null) {

lock =node.getLock();

if (lock.tryLock()){

// 当前没有其他线程验证该节点

logger.info("当前没有其他线程验证节点" + node.getName() + "[" +

wsdl +"]");

try {

Node result =MockNodeValidator.validateNode(wsdl);

mergeNode(result,node);

} finally {

lock.unlock();

}

} else {

// 当前有别的线程正在验证该节点,等待结果

logger.info("当前有别的线程正在验证节点" + node.getName() + "[" +

wsdl + "],等待结果");

lock.lock();

lock.unlock();

}

} else {

// 从未进行过验证,这种情况应该只出现在系统启动初期

// 这时是在做初始化,不应该有冲突发生

logger.info("首次验证节点:" + wsdl);

node =MockNodeValidator.validateNode(wsdl);

ValidationService.NODE_MAP.put(wsdl,node);

}

logger.info("节点" + node.getName() + "[" + wsdl +"]验证结束,验证结果:

" +node.getResult());

return node;

}

/**

* 将src 的内容合并进dest 节点中,不进行深度拷贝

*/

private NodemergeNode(Node src, Node dest) {

dest.setName(src.getName());

dest.setWsdl(src.getWsdl());

dest.setDependencies(src.getDependencies());

dest.setResult(src.getResult());

return dest;

}

}

请注意ValidationTask 的call()方法,这里会先检查节点是否被锁定,如果被锁定则表示

当前有另一个线程正在验证该节点,那就不用重复进行验证。第50 行和第51 行,那到锁后

立即释放,这里只是为了等待验证结束。

讲到Lock,就不能不讲Conditon,前者代替了synchronized,而后者则代替了Object对

象上的wait()、notify()和notifyAll()方法(Condition中提供了await()、signal()和

signalAll()方法),当满足运行条件前挂起线程。Condition 是与Lock 结合使用的,通过

Lock.newCondition()方法能够创建与Lock 绑定的Condition 实例。JDK 的JavaDoc 中有一

个例子能够很好地说明Condition 的用途及用法:

Java 代码

class BoundedBuffer{

final Lock lock =new ReentrantLock();

final Condition notFull= lock.newCondition();

final ConditionnotEmpty = lock.newCondition();

final Object[]items = new Object[100];

int putptr,takeptr, count;

public voidput(Object x) throws InterruptedException {

lock.lock();

try {

while (count ==items.length)

notFull.await();

items[putptr] = x;

if (++putptr ==items.length) putptr = 0;

++count;

notEmpty.signal();

} finally {

lock.unlock();

}

}

public Objecttake() throws InterruptedException {

lock.lock();

try {

while (count == 0)

notEmpty.await();

Object x = items[takeptr];

if (++takeptr ==items.length) takeptr = 0;

--count;

notFull.signal();

return x;

} finally {

lock.unlock();

}

}

}

class BoundedBuffer{

final Lock lock =new ReentrantLock();

final ConditionnotFull = lock.newCondition();

final ConditionnotEmpty = lock.newCondition();

final Object[]items = new Object[100];

int putptr,takeptr, count;

public voidput(Object x) throws InterruptedException {

lock.lock();

try {

while (count ==items.length)

notFull.await();

items[putptr] = x;

if (++putptr ==items.length) putptr = 0;

++count;

notEmpty.signal();

} finally {

lock.unlock();

}

}

public Objecttake() throws InterruptedException {

lock.lock();

try {

while (count == 0)

notEmpty.await();

Object x =items[takeptr];

if (++takeptr ==items.length) takeptr = 0;

--count;

notFull.signal();

return x;

} finally {

lock.unlock();

}

}

}

说到这里,让我解释一下之前的例子里为什么没有选择Condition 来等待验证结束。await()

方法在调用时当前线程先要获得对应的锁,既然我都拿到锁了,那也就是说验证已经结束

了。。。

3、并发集合类

集合类是大家编程时经常要使用的东西,ArrayList、HashMap 什么的,java.util 包中的集

合类有的是线程安全的,有的则不是,在编写多线程的程序时使用线程安全的类能省去很多

麻烦,但这些类的性能如何呢?java.util.concurrent 包中提供了几个并发结合类,例如

ConcurrentHashMap、ConcurrentLinkedQueue 和CopyOnWriteArrayList等等,根据不同的

使用场景,开发者可以用它们替换java.util 包中的相应集合类。

CopyOnWriteArrayList是ArrayList 的一个变体,比较适合用在读取比较频繁、修改较少

的情况下,因为每次修改都要复制整个底层数组。ConcurrentHashMap 中为Map 接口增加了

一些方法(例如putIfAbsenct()),同时做了些优化,总之灰常之好用,下面的代码中使用

ConcurrentHashMap 来作为全局节点表,完全无需考虑并发问题。ValidationService 中只

是声明(第17 行),具体的使用是在上面的ValidationTask 中。

Java 代码

package service;

importjava.util.ArrayList;

importjava.util.List;

importjava.util.Map;

importjava.util.concurrent.ConcurrentHashMap;

/**

* 执行验证的服务类

*

* @authorDigitalSonic

*/

public classValidationService {

/**

* 全局节点表

*/

public static finalMap<String, Node> NODE_MAP = new ConcurrentHashMap<Stri

ng, Node>();

privateThreadPoolService threadPoolService;

publicValidationService(ThreadPoolService threadPoolService) {

this.threadPoolService= threadPoolService;

}

/**

* 给出一个入口节点的WSDL,通过广度遍历的方式验证与其相关的各个节点

*

* @param wsdl 入口节点WSDL

*/

public voidvalidate(List<String> wsdl) {

List<String>visitedNodes = new ArrayList<String>();

List<String>nextRoundNodes = new ArrayList<String>();

nextRoundNodes.addAll(wsdl);

while(nextRoundNodes.size() > 0) {

List<ValidationTask>tasks = getTasks(nextRoundNodes);

List<Node>nodes = threadPoolService.invokeAll(tasks);

visitedNodes.addAll(nextRoundNodes);

nextRoundNodes.clear();

getNextRoundNodes(nodes,visitedNodes, nextRoundNodes);

}

}

privateList<String> getNextRoundNodes(List<Node> nodes,

List<String>visitedNodes, List<String> nextRoundNodes) {

for (Node node :nodes) {

for (String wsdl :node.getDependencies()) {

if(!visitedNodes.contains(wsdl)) {

nextRoundNodes.add(wsdl);

}

}

}

returnnextRoundNodes;

}

privateList<ValidationTask> getTasks(List<String> nodes) {

List<ValidationTask>tasks = new ArrayList<ValidationTask>(nodes.size())

;

for (String wsdl :nodes) {

tasks.add(newValidationTask(wsdl));

}

return tasks;

}

}

package service;

importjava.util.ArrayList;

importjava.util.List;

importjava.util.Map;

importjava.util.concurrent.ConcurrentHashMap;

/**

* 执行验证的服务类

*

* @authorDigitalSonic

*/

public classValidationService {

/**

* 全局节点表

*/

public static finalMap<String, Node> NODE_MAP = new ConcurrentHashMap<String,

Node>();

privateThreadPoolService threadPoolService;

publicValidationService(ThreadPoolService threadPoolService) {

this.threadPoolService= threadPoolService;

}

/**

* 给出一个入口节点的WSDL,通过广度遍历的方式验证与其相关的各个节点

*

* @param wsdl 入口节点WSDL

*/

public voidvalidate(List<String> wsdl) {

List<String>visitedNodes = new ArrayList<String>();

List<String>nextRoundNodes = new ArrayList<String>();

nextRoundNodes.addAll(wsdl);

while(nextRoundNodes.size() > 0) {

List<ValidationTask>tasks = getTasks(nextRoundNodes);

List<Node>nodes = threadPoolService.invokeAll(tasks);

visitedNodes.addAll(nextRoundNodes);

nextRoundNodes.clear();

getNextRoundNodes(nodes,visitedNodes, nextRoundNodes);

}

}

privateList<String> getNextRoundNodes(List<Node> nodes,

List<String>visitedNodes, List<String> nextRoundNodes) {

for (Node node :nodes) {

for (String wsdl :node.getDependencies()) {

if(!visitedNodes.contains(wsdl)) {

nextRoundNodes.add(wsdl);

}

}

}

returnnextRoundNodes;

}

privateList<ValidationTask> getTasks(List<String> nodes) {

List<ValidationTask>tasks = new ArrayList<ValidationTask>(nodes.size());

for (String wsdl :nodes) {

tasks.add(newValidationTask(wsdl));

}

return tasks;

}

}

4、AtomicInteger

对变量的读写操作都是原子操作(除了long 或者double 的变量),但像数值类型的++ --

操作不是原子操作,像i++中包含了获得i 的原始值、加1、写回i、返回原始值,在进行

类似i++这样的操作时如果不进行同步问题就大了。好在java.util.concurrent.atomic 为

我们提供了很多工具类,可以以原子方式更新变量。

以AtomicInteger 为例,提供了代替++--的getAndIncrement()、incrementAndGet()、

getAndDecrement()和decrementAndGet()方法,还有加减给定值的方法、当前值等于预期

值时更新的compareAndSet()方法。

下面的例子中用AtomicInteger 保存全局验证次数(第69 行做了自增的操作),因为

validateNode()方法会同时被多个线程调用,所以直接用int 不同步是不行的,但用

AtomicInteger 在这种场合下就很合适。

Java 代码

packageservice.mock;

importjava.util.ArrayList;

importjava.util.HashMap;

importjava.util.List;

importjava.util.Map;

importjava.util.concurrent.atomic.AtomicInteger;

importjava.util.logging.Logger;

importservice.Node;

/**

* 模拟执行节点验证的Mock 类

*

* @authorDigitalSonic

*/

public classMockNodeValidator {

public static finalList<Node> ENTRIES = new ArrayList<Node>();

private staticfinal Map<String, Node> NODE_MAP = new HashMap<String, Node>

();

private staticAtomicInteger count = new AtomicInteger(0);

private staticLogger logger = Logger.getLogger("MockNod

eValidator");

/*

* 构造模拟数据

*/

static {

Node node0 = newNode("NODE0", "http://node0/check?wsdl"); //入口0

Node node1 = newNode("NODE1", "http://node1/check?wsdl");

Node node2 = newNode("NODE2", "http://node2/check?wsdl");

Node node3 = newNode("NODE3", "http://node3/check?wsdl");

Node node4 = newNode("NODE4", "http://node4/check?wsdl");

Node node5 = newNode("NODE5", "http://node5/check?wsdl");

Node node6 = newNode("NODE6", "http://node6/check?wsdl"); //入口1

Node node7 = newNode("NODE7", "http://node7/check?wsdl");

Node node8 = newNode("NODE8", "http://node8/check?wsdl");

Node node9 = newNode("NODE9", "http://node9/check?wsdl");

node0.setDependencies(newString[] { node1.getWsdl(), node2.getWsdl() })

;

node1.setDependencies(newString[] { node3.getWsdl(), node4.getWsdl() })

;

node2.setDependencies(newString[] { node5.getWsdl() });

node6.setDependencies(newString[] { node7.getWsdl(), node8.getWsdl() })

;

node7.setDependencies(newString[] { node5.getWsdl(), node9.getWsdl() })

;

node8.setDependencies(newString[] { node3.getWsdl(), node4.getWsdl() })

;

node2.setResult("FAILED");

NODE_MAP.put(node0.getWsdl(),node0);

NODE_MAP.put(node1.getWsdl(),node1);

NODE_MAP.put(node2.getWsdl(),node2);

NODE_MAP.put(node3.getWsdl(),node3);

NODE_MAP.put(node4.getWsdl(),node4);

NODE_MAP.put(node5.getWsdl(),node5);

NODE_MAP.put(node6.getWsdl(),node6);

NODE_MAP.put(node7.getWsdl(),node7);

NODE_MAP.put(node8.getWsdl(),node8);

NODE_MAP.put(node9.getWsdl(),node9);

ENTRIES.add(node0);

ENTRIES.add(node6);

}

/**

* 模拟执行远程验证返回节点,每次调用等待500ms

*/

public static NodevalidateNode(String wsdl) {

Node node =cloneNode(NODE_MAP.get(wsdl));

logger.info(" 验证节点

" +node.getName() + "[" + node.getWsdl() + "]");

count.getAndIncrement();

try {

Thread.sleep(500);

} catch (InterruptedExceptione) {

e.printStackTrace();

}

return node;

}

/**

* 获得计数器的值

*/

public static intgetCount() {

returncount.intValue();

}

/**

* 克隆一个新的Node 对象(未执行深度克隆)

*/

public static NodecloneNode(Node originalNode) {

Node newNode = newNode();

newNode.setName(originalNode.getName());

newNode.setWsdl(originalNode.getWsdl());

newNode.setResult(originalNode.getResult());

newNode.setDependencies(originalNode.getDependencies());

return newNode;

}

}

packageservice.mock;

importjava.util.ArrayList;

importjava.util.HashMap;

importjava.util.List;

importjava.util.Map;

importjava.util.concurrent.atomic.AtomicInteger;

importjava.util.logging.Logger;

importservice.Node;

/**

* 模拟执行节点验证的Mock 类

*

* @authorDigitalSonic

*/

public classMockNodeValidator {

public static finalList<Node> ENTRIES = new ArrayList<Node>();

private staticfinal Map<String, Node> NODE_MAP = new HashMap<String, Node>();

private staticAtomicInteger count = new AtomicInteger(0);

private staticLogger logger =

Logger.getLogger("MockNodeValidator");

/*

* 构造模拟数据

*/

static {

Node node0 = newNode("NODE0", "http://node0/check?wsdl"); //入口0

Node node1 = newNode("NODE1", "http://node1/check?wsdl");

Node node2 = newNode("NODE2", "http://node2/check?wsdl");

Node node3 = newNode("NODE3", "http://node3/check?wsdl");

Node node4 = newNode("NODE4", "http://node4/check?wsdl");

Node node5 = newNode("NODE5", "http://node5/check?wsdl");

Node node6 = newNode("NODE6", "http://node6/check?wsdl"); //入口1

Node node7 = newNode("NODE7", "http://node7/check?wsdl");

Node node8 = newNode("NODE8", "http://node8/check?wsdl");

Node node9 = newNode("NODE9", "http://node9/check?wsdl");

node0.setDependencies(newString[] { node1.getWsdl(), node2.getWsdl() });

node1.setDependencies(newString[] { node3.getWsdl(), node4.getWsdl() });

node2.setDependencies(newString[] { node5.getWsdl() });

node6.setDependencies(newString[] { node7.getWsdl(), node8.getWsdl() });

node7.setDependencies(newString[] { node5.getWsdl(), node9.getWsdl() });

node8.setDependencies(newString[] { node3.getWsdl(), node4.getWsdl() });

node2.setResult("FAILED");

NODE_MAP.put(node0.getWsdl(),node0);

NODE_MAP.put(node1.getWsdl(),node1);

NODE_MAP.put(node2.getWsdl(),node2);

NODE_MAP.put(node3.getWsdl(),node3);

NODE_MAP.put(node4.getWsdl(),node4);

NODE_MAP.put(node5.getWsdl(),node5);

NODE_MAP.put(node6.getWsdl(),node6);

NODE_MAP.put(node7.getWsdl(),node7);

NODE_MAP.put(node8.getWsdl(),node8);

NODE_MAP.put(node9.getWsdl(),node9);

ENTRIES.add(node0);

ENTRIES.add(node6);

}

/**

* 模拟执行远程验证返回节点,每次调用等待500ms

*/

public static NodevalidateNode(String wsdl) {

Node node =cloneNode(NODE_MAP.get(wsdl));

logger.info("验证节点" + node.getName() + "[" +node.getWsdl() + "]");

count.getAndIncrement();

try {

Thread.sleep(500);

} catch(InterruptedException e) {

e.printStackTrace();

}

return node;

}

/**

* 获得计数器的值

*/

public static intgetCount() {

returncount.intValue();

}

/**

* 克隆一个新的Node 对象(未执行深度克隆)

*/

public static NodecloneNode(Node originalNode) {

Node newNode = newNode();

newNode.setName(originalNode.getName());

newNode.setWsdl(originalNode.getWsdl());

newNode.setResult(originalNode.getResult());

newNode.setDependencies(originalNode.getDependencies());

return newNode;

}

}

上述代码还有另一个功能,就是构造测试用的节点数据,一共10 个节点,有2 个入口点,

通过这两个点能够遍历整个系统。每次调用会模拟远程访问,等待500ms。环境间节点依赖

如下:

环境依赖

Node0 [Node1,Node2]

Node1 [Node3,Node4]

Node2 [Node5]

Node6 [Node7,Node8]

Node7 [Node5,Node9]

Node8 [Node3,Node4]

 

5、CountDownLatch

CountDownLatch 是一个一次性的同步辅助工具,允许一个或多个线程一直等待,直到计数

器值变为0。它有一个构造方法,设定计数器初始值,即在await()结束等待前需要调用多

少次countDown()方法。CountDownLatch的计数器不能重置,所以说它是“一次性”的,如果需要重置计数器, 可以使用CyclicBarrier 。在运行环境检查的主类中, 使用了

CountDownLatch 来等待所有验证结束,在各个并发验证的线程完成任务结束前都会调用

countDown(),因为有3 个并发的验证,所以将计数器设置为3。

最后将所有这些类整合起来,运行环境检查的主类如下。它会创建线程池服务和验证服务,

先做一次验证(相当于是对系统做次初始化),随后并发3 个验证请求。系统运行完毕会显示

实际执行的节点验证次数和执行时间。如果是顺序执行,验证次数应该是13*4=52,但实际

的验证次数会少于这个数字(我这里最近一次执行了33次验证),因为如果同时有两个线程

要验证同一节点时只会做一次验证。关于时间,如果是顺序执行,52 次验证每次等待500ms,

那么验证所耗费的时间应该是26000ms,使用了多线程后的实际耗时远小于该数字(最近一

次执行耗时4031ms)。

Java 代码

packageservice.mock;

importjava.util.ArrayList;

importjava.util.List;

importjava.util.concurrent.CountDownLatch;

importservice.Node;

importservice.ThreadPoolService;

importservice.ValidationService;

/**

* 模拟执行这个环境的验证

*

* @authorDigitalSonic

*/

public classValidationStarter implements Runnable {

privateList<String> entries;

privateValidationService validationService;

privateCountDownLatch signal;

publicValidationStarter(

List<String>entries,

ValidationServicevalidation

Service,

CountDownLatchsignal)

{

this.entries =entries;

this.validationService= validationService;

this.signal =signal;

}

/**

* 线程池大小为10,初始化执行一次,随后并发三个验证

*/

public static voidmain(String[] args) {

ThreadPoolServicethreadPoolService = new ThreadPoolService(10);

ValidationServicevalidationService = new ValidationService(threadPoolS

ervice);

List<String>entries = new ArrayList<String>();

CountDownLatchsignal = new CountDownLatch(3);

long start;

long stop;

for (Node node :MockNodeValidator.ENTRIES) {

entries.add(node.getWsdl());

}

start =System.currentTimeMillis();

validationService.validate(entries);

threadPoolService.execute(newValidationStarter(entries, validationServ

ice, signal));

threadPoolService.execute(newValidationStarter(entries, validationServ

ice, signal));

threadPoolService.execute(newValidationStarter(entries, validationServ

ice, signal));

try {

signal.await();

} catch(InterruptedException e) {

e.printStackTrace();

}

stop =System.currentTimeMillis();

threadPoolService.destoryExecutorService(1000);

System.out.println("实际执行验证次

数: " + MockNodeValidator.getCount());

System.out.println("实际执行时间: " + (stop - start) + "ms");

}

@Override

public void run() {

validationService.validate(entries);

signal.countDown();

}

}

 

java5多线程新特性

在Java 5.0之前Java里的多线程编程主要是通过Thread类,Runnable接口,Object对象中的wait()、 notify()、notifyAll()等方法和synchronized关键词来实现的。这些工具虽然能在大多数情况下解决对共享资源的管理和线程间的调度,但存在以下几个问题

1.     过于原始,拿来就能用的功能有限,即使是要实现简单的多线程功能也需要编写大量的代码。这些工具就像汇编语言一样难以学习和使用,比这更糟糕的是稍有不慎它们还可能被错误地使用,而且这样的错误很难被发现。

2.     如果使用不当,会使程序的运行效率大大降低。

3.     为了提高开发效率,简化编程,开发人员在做项目的时候往往需要写一些共享的工具来实现一些普遍适用的功能。但因为没有规范,相同的工具会被重复地开发,造成资源浪费。

4.     因为锁定的功能是通过Synchronized来实现的,这是一种块结构,只能对代码中的一段代码进行锁定,而且锁定是单一的。如以下代码所示:

synchronized(lock){

    //执行对共享资源的操作

    ……

}

    一些复杂的功能就很难被实现。比如说如果程序需要取得lock A和lock B来进行操作1,然后需要取得lock C并且释放lock A来进行操作2,Java 5.0之前的多线程框架就显得无能为力了。

   因为这些问题,程序员对旧的框架一直颇有微词。这种情况一直到Java 5.0才有较大的改观,一系列的多线程工具包被纳入了标准库文件。这些工具包括了一个新的多线程程序的执行框架,使编程人员可方便地协调和调度线程的运行,并且新加入了一些高性能的常用的工具,使程序更容易编写,运行效率更高。本文将分类并结合例子来介绍这些新加的多线程工具。

   在我们开始介绍Java 5.0里的新Concurrent工具前让我们先来看一下一个用旧的多线程工具编写的程序,这个程序里有一个Server线程,它需要启动两个 Component,Server线程需等到Component线程完毕后再继续。相同的功能在Synchronizer一章里用新加的工具 CountDownLatch有相同的实现。两个程序,孰优孰劣,哪个程序更容易编写,哪个程序更容易理解,相信大家看过之后不难得出结论。

public class ServerThread {

      Object concLock = new Object();

      int count = 2;

public void runTwoThreads() {

      //启动两个线程去初始化组件

            new Thread(new ComponentThread1(this)).start();

            new Thread(new ComponentThread1(this)).start();

            // Wait for other thread

while(count != 0) {

                  synchronized(concLock) {

                        try {

                              concLock.wait();

                              System.out.println("Wake up.");

                        } catch (InterruptedException ie) { //处理异常}

                  }

            }

            System.out.println("Server is up.");

      }

      public void callBack() {

synchronized(concLock) {

                  count--;

                  concLock.notifyAll();

            }

      }

      public static void main(String[] args){

            ServerThread server = new ServerThread();

            server.runTwoThreads();

      }

}

 

public class ComponentThread1 implements Runnable {

      private ServerThread server;

      public ComponentThread1(ServerThread server) {

            this.server = server;

      }

public void run() {

      //做组件初始化的工作

            System.out.println("Do component initialization.");

            server.callBack();

      }

}

1:三个新加的多线程包

   Java5.0里新加入了三个多线程包:java.util.concurrent,java.util.concurrent.atomic, java.util.concurrent.locks.

  • java.util.concurrent包含了常用的多线程工具,是新的多线程工具的主体。
  • java.util.concurrent.atomic 包含了不用加锁情况下就能改变值的原子变量,比如说AtomicInteger提供了addAndGet()方法。Add和Get是两个不同的操作,为了保证别的线程不干扰,以往的做法是先锁定共享的变量,然后在锁定的范围内进行两步操作。但用AtomicInteger.addAndGet()就不用担心锁定的事了,其内部实现保证了这两步操作是在原子量级发生的,不会被别的线程干扰。
  • java.util.concurrent.locks包包含锁定的工具。

2:Callable 和Future接口

  Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。Callable和Runnable有几点不同:

  • Callable规定的方法是call(),而Runnable规定的方法是run().
  • Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
  • call()方法可抛出异常,而run()方法是不能抛出异常的。
  • 运行Callable任务可拿到一个Future对象,通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。

以下是Callable的一个例子:

public class DoCallStuff implements Callable{ // *1

        private int aInt;

        public DoCallStuff(int aInt) {

                this.aInt = aInt;

        }

        public String call() throws Exception { //*2

                boolean resultOk = false;

                if(aInt == 0){

                        resultOk = true;

                } else if(aInt == 1){

                        while(true){ //infinite loop

                                System.out.println("looping....");

                                Thread.sleep(3000);

                        }

                } else {

                        throw new Exception("Callable terminated with Exception!"); //*3

                }

                if(resultOk){

                        return "Task done.";

                } else {

                        return "Task failed";

                }

        }

}

*1: 名为DoCallStuff类实现了Callable,String将是call方法的返回值类型。例子中用了String,但可以是任何Java类。

*2: call方法的返回值类型为String,这是和类的定义相对应的。并且可以抛出异常。

*3: call方法可以抛出异常,如加重的斜体字所示。

以下是调用DoCallStuff的主程序。

import java.util.concurrent.ExecutionException;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.Future;

public class Executor {

        public static void main(String[] args){

                //*1

                DoCallStuff call1 = new DoCallStuff(0);

                DoCallStuff call2 = new DoCallStuff(1);

                DoCallStuff call3 = new DoCallStuff(2);

                //*2

                ExecutorService es = Executors.newFixedThreadPool(3);

                //*3

                Future future1 = es.submit(call1);

                Future future2 = es.submit(call2);

                Future future3 = es.submit(call3);

                try {

                        //*4

                        System.out.println(future1.get());

                         //*5

                        Thread.sleep(3000);

                        System.out.println("Thread 2 terminated? :" + future2.cancel(true));

                        //*6

                        System.out.println(future3.get());

                } catch (ExecutionException ex) {

                        ex.printStackTrace();

                } catch (InterruptedException ex) {

                        ex.printStackTrace();

                }

        }

}

 

*1: 定义了几个任务

*2: 初始了任务执行工具。任务的执行框架将会在后面解释。

*3: 执行任务,任务启动时返回了一个Future对象,如果想得到任务执行的结果或者是异常可对这个Future对象进行操作。Future所含的值必须跟Callable所含的值对映,比如说例子中Future对印Callable

*4: 任务1正常执行完毕,future1.get()会返回线程的值

*5: 任务2在进行一个死循环,调用future2.cancel(true)来中止此线程。传入的参数标明是否可打断线程,true表明可以打断。

*6: 任务3抛出异常,调用future3.get()时会引起异常的抛出。

 运行Executor会有以下运行结果:

looping....

Task done. //*1

looping....

looping....//*2

looping....

looping....

looping....

looping....

Thread 2 terminated? :true //*3

//*4

java.util.concurrent.ExecutionException: java.lang.Exception: Callable terminated with Exception!

        at java.util.concurrent.FutureTask$Sync.innerGet(FutureTask.java:205)

        at java.util.concurrent.FutureTask.get(FutureTask.java:80)

        at concurrent.Executor.main(Executor.java:43)

        …….

*1: 任务1正常结束

*2: 任务2是个死循环,这是它的打印结果

*3: 指示任务2被取消

*4: 在执行future3.get()时得到任务3抛出的异常

3:新的任务执行架构

   在Java 5.0之前启动一个任务是通过调用Thread类的start()方法来实现的,任务的提于交和执行是同时进行的,如果你想对任务的执行进行调度或是控制同时执行的线程数量就需要额外编写代码来完成。5.0里提供了一个新的任务执行架构使你可以轻松地调度和控制任务的执行,并且可以建立一个类似数据库连接池的线程池来执行任务。这个架构主要有三个接口和其相应的具体类组成。这三个接口是Executor, ExecutorService和ScheduledExecutorService,让我们先用一个图来显示它们的关系:

 

  图的左侧是接口,图的右侧是这些接口的具体类。注意Executor是没有直接具体实现的。

Executor接口:

是用来执行Runnable任务的,它只定义一个方法:

  • execute(Runnable command):执行Ruannable类型的任务

ExecutorService接口:

ExecutorService继承了Executor的方法,并提供了执行Callable任务和中止任务执行的服务,其定义的方法主要有:

  • submit(task):可用来提交Callable或Runnable任务,并返回代表此任务的Future对象
  • invokeAll(collection of tasks):批处理任务集合,并返回一个代表这些任务的Future对象集合
  • shutdown():在完成已提交的任务后关闭服务,不再接受新任务
  • shutdownNow():停止所有正在执行的任务并关闭服务。
  • isTerminated():测试是否所有任务都执行完毕了。
  • isShutdown():测试是否该ExecutorService已被关闭

ScheduledExecutorService接口

在ExecutorService的基础上,ScheduledExecutorService提供了按时间安排执行任务的功能,它提供的方法主要有:

  • schedule(task, initDelay): 安排所提交的Callable或Runnable任务在initDelay指定的时间后执行。
  • scheduleAtFixedRate():安排所提交的Runnable任务按指定的间隔重复执行
  • scheduleWithFixedDelay():安排所提交的Runnable任务在每次执行完后,等待delay所指定的时间后重复执行。

代码:ScheduleExecutorService的例子

public class ScheduledExecutorServiceTest {

        public static void main(String[] args)

               throws InterruptedException, ExecutionException{

               //*1

                ScheduledExecutorService service = Executors.newScheduledThreadPool(2);

                //*2

                Runnable task1 = new Runnable() {

                     public void run() {

                        System.out.println("Task repeating.");

                     }

                };

                //*3

                final ScheduledFuture future1 =

                        service.scheduleAtFixedRate(task1, 0, 1, TimeUnit.SECONDS);

                //*4

                ScheduledFuture future2 = service.schedule(new Callable(){

                     public String call(){

                             future1.cancel(true);

                             return "task cancelled!";

                     }

                }, 5, TimeUnit.SECONDS);

                System.out.println(future2.get());

//*5

service.shutdown();

        }

}

   这个例子有两个任务,第一个任务每隔一秒打印一句“Task repeating”,第二个任务在5秒钟后取消第一个任务。

*1: 初始化一个ScheduledExecutorService对象,这个对象的线程池大小为2。

*2: 用内函数的方式定义了一个Runnable任务。

*3: 调用所定义的ScheduledExecutorService对象来执行任务,任务每秒执行一次。能重复执行的任务一定是Runnable类型。注意我们可以用TimeUnit来制定时间单位,这也是Java5.0里新的特征,5.0以前的记时单位是微秒,现在可精确到奈秒。

*4: 调用ScheduledExecutorService对象来执行第二个任务,第二个任务所作的就是在5秒钟后取消第一个任务。

*5: 关闭服务。

Executors类

   虽然以上提到的接口有其实现的具体类,但为了方便Java 5.0建议使用Executors的工具类来得到Executor接口的具体对象,需要注意的是Executors是一个类,不是Executor的复数形式。Executors提供了以下一些static的方法:

  • callable(Runnable task): 将Runnable的任务转化成Callable的任务
  • newSingleThreadExecutor: 产生一个ExecutorService对象,这个对象只有一个线程可用来执行任务,若任务多于一个,任务将按先后顺序执行。
  • newCachedThreadPool(): 产生一个ExecutorService对象,这个对象带有一个线程池,线程池的大小会根据需要调整,线程执行完任务后返回线程池,供执行下一次任务使用。
  • newFixedThreadPool(int poolSize):产生一个ExecutorService对象,这个对象带有一个大小为poolSize的线程池,若任务数量大于poolSize,任务会被放在一个queue里顺序执行。
  • newSingleThreadScheduledExecutor:产生一个ScheduledExecutorService对象,这个对象的线程池大小为1,若任务多于一个,任务将按先后顺序执行。
  • newScheduledThreadPool(int poolSize): 产生一个ScheduledExecutorService对象,这个对象的线程池大小为poolSize,若任务数量大于poolSize,任务会在一个queue里等待执行

以下是得到和使用ExecutorService的例子:

代码:如何调用Executors来获得各种服务对象

//Single Threaded ExecutorService

     ExecutorService singleThreadeService = Executors.newSingleThreadExecutor();

//Cached ExecutorService

     ExecutorService cachedService = Executors.newCachedThreadPool();

//Fixed number of ExecutorService

     ExecutorService fixedService = Executors.newFixedThreadPool(3);

//Single ScheduledExecutorService

     ScheduledExecutorService singleScheduledService =

          Executors.newSingleThreadScheduledExecutor();

//Fixed number of ScheduledExecutorService

ScheduledExecutorService fixedScheduledService =

     Executors.newScheduledThreadPool(3);

4:Lockers和Condition接口

   在多线程编程里面一个重要的概念是锁定,如果一个资源是多个线程共享的,为了保证数据的完整性,在进行事务性操作时需要将共享资源锁定,这样可以保证在做事务性操作时只有一个线程能对资源进行操作,从而保证数据的完整性。在5.0以前,锁定的功能是由Synchronized关键字来实现的,这样做存在几个问题:

  • 每次只能对一个对象进行锁定。若需要锁定多个对象,编程就比较麻烦,一不小心就会出现死锁现象。
  • 如果线程因拿不到锁定而进入等待状况,是没有办法将其打断的

在Java 5.0里出现两种锁的工具可供使用,下图是这两个工具的接口及其实现:

Lock接口

ReentrantLock是Lock的具体类,Lock提供了以下一些方法:

  • lock(): 请求锁定,如果锁已被别的线程锁定,调用此方法的线程被阻断进入等待状态。
  • tryLock():如果锁没被别的线程锁定,进入锁定状态,并返回true。若锁已被锁定,返回false,不进入等待状态。此方法还可带时间参数,如果锁在方法执行时已被锁定,线程将继续等待规定的时间,若还不行才返回false。
  • unlock():取消锁定,需要注意的是Lock不会自动取消,编程时必须手动解锁。

代码:

//生成一个锁

Lock lock = new ReentrantLock();

public void accessProtectedResource() {

 lock.lock(); //取得锁定

 try {

    //对共享资源进行操作

 } finally {

    //一定记着把锁取消掉,锁本身是不会自动解锁的

    lock.unlock();

 }

}

ReadWriteLock接口

   为了提高效率有些共享资源允许同时进行多个读的操作,但只允许一个写的操作,比如一个文件,只要其内容不变可以让多个线程同时读,不必做排他的锁定,排他的锁定只有在写的时候需要,以保证别的线程不会看到数据不完整的文件。ReadWriteLock可满足这种需要。ReadWriteLock内置两个 Lock,一个是读的Lock,一个是写的Lock。多个线程可同时得到读的Lock,但只有一个线程能得到写的Lock,而且写的Lock被锁定后,任何线程都不能得到Lock。ReadWriteLock提供的方法有:

  • readLock(): 返回一个读的lock
  • writeLock(): 返回一个写的lock, 此lock是排他的。

ReadWriteLock的例子:

public class FileOperator{

      //初始化一个ReadWriteLock

      ReadWriteLock lock = new ReentrantReadWriteLock();

public String read() {

      //得到readLock并锁定

            Lock readLock = lock.readLock();

            readLock.lock();

            try {

                  //做读的工作

                  return "Read something";

            } finally {

                 readLock.unlock();

            }

      }

     

public void write(String content) {

      //得到writeLock并锁定

            Lock writeLock = lock.writeLock();

            writeLock.lock();

            try {

                  //做读的工作

            } finally {

                 writeLock.unlock();

            }

      }

}

 

   需要注意的是ReadWriteLock提供了一个高效的锁定机理,但最终程序的运行效率是和程序的设计息息相关的,比如说如果读的线程和写的线程同时在等待,要考虑是先发放读的lock还是先发放写的lock。如果写发生的频率不高,而且快,可以考虑先给写的lock。还要考虑的问题是如果一个写正在等待读完成,此时一个新的读进来,是否要给这个新的读发锁,如果发了,可能导致写的线程等很久。等等此类问题在编程时都要给予充分的考虑。

Condition接口:

   有时候线程取得lock后需要在一定条件下才能做某些工作,比如说经典的Producer和Consumer问题,Consumer必须在篮子里有苹果的时候才能吃苹果,否则它必须暂时放弃对篮子的锁定,等到Producer往篮子里放了苹果后再去拿来吃。而Producer必须等到篮子空了才能往里放苹果,否则它也需要暂时解锁等Consumer把苹果吃了才能往篮子里放苹果。在Java 5.0以前,这种功能是由Object类的wait(),notify()和notifyAll()等方法实现的,在5.0里面,这些功能集中到了Condition这个接口来实现,Condition提供以下方法:

  • await():使调用此方法的线程放弃锁定,进入睡眠直到被打断或被唤醒。
  • signal(): 唤醒一个等待的线程
  • signalAll():唤醒所有等待的线程

Condition的例子:

public class Basket {     

Lock lock = new ReentrantLock();

//产生Condition对象

     Condition produced = lock.newCondition();

     Condition consumed = lock.newCondition();

     boolean available = false;

     

     public void produce() throws InterruptedException {

           lock.lock();

           try {

                 if(available){

                    consumed.await(); //放弃lock进入睡眠 

                 }

                 /*生产苹果*/

                 System.out.println("Apple produced.");

                 available = true;

                 produced.signal(); //发信号唤醒等待这个Condition的线程

           } finally {

                 lock.unlock();

           }

     }

    

     public void consume() throws InterruptedException {

           lock.lock();

           try {

                 if(!available){

                       produced.await();//放弃lock进入睡眠 

                 }

                 /*吃苹果*/

                 System.out.println("Apple consumed.");

                 available = false;

                 consumed.signal();//发信号唤醒等待这个Condition的线程

           } finally {

                 lock.unlock();

           }

     }     

}

ConditionTester:

public class ConditionTester {

     

      public static void main(String[] args) throws InterruptedException{

final Basket basket = new Basket();

//定义一个producer

            Runnable producer = new Runnable() {

                  public void run() {

                        try {

                              basket.produce();

                        } catch (InterruptedException ex) {

                              ex.printStackTrace();

                        }

                  }

};

//定义一个consumer

            Runnable consumer = new Runnable() {

                  public void run() {

                        try {

                              basket.consume();

                        } catch (InterruptedException ex) {

                              ex.printStackTrace();

                        }

                  }

};

//各产生10个consumer和producer

            ExecutorService service = Executors.newCachedThreadPool();

            for(int i=0; i < 10; i++)

                  service.submit(consumer);

            Thread.sleep(2000);

            for(int i=0; i<10; i++)

                  service.submit(producer);

            service.shutdown();

      }     

}

5: Synchronizer:同步装置

   Java5.0里新加了4个协调线程间进程的同步装置,它们分别是Semaphore, CountDownLatch, CyclicBarrier和Exchanger.

Semaphore:

   用来管理一个资源池的工具,Semaphore可以看成是个通行证,线程要想从资源池拿到资源必须先拿到通行证,Semaphore提供的通行证数量和资源池的大小一致。如果线程暂时拿不到通行证,线程就会被阻断进入等待状态。以下是一个例子:

public class Pool {

      ArrayList pool = null;

      Semaphore pass = null;

      public Pool(int size){

            //初始化资源池

            pool = new ArrayList();

            for(int i=0; i

                  pool.add("Resource "+i);

            }

            //Semaphore的大小和资源池的大小一致

            pass = new Semaphore(size);

      }

      public String get() throws InterruptedException{

            //获取通行证,只有得到通行证后才能得到资源

            pass.acquire();

            return getResource();

      }

      public void put(String resource){

            //归还通行证,并归还资源

            pass.release();

            releaseResource(resource);

      }

     private synchronized String getResource() {

            String result = pool.get(0);

            pool.remove(0);

            System.out.println("Give out "+result);

            return result;

      }

      private synchronized void releaseResource(String resource) {

            System.out.println("return "+resource);

            pool.add(resource);

      }

}

SemaphoreTest:

public class SemaphoreTest {

      public static void main(String[] args){

            final Pool aPool = new Pool(2);

            Runnable worker = new Runnable() {

                  public void run() {

                        String resource = null;

                        try {

                              //取得resource

                              resource = aPool.get();

                        } catch (InterruptedException ex) {

                              ex.printStackTrace();

                        }

                        //用resource做工作

                        System.out.println("I worked on "+resource);

                        //归还resource

                        aPool.put(resource);

                  }

            };

            ExecutorService service = Executors.newCachedThreadPool();

            for(int i=0; i<20; i++){

                  service.submit(worker);

            }

            service.shutdown();

      }    

}

 

CountDownLatch:

  CountDownLatch是个计数器,它有一个初始数,等待这个计数器的线程必须等到计数器倒数到零时才可继续。比如说一个Server启动时需要初始化4个部件,Server可以同时启动4个线程去初始化这4个部件,然后调用CountDownLatch(4).await()阻断进入等待,每个线程完成任务后会调用一次CountDownLatch.countDown()来倒计数, 当4个线程都结束时CountDownLatch的计数就会降低为0,此时Server就会被唤醒继续下一步操作。CountDownLatch的方法主要有:

  • await():使调用此方法的线程阻断进入等待
  • countDown(): 倒计数,将计数值减1
  • getCount(): 得到当前的计数值

  CountDownLatch的例子:一个server调了三个ComponentThread分别去启动三个组件,然后server等到组件都启动了再继续。

public class Server {

      public static void main(String[] args) throws InterruptedException{

            System.out.println("Server is starting.");

            //初始化一个初始值为3的CountDownLatch

            CountDownLatch latch = new CountDownLatch(3);

            //起3个线程分别去启动3个组件

            ExecutorService service = Executors.newCachedThreadPool();

            service.submit(new ComponentThread(latch, 1));

            service.submit(new ComponentThread(latch, 2));

            service.submit(new ComponentThread(latch, 3));

            service.shutdown();

            //进入等待状态

            latch.await();

            //当所需的三个组件都完成时,Server就可继续了

            System.out.println("Server is up!");

      }

}

 

public class ComponentThread implements Runnable{

      CountDownLatch latch;

      int ID;

      /** Creates a new instance of ComponentThread */

      public ComponentThread(CountDownLatch latch, int ID) {

            this.latch = latch;

            this.ID = ID;

      }

      public void run() {

            System.out.println("Component "+ID + " initialized!");

            //将计数减一

            latch.countDown();

      }    

}

运行结果:

Server is starting.

Component 1 initialized!

Component 3 initialized!

Component 2 initialized!

Server is up!

CyclicBarrier:

  CyclicBarrier类似于CountDownLatch也是个计数器,不同的是CyclicBarrier数的是调用了 CyclicBarrier.await()进入等待的线程数,当线程数达到了CyclicBarrier初始时规定的数目时,所有进入等待状态的线程被唤醒并继续。CyclicBarrier就象它名字的意思一样,可看成是个障碍,所有的线程必须到齐后才能一起通过这个障碍。CyclicBarrier 初始时还可带一个Runnable的参数,此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。

CyclicBarrier提供以下几个方法:

  • await():进入等待
  • getParties():返回此barrier需要的线程数
  • reset():将此barrier重置

   以下是使用CyclicBarrier的一个例子:两个线程分别在一个数组里放一个数,当这两个线程都结束后,主线程算出数组里的数的和(这个例子比较无聊,我没有想到更合适的例子)

public class MainThread {

public static void main(String[] args)

      throws InterruptedException, BrokenBarrierException, TimeoutException{

            final int[] array = new int[2];

            CyclicBarrier barrier = new CyclicBarrier(2,

                  new Runnable() {//在所有线程都到达Barrier时执行

                  public void run() {

                        System.out.println("Total is:"+(array[0]+array[1]));

                  }

            });           

            //启动线程

            new Thread(new ComponentThread(barrier, array, 0)).start();

            new Thread(new ComponentThread(barrier, array, 1)).start();   

      }     

}

 

public class ComponentThread implements Runnable{

      CyclicBarrier barrier;

      int ID;

      int[] array;

      public ComponentThread(CyclicBarrier barrier, int[] array, int ID) {

            this.barrier = barrier;

            this.ID = ID;

            this.array = array;

      }

      public void run() {

            try {

                  array[ID] = new Random().nextInt();

                  System.out.println(ID+ " generates:"+array[ID]);

                  //该线程完成了任务等在Barrier处

                  barrier.await();

            } catch (BrokenBarrierException ex) {

                  ex.printStackTrace();

            } catch (InterruptedException ex) {

                  ex.printStackTrace();

            }

      }

}

Exchanger:

   顾名思义Exchanger让两个线程可以互换信息。用一个例子来解释比较容易。例子中服务生线程往空的杯子里倒水,顾客线程从装满水的杯子里喝水,然后通过Exchanger双方互换杯子,服务生接着往空杯子里倒水,顾客接着喝水,然后交换,如此周而复始。

class FillAndEmpty {

      //初始化一个Exchanger,并规定可交换的信息类型是DataCup

      Exchanger exchanger = new Exchanger();

      Cup initialEmptyCup = ...; //初始化一个空的杯子

      Cup initialFullCup = ...; //初始化一个装满水的杯子

      //服务生线程

      class Waiter implements Runnable {

            public void run() {

                  Cup currentCup = initialEmptyCup;

                  try {

                        //往空的杯子里加水

                        currentCup.addWater();

                        //杯子满后和顾客的空杯子交换

                        currentCup = exchanger.exchange(currentCup);

                  } catch (InterruptedException ex) { ... handle ... }

            }

      }

      //顾客线程

      class Customer implements Runnable {

            public void run() {

                  DataCup currentCup = initialFullCup;

                  try {

                        //把杯子里的水喝掉

                        currentCup.drinkFromCup();

                        //将空杯子和服务生的满杯子交换

                        currentCup = exchanger.exchange(currentCup);

                  } catch (InterruptedException ex) { ... handle ...}

            }

      }

     

      void start() {

            new Thread(new Waiter()).start();

            new Thread(new Customer()).start();

      }

}

6: BlockingQueue接口

  BlockingQueue是一种特殊的Queue,若BlockingQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态直到BlocingkQueue进了新货才会被唤醒。同样,如果BlockingQueue是满的任何试图往里存东西的操作也会被阻断进入等待状态,直到BlockingQueue里有新的空间才会被唤醒继续操作。BlockingQueue提供的方法主要有:

  • add(anObject): 把anObject加到BlockingQueue里,如果BlockingQueue可以容纳返回true,否则抛出IllegalStateException异常。
  • offer(anObject):把anObject加到BlockingQueue里,如果BlockingQueue可以容纳返回true,否则返回false。
  • put(anObject):把anObject加到BlockingQueue里,如果BlockingQueue没有空间,调用此方法的线程被阻断直到BlockingQueue里有新的空间再继续。
  • poll(time):取出BlockingQueue里排在首位的对象,若不能立即取出可等time参数规定的时间。取不到时返回null。
  • take():取出BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的对象被加入为止。

根据不同的需要BlockingQueue有4种具体实现:

  • ArrayBlockingQueue:规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小。其所含的对象是以FIFO(先入先出)顺序排序的。
  • LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的 BlockingQueue的大小由Integer.MAX_VALUE来决定。其所含的对象是以FIFO(先入先出)顺序排序的。 LinkedBlockingQueue和ArrayBlockingQueue比较起来,它们背后所用的数据结构不一样,导致 LinkedBlockingQueue的数据吞吐量要大于ArrayBlockingQueue,但在线程数量很大时其性能的可预见性低于 ArrayBlockingQueue。
  • PriorityBlockingQueue:类似于LinkedBlockingQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序。
  • SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。

下面是用BlockingQueue来实现Producer和Consumer的例子:

public class BlockingQueueTest {

      static BlockingQueue basket;

      public BlockingQueueTest() {

            //定义了一个大小为2的BlockingQueue,也可根据需要用其他的具体类

            basket = new ArrayBlockingQueue(2);

      }

      class Producor implements Runnable {

            public void run() {

                  while(true){

                        try {

                              //放入一个对象,若basket满了,等到basket有位置

                              basket.put("An apple");

                        } catch (InterruptedException ex) {

                              ex.printStackTrace();

                        }

                  }

            }

      }

      class Consumer implements Runnable {

           public void run() {

                  while(true){

                        try {

                              //取出一个对象,若basket为空,等到basket有东西为止

                              String result = basket.take();

                        } catch (InterruptedException ex) {

                              ex.printStackTrace();

                        }

                  }

            }           

      }

      public void execute(){

            for(int i=0; i<10; i++){

                  new Thread(new Producor()).start();

                  new Thread(new Consumer()).start();

            }           

      }

      public static void main(String[] args){

            BlockingQueueTest test = new BlockingQueueTest();

            test.execute();

      }     

}

7:Atomics 原子级变量

   原子量级的变量,主要的类有AtomicBoolean, AtomicInteger, AotmicIntegerArray,AtomicLong, AtomicLongArray, AtomicReference ……。这些原子量级的变量主要提供两个方法:

  • compareAndSet(expectedValue, newValue): 比较当前的值是否等于expectedValue,若等于把当前值改成newValue,并返回true。若不等,返回false。
  • getAndSet(newValue): 把当前值改为newValue,并返回改变前的值。

   这些原子级变量利用了现代处理器(CPU)的硬件支持可把两步操作合为一步的功能,避免了不必要的锁定,提高了程序的运行效率。

8:Concurrent Collections 共点聚集

   在Java的聚集框架里可以调用Collections.synchronizeCollection(aCollection)将普通聚集改变成同步聚集,使之可用于多线程的环境下。 但同步聚集在一个时刻只允许一个线程访问它,其它想同时访问它的线程会被阻断,导致程序运行效率不高。Java 5.0里提供了几个共点聚集类,它们把以前需要几步才能完成的操作合成一个原子量级的操作,这样就可让多个线程同时对聚集进行操作,避免了锁定,从而提高了程序的运行效率。Java 5.0目前提供的共点聚集类有:ConcurrentHashMap,ConcurrentLinkedQueue, CopyOnWriteArrayList和CopyOnWriteArraySet.

 

线 程

本模块讨论多线程,它允许一个程序同时执行多个任务。

第一节  相关问题

讨论 - 以下为与本模块内容有关的问题:

l   我如何使我的程序执行多个任务?

第二节  目 标

在完成了本模块的学习后,你应当能够:

l   定义一个线程

l   在一个Java程序中创建若干分离的线程,控制线程使用的代码和数据

l   控制线程的执行,并用线程编写独立于平台的代码

l   描述在多个线程共享数据时可能会碰到的困难

l   使用synchronized关键字保护数据不受破坏

l   使用wait()和notify()使线程间相互通信

l   解释为什么在JDK1.2中不赞成使用suspend()、resume()和stop()方法?

第三节  线 程

 

13.3.1  什么是线程?

一个关于计算机的简化的视图是:它有一个执行计算的处理机、包含处理机所执行的程序的ROM(只读存储器)、包含程序所要操作的数据的RAM(只读存储器)。在这个简化视图中,只能执行一个作业。一个关于最现代计算机比较完整的视图允许计算机在同时执行一个以上的作业。

你不需关心这一点是如何实现的,只需从编程的角度考虑就可以了。如果你要执行一个以上的作业,这类似有一台以上的计算机。在这个模型中,线程或执行上下文,被认为是带有自己的程序代码和数据的虚拟处理机的封装。java.lang.Thread类允许用户创建并控制他们的线程。

 


注-在这个模块中,使用“Thread”时是指java.lang.Thread而使用“thread”时是指执行上下文。

13.3.2  线程的三个部分

进程是正在执行的程序。一个或更多的线程构成了一个进程。一个线程或执行上下文由三个主要部分组成

l   一个虚拟处理机

l   CPU执行的代码

l   代码操作的数据

代码可以或不可以由多个线程共享,这和数据是独立的。两个线程如果执行同一个类的实例代码,则它们可以共享相同的代码。

类似地,数据可以或不可以由多个线程共享,这和代码是独立的。两个线程如果共享对一个公共对象的存取,则它们可以共享相同的数据。

在Java编程中,虚拟处理机封装在Thread类的一个实例里。构造线程时,定义其上下文的代码和数据是由传递给它的构造函数的对象指定的。

第四节  Java编程中的线程

13.4.1  创建线程

本节介绍了如何创建线程,以及如何使用构造函数参数来为一个线程提供运行时的数据和代码。

一个Thread类构造函数带有一个参数,它是Runnable的一个实例。一个Runnable是由一个实现了Runnable接口(即,提供了一个public void run()方法)的类产生的。

例如:

   1.public class ThreadTest {

   2.public static void main(String args[]) {

   3.Xyz r = new Xyz();

   4.Thread t = new Thread(r);

   5.}

   6.}

   7.

   8.class Xyz implements Runnable {

   9.int i;

  10.

  11.public void run() {

  12.while (true) {

  13.System.out.println("Hello " +i++);

  14.if (i == 50) break;

  15.}

  16.}

  17.}

首先,main()方法构造了Xyz类的一个实例r。实例r有它自己的数据,在这里就是整数i。因为实例r是传给Thread的类构造函数的,所以r的整数i就是线程运行时刻所操作的数据。线程总是从它所装载的Runnable实例(在本例中,这个实例就是r。)的run()方法开始运行。

一个多线程编程环境允许创建基于同一个Runnable实例的多个线程。这可以通过以下方法来做到:

Thread t1= newThread(r);

Thread t2= newThread(r);

此时,这两个线程共享数据和代码。

总之,线程通过Thread对象的一个实例引用。线程从装入的Runnble实例的run()方法开始执行。线程操作的数据从传递给Thread构造函数的Runnable的特定实例处获得。

13.4.2  启动线程

一个新创建的线程并不自动开始运行。你必须调用它的start()方法。例如,你可以发现上例中第4行代码中的命令:

t.start();

调用start()方法使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。

13.4.3  线程调度

一个Thread对象在它的生命周期中会处于各种不同的状态。下图形象地说明了这点:

尽管线程变为可运行的,但它并不立即开始运行。在一个只带有一个

 处理机的机器上,在一个时刻只能进行一个动作。下节描述了如果有一个以上可运行线程时,如何分配处理机。

在Java中,线程是抢占式的,但并不一定是分时的 (一个常见的错误是认为“抢占式”只不过是“分时”的一种新奇的称呼而已) 。

抢占式调度模型是指可能有多个线程是可运行的,但只有一个线程在实际运行。这个线程会一直运行,直至它不再是可运行的,或者另一个具有更高优先级的线程成为可运行的。对于后面一种情形,低优先级线程被高优先级线程抢占了运行的机会。

一个线程可能因为各种原因而不再是可运行的。线程的代码可能执行了一个Thread.sleep()调用,要求这个线程暂停一段固定的时间。这个线程可能在等待访问某个资源,而且在这个资源可访问之前,这个线程无法继续运行。

所有可运行线程根据优先级保存在池中。当一个被阻塞的线程变成可运行时,它会被放回相应的可运行池。优先级最高的非空池中的线程会得到处理机时间(被运行)。

因为Java线程不一定是分时的,所有你必须确保你的代码中的线程会不时地给另外一个线程运行的机会。这可以通过在各种时间间隔中发出sleep()调用来做到。

   1.public class Xyz implements Runnable {

   2.public void run() {

   3.while (true) {

   4.// do lots of interesting stuff

   5.:

   6.// Give other threads a chance

   7.try {

   8.Thread.sleep(10);

   9.} catch (InterruptedException e) {

  10.// This thread's sleep was interrupted

  11.// by another thread

  12.}

  13.}

  14.}

  15.}

注意try和catch块的使用。Thread.sleep()和其它使线程暂停一段时间的方法是可中断的。线程可以调用另外一个线程的interrupt()方法,这将向暂停的线程发出一个InterruptedException。

注意Thread类的sleep()方法对当前线程操作,因此被称作Thread.sleep(x),它是一个静态方法。sleep()的参数指定以毫秒为单位的线程最小休眠时间。除非线程因为中断而提早恢复执行,否则它不会在这段时间之前恢复执行。

Thread类的另一个方法yield(),可以用来使具有相同优先级的线程获得执行的机会。如果具有相同优先级的其它线程是可运行的,yield()将把调用线程放到可运行池中并使另一个线程运行。如果没有相同优先级的可运行进程,yield()什么都不做。

注意sleep()调用会给较低优先级线程一个运行的机会。yield()方法只会给相同优先级线程一个执行的机会。

第五节  线程的基本控制

13.5.1  终止一个线程

当一个线程结束运行并终止时,它就不能再运行了。

可以用一个指示run()方法必须退出的标志来停止一个线程。

   1.public class Xyz implements Runnable {

   2.private boolean timeToQuit=false;

   3.

   4.public void run() {

   5.while(! timeToQuit) {

   6....

   7.}

   8.// clean up before run() ends

   9.}

  10.

  11.public void stopRunning() {

  12.timeToQuit=true;

  13.}

  14.}

  15.

  16.public class ControlThread {

  17.private Runnable r = new Xyz();

  18.private Thread t = new Thread(r);

  19.

  20.public void startThread() {

  21.t.start();

  22.}

  23.

  24.public void stopThread() {

  25.// use specific instance of Xyz

  26.r.stopRunning();

  27.}

  28.}

在一段特定的代码中,可以使用静态Thread方法currentThread()来获取对当前线程的引用,例如:

   1.public class Xyz implements Runnable {

   2.public void run() {

   3.while (true) {

   4.// lots of interesting stuff

   5.// Print name of the current thread

   6.System.out.println("Thread" +

   7.Thread.currentThread().getName()+

   8."completed");

   9.}

  10.}

  11.}

13.5.2  测试一个线程

有时线程可处于一个未知的状态。isAlive()方法用来确定一个线程是否仍是活的。活着的线程并不意味着线程正在运行;对于一个已开始运行但还没有完成任务的线程,这个方法返回true。

13.5.3  延迟线程

存在可以使线程暂停执行的机制。也可以恢复运行,就好象什么也每发生过一样,线程看上去就象在很慢地执行一条指令。

sleep()

sleep()方法是使线程停止一段时间的方法。在sleep时间间隔期满后,线程不一定立即恢复执行。这是因为在那个时刻,其它线程可能正在运行而且没有被调度为放弃执行,除非

(a)“醒来”的线程具有更高的优先级

(b)正在运行的线程因为其它原因而阻塞

   1.public class Xyz implements Runnable {

   2.public void run() {

   3.while (true) {

   4.// lots of interesting stuff

   5.// Print name of the current thread

   6.System.out.println("Thread" +

   7.Thread.currentThread().getName()+

   8."completed");

   9.}

  10.}

  11.}

join()

join()方法使当前线程停下来等待,直至另一个调用join方法的线程终止。例如:

public voiddoTask() {

TimerThread tt =new TimerThread (100);

tt.start ();

...

// Do stuff inparallel with the other thread for

// a while

...

// Wait here forthe timer thread to finish

try {

tt.join ();

} catch (InterruptedExceptione) {

// tt came backearly

}

...

// Now continue inthis thread

...

}

可以带有一个以毫秒为单位的时间值来调用join方法,例如:

        void join (long timeout);

其中join()方法会挂起当前线程。挂起的时间或者为timeout毫秒,或者挂起当前线程直至它所调用的线程终止。

第六节  创建线程的其它方法

到目前为止,你已经知道如何用实现了Runnable的分离类来创建线程上下文。事实上,这不是唯一的方法。Thread类自身实现了Runnable接口,所以可以通过扩展Thread类而不是实现Runnable来创建线程。

   1.public class MyThread extends Thread {

   2.public void run() {

   3.while (running) {

   4.// do lots of interesting stuff

   5.try {

   6.sleep(100);

   7.} catch (InterruptedException e) {

   8.// sleep interrupted

   9.}

  10.}

  11.}

  12.

  13.public static void main(String args[]) {

  14.Thread t = new MyThread();

  15.t.start();

  16.}

  17.}

13.6.1  使用那种方法?

给定各种方法的选择,你如何决定使用哪个?每种方法都有若干优点。

实现Runnable的优点

l   从面向对象的角度来看,Thread类是一个虚拟处理机严格的封装,因此只有当处理机模型修改或扩展时,才应该继承类。正因为这个原因和区别一个正在运行的线程的处理机、代码和数据部分的意义,本教程采用了这种方法。

l   由于Java技术只允许单一继承,所以如果你已经继承了Thread,你就不能再继承其它任何类,例如Applet。在某些情况下,这会使你只能采用实现Runnable的方法。

l   因为有时你必须实现Runnable,所以你可能喜欢保持一致,并总是使用这种方法。

继承Thread的优点

l   当一个run()方法体现在继承Thread类的类中,用this指向实际控制运行的Thread实例。因此,代码不再需要使用如下控制:

Thread.currentThread().join();

而可以简单地用:

join();

因为代码简单了一些,许多Java编程语言的程序员使用扩展Thread的机制。注意:如果你采用这种方法,在你的代码生命周期的后期,单继承模型可能会给你带来困难。

第七节  使用Java技术中的synchronized

本节讨论关键字synchronized的使用。它提供Java编程语言一种机制,允许程序员控制共享数据的线程。

13.7.1  问题

想象一个表示栈的类。这个类最初可能象下面那样:

   1.public class MyStack {

   2.

   3.int idx = 0;

   4.char [] data = new char[6];

   5.

   6.public void push(char c) {

   7.data[idx] = c;

   8.idx++;

   9.}

  10.

  11.public char pop() {

  12.idx--;

  13.return data[idx];

  14.}

  15.}

 

注意这个类没有处理栈的上溢和下溢,所以栈的容量是相当有限的。这些方面和本讨论无关。

这个模型的行为要求索引值包含栈中下一个空单元的数组下标。“先进后出”方法用来产生这个信息。

现在想象两个线程都有对这个类里的一个单一实例的引用。一个线程将数据推入栈,而另一个线程,或多或少独立地,将数据弹出栈。通常看来,数据将会正确地被加入或移走。然而,这存在着潜在的问题。

假设线程a正在添加字符,而线程b正在移走字符。线程a已经放入了一个字符,但还没有使下标加1。因为某个原因,这个线程被剥夺(运行的机会)。这时,对象所表示的数据模型是不一致的。

buffer |p|q|r| | ||

idx = 2     ^

特别地,一致性会要求idx=3,或者还没有添加字符。

如果线程a恢复运行,那就可能不造成破坏,但假设线程b正等待移走一个字符。在线程a等待另一个运行的机会时,线程b正在等待移走一个字符的机会。

pop()方法所指向的条目存在不一致的数据,然而pop方法要将下标值减1。

buffer |p|q|r| | ||

idx = 1   ^

这实际上将忽略了字符“r”。此后,它将返回字符“q”。至此,从其行为来看,就好象没有推入字母“r”,所以很难说是否存在问题。现在看一看如果线程a继续运行,会发生什么。

线程a从上次中断的地方开始运行,即在push()方法中,它将使下标值加1。现在你可以看到:

buffer |p|q|r| | ||

idx = 2     ^

注意这个配置隐含了:“q”是有效的,而含有“r”的单元是下一个空单元。也就是说,读取“q”时,它就象被两次推入了栈,而字母“r”则永远不会出现。

这是一个当多线程共享数据时会经常发生的问题的一个简单范例。需要有机制来保证共享数据在任何线程使用它完成某一特定任务之前是一致的。

 


注-有一种方法可以保证线程a在执行完成关键部分的代码时不被调出。这种方法常用在底层的机器语言编程中,但不适合多用户系统。

 


注-另外一种方法,它可以被Java技术采用。这种方法提供精细地处理数据的机制。这种方法允许无论线程是否会在执行存取的中间被调出,线程对数据的存取都是不可分割的,

13.7.2  对象锁标志

在Java技术中,每个对象都有一个和它相关联的标志。这个标志可以被认为是“锁标志”。 synchronized关键字使能和这个标志的交互,即允许独占地存取对象。看一看下面修改过的代码片断:

public voidpush(char c) {

synchronized(this){

data[idx] = c;

idx++;

}

}

当线程运行到synchronized语句,它检查作为参数传递的对象,并在继续执行之前试图从对象获得锁标志。

对象锁标志

意识到它自身并没有保护数据是很重要的。因为如果同一个对象的pop()方法没有受到synchronized的影响,且pop()是由另一个线程调用的,那么仍然存在破坏data的一致性的危险。如果要使锁有效,所有存取共享数据的方法必须在同一把锁上同步。

下图显示了如果pop()受到synchronized的影响,且另一个线程在原线程持有那个对象的锁时试图执行pop()方法时所发生的事情:

  

当线程试图执行synchronized(this)语句时,它试图从this对象获取锁标志。由于得不到标志,所以线程不能继续运行。然后,线程加入到与那个对象锁相关联的等待线程池中。当标志返回给对象时,某个等待这个标志的线程将得到这把锁并继续运行。

13.7.3  释放锁标志

由于等待一个对象的锁标志的线程在得到标志之前不能恢复运行,所以让持有锁标志的线程在不再需要的时候返回标志是很重要的。

锁标志将自动返回给它的对象。持有锁标志的线程执行到synchronized()代码块末尾时将释放锁。Java技术特别注意了保证即使出现中断或异常而使得执行流跳出synchronized()代码块,锁也会自动返回。此外,如果一个线程对同一个对象两次发出synchronized调用,则在跳出最外层的块时,标志会正确地释放,而最内层的将被忽略。

这些规则使得与其它系统中的等价功能相比,管理同步块的使用简单了很多。

13.7.4  synchronized――放在一起

正如所暗示的那样,只有当所有对易碎数据的存取位于同步块内,synchronized()才会发生作用。

所有由synchronized块保护的易碎数据应当标记为private。考虑来自对象的易碎部分的数据的可存取性。如果它们不被标记为private,则它们可以由位于类定义之外的代码存取。这样,你必须确信其他程序员不会省略必需的保护。

一个方法,如果它全部属于与这个实例同步的块,它可以把synchronized关键字放到它的头部。下面两段代码是等价的:

public voidpush(char c) {

synchronized(this){

:

:

}

}

public synchronizedvoid push(char c) {

:

:

}

为什么使用另外一种技术?

如果你把synchronized作为一种修饰符,那么整个块就成为一个同步块。这可能会导致不必要地持有锁标志很长时间,因而是低效的。

然而,以这种方式来标记方法可以使方法的用户由javadoc产生的文档了解到:正在同步。这对于设计时避免死锁(将在下一节讨论)是很重要的。注意javadoc文档生成器将synchronized关键字传播到文档文件中,但它不能为在方法块内的synchronized(this)做到这点。

13.7.5  死锁

如果程序中有多个线程竞争多个资源,就可能会产生死锁。当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。在这种情况下,除非另一个已经执行到synchronized块的末尾,否则没有一个线程能继续执行。由于没有一个线程能继续执行,所以没有一个线程能执行到块的末尾。

Java技术不监测也不试图避免这种情况。因而保证不发生死锁就成了程序员的责任。避免死锁的一个通用的经验法则是:决定获取锁的次序并始终遵照这个次序。按照与获取相反的次序释放锁。

第八节  线程交互-wait()和notify()

经常创建不同的线程来执行不相关的任务。然而,有时它们所执行的任务是有某种联系的,为此必须编写使它们交互的程序。

13.8.1  场景

把你自己和出租车司机当作两个线程。你需要出租车司机带你到终点,而出租车司机需要为乘客服务来获得车费。所以,你们两者都有一个任务。

13.8.2  问题

你希望坐到出租车里,舒服地休息,直到出租车司机告诉你已经到达终点。如果每2秒就问一下“我们到了哪里?”,这对出租车司机和你都会是很烦的。出租车司机想睡在出租车里,直到一个乘客想到另外一个地方去。出租车司机不想为了查看是否有乘客的到来而每5分钟就醒来一次。所以,两个线程都想用一种尽量轻松的方式来达到它们的目的。

13.8.3  解决方案

出租车司机和你都想用某种方式进行通信。当你正忙着走向出租车站时,司机正在车中安睡。当你告诉司机你想坐他的车时,司机醒来并开始驾驶,然后你开始等待并休息。到达终点时,司机会通知你,所以你必须继续你的任务,即走出出租车,然后去工作。出租车司机又开始等待和休息,直到下一个乘客的到来。

13.8.4  wait()notify()

java.lang.Object类中提供了两个用于线程通信的方法:wait()和notify()。如果线程对一个同步对象x发出一个wait()调用,该线程会暂停执行,直到另一个线程对同一个同步对象x也发出一个wait()调用。

在上个场景中,在车中等待的出租车司机被翻译成执行cab.wait()调用的“出租车司机”线程,而你使用出租车的需求被翻译成执行cab.notify()调用的“你”线程。

为了让线程对一个对象调用wait()或notify(),线程必须锁定那个特定的对象。也就是说,只能在它们被调用的实例的同步块内使用wait()和notify()。对于这个实例来说,需要一个以synchronized(cab)开始的块来允许执行cab.wait()和cab.notify()调用。

关于池

当线程执行包含对一个特定对象执行wait()调用的同步代码时,那个线程被放到与那个对象相关的等待池中。此外,调用wait()的线程自动释放对象的锁标志。可以调用不同的wait():

wait()  或  wait(long timeout);

对一个特定对象执行notify()调用时,将从对象的等待池中移走一个任意的线程,并放到锁池中,那里的对象一直在等待,直到可以获得对象的锁标记。 notifyAll()方法将从等待池中移走所有等待那个对象的线程并放到锁池中。只有锁池中的线程能获取对象的锁标记,锁标记允许线程从上次因调用wait()而中断的地方开始继续运行。

在许多实现了wait()/notify()机制的系统中,醒来的线程必定是那个等待时间最长的线程。然而,在Java技术中,并不保证这点。

注意,不管是否有线程在等待,都可以调用notify()。如果对一个对象调用notify()方法,而在这个对象的锁标记等待池中并没有阻塞的线程,那么notify()调用将不起任何作用。对notify()的调用不会被存储。

13.8.5  同步的监视模型

协调两个需要存取公共数据的线程可能会变得非常复杂。你必须非常小心,以保证可能有另一个线程存取数据时,共享数据的状态是一致的。因为线程不能在其他线程在等待这把锁的时候释放合适的锁,所以你必须保证你的程序不发生死锁,

在出租车范例中,代码依赖于一个同步对象――出租车,在其上执行wait()和notify()。如果有任何人在等待一辆公共汽车,你就需要一个独立的公共汽车对象,在它上面施用notify()。记住,在同一个等待池中的所有线程都因来自等待池的控制对象的通知而满足。永远不要设计这样的程序:把线程放在同一个等待池中,但它们却在等待不同条件的通知。

13.8.6  放在一起

下面将给出一个线程交互的实例,它说明了如何使用wait()和notify()方法来解决一个经典的生产者-消费者问题。

我们先看一下栈对象的大致情况和要存取栈的线程的细节。然后再看一下栈的详情,以及基于栈的状态来保护栈数据和实现线程通信的机制。

实例中的栈类称为SyncStack,用来与核心java.util.Stack相区别,它提供了如下公共的API:

public synchronizedvoid push(char c);

public synchronizedchar pop();

生产者线程运行如下方法:

public void run() {

char c;

for (int i = 0; i< 200; i++) {

c =(char)(Math.random() * 26 + 'A');

theStack.push(c);

System.out.println("Producer"+ num + ": " + c);

try {

Thread.sleep((int)(Math.random()* 300));

} catch(InterruptedException e) {

// ignore it

}

}

}

这将产生200个随机的大写字母并将其推入栈中,每个推入操作之间有0到300毫秒的随机延迟。每个被推入的字符将显示到控制台上,同时还显示正在执行的生产者线程的标识。

消费者

消费者线程运行如下方法:

public void run() {

char c;

for (int i = 0; i< 200; i++) {

c = theStack.pop();

System.out.println("Consumer" + num + ": " + c);

try {

Thread.sleep((int)(Math.random()* 300));

} catch(InterruptedException e) {

// ignore it

}

}

}

上面这个程序从栈中取出200个字符,每两个取出操作的尝试之间有0到300毫秒的随机延迟。每个被弹出的字符将显示在控制台上,同时还显示正在执行的消费者线程的标识。

现在考虑栈类的构造。你将使用Vector类创建一个栈,它看上去有无限大的空间。按照这种设计,你的线程只要在栈是否为空的基础上进行通信即可。

SyncStack

一个新构造的SyncStack对象的缓冲应当为空。下面这段代码用来构造你的类:

public classSyncStack {

private Vectorbuffer = new Vector(400,200);

public synchronizedchar pop() {

}

public synchronizedvoid push(char c) {

}

}

请注意,其中没有任何构造函数。包含有一个构造函数是一种相当好的风格,但为了保持简洁,这里省略了构造函数。

现在考虑push()和pop()方法。为了保护共享缓冲,它们必须均为synchronized。此外,如果要执行pop()方法时栈为空,则正在执行的线程必须等待。若执行push()方法后栈不再为空,正在等待的线程将会得到通知。

pop()方法如下:

public synchronizedchar pop() {

char c;

while(buffer.size() == 0) {

try {

this.wait();

} catch(InterruptedException e) {

// ignore it

}

}

c =((Character)buffer.remove(buffer.size()-1)).charValue();

return c;

}

注意这里显式地调用了栈对象的wait(),这说明了如何对一个特定对象进行同步。如果栈为空,则不会弹出任何数据,所以一个线程必须等到栈不再为空时才能弹出数据。

由于一个interrupt()的调用可能结束线程的等待阶段,所以wait()调用被放在一个try/catch块中。对于本例,wait()还必须放在一个循环中。如果wait()被中断,而栈仍为空,则线程必须继续等待。

栈的pop()方法为synchronized是出于两个原因。首先,将字符从栈中弹出影响了共享数据buffer。其次,this.wait()的调用必须位于关于栈对象的一个同步块中,这个块由this表示。

你将看到push()方法如何使用this.notify()方法将一个线程从栈对象的等待池中释放出来。一旦线程被释放并可随后再次获得栈的锁,该线程就可以继续执行pop()完成从栈缓冲区中移走字符任务的代码。

 


注 - 在pop()中,wait()方法在对栈的共享数据作修改之前被调用。这是非常关键的一点,因为在对象锁被释放和线程继续执行改变栈数据的代码之前,数据必须保持一致的状态。你必须使你所设计的代码满足这样的假设:在进入影响数据的代码时,共享数据是处于一致的状态。

需要考虑的另一点是错误检查。你可能已经注意到没有显式的代码来保证栈不发生下溢。这不是必需的,因为从栈中移走字符的唯一方法是通过pop()方法,而这个方法导致正在执行的线程在没有字符的时候会进入wait()状态。因此,错误检查不是必要的。push()在影响共享缓冲方面与此类似,因此也必须被同步。此外,由于push()将一个字符加入缓冲区,所以由它负责通知正在等待非空栈的线程。这个通知的完成与栈对象有关。

push()方法如下:

public synchronizedvoid push(char c) {

this.notify();

Character charObj =new Character(c);

buffer.addElement(charObj);

}

对this.notify()的调用将释放一个因栈空而调用wait()的单个线程。在共享数据发生真正的改变之前调用notify()不会产生任何结果。只有退出该synchronized块后,才会释放对象的锁,所以当栈数据在被改变时,正在等待锁的线程不会获得这个锁。

13.8.7  SyncStack范例

完整的代码

现在,生产者、消费者和栈代码必须组装成一个完整的类。还需要一个测试工具将这些代码集成为一体。特别要注意,SyncTest是如何只创建一个由所有线程共享的栈对象的。

SyncTest.java

   1.package mod14;

   2.public class SyncTest {

   3.public static void main(String args[]) {

   4.

   5.SyncStack stack = new SyncStack();

   6.

   7.Producer p1 = new Producer(stack);

   8.Thread prodT1= new Thread(p1);

   9.prodT1.start();

  10.

  11.Producer p2 = new Producer(stack);

  12.Thread prodT2= new Thread(p2);

  13.prodT2.start();

  14.

  15.Consumer c1 = new Consumer(stack);

  16.Thread consT1 = new Thread(c1);

  17.consT1.start();

  18.

  19.Consumer c2 = new Consumer(stack);

  20.Thread consT2 = new Thread(c2);

  21.constT2.start();

  22.}

  23.}

Producer.java

   1.package mod14;

   2.public class Producer implements Runnable{

   3.private SyncStack theStack;

   4.private int num;

   5.private static int counter = 1;

Producer.java(续)

   1.

                   2.public Producer (SyncStack s) {

                   3.theStack = s;

                   4.num = counter++;

                   5.}

                   6.

                   7.public void run() {

                   8.char c;

                   9.

                   10.for (int i = 0; i < 200; i++) {

                   11.c = (char)(Math.random() * 26 + `A');

                   12.theStack.push(c);

                   13.System.out.println("Producer" +num + ": " + c);

                    14.try {

  15.Thread.sleep((int)(Math.random() * 300));

  16.} catch (InterruptedException e) {

  17.// ignore it

  18.}

  19.}

  20.}

  21.}

Consumer.java

   1.package mod14;

   2.public class Consumer implements Runnable{

   3.private SyncStack theStack;

   4.private int num;

   5.private static int counter = 1;

   6.

   7.public Consumer (SyncStack s) {

   8.theStack = s;

   9.num = counter++;

  10.}

  11.

  12.public void run() {

Consumer.java(续)

   1.char c;

   2.

   3.for (int i=0; i < 200; i++) {

   4.c = theStack.pop();

   5.System.out.println("Consumer" +num + ": " + c);

   6.try {

   7.Thread.sleep((int)(Math.random() * 300));

   8.} catch (InterruptedException e) {

   9.// ignore it

  10.}

  11.}

  12.}

  13.}

SyncStack.java

   1.package mod14;

   2.

   3.import java.util.Vector;

   4.

                     5.public class SyncStack {

   6.private Vector buffer = newVector(400,200);

   7.

   8.public synchronized char pop() {

   9.char c;

  10.

  11.while (buffer.size() == 0) {

  12.try {

  13.this.wait();

  14.} catch (InterruptedException e) {

  15.// ignore it

  16.}

  17.}

  18.

  19. c =((Character)buffer.remove(buffer.size()- 1).charValue();

SyncStack.java(续)

   1.return c;

   2.}

   3.

   4.public synchronized void push(char c) {

   5.this.notify();

   6.

                     7.Character charObj = new Character(c);

   8.buffer.addelement(charObj);

   9.}

  10.}

运行javamodB.SyncTest的输出如下。请注意每次运行线程代码时,结果都会有所不同。

Producer2: F

Consumer1: F

Producer2: K

Consumer2: K

Producer2: T

Producer1: N

Producer1: V

Consumer2: V

Consumer1: N

Producer2: V

Producer2: U

Consumer2: U

Consumer2: V

Producer1: F

Consumer1: F

Producer2: M

Consumer2: M

Consumer2: T

第九节  JDK1.2中的线程控制

13.9.1  suspend()resume()方法

JDK1.2中不赞成使用suspend()和resume()方法。resume()方法的唯一作用就是恢复被挂起的线程。所以,如果没有suspend(),resume()也就没有存在的必要。从设计的角度来看,有两个原因使suspend()非常危险:它容易产生死锁;它允许一个线程控制另一个线程代码的执行。下面将分别介绍这两种危险。

假设有两个线程:threadA和threadB。当正在执行它的代码时,threadB获得一个对象的锁,然后继续它的任务。现在threadA的执行代码调用threadB.suspend(),这将使threadB停止执行它的代码。

如果threadB.suspend()没有使threadB释放它所持有的锁,就会发生死锁。如果调用threadB.resume()的线程需要threadB仍持有的锁,这两个线程就会陷入死锁。

假设threadA调用threadB.suspend()。如果threadB被挂起时threadA获得控制,那么threadB就永远得不到机会来进行清除工作,例如使它正在操作的共享数据处于稳定状态。为了安全起见,只有threadB才可以决定何时停止它自己的代码。

你应该使用对同步对象调用wait()和notify()的机制来代替suspend()和resume()进行线程控制。这种方法是通过执行wait()调用来强制线程决定何时“挂起”自己。这使得同步对象的锁被自动释放,并给予线程一个在调用wait()之前稳定任何数据的机会。

13.9.2  stop()方法

stop()方法的情形是类似的,但结果有所不同。如果一个线程在持有一个对象锁的时候被停止,它将在终止之前释放它持有的锁。这避免了前面所讨论的死锁问题,但它又引入了其他问题。

在前面的范例中,如果线程在已将字符加入栈但还没有使下标值加1之后被停止,你在释放锁的时候会得到一个不一致的栈结构。

总会有一些关键操作需要不可分割地执行,而且在线程执行这些操作时被停止就会破坏操作的不可分割性。

一个关于停止线程的独立而又重要的问题涉及线程的总体设计策略。创建线程来执行某个特定作业,并存活于整个程序的生命周期。换言之,你不会这样来设计程序:随意地创建和处理线程,或创建无数个对话框或socket端点。每个线程都会消耗系统资源,而系统资源并不是无限的。这并不是暗示一个线程必须连续执行;它只是简单地意味着应当使用合适而安全的wait()和notify()机制来控制线程。

13.9.3  合适的线程控制

既然你已经知道如何来设计具有良好行为的线程,并使用wait()和notify()进行通信,而不需要再使用suspend()和stop(),那就可以考察下面的代码。注意:其中的run()方法保证了在执行暂停或终止之前,共享数据处于一致的状态,这是非常重要的。

   1.public class ControlledThread extendsThread {

   2.static final int SUSP=1;

   3.static final int STOP=2;

   4.static final int RUN=0;

   5.private int state = RUN;

   6.

   7.public synchronized void setState( int s){

   8.state = s;

   9.if (s == RUN)

  10.notify();

  11.}

  12.

  13.public synchronized boolean checkState() {

  14.while(state == SUSP) {

  15.try {

  16.wait();

  17.} catch (InterruptedException e) { }

  18.}

  19.if (state == STOP){

  20.return false;

  21.}

  22.return true;

  23.}

  24.

  25.public void run() {

  26.while(true) {

  27.doSomething();

  28.// be sure shared data is in

  29.// consistent state in case the

  30.// thread is waited or marked for

  31.// exiting from run().

  32.if (!checkState())

  33.break;

  34.}

  35.}//of run

  36.}//of producer

一个要挂起、恢复或终止生产者线程的线程用合适的值来调用生产者线程的setState()方法。当生产者线程确定进行上述操作是安全的时候,它会挂起自己(通过使用wait()方法)或者停止自己(通过退出run()方法)。

关于此问题更详细的讨论已超出本模块的范围。

练习:使用多线程编程

练习目标-在这个练习中,你将通过编写一些多线程的程序来熟悉多线程的概念。创建一个多线程的Applet。

一、准备

为了很好地完成这个练习,你必须理解本模块中讨论的多线程概念。

二、任务

水平1:创建三个线程

1.  创建简单的程序ThreeThreads.java,它将创建三个线程。每个线程应当显示它所运行的时间。(考虑使用Date()类)

水平2:使用动画

1.         创建一个Applet ThreadedAnimation.java,它读取10幅DukeTM waving图像(在graphics/Duke目录中)并按照Duke波动的顺序来显示它们。

2.         用MediaTracker类使这些图像的装载更平滑。

3.         允许用户连续点击鼠标来停止和启动动画。

三、练习小结

         讨论-花几分钟时间讨论一下,在本实验练习过程中你都经历、提出和发现了什么。

l 经验

l 解释

l 总结

l 应用

四、检查你的进度

在进入下一个模块的学习之前,请确认你能够:

l   定义一个线程

l   在一个Java程序中创建若干分离的线程,控制线程使用的代码和数据

l   控制线程的执行,并用线程编写独立于平台的代码

l   描述在多个线程共享数据时可能会碰到的困难

l   使用synchronized关键字保护数据不受破坏

l   使用wait()和notify()使线程间相互通信

l   使用synchronized关键字保护数据不受破坏

l   解释为什么在JDK1.2中不赞成使用suspend()、resume()和stop()方法?

五、思考

你是否有受益于多线程的应用程序?

 

Java线程类小结

一. 多线程的意义

1.       原由:两个或两个以上的代码块需要同时运行

2.       创建方法:

a.继承Thread类覆盖run方法

b.实现Runnable接口中的run方法,把实现Runnable接口的子类对象传递给Thread类的构造函数。

下面是a和b两种方法的代码例子

/*

多线程实现的第一种方法,继承Thread类

*/

/*

class TestThreadextends Thread

{

         public void run()

         {

                   while(true)

                   {

                   System.out.println("TestThread" );

                   }

         }

}

*/

/*

多线程实现的第二种方法,实现Runnable接口

*/

class TestThreadimplements Runnable

{

         public void run()

         {

                   while(true)

                   {

                   System.out.println("TestThread" );

                   }

         }

}

class BaseThread

{

         public static void main(String ars[])

         {

                   //第一种启动多线程的方法

                   //new TestThread().start();

                  

                   //第二种启动多线程的方法

                   new Thread( new TestThread()).start();

                  

                   while(true)

                   {

                   System.out.println("main" );

                   }

         }

}

定义Runnable接口的好处:不需要加入Thread类的继承体系中,如果该类已继承了父类,但需要同时运行多块代码,java为了实现这个功能,暴露了Runnable接口,只要覆盖了其中的run方法,把需要运行的代码写入其中即可,避免了单继承的局限性。

下面的代码例子简单的模拟了一下Thread类的设计思想:

/*

演示了多线程两种实现方法的实现机制

*/

interfaceMyRunnable

{

         public void run();

}

class MyThread

{

         private MyRunnable mr;

         public MyThread(){}

         public MyThread( MyRunnable mr )

         {

                   this.mr=mr;

         }

         public void run()

         {

                   mr.run();

         }

        

         public void start()

         {

                   this.run();

         }

        

}

/*

//通过继承的方法实现多线程

class Test extendsMyThread

{

         public void run()

         {

                   System.out.println( "entends run " );

         }

}

*/

//通过实现接口的方法实现多线程

class Testimplements MyRunnable

{

         public void run()

         {

                   System.out.println( "interface run " );

         }

}

class Demo

{

         public static void main( String args[])

         {

                   //通过继承的方法实现多线程

         /*      Testt=new Test();

                   t.start();

         */               

                   //通过实现接口的方法实现多线程

                   Test t=new Test();

                   MyThread mt=new MyThread( t);

                   mt.start();

         }

}

二. 线程安全问题

1.       线程的最大特点:随机性

2.       使用同步可解决以上的问题:

a,    原理:把代码封装并加锁

b,   注意两点:

1.       两个或两个以上的线程使用同一资源

2.       使用同一把锁

c,    两种方法:

1.   使用同步代码块(Synchronized)

2.   使用同步函数(Synchronized)

3.要注意由于线程嵌套而出现死锁的情况。

三.线程间的通信

1.       常用的函数

sleep(),wait(),notify(),notifyAll(),toString(),setPriority()

其中wait(),notify(),notifyAll()都是在Object类中,因为可以被任意对象所调用的方法。一定定义在Object类中。

sleep()和wait()的区别

sleep():释放资源,不释放锁

wait():、释放资源,释放锁

下面是一个线程通信的例子

/*

线程的通信问题, 一个输出必须在一个输入完成后才可以进行,用到了线程间通信的知识

*/

class Res

{

         String name;

         String sex;

         boolean b=true;

         public synchronized void set( Stringname, String sex )

         {

                   if( b)

                            try{this.wait();}catch( Exception e ){}

                   this.name=name;

                   this.sex=sex;

                   b=true;

                   notify();

         }

        

         public synchronized  void out()

         {

                   if( !b)

                            try{this.wait();}catch( Exception e ){}

                   b=false;

                   notify();

                   System.out.println(Thread.currentThread().getName()

                                     +"---"+name+""+sex  );

         }

}

class Inputimplements Runnable

{

         Res r;

         int i=0;

         Input( Res r)

         {

                   this.r=r;

         }

         public void run()

         {

                   while( true )

                   {

                           

                           

                            if( i==0)

                            {

                                     r.set(" dsdf","nan");

                            }

                            else

                            {

                                     r.set(" 丽丽","女");

                                    

                            }

                           

                            i=(i+1)%2;

                           

                           

                   }

         }

        

}

class Outputimplements Runnable

{

         Res r;

         Output( Res r )

         {

                   this.r=r;

         }

         public void run()

         {

                   while( true )

                   {

                            r.out();

                           

                   }

         }

}

class Demo1

{

         public static void main( String aers[])

         {

                   Res r=new Res();

                   new Thread( newInput(r)).start();

                   new Thread( newOutput(r)).start();

         }

}

2.如何结束运行的线程

 

 以前通过stop()方法。但该方法已过时。现在可以通过定义标记的形式来控制循环。因为线程一般都是循环做某些事情,所以可通过结束标志的方法结束线程。

 

Java中堆与栈的区别

1. 栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

  2. 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享,详见第3点。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

  3. Java中的数据类型有两种。
  一种是基本类型(primitive types), 共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如inta = 3; long b = 255L;的形式来定义的,称为自动变量。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。
  另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义

  int a = 3;
  int b = 3;

  编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
  特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。
  另一种是包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。

  4. String是一个特殊的包装类数据。即可以用String str = new String("abc");的形式来创建,也可以用Stringstr = "abc";的形式来创建(作为对比,在JDK5.0之前,你从未见过Integer i = 3;的表达式,因为类与字面值是不能通用的,除了String。而在JDK 5.0中,这种表达式是可以的!因为编译器在后台进行Integer i = new Integer(3)的转换)。前者是规范的类的创建过程,即在Java中,一切都是对象,而对象是类的实例,全部通过new()的形式来创建。Java 中的有些类,如DateFormat类,可以通过该类的getInstance()方法来返回一个新创建的类,似乎违反了此原则。其实不然。该类运用了单例模式来返回类的实例,只不过这个实例是在该类内部通过new()来创建的,而getInstance()向外部隐藏了此细节。那为什么在String str = "abc";中,并没有通过new()来创建实例,是不是违反了上述原则?其实没有。

  5. 关于String str = "abc"的内部工作。Java内部将此语句转化为以下几个步骤:
  (1)先定义一个名为str的对String类的对象引用变量:String str;
  (2)在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o 的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。
  (3)将str指向对象o的地址。
  值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用!
  
  为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。

  String str1 = "abc";
  String str2 = "abc";
  System.out.println(str1==str2);   //true
  
  注意,我们这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。
  结果说明,JVM创建了两个引用str1和str2,但只创建了一个对象,而且两个引用都指向了这个对象。

  我们再来更进一步,将以上代码改成:

  String str1 = "abc";
  String str2 = "abc";
  str1 = "bcd";
  System.out.println(str1 + "," +str2);   //bcd, abc
  System.out.println(str1==str2);   //false

  这就是说,赋值的变化导致了类对象引用的变化,str1指向了另外一个新对象!而str2仍旧指向原来的对象。上例中,当我们将str1的值改为"bcd"时,JVM发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。
  事实上,String类被设计成为不可改变(immutable)的类。如果你要改变其值,可以,但JVM在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。这个创建过程虽说是完全自动进行的,但它毕竟占用了更多的时间。在对时间要求比较敏感的环境中,会带有一定的不良影响。

  再修改原来代码:

  String str1 = "abc";
  String str2 = "abc";
  
  str1 = "bcd";
  
  String str3 = str1;
  System.out.println(str3);   //bcd

  String str4 = "bcd";
  System.out.println(str1 == str4);   //true
    
  str3 这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。

  我们再接着看以下的代码。

  String str1 = new String("abc");
  String str2 = "abc";
  System.out.println(str1==str2);   //false

  创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。

  String str1 = "abc";
  String str2 = new String("abc");
  System.out.println(str1==str2);   //false

  创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。

  以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。

  6. 数据类型包装类的值不可修改。不仅仅是String类的值不可修改,所有的数据类型包装类都不能更改其内部的值。

  7. 结论与建议:

  (1)我们在使用诸如String str = "abc";的格式定义类时,总是想当然地认为,我们创建了String类的对象str。担心陷阱!对象可能并没有被创建!唯一可以肯定的是,指向 String类的引用被创建了。至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,除非你通过new()方法来显要地创建一个新的对象。因此,更为准确的说法是,我们创建了一个指向String类的对象的引用变量str,这个对象引用变量指向了某个值为"abc"的String类。清醒地认识到这一点对排除程序中难以发现的bug是很有帮助的。

  (2)使用String str = "abc";的方式,可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。这个思想应该是享元模式的思想,但JDK的内部在这里实现是否应用了这个模式,不得而知。

  (3)当比较包装类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==。

  (4)由于String类的immutable性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。

 

简单的说:

Java把内存划分成两种:一种是栈内存,一种是堆内存。  

  在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。  

  当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。  

  堆内存用来存放由new创建的对象和数组。  

  在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。  

  在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。  

  引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。  

具体的说:

栈与堆都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

  Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

 栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(,int, short, long, byte, float, double,boolean, char)和对象句柄。

 栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:

int a = 3;

int b = 3;

编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b =3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。这时,如果再令a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。

String是一个特殊的包装类数据。可以用:

String str = newString("abc");

String str ="abc";

两种的形式来创建,第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。

而第二种是先在栈中创建一个对String类的对象引用变量str,然后查找栈中有没有存放"abc",如果没有,则将"abc"存放进栈,并令str指向”abc”,如果已经有”abc” 则直接令str指向“abc”。

   比较类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==,下面用例子说明上面的理论。

String str1 ="abc";

String str2 ="abc";

System.out.println(str1==str2);//true

可以看出str1和str2是指向同一个对象的。

String str1 =newString ("abc");

String str2 =newString ("abc");

System.out.println(str1==str2);// false

用new的方式是生成不同的对象。每一次生成一个。

   因此用第一种方式创建多个”abc”字符串,在内存中其实只存在一个对象而已. 这种写法有利与节省内存空间.同时它可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。

   另一方面, 要注意: 我们在使用诸如String str = "abc";的格式定义类时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象。由于String类的immutable性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。

java中内存分配策略及堆和栈的比较

2.1 内存分配策略

按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的.

静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间.这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求.

栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的.和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存.和我们在数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。

静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放.

2.2 堆和栈的比较

上面的定义从编译原理的教材中总结而来,除静态存储分配之外,都显得很呆板和难以理解,下面撇开静态存储分配,集中比较堆和栈:

从堆和栈的功能和作用来通俗的比较,堆主要用来存放对象的,栈主要是用来执行程序的.而这种不同又主要是由于堆和栈的特点决定的:

在编程中,例如C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带(conveyorbelt)一样,Stack Pointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行.退出函数的时候,修改栈指针就可以把栈中的内容销毁.这样的模式速度最快, 当然要用来运行程序了.需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时.

堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率非常低.但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定.在C++中,要求创建一个对象时,只需用 new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存.当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是导致我们刚才所说的效率低的原因,看来列宁同志说的好,人的优点往往也是人的缺点,人的缺点往往也是人的优点(晕~).

2.3 JVM中的堆和栈

JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。

我们知道,某个线程正在执行的方法称为此线程的当前方法.我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据.这个帧在这里和编译原理中的活动纪录的概念是差不多的.

从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。

每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享.跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。

Java栈与堆

----对这两个概念的不明好久,终于找到一篇好文,拿来共享

1. 栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

2. 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享,详见第3点。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

3. Java中的数据类型有两种。

一种是基本类型(primitive types), 共有8种,即int, short, long, byte, float, double,boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。

另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:

复制内容到剪贴板代码:

int a = 3;

int b = 3;

编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

另一种是包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。

4. String是一个特殊的包装类数据。即可以用String str = new String("abc");的形式来创建,也可以用String str = "abc";的形式来创建(作为对比,在JDK 5.0之前,你从未见过Integeri = 3;的表达式,因为类与字面值是不能通用的,除了String。而在JDK 5.0中,这种表达式是可以的!因为编译器在后台进行Integer i = new Integer(3)的转换)。前者是规范的类的创建过程,即在Java中,一切都是对象,而对象是类的实例,全部通过new()的形式来创建。Java中的有些类,如DateFormat类,可以通过该类的getInstance()方法来返回一个新创建的类,似乎违反了此原则。其实不然。该类运用了单例模式来返回类的实例,只不过这个实例是在该类内部通过new()来创建的,而getInstance()向外部隐藏了此细节。那为什么在String str = "abc";中,并没有通过new()来创建实例,是不是违反了上述原则?其实没有。

5. 关于String str = "abc"的内部工作。Java内部将此语句转化为以下几个步骤:

(1)先定义一个名为str的对String类的对象引用变量:Stringstr;

(2)在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。

(3)将str指向对象o的地址。

值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用!

为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。

复制内容到剪贴板代码:

String str1 ="abc";

String str2 ="abc";

System.out.println(str1==str2);//true

注意,我们这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。

结果说明,JVM创建了两个引用str1和str2,但只创建了一个对象,而且两个引用都指向了这个对象。

我们再来更进一步,将以上代码改成:

复制内容到剪贴板代码:

String str1 ="abc";

String str2 ="abc";

str1 ="bcd";

System.out.println(str1+ "," + str2); //bcd, abc

System.out.println(str1==str2);//false

这就是说,赋值的变化导致了类对象引用的变化,str1指向了另外一个新对象!而str2仍旧指向原来的对象。上例中,当我们将str1的值改为"bcd"时,JVM发现在栈中没有存放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。

事实上,String类被设计成为不可改变(immutable)的类。如果你要改变其值,可以,但JVM在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。这个创建过程虽说是完全自动进行的,但它毕竟占用了更多的时间。在对时间要求比较敏感的环境中,会带有一定的不良影响。

再修改原来代码:

复制内容到剪贴板代码:

String str1 ="abc";

String str2 ="abc";

str1 ="bcd";

String str3 = str1;

System.out.println(str3);//bcd

String str4 ="bcd";

System.out.println(str1== str4); //true

str3这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。

我们再接着看以下的代码。

复制内容到剪贴板代码:

String str1 = newString("abc");

String str2 ="abc";

System.out.println(str1==str2);//false 创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。

String str1 ="abc";

String str2 = newString("abc");

System.out.println(str1==str2);//false

创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。

以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。

6. 数据类型包装类的值不可修改。不仅仅是String类的值不可修改,所有的数据类型包装类都不能更改其内部的值。 7. 结论与建议:

(1)我们在使用诸如String str = "abc";的格式定义类时,总是想当然地认为,我们创建了String类的对象str。担心陷阱!对象可能并没有被创建!唯一可以肯定的是,指向String类的引用被创建了。至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,除非你通过new()方法来显要地创建一个新的对象。因此,更为准确的说法是,我们创建了一个指向String类的对象的引用变量str,这个对象引用变量指向了某个值为"abc"的String类。清醒地认识到这一点对排除程序中难以发现的bug是很有帮助的。

(2)使用String str = "abc";的方式,可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。这个思想应该是享元模式的思想,但JDK的内部在这里实现是否应用了这个模式,不得而知。

(3)当比较包装类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==。

(4)由于String类的immutable性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。

 

Java把内存划分成两种:一种是栈内存,一种是堆内存。

        在方法中定义的一些基本类型(原生类型)的变量和对象的引用变量都在函数的栈内存中分配。

        当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

        堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

        在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。

具体说:
        栈与堆都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

        Java的堆是一个运行时数据区,类的对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

        栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(int, short, long, byte, float, double,boolean, char)和对象句柄。

        栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:

1.       int a = 3;  

2.       int b = 3;  

int a = 3;

int b = 3;


        编译器先处理int a= 3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b = 3;在创建完b的引用变量后,因为在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的情况。这时,如果再令a=4;那么编译器会重新搜索栈中是否有4值,如果没有,则将4存放进来,并令a指向4;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。(对这里基本数据类型的说法存疑)

        String是一个特殊的包装类数据。可以用:

 

1.       String str = new String("abc");  

2.       String str = "abc";  

String str = new String("abc");

String str = "abc";


        两种的形式来创建,第一种是用new()来新建对象的,它会在存放于堆中。每调用一次就会创建一个新的对象。具体可以参见String的一点东西

        用第一种方式创建多个”abc”字符串,在内存中其实只存在一个对象而已. 这种写法有利与节省内存空间.同时它可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。

        另一方面, 要注意: 我们在使用诸如String str = "abc";的格式定义类时,总是想当然地认为,创建了String类的对象str。担心陷阱!对象可能并没有被创建!而可能只是指向一个先前已经创建的对象。只有通过new()方法才能保证每次都创建一个新的对象。

        由于String类的immutable性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。


        C语言中的堆和栈
        栈:C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带(conveyor belt)一样,StackPointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行.退出函数的时候,修改栈指针就可以把栈中的内容销毁.这样的模式速度最快, 当然要用来运行程序了.需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时.

        堆:堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率非常低.但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定.在C++中,要求创建一个对象时,只需用 new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存.当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是导致我们刚才所说的效率低的原因

        申请后系统的响应

        栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

        堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

        申请大小的限制

        栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也可能是1M,它是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

        堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

        申请效率的比较:

        栈由系统自动分配,速度较快。但程序员是无法控制的。

        堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

        另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

        堆和栈中的存储内容

        栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
        当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

        堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

        存取效率的比较

1.       char s1[] = "aaaaaaaaaaaaaaa";  

2.       char *s2 = "bbbbbbbbbbbbbbbbb";  

char s1[] = "aaaaaaaaaaaaaaa";

char *s2 = "bbbbbbbbbbbbbbbbb";



        aaaaaaaaaaa是在运行时刻赋值的;而bbbbbbbbbbb是在编译时就确定的;

        但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。

1.       void main()    

2.       {   

3.         char a = 1;    

4.         char c[] = "1234567890";    

5.         char *p ="1234567890";    

6.         a = c[1];    

7.         a = p[1];    

8.         return;    

9.       }  

void main()

{

  char a = 1;

  char c[] = "1234567890";

  char *p ="1234567890";

  a = c[1];

  a = p[1];

  return;

}


        对应的汇编代码

       10: a = c[1];
       00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
       0040106A 88 4D FC mov byte ptr [ebp-4],cl
       11: a = p[1];
       0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
       00401070 8A 42 01 mov al,byte ptr [edx+1]
       00401073 88 45 FC mov byte ptr [ebp-4],al
        第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。

        JVM中的堆和栈
        JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。(在这里堆栈即前面所说的栈。)

        我们知道,某个线程正在执行的方法称为此线程的当前方法.我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据.这个帧在这里和编译原理中的活动纪录的概念是差不多的.

        从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。

        每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享.跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。

        对于方法来说:
        1、方法本身是指令的操作码部分,保存在Stack中;

        2、方法内部变量作为指令的操作数部分,跟在指令的操作码之后,保存在Stack中(实际上是简单类型保存在Stack中,对象类型在Stack中保存地址,在Heap中保存其本身);上述的指令操作码和指令操作数构成了完整的Java指令。

        总结:
        在java中:
        1、成员变量:实例变量存放在堆中,其引用在栈中;静态变量存放在栈中。
        2、方法中的局部变量分为引用变量和基本类型变量。引用变量所指向的对象存放在堆中,而引用变量本身存放在栈中;基本类型变量存放在栈中。
        3、所有new出来的对象都存放在堆中,其引用放在栈中。
        4、方法本身相当于指令的操作码部分,存放在栈中。

        通过以上分析可以看出:堆存放了数据,栈存放了逻辑。堆与栈职能的分离有利于数据之间的共享,更体现出了面向对象的思想。

 

 

java中内存分配策略及堆和栈的比较

 

内存分配策略

按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的. 静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间.这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求.

栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的.和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存.和我们在数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。

静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放.

堆和栈的比较

上面的定义从编译原理的教材中总结而来,除静态存储分配之外,都显得很呆板和难以理解,下面撇开静态存储分配,集中比较堆和栈: 从堆和栈的功能和作用来通俗的比较,堆主要用来存放对象的,栈主要是用来执行程序的.而这种不同又主要是由于堆和栈的特点决定的: 在编程中,例如C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带(conveyor belt)一样,StackPointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行.退出函数的时候,修改栈指针就可以把栈中的内容销毁.这样的模式速度最快,当然要用来运行程序了.需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时. 堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率非常低.但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定.在C++中,要求创建一个对象时,只需用 new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存.当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是导致我们刚才所说的效率低的原因,看来列宁同志说的好,人的优点往往也是人的缺点,人的缺点往往也是人的优点(晕~).

JVM中的堆和栈

JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。 我们知道,某个线程正在执行的方法称为此线程的当前方法.我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据.这个帧在这里和编译原理中的活动纪录的概念是差不多的.

从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。

每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程 共享.跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。

GC的思考

Java为什么慢?JVM的存在当然是一个原因,但有人说,在Java中,除了简单类型(int,char等)的数据结构,其它都是在堆中分配内存(所以说Java的一切都是对象),这也是程序慢的原因之一。

我的想法是(应该说代表TIJ的观点),如果没有Garbage Collector(GC),上面的说法就是成立的.堆不象栈是连续的空间,没有办法指望堆本身的内存分配能够象堆栈一样拥有传送带般的速度,因为,谁会为你整理庞大的堆空间,让你几乎没有延迟的从堆中获取新的空间呢?

这个时候,GC站出来解决问题.我们都知道GC用来清除内存垃圾,为堆腾出空间供程序使用,但GC同时也担负了另外一个重要的任务,就是要让Java中堆的内存分配和其他语言中堆栈的内存分配一样快,因为速度的问题几乎是众口一词的对Java的诟病.要达到这样的目的,就必须使堆的分配也能够做到象传送带一样,不用自己操心去找空闲空间.这样,GC除了负责清除Garbage外,还要负责整理堆中的对象,把它们转移到一个远离Garbage的纯净空间中无间隔的排列起来,就象堆栈中一样紧凑,这样Heap Pointer就可以方便的指向传送带的起始位置,或者说一个未使用的空间,为下一个需要分配内存的对象"指引方向".因此可以这样说,垃圾收集影响了对象的创建速度,听起来很怪,对不对?

那GC怎样在堆中找到所有存活的对象呢?前面说了,在建立一个对象时,在堆中分配实际建立这个对象的内存,而在堆栈中分配一个指向这个堆对象的指针(引 用),那么只要在堆栈(也有可能在静态存储区)找到这个引用,就可以跟踪到所有存活的对象.找到之后,GC将它们从一个堆的块中移到另外一个堆的块中,并 将它们一个挨一个的排列起来,就象我们上面说的那样,模拟出了一个栈的结构,但又不是先进后出的分配,而是可以任意分配的,在速度可以保证的情况下,Isn't it great?

 但是,列宁同志说了,人的优点往往也是人的缺点,人的缺点往往也是人的优点(再晕~~).GC()的运行要占用一个线程,这本身就是一个降低程序运行性能 的缺陷,更何况这个线程还要在堆中把内存翻来覆去的折腾.不仅如此,如上面所说,堆中存活的对象被搬移了位置,那么所有对这些对象的引用都要重新赋值.这 些开销都会导致性能的降低.

基础数据类型直接在栈空间分配,方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收。引用数据类型,需要用new来创建,既在栈空间 分配一个地址空间,又在堆空间分配对象的类变量 。方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完成后从栈空间回收。局部变量new出来时,在栈空间和堆空间中分配空 间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收。方法调用时传入的literal参数,先在栈空间分配,在方法调用完成后从栈 空间分配。字符串常量在DATA区域分配,this在堆空间分配。数组既在栈空间分配数组名称,又在堆空间分配数组实际的大小!

JVM中的堆和栈

JVM是基于堆栈的虚拟机。JVM为每个新创建的线程都分配一个堆栈。也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。 我们知道,某个线程正在执行的方法称为此线程的当前方法。我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据。这个帧在这里 和编译原理中的活动纪录的概念是差不多的。 从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。

每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有 的线程共享。跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分 配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。

 

Java内存模型

原 本准备把内存模型单独放到某一篇文章的某个章节里面讲解,后来查阅了国外很多文档才发现其实JVM内存模型的内容还蛮多的,所以直接作为一个章节的基础知 识来讲解,可能该章节概念的东西比较多。一个开发Java的开发者,一旦了解了JVM内存模型就能够更加深入地了解该语言的语言特性,可能这个章节更多的 是概念,没有太多代码实例,所以希望读者谅解,有什么笔误来Email告知:silentbalanceyh@126.com,本文尽量涵盖所有Java语言可以碰到的和内存相关的内容,同样也会提到一些和内存相关的计算机语言的一些知识,为草案。因为平时开发的时候没有特殊情况 不会进行内存管理,所以有可能有笔误的地方比较多,我用的是Windows平台,所以本文涉及到的与操作系统相关的只是仅仅局限于Windows平台。不 仅仅如此,这一个章节牵涉到的多线程和另外一些内容并没有讲到,这里主要是结合JVM内部特性把本章节作为核心的概念性章节来讲解,这样方便初学者深入以 及彻底理解Java语言)
本文章节:

1.JMM简介

2.堆和栈

3.本机内存

4.防止内存泄漏

 

1.JMM简介

  i.内存模型概述

  Java平台自动集成了线程以及多处理器技术,这种集成程度比Java以前诞生的计算机语言要厉害很多,该语言针对多种异构平台的平台独立性而使用的多线程技术支持也是具有开拓性的一面,有时候在开发Java同步和线程安全要求很严格的程序时,往往容易混淆的一个概念就是内存模型。究竟什么是内存模型?内存模型描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节,对象最终是存储在内存里面的,这点没有错,但是编译器、运行库、处理器或者系统缓存可以有特权在变量指定内存位置存储或者取出变量的值。【JMM】(JavaMemory Model的缩写)允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了final或synchronized明确请求了某些可见性的保证。

  1)JSR133:

  在Java语言规范里面指出了JMM是一个比较开拓性的尝试,这种尝试视图定义一个一致的、跨平台的内存模型,但是它有一些比较细微而且很重要的缺点。其实Java语言里面比较容易混淆的关键字主要是synchronized和volatile,也因为这样在开发过程中往往开发者会忽略掉这些规则,这也使得编写同步代码比较困难。

  JSR133本身的目的是为了修复原本JMM的一些缺陷而提出的,其本身的制定目标有以下几个:

  • 保留目前JVM的安全保证,以进行类型的安全检查
  • 提供(out-of-thin-air safety)无中生有安全性,这样“正确同步的”应该被正式而且直观地定义
  • 程序员要有信心开发多线程程序,当然没有其他办法使得并发程序变得很容易开发,但是该规范的发布主要目标是为了减轻程序员理解内存模型中的一些细节负担
  • 提供大范围的流行硬件体系结构上的高性能JVM实现,现在的处理器在它们的内存模型上有着很大的不同,JMM应该能够适合于实际的尽可能多的体系结构而不以性能为代价,这也是Java跨平台型设计的基础
  • 提供一个同步的习惯用法,以允许发布一个对象使他不用同步就可见,这种情况又称为初始化安全(initialization safety)的新的安全保证
  • 对现有代码应该只有最小限度的影响

  2)同步、异步【这里仅仅指概念上的理解,不牵涉到计算机底层基础的一些操作】:

  在系统开发过程,经常会遇到这几个基本概念,不论是网络通讯、对象之间的消息通讯还是Web开发人员常用的Http请求都会遇到这样几个概念,经常有人提到Ajax是异步通讯方式,那么究竟怎样的方式是这样的概念描述呢?

  同步:同步就是在发出一个功能调用的时候,在没有得到响应之前,该调用就不返回, 按照这样的定义,其实大部分程序的执行都是同步调用的,一般情况下,在描述同步和异步操作的时候,主要是指代需要其他部件协作处理或者需要协作响应的一些 任务处理。比如有一个线程A,在A执行的过程中,可能需要B提供一些相关的执行数据,当然触发B响应的就是A向B发送一个请求或者说对B进行一个调用操 作,如果A在执行该操作的时候是同步的方式,那么A就会停留在这个位置等待B给一个响应消息,在B没有任何响应消息回来的时候,A不能做其他事情,只能等 待,那么这样的情况,A的操作就是一个同步的简单说明。

  异步:异步就是在发出一个功能调用的时候,不需要等待响应,继续进行它该做的事情,一旦得到响应了过后给予一定的处理, 但是不影响正常的处理过程的一种方式。比如有一个线程A,在A执行的过程中,同样需要B提供一些相关数据或者操作,当A向B发送一个请求或者对B进行调用 操作过后,A不需要继续等待,而是执行A自己应该做的事情,一旦B有了响应过后会通知A,A接受到该异步请求的响应的时候会进行相关的处理,这种情况下A 的操作就是一个简单的异步操作。

  3)可见性、可排序性

  Java内存模型的两个关键概念:可见性(Visibility)可排序性(Ordering)

  开发过多线程程序的程序员都明白,synchronized关键字强制实施一个线程之间的互斥锁(相互排斥),该互斥锁防止每次有多个线程进入一个给定监控器所保护的同步语句块,也就是说在该情况下,执行程序代码所独有的某些内存是独占模式其他的线程是不能针对它执行过程所独占的内存进行访问的,这种情况称为该内存不可见。但是在该模型的同步模式中,还有另外一个方面:JMM中指出了,JVM在处理该强制实施的时候可以提供一些内存的可见规则,在该规则里面,它确保当存在一个同步块时,缓存被更新,当输入一个同步块时,缓存失效。因此在JVM内部提供给定监控器保护的同步块之中,一个线程所写入的值对于其余所有的执行由同一个监控器保护的同步块线程来说是可见的,这就是一个简单的可见性的描述。这种机器保证编译器不会把指令从一个同步块的内部移到外部,虽然有时候它会把指令由外部移动到内部。JMM在缺省情况下不做这样的保证——只要有多个线程访问相同变量时必须使用同步。简单总结:

  可见性就是在多核或者多线程运行过程中内存的一种共享模式,在JMM模型里面,通过并发线程修改变量值的时候,必须将线程变量同步回主存过后,其他线程才可能访问到。

  【*:简单讲,内存的可见性使内存资源可以共享,当一个线程执行的时候它所占有的内存,如果它占有的内存资源是可见的,那么这时候其他线程在一定规则内是可以访问该内存资源的,这种规则是由JMM内部定义的,这种情况下内存的该特性称为其可见性。】

  可排序性提供了内存内部的访问顺序,在不同的程序针对不同的内存块进行访问的时候,其访问不是无序的, 比如有一个内存块,A和B需要访问的时候,JMM会提供一定的内存分配策略有序地分配它们使用的内存,而在内存的调用过程也会变得有序地进行,内存的折中 性质可以简单理解为有序性。而在Java多线程程序里面,JMM通过Java关键字volatile来保证内存的有序访问。

  ii.JMM结构:

  1)简单分析:

  Java语言规范中提到过,JVM中存在一个主存区(Main Memory或Java Heap Memory),Java中所有变量都是存在主存中的,对于所有线程进行共享,而每个线程又存在自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。而在多核处理器下,大部分数据存储在高速缓存中,如果高速缓存不经过内存的时候,也是不可见的一种表现。在Java程序中,内存本身是比较昂贵的资源,其实不仅仅针对Java应用程序,对操作系统本身而言内存也属于昂贵资源,Java程序在性能开销过程中有几个比较典型的可控制的来源。synchronized和volatile关键字提供的内存中模型的可见性保证程序使用一个特殊的、存储关卡(memory barrier)的指令,来刷新缓存,使缓存无效,刷新硬件的写缓存并且延迟执行的传递过程,无疑该机制会对Java程序的性能产生一定的影响。

  JMM的最初目的,就是为了能够支持多线程程序设计的,每个线程可以认为是和其他线程不同的CPU上运行,或者对于多处理器的机器而言,该模型需要实现的就是使得每一个线程就像运行在不同的机器、不同的CPU或者本身就不同的线程上一样,这种情况实际上在项目开发中是常见的。对于CPU本身而言,不能直接访问其他CPU的寄存器,模型必须通过某种定义规则来使得线程和线程在工作内存中进行相互调用而实现CPU本身对其他CPU、或者说线程对其他线程的内存中资源的访问,而表现这种规则的运行环境一般为运行该程序的运行宿主环境(操作系统、服务器、分布式系统等),而程序本身表现就依赖于编写该程序的语言特性,这里也就是说用Java编写的应用程序在内存管理中的实现就是遵循其部分原则,也就是前边提及到的JMM定义了Java语言针对内存的一些的相关规则。然而,虽然设计之初是为了能够更好支持多线程,但是该模型的应用和实现当然不局限于多处理器,而在JVM编译器编译Java编写的程序的时候以及运行期执行该程序的时候,对于单CPU的系统而言,这种规则也是有效的,这就是是上边提到的线程和线程之间的内存策略。JMM本身在描述过程没有提过具体的内存地址以及在实现该策略中的实现方法是由JVM的哪一个环节(编译器、处理器、缓存控制器、其他)提供的机制来实现的,甚至针对一个开发非常熟悉的程序员,也不一定能够了解它内部对于类、对象、方法以及相关内容的一些具体可见的物理结构。相反,JMM定义了一个线程与主存之间的抽象关系,其实从上边的图可以知道,每一个线程可以抽象成为一个工作内存(抽象的高速缓存和寄存器),其中存储了Java的一些值,该模型保证了Java里面的属性、方法、字段存在一定的数学特性,按照该特性,该模型存储了对应的一些内容,并且针对这些内容进行了一定的序列化以及存储排序操作,这样使得Java对象在工作内存里面被JVM顺利调用,(当然这是比较抽象的一种解释)既然如此,大多数JMM的规则在实现的时候,必须使得主存和工作内存之间的通信能 够得以保证,而且不能违反内存模型本身的结构,这是语言在设计之处必须考虑到的针对内存的一种设计方法。这里需要知道的一点是,这一切的操作在Java语 言里面都是依靠Java语言自身来操作的,因为Java针对开发人员而言,内存的管理在不需要手动操作的情况下本身存在内存的管理策略,这也是Java自 己进行内存管理的一种优势。

  [1]原子性(Atomicity):

  这一点说明了该模型定义的规则针对原子级别的内容存在独立的影响,对于模型设计最初,这些规则需要说明的仅仅是最简单的读取和存储单元写入的的一些操作,这种原子级别的包括——实例、静态变量、数组元素,只是在该规则中不包括方法中的局部变量。

  [2]可见性(Visibility):

  在该规则的约束下,定义了一个线程在哪种情况下可以访问另外一个线程或者影响另外一个线程,从JVM的操作上讲包括了从另外一个线程的可见区域读取相关数据以及将数据写入到另外一个线程内。

  [3]可排序性(Ordering):

  该规则将会约束任何一个违背了规则调用的线程在操作过程中的一些顺序,排序问题主要围绕了读取、写入和赋值语句有关的序列。

  如果在该模型内部使用了一致的同步性的时候,这些属性中的每一个属性都遵循比较简单的原则:和所有同步的内存块一样,每个同步块之内的任何变化都具备了原子性以及可见性,和其他同步方法以及同步块遵循同样一致的原则,而且在这样的一个模型内,每个同步块不能使用同一个锁,在整个程序的调用过程是按照编写的程序指定指令运行的。即使某一个同步块内的处理可能会失效,但是该问题不会影响到其他线程的同步问题,也不会引起连环失效。简单讲:当程序运行的时候使用了一致的同步性的时候,每个同步块有一个独立的空间以及独立的同步控制器和锁机制,然后对外按照JVM的执行指令进行数据的读写操作。这种情况使得使用内存的过程变得非常严谨!

  如果不使用同步或者说使用同步不一致(这里可以理解为异步,但不一定是异步操作), 该程序执行的答案就会变得极其复杂。而且在这样的情况下,该内存模型处理的结果比起大多数程序员所期望的结果而言就变得十分脆弱,甚至比起JVM提供的实 现都脆弱很多。因为这样所以出现了Java针对该内存操作的最简单的语言规范来进行一定的习惯限制,排除该情况发生的做法在于:

  JVM线程必须依靠自身来维持对象的可见性以及对象自身应该提供相对应的操作而实现整个内存操作的三个特性,而不是仅仅依靠特定的修改对象状态的线程来完成如此复杂的一个流程。

  【*:综上所属,JMM在JVM内部实现的结构就变得相对复杂,当然一般的Java初学者可以不用了解得这么深入。】

  [4]三个特性的解析(针对JMM内部):

  原子性(Atomicity):

  访问存储单元内的任何类型的字段的值以及对其更新操作的时候,除开long类型和double类型,其他类型的字段是必须要保证其原子性的,这些字段也包括为对象服务的引用。此外,该原子性规则扩展可以延伸到基于long和double的另外两种类型volatile longvolatiledouble(volatile为java关键字),没有被volatile声明的long类型以及double类型的字段值虽然不保证其JMM中的原子性,但是是被允许的。针对non-long/non-double的字段在表达式中使用的时候,JMM的原子性有这样一种规则:如果你获得或者初始化该值或某一些值的时候,这些值是由其他线程写入,而且不是从两个或者多个线程产生的数据在同一时间戳混合写入的时候,该字段的原子性在JVM内部是必须得到保证的。也就是说JMM在定义JVM原子性的时候,只要在该规则不违反的条件下,JVM本身不去理睬该数据的值是来自于什么线程,因为这样使得Java语言在并行运算的设计的过程中针对多线程的原子性设计变得极其简单,而且即使开发人员没有考虑到最终的程序也没有太大的影响。再次解释一下:这里的原子性指的是原子级别的操作,比如最小的一块内存的读写操作,可以理解为Java语言最终编译过后最接近内存的最底层的操作单元,这种读写操作的数据单元不是变量的值,而是本机码,也就是前边在讲《Java基础知识》中提到的由运行器解释的时候生成的Native Code

  可见性(Visibility):

  当一个线程需要修改另外线程的可见单元的时候必须遵循以下原则:

  • 一个写入线程释放的同步锁和紧随其后进行读取的读线程的同步锁是同一个
    从本质上讲,释放锁操作强迫它的隶属线程【释放锁的线程】从工作内存中的写入缓存里面刷新(专业上讲这里不应该是刷新,可以理解为提供)数据(flush操作),然后获取锁操作使得另外一个线程【获得锁的线程】直接读取前一个线程可访问域(也就是可见区域)的字段的值。因为该锁内部提供了一个同步方法或者同步块,该同步内容具有线程排他性,这样就使得上边两个操作只能针对单一线程在同步内容内部进行操作,这样就使得所有操作该内容的单一线程具有该同步内容(加锁的同步方法或者同步块)内的线程排他性,这种情况的交替也可以理解为具有“短暂记忆效应”。
    这里需要理解的是同步双重含义:使用锁机制允许基于高层同步协议进行处理操作,这是最基本的同步;同时系统内存(很多时候这里是指基于机器指令的底层存储关卡memory barrier,前边提到过)在处理同步的时候能够跨线程操作,使得线程和线程之间的数据是同步的。这 样的机制也折射出一点,并行编程相对于顺序编程而言,更加类似于分布式编程。后一种同步可以作为JMM机制中的方法在一个线程中运行的效果展示,注意这里 不是多个线程运行的效果展示,因为它反应了该线程愿意发送或者接受的双重操作,并且使得它自己的可见区域可以提供给其他线程运行或者更新,从这个角度来 看,使用锁和消息传递可以视为相互之间的变量同步,因为相对其他线程而言,它的操作针对其他线程也是对等的。
  • 一旦某个字段被申明为volatile,在任何一个写入线程在工作内存中刷新缓存的之前需要进行进一步的内存操作,也就是说针对这样的字段进行立即刷新,可以理解为这种volatile不会出现一般变量的缓存操作,而读取线程每次必须根据前一个线程的可见域里面重新读取该变量的值,而不是直接读取。
  • 当某个线程第一次去访问某个对象的域的时候,它要么初始化该对象的值,要么从其他写入线程可见域里面去读取该对象的值;这里结合上边理解,在满足某种条件下,该线程对某对象域的值的读取是直接读取,有些时候却需要重新读取。
    这里需要小心一点的是,在并发编程里面,不好的一个实践就是使用一个合法引用去引用不完全构造的对象,这 种情况在从其他写入线程可见域里面进行数据读取的时候发生频率比较高。从编程角度上讲,在构造函数里面开启一个新的线程是有一定的风险的,特别是该类是属 于一个可子类化的类的时候。Thread.start由调用线程启动,然后由获得该启动的线程释放锁具有相同的“短暂记忆效应”,如果一个实现了 Runnable接口的超类在子类构造子执行之前调用了Thread(this).start()方法,那么就可能使得该对象在线程方法run执行之前并没有被完全初始化,这样就使得一个指向该对象的合法引用去引用了不完全构造的一个对象。同样的,如果创建一个新的线程T并且启动该线程,然后再使用线程T来创建对象X,这种情况就不能保证X对象里面所有的属性针对线程T都是可见的除非是在所有针对X对象的引用中进行同步处理,或者最好的方法是在T线程启动之前创建对象X。
  • 若一个线程终止,所有的变量值都必须从工作内存中刷到主存,比如,如果一个同步线程因为另一个使用Thread.join方法的线程而终止,那么该线程的可见域针对那个线程而言其发生的改变以及产生的一些影响是需要保证可知道的。

  注意:如果在同一个线程里面通过方法调用去传一个对象的引用是绝对不会出现上边提及到的可见性问题的。JMM保证所有上边的规定以及关于内存可见性特性的描述——一个特殊的更新、一个特定字段的修改都是某个线程针对其他线程的一个“可见性”的概念,最终它发生的场所在内存模型中Java线程和线程之间,至于这个发生时间可以是一个任意长的时间,但是最终会发生,也就是说,Java内存模型中的可见性的特性主要是针对线程和线程之间使用内存的一种规则和约定,该约定由JMM定义。

   不仅仅如此,该模型还允许不同步的情况下可见性特性。比如针对一个线程提供一个对象或者字段访问域的原始值进行操作,而针对另外一个线程提供一个对象或 者字段刷新过后的值进行操作。同样也有可能针对一个线程读取一个原始的值以及引用对象的对象内容,针对另外一个线程读取一个刷新过后的值或者刷新过后的引 用。

  尽管如此,上边的可见性特性分析的一些特征在跨线程操作的时候是有可能失败的,而且不能够避免这些故障发生。这是一个不争的事实,使用同步多线程的代码并不能绝对保证线 程安全的行为,只是允许某种规则对其操作进行一定的限制,但是在最新的JVM实现以及最新的Java平台中,即使是多个处理器,通过一些工具进行可见性的 测试发现其实是很少发生故障的。跨线程共享CPU的共享缓存的使用,其缺陷就在于影响了编译器的优化操作,这也体现了强有力的缓存一致性使 得硬件的价值有所提升,因为它们之间的关系在线程与线程之间的复杂度变得更高。这种方式使得可见度的自由测试显得更加不切实际,因为这些错误的发生极为罕 见,或者说在平台上我们开发过程中根本碰不到。在并行程开发中,不使用同步导致失败的原因也不仅仅是对可见度的不良把握导致的,导致其程序失败的原因是多 方面的,包括缓存一致性、内存一致性问题等。

  可排序性(Ordering):

  可排序规则在线程与线程之间主要有下边两点:

  • 从操作线程的角度看来,如果所有的指令执行都是按照普通顺序进行,那么对于一个顺序运行的程序而言,可排序性也是顺序的
  • 从其他操作线程的角度看来,排序性如同在这个线程中运行在非同步方法中的一个“间谍”,所以任何事情都有可能发生。唯一有用的限制是同步方法和同步块的相对排序,就像操作volatile字段一样,总是保留下来使用

  【*: 如何理解这里“间谍”的意思,可以这样理解,排序规则在本线程里面遵循了第一条法则,但是对其他线程而言,某个线程自身的排序特性可能使得它不定地访问执 行线程的可见域,而使得该线程对本身在执行的线程产生一定的影响。举个例子,A线程需要做三件事情分别是A1、A2、A3,而B是另外一个线程具有操作 B1、B2,如果把参考定位到B线程,那么对A线程而言,B的操作B1、B2有可能随时会访问到A的可见区域,比如A有一个可见区域a,A1就是把a修改称为1,但是B线程在A线程调用了A1过后,却访问了a并且使用B1或者B2操作使得a发生了改变,变成了2,那么当A按照排序性进行A2操作读取到a的值的时候,读取到的是2而不是1,这样就使得程序最初设计的时候A线程的初衷发生了改变,就是排序被打乱了,那么B线程对A线程而言,其身份就是“间 谍”,而且需要注意到一点,B线程的这些操作不会和A之间存在等待关系,那么B线程的这些操作就是异步操作,所以针对执行线程A而言,B的身份就是“非同步方法中的‘间谍’。】

   同样的,这仅仅是一个最低限度的保障性质,在任何给定的程序或者平台,开发中有可能发现更加严格的排序,但是开发人员在设计程序的时候不能依赖这种排 序,如果依赖它们会发现测试难度会成指数级递增,而且在复合规定的时候会因为不同的特性使得JVM的实现因为不符合设计初衷而失败。

  注意:第 一点在JLS(JavaLanguage Specification)的所有讨论中也是被采用的,例如算数表达式一般情况都是从上到下、从左到右的顺序,但是这一点需要理解的是,从其他操作线程 的角度看来这一点又具有不确定性,对线程内部而言,其内存模型本身是存在排序性的。【*:这里讨论的排序是最底层的内存里面执行的时候的NativeCode的排序,不是说按照顺序执行的Java代码具有的有序性质,本文主要分析的是JVM的内存模型,所以希望读者明白这里指代的讨论单元是内存区。】

  iii.原始JMM缺陷:

  JMM最初设计的时候存在一定的缺陷,这种缺陷虽然现有的JVM平台已经修复,但是这里不得不提及,也是为了读者更加了解JMM的设计思路,这一个小节的概念可能会牵涉到很多更加深入的知识,如果读者不能读懂没有关系先看了文章后边的章节再返回来看也可以。

  1)问题1:不可变对象不是不可变的

  学过Java的朋友都应该知道Java中的不可变对象,这一点在本文最后讲解String类的时候也会提及,而JMM最初设计的时候,这个问题一直都存在,就是:不可变对象似乎可以改变它们的值(这种对象的不可变指通过使用final关键字来得到保证),(PublisService Reminder:让一个对象的所有字段都为final并不一定使得这个对象不可变——所有类型还必须是原始类型而不能是对象的引用。而不可变对象被认为不要求同步的。但是,因为在将内存写方面的更改从一个线程传播到另外一个线程的时候存在潜在的延迟,这样就使得有可能存在一种竞态条件,即允许一个线程首先看到不可变对象的一个值,一段时间之后看到的是一个不同的值。这种情况以前怎么发生的呢?在JDK 1.4中的String实现里,这儿基本有三个重要的决定性字段:对字符数组的引用、长度和描述字符串的开始数组的偏移量。String就是以这样的方式在JDK1.4中实现的,而不是只有字符数组,因此字符数组可以在多个String和StringBuffer对象之间共享,而不需要在每次创建一个String的时候都拷贝到一个新的字符数组里。假设有下边的代码:

String s1= "/usr/tmp";

String s2 =s1.substring(4); // "/tmp"

  这种情况下,字符串s2将具有大小为4的长度和偏移量,但是它将和s1共享“/usr/tmp”里面的同一字符数组,在String构造函数运行之前,Object的构造函数将用它们默认的值初始化所有的字段,包括决定性的长度和偏移字段。当String构造函数运行的时候,字符串长度和偏移量被设置成所需要的值。但是在旧的内存模型中,因为缺乏同步,有可能另一个线程会临时地看到偏移量字段具有初始默认值0,而后又看到正确的值4,结果是s2的值从“/usr”变成了“/tmp”,这并不是我们真正的初衷,这个问题就是原始JMM的第一个缺陷所在,因为在原始JMM模型里面这是合理而且合法的,JDK 1.4以下的版本都允许这样做。

  2)问题2:重新排序的易失性和非易失性存储

  另一个主要领域是与volatile字段的内存操作重新排序有关,这个领域中现有的JMM引起了一些比较混乱的结果。现有的JMM表明易失性的读和写是直接和主存打交道的,这样避免了把值存储到寄存器或者绕过处理器特定的缓存, 这使得多个线程一般能看见一个给定变量最新的值。可是,结果是这种volatile定义并没有最初想象中那样如愿以偿,并且导致了volatile的重大 混乱。为了在缺乏同步的情况下提供较好的性能,编译器、运行时和缓存通常是允许进行内存的重新排序操作的,只要当前执行的线程分辨不出它们的区别。(这就 是within-thread as-if-serial semantics[线程内似乎是串行]的解释)但是,易失性的读和写是完全跨线程安排的,编译器或缓存不能在彼此之间重新排序易失性的读和写。遗憾的是,通过参考普通变量的读写,JMM允许易失性的读和写被重排序,这样以为着开发人员不能使用易失性标志作为操作已经完成的标志。比如:

Map configOptions;

char[] configText;

volatileboolean initialized = false;

 

// 线程1

configOptions= new HashMap();

configText =readConfigFile(filename);

processConfigOptions(configText,configOptions);

initialized= true;

 

// 线程2

while(!initialized)

   sleep();

   这里的思想是使用易失性变量initialized担任守卫来表明一套别的操作已经完成了,这是一个很好的思想,但是不能在JMM下工作,因为旧的 JMM允许非易失性的写(比如写到configOptions字段,以及写到由configOptions引用Map的字段中)与易失性的写一起重新排 序,因此另外一个线程可能会看到initialized为true,但是对于configOptions字段或它所引用的对象还没有一个一致的或者说当前的针对内存的视图变量,volatile的旧语义只承诺在读和写的变量的可见性,而不承诺其他变量,虽然这种方法更加有效的实现,但是结果会和我们设计之初大相径庭。

 

2.堆和栈

  i.Java内存管理简介:

   内存管理在Java语言中是JVM自动操作的,当JVM发现某些对象不再需要的时候,就会对该对象占用的内存进行重分配(释放)操作,而且使得分配出来 的内存能够提供给所需要的对象。在一些编程语言里面,内存管理是一个程序的职责,但是书写过C++的程序员很清楚,如果该程序需要自己来书写很有可能引起 很严重的错误或者说不可预料的程序行为,最终大部分开发时间都花在了调试这种程序以及修复相关错误上。一般情况下在Java程序开发过程把手动内存管理称 为显示内存管理,而显示内存管理经常发生的一个情况就是引用悬挂——也就是说有可能在重新分配过程释放掉了一个被某个对象引用正在使用的内存空间,释放掉该空间过后,该引用就处于悬挂状态。如果这个被悬挂引用指向的对象试图进行原来对象(因为这个时候该对象有可能已经不存在了)进行操作的时候,由于该对象本身的内存空间已经被手动释放掉了,这个结果是不可预知的。显示内存管理另外一个常见的情况是内存泄漏,当某些引用不再引用该内存对象的时候,而该对象原本占用的内存并没有被释放,这种情况简言为内存泄漏。 比如,如果针对某个链表进行了内存分配,而因为手动分配不当,仅仅让引用指向了某个元素所处的内存空间,这样就使得其他链表中的元素不能再被引用而且使得 这些元素所处的内存让应用程序处于不可达状态而且这些对象所占有的内存也不能够被再使用,这个时候就发生了内存泄漏。而这种情况一旦在程序中发生,就会一直消耗系统的可用内存直到可用内存耗尽,而针对计算机而言内存泄漏的严重程度大了会使得本来正常运行的程序直接因为内存不足而中断,并不是Java程序里面出现Exception那么轻量级。

  在以前的编程过程中,手动内存管理带了计算机程序不可避免的错误,而且这种错误对计算机程序是毁灭性的,所以内存管理就成为了一个很重要的话题,但是针对大多数纯面向对象语言而言,比如Java,提供了语言本身具有的内存特性:自动化内存管理,这种语言提供了一个程序垃圾回收器(Garbage Collector[GC]),自动内存管理提供了一个抽象的接口以及更加可靠的代码使得内存能够在程序里面进行合理的分配。最常见的情况就是垃圾回收器避免了悬挂引用的问题,因为一旦这些对象没有被任何引用“可达”的时候,也就是这些对象在JVM的内存池里面成为了不可引用对象,该垃圾回收器会直接回收掉这些对象占用的内存,当然这些对象必须满足垃圾回收器回收的某些对象规则,而垃圾回收器在回收的时候会自动释放掉这些内存。不仅仅如此,垃圾回收器同样会解决内存泄漏问题。

  ii.详解堆和栈[图片以及部分内容来自《Inside JVM》]:

  1)通用简介

  [编译原理]学过编译原理的人都明白,程序运行时有三种内存分配策略:静态的、栈式的、堆式的

  静态存储——是指在编译时就能够确定每个数据目标在运行时的存储空间需求,因而在编译时就可以给它们分配固定的内存空间。这种分配策略要求程序代码中不允许有可变数据结构的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间。

  栈式存储—— 该分配可成为动态存储分配,是由一个类似于堆栈的运行栈来实现的,和静态存储的分配方式相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知 的,只有到了运行的时候才能知道,但是规定在运行中进入一个程序模块的时候,必须知道该程序模块所需要的数据区的大小才能分配其内存。和我们在数据结构中 所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。

  堆式存储——堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例,堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。

  [C++语言]对比C++语言里面,程序占用的内存分为下边几个部分:

  [1]栈区(Stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。我们在程序中定义的局部变量就是存放在栈里,当局部变量的生命周期结束的时候,它所占的内存会被自动释放。

  [2]堆区(Heap):一 般由程序员分配和释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。我们在程序中使用c++中 new或者c中的malloc申请的一块内存,就是在heap上申请的,在使用完毕后,是需要我们自己动手释放的,否则就会产生“内存泄露”的问题。

  [3]全局区(静态区)(Static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。

  [4]文字常量区:常量字符串就是放在这里的,程序结束后由系统释放。在Java中对应有一个字符串常量池。

  [5]程序代码区:存放函数体的二进制代码

  2)JVM结构【堆、栈解析】:

  在Java虚拟机规范中,一个虚拟机实例的行为主要描述为:子系统内存区域数据类型指令, 这些组件在描述了抽象的JVM内部的一个抽象结构。与其说这些组成部分的目的是进行JVM内部结构的一种支配,更多的是提供一种严格定义实现的外部行为, 该规范定义了这些抽象组成部分以及相互作用的任何Java虚拟机执行所需要的行为。下图描述了JVM内部的一个结构,其中主要包括主要的子系统、内存区域,如同以前在《Java基础知识》中描述的:Java虚拟机有一个类加载器作为JVM的子系统,类加载器针对Class进行检测以鉴定完全合格的类接口,而JVM内部也有一个执行引擎:

  当JVM运行一个程序的时候,它的内存需要用来存储很多内容,包括字节码、以及从类文件中提取出来的一些附加信息、以及程序中实例化的对象、方法参数、返回值、局部变量以及计算的中间结果。JVM的内存组织需要在不同的运行时数据区进行以上的几个操作,下边针对上图里面出现的几个运行时数据区进行详细解析:一些运行时数据区共享了所有应用程序线程和其他特有的单个线程,每个JVM实例有一个方法区和一个内存堆,这些是共同在虚拟机内运行的线程。在Java程序里面,每个新的线程启动过后,它就会被JVM在内部分配自己的PC寄存器[PCregisters]程序计数器器)和Java堆栈Java stacks)。若该线程正在执行一个非本地Java方法,在PC寄存器的值指示下一条指令执行,该线程在Java内存栈中保存了非本地Java方法调用状态,其状态包括局部变量、被调用的参数、它的返回值、以及中间计算结果。而本地方法调用的状态则是存储在独立的本地方法内存栈里面(native method stacks),这种情况下使得这些本地方法和其他内存运行时数据区的内容尽可能保证和其他内存运行时数据区独立,而且该方法的调用更靠近操作系统,这些方法执行的字节码有可能根据操作系统环境的不同使得其编译出来的本地字节码的结构也有一定的差异。JVM中的内存栈是一个栈帧的组合,一个栈帧包含了某个Java方法调用的状态,当某个线程调用方法的时候,JVM就会将一个新的帧压入到Java内存栈,当方法调用完成过后,JVM将会从内存栈中移除该栈帧。JVM里面不存在一个可以存放中间计算数据结果值的寄存器,其内部指令集使用Java栈空间来存储中间计算的数据结果值,这种做法的设计是为了保持Java虚拟机的指令集紧凑,使得与寄存器原理能够紧密结合并且进行操作。

  1)方法区(MethodArea)

  在JVM实例中,对装载的类型信息是存储在一个逻辑方法内存区中,当Java虚拟机加载了一个类型的时候,它会跟着这个Class的类型去路径里面查找对应的Class文件,类加载器读取类文件(线性二进制数据),然后将该文件传递给Java虚拟机,JVM从二进制数据中提取信息并且将这些信息存储在方法区,而类中声明(静态)变量就是来自于方法区中存储的信息。在JVM里面用什么样的方式存储该信息是由JVM设计的时候决定的,例如:当数据进入方法的时候,多类文件字节的存储量以Big-Endian(第一次最重要的字节)的顺序存储,尽管如此,一个虚拟机可以用任何方式针对这些数据进行存储操作,若它存储在一个Little-Endian处理器上,设计的时候就有可能将多文件字节的值按照Little-Endian顺寻存储。

  ——【$Big-Endian和Little-Endian】——

  程序存储数据过程中,如果数据是跨越多个字节对象就必须有一种约定:

  • 它的地址是多少:对于跨越多个字节的对象,一般它所占的字节都是连续的,它的地址等于它所占字节最低地址,这种情况链表可能存储的仅仅是表头
  • 它的字节在内存中是如何组织的

  比如:int x,它的地址为0x100,那么它占据了内存中的0x100、0x101、0x102、0x103四个字节,所以一般情况我们觉得int是4个字节。上边只是内存组织的一种情况,多字节对象在内存中的组织有两种约定,还有一种情况:若一个整数为W位,它的表示如下:

  每一位表示为:[Xw-1,Xw-2,...,X1,X0]

  它的最高有效字节MSB(MostSignificant Byte)为:[Xw-1,Xw-2,...,Xw-8]

  最低有效字节LSB(LeastSignificant Byte)为:[X7,X6,...,X0]

  其余字节则位于LSB和MSB之间

  LSB和MSB谁位于内存的最低地址,即代表了该对象的地址,这样就引出了Big-Endian和Little-Endian的问题,如果LSB在MSB前,LSB是最低地址,则该机器是小端,反之则是大端。DES(DigitalEquipment Corporation,现在是Compaq公司的一部分)和Intel机器(x86平台)一般采用小端,IBM、Motorola(Power PC)、Sun的机器一般采用大端。当然这种不能代表所有情况,有的CPU既能工作于小端、又可以工作于大端,比如ARM、Alpha、摩托罗拉的 PowerPC,这些情况根据具体的处理器型号有所不同。但是大部分操作系统(Windows、FreeBSD、Linux)一般都是Little Endian的,少部分系统(MacOS)是Big Endian的,所以用什么方式存储还得依赖宿主操作系统环境。

  由上图可以看到,映射访问(“写32位地址的0”)主要是由寄存器到内存、由内存到寄存器的一种数据映射方式,Big-Endian在上图可以看出的原子内存单位(Atomic Unit)在系统内存中的增长方向为从左到右,而Little-Endian的地址增长方向为从右到左。举个例子:

  若要存储数据0x0A0B0C0D

  Big-Endian:

  以8位为一个存储单位,其存储的地址增长为:

  上图中可以看出MSB的值存储了0x0A,这种情况下数据的高位是从内存的低地址开始存储的,然后从左到右开始增长,第二位0x0B就是存储在第二位的,如果是按照16位为一个存储单位,其存储方式又为:

  则可以看到Big-Endian的映射地址方式为:

 

  MSB:在计算机中,最高有效位(MSB)是指位值的存储位置为转换为二进制数据后的最大值,MSB有时候在Big-Endian的架构中称为最左最大数据位,这种情况下再往左边的内存位则不是数据位了,而是有效位数位置的最高符号位,不仅仅如此,MSB也可以对应一个二进制符号位的符号位补码标记:“1”的含义为负,“0”的含义为正。最高位代表了“最重要字节”,也就是说当某些多字节数据拥有了最大值的时候它就是存储的时候最高位数据的字节对应的内存位置:

  Little-Endian:

  与Big-Endian相对的就是Little-Endian的存储方式,同样按照8位为一个存储单位上边的数据0x0A0B0C0D存储格式为:

  可以看到LSB的值存储的0x0D,也就是数据的最低位是从内存的低地址开始存储的,它的高位是从右到左的顺序逐渐增加内存分配空间进行存储的,如果按照十六位为存储单位存储格式为:

  从上图可以看到最低的16位的存储单位里面存储的值为0x0C0D,接着才是0x0A0B,这样就可以看到按照数据从高位到低位在内存中存储的时候是从右到左进行递增存储的,实际上可以从写内存的顺序来理解,实际上数据存储在内存中无非在使用的时候是写内存读内存,针对LSB的方式最好的书面解释就是向左增加来看待,如果真正在进行内存读写的时候使用这样的顺序,其意义就体现出来了:

  按照这种读写格式,0x0D存储在最低内存地址,而从右往左的增长就可以看到LSB存储的数据为0x0D,和初衷吻合,则十六位的存储就可以按照下边的格式来解释:

  实际上从上边的存储还会考虑到另外一个问题,如果按照这种方式从右往左的方式进行存储,如果是遇到Unicode文字就和从左到右的语言显示方式相反。比如一个单词“XRAY”,使用Little-Endian的方式存储格式为:

  使用这种方式进行内存读写的时候就会发现计算机语言和语言本身的 顺序会有冲突,这种冲突主要是以使用语言的人的习惯有关,而书面化的语言从左到右就可以知道其冲突是不可避免的。我们一般使用语言的阅读方式都是从左到 右,而低端存储(Little-Endian)的这种内存读写的方式使得我们最终从计算机里面读取字符需要进行倒序,而且考虑另外一个问题,如果是针对中文而言,一个字符是两个字节,就会出现整体顺序和每一个位的顺序会进行两次倒序操作, 这种方式真正在制作处理器的时候也存在一种计算上的冲突,而针对使用文字从左到右进行阅读的国家而言,从右到左的方式(Big-Endian)则会有这样 的文字冲突,另外一方面,尽管有很多国家使用语言是从右到左,但是仅仅和Big-Endian的方式存在冲突,这些国家毕竟占少数,所以可以理解的是,为 什么主流的系统都是使用的Little-Endian的方式

  【*:这里不解释Middle-Endian的方式以及Mixed-Endian的方式】

  LSB:在计算机中,最低有效位是一个二进制给予单位的整数,位的位置确定了该数据是一个偶数还是奇数,LSB有时被称为最右位。在使用具体位二进制数之内,常见的存储方式就是每一位存储1或者0的方式,从0向上到1每一比特逢二进一的存储方式。LSB的这种特性用来指定单位位,而不是位的数字,而这种方式也有可能产生一定的混乱。

  ——以上是关于Big-Endian和Little-Endian的简单讲解——

   JVM虚拟机将搜索和使用类型的一些信息也存储在方法区中以方便应用程序加载读取该数据。设计者在设计过程也考虑到要方便JVM进行Java应用程序的 快速执行,而这种取舍主要是为了程序在运行过程中内存不足的情况能够通过一定的取舍去弥补内存不足的情况。在JVM内部,所有的线程共享相同方法区,因此,访问方法区的数据结构必须是线程安全的,如果两个线程都试图去调用去找一个名为Lava的类,比如Lava还没有被加载,只有一个线程可以加载该类而另外的线程只能够等待。方法区的大小在分配过程中是不固定的,随着Java应用程序的运行,JVM可以调整其大小,需要注意一点,方法区的内存不需要是连续的,因为方法区内存可以分配内存堆中,即使是虚拟机JVM实例对象自己所在的内存堆也是可行的,而在实现过程是允许程序员自身来指定方法区的初始化大小的。

  同样的,因为Java本身的自动内存管理,方法区也会被垃圾回收的,Java程序可以通过类扩展动态加载器对象,类可以成为“未引用”向垃圾回收器进行申请,如果一个类是“未引用”的,则该类就可能被卸载,

  而方法区针对具体的语言特性有几种信息是存储在方法区内的:

  【类型信息】

  • 类型的完全限定名(java.lang.String格式)
  • 类型的完全限定名的直接父类的完全限定名(除非这个父类的类型是一个接口或者java.lang.Object)
  • 不论类型是一个类或者接口
  • 类型的修饰符(例如public、abstract、final)
  • 任何一个直接超类接口的完全限定名的列表

   在JVM和类文件名的内部,类型名一般都是完全限定名(java.lang.String)格式,在Java源文件里面,完全限定名必须加入包前缀,而 不是我们在开发过程写的简单类名,而在方法上,只要是符合Java语言规范的类的完全限定名都可以,而JVM可能直接进行解析,比如:(java.lang.String)在JVM内部名称为java/lang/String,这就是我们在异常捕捉的时候经常看到的ClassNotFoundException的异常里面类信息的名称格式。

  除此之外,还必须为每一种加载过的类型在JVM内进行存储,下边的信息不存储在方法区内,下边的章节会一一说明

  • 类型常量池
  • 字段信息
  • 方法信息
  • 所有定义在Class内部的(静态)变量信息,除开常量
  • 一个ClassLoader的引用
  • Class的引用

  【常量池】

   针对类型加载的类型信息,JVM将这些存储在常量池里,常量池是一个根据类型定义的常量的有序常量集,包括字面量(String、Integer、 Float常量)以及符号引用(类型、字段、方法),整个长量池会被JVM的一个索引引用,如同数组里面的元素集合按照索引访问一样,JVM针对这些常量 池里面存储的信息也是按照索引方式进行。实际上长量池在Java程序的动态链接过程起到了一个至关重要的作用。

  【字段信息】

  针对字段的类型信息,下边的信息是存储在方法区里面的:

  • 字段名
  • 字段类型
  • 字段修饰符(public,private,protected,static,final,volatile,transient)

  【方法信息】

  针对方法信息,下边信息存储在方法区上:

  • 方法名
  • 方法的返回类型(包括void)
  • 方法参数的类型、数目以及顺序
  • 方法修饰符(public,private,protected,static,final,synchronized,native,abstract)

  针对非本地方法,还有些附加方法信息需要存储在方法区内:

  • 方法字节码
  • 方法中局部变量区的大小、方法栈帧
  • 异常表

  【类变量】

  类变量在一个类的多个实例之间共享,这些变量直接和类相关,而不是和类的实例相关,(定义过程简单理解为类里面定义的static类型的变量),针对类变量,其逻辑部分就是存储在方法区内的。在JVM使用这些类之前,JVM先要在方法区里面为定义的non-final变量分配内存空间;常量(定义为final)则在JVM内部则不是以同样的方式来进行存储的,尽管针对常量而言,一个final的类变量是拥有它自己的常量池,作为常量池里面的存储某部分,类常量是存储在方法区内的,而其逻辑部分则不是按照上边的类变量的方式来进行内存分配的。虽然non-final类变量是作为这些类型声明中存储数据的某一部分,final变量存储为任何使用它类型的一部分的数据格式进行简单存储。

  【ClassLoader引用】

  对于每种类型的加载,JVM必须检测其类型是否符合了JVM的语言规范,对于通过类加载器加载的对象类型,JVM必须存储对类的引用,而这些针对类加载器的引用是作为了方法区里面的类型数据部分进行存储的。

  【类Class的引用】

  JVM在加载了任何一个类型过后会创建一个java.lang.Class的实例,虚拟机必须通过一定的途径来引用该类型对应的一个Class的实例,并且将其存储在方法区内

  【方法表】

  为了提高访问效率,必须仔细的设计存储在方法区中的数据信息结构。除了以上讨论的结构,jvm的实现者还添加一些其他的数据结构,如方法表【下边会说明】

  2)内存栈(Stack):

  当一个新线程启动的时候,JVM会为Java线程创建每个线程的独立内存栈,如前所言Java的内存栈是由栈帧构成,栈帧本身处于游离状态,在JVM里面,栈帧的操作只有两种:出栈入栈。正在被线程执行的方法一般称为当前线程方法,而该方法的栈帧就称为当前帧,而在该方法内定义的类称为当前类,常量池也称为当前常量池。当执行一个方法如此的时候,JVM保留当前类和当前常量池的跟踪,当虚拟机遇到了存储在栈帧中的数据上的操作指令的时候,它就执行当前帧的操作。当一个线程调用某个Java方法时,虚拟机创建并且将一个新帧压入到内存堆栈中,而这个压入到内存栈中的帧成为当前栈帧,当该方法执行的时候,JVM使用内存栈来存储参数、局部变量、中间计算结果以及其他相关数据。方法在执行过程有可能因为两种方式而结束:如果一个方法返回完成就属于方法执行的正常结束,如果在这个过程抛出异常而结束,可以称为非正常结束,不论是正常结束还是异常结束,JVM都会弹出或者丢弃该栈帧,则上一帧的方法就成为了当前帧。

   在JVM中,Java线程的栈数据是属于某个线程独有的,其他的线程不能够修改或者通过其他方式来访问该线程的栈帧,正因为如此这种情况不用担心多线程 同步访问Java的局部变量,当一个线程调用某个方法的时候,方法的局部变量是在方法内部进行的Java栈帧的存储,只有当前线程可以访问该局部变量,而其他线程不能随便访问该内存栈里面存储的数据。内存栈内的栈帧数据和方法区以及内存堆一样,Java栈的栈帧不需要分配在连续的堆栈内,或者说它们可能是在堆,或者两者组合分配,实际数据用于表示Java堆栈和栈帧结构是JVM本身的设计结构决定的,而且在编程过程可以允许程序员指定一个用于Java堆栈的初始大小以及最大、最小尺寸。

  【概念区分】

  • 内存栈:这里的内存栈和物理结构内存堆栈有点点区别,是内存里面数据存储的一种抽象数据结构。从操作系统上讲,在程序执行过程对内存的使用本身常用的数据结构就是内存堆栈,而这里的内存堆栈指代的就是JVM在使用内存过程整个内存的存储结构,多指内存的物理结构,而Java内存栈不是指代的一个物理结构,更多的时候指代的是一个抽象结构,就是符合JVM语言规范的内存栈的一个抽象结构。因为物理内存堆栈结构和Java内存栈的抽象模型结构本身比较相似,所以我们在学习过程就正常把这两种结构放在一起考虑了,而且二者除了概念上有一点点小的区别,理解成为一种结构对于初学者也未尝不可,所以实际上也可以觉得二者没有太大的本质区别。但是在学习的时候最好分清楚内存堆栈和Java内存栈的一小点细微的差距,前者是物理概念和本身模型,后者是抽象概念和本身模型的一个共同体。而内存堆栈更多的说法可以理解为一个内存块,因为内存块可以通过索引和指针进行数据结构的组合,内存栈就是内存块针对数据结构的一种表示,而内存堆则是内存块的另外一种数据结构的表示,这样理解更容易区分内存栈和内存堆栈(内存块)的概念。
  • 栈帧:栈帧是内存栈里面的最小单位,指的是内存栈里面每一个最小内存存储单元,它针对内存栈仅仅做了两个操作:入栈和出栈,一般情况下:所说的堆栈帧栈帧倒是一个概念,所以在理解上记得加以区分
  • 内存堆:这里的内存堆和内存栈是相对应的,其实内存堆里面的数据也是存储在系统内存堆栈里面的,只是它使用了另外一种方式来进行堆里面内存的管理,而本章题目要讲到的就是Java语言本身的内存堆和内存栈,而这两个概念都是抽象的概念模型,而且是相对的。

  栈帧:栈帧主要包括三个部分:局部变量操作数栈帧(操作帧)帧数据(数据帧)。 本地变量和操作数帧的大小取决于需要,这些大小是在编译时就决定的,并且在每个方法的类文件数据中进行分配,帧的数据大小则不一样,它虽然也是在编译时就 决定的但是它的大小和本身代码实现有关。当JVM调用一个Java方法的时候,它会检查类的数据来确定在本地变量和操作方法要求的栈大小,它计算该方法所 需要的内存大小,然后将这些数据分配好内存空间压入到内存堆栈中。

  栈帧——局部变量:局部变量是以Java栈帧组合成为的一个以零为基的数组,使用局部变量的时候使用的实际上是一个包含了0的一个基于索引的数组结构。int类型、float、引用以及返回值都占据了一个数组中的局部变量的条目,而byte、short、char则在存储到局部变量的时候是先转化成为int再进行操作的,则long和double则是在这样一个数组里面使用了两个元素的空间大小,在局部变量里面存储基本数据类型的时候使用的就是这样的结构。举个例子:

class Example3a{

   public staticint runClassMethod(int i,long l,float f,double d,Objecto,byte b)

   {

       return 0;

   }

   public int runInstanceMethod(char c,double d,short s,boolean b)

   {

       return 0;

   }

}

  栈帧——操作帧:和局部变量一样,操作帧也是一组有组织的数组的存储结构,但是和局部变量不一样的是这个不是通过数组的索引访问的,而是直接进行的入栈和出栈的操作,当操作指令直接压入了操作栈帧过后,从栈帧里面出来的数据会直接在出栈的时候被读取使用。除了程序计数器以外,操作帧也是可以直接被指令访问到的,JVM里面没有寄存器。处理操作帧的时候Java虚拟机是基于内存栈的而不是基于寄存器的,因为它在操作过程是直接对内存栈进行操作而不是针对寄存器进行操作。而JVM内部的指令也可以来源于其他地方比如紧接着操作符以及操作数的字节码流或者直接从常量池里面进行操作。 JVM指令其实真正在操作过程的焦点是集中在内存栈栈帧的操作帧上的。JVM指令将操作帧作为一个工作空间,有许多指令都是从操作帧里面出栈读取的,对指 令进行操作过后将操作帧的计算结果重新压入内存堆栈内。比如iadd指令将两个整数压入到操作帧里面,然后将两个操作数进行相加,相加的时候从内存栈里面 读取两个操作数的值,然后进行运算,最后将运算结果重新存入到内存堆栈里面。举个简单的例子:

begin

iload_0 //将整数类型的局部变量0压入到内存栈里面

iload_1 //将整数类型的局部变量1压入到内存栈里面

iadd    //将两个变量出栈读取,然后进行相加操作,将结果重新压入栈中

istore_2 //将最终输出结果放在另外一个局部变量里面

end

  综上所述,就是整个计算过程针对内存的一些操作内容,而整体的结构可以用下图来描述:

  栈帧——数据帧: 除了局部变量和操作帧以外,Java栈帧还包括了数据帧,用于支持常量池、普通的方法返回以及异常抛出等,这些数据都是存储在Java内存栈帧的数据帧中 的。很多JVM的指令集实际上使用的都是常量池里面的一些条目,一些指令,只是把int、long、float、double或者String从常量池里 面压入到Java栈帧的操作帧上边,一些指令使用常量池来管理类或者数组的实例化操作、字段的访问控制、或者方法的调用,其他的指令就用来决定常量池条目 中记录的某一特定对象是否某一类或者常量池项中指定的接口。常量池会判断类型、字段、方法、类、接口、类字段以及引用是如何在JVM进行符号化描述,而这 个过程由JVM本身进行对应的判断。这里就可以理解JVM如何来判断我们通常说的:“原始变量存储在内存栈上,而引用的对象存储在内存堆上边。”除了常量池判断帧数据符号化描述特性以外,这些数据帧必须在JVM正常执行或者异常执行过程辅助它进行处理操作。如果一个方法是正常结束的,JVM必须恢复栈帧调用方法的数据帧,而且必须设置PC寄存器指向调用方法后边等待的指令完成该调用方法的位置。如果该方法存在返回值,JVM也必须将这个值压入到操作帧里面以提供给需要这些数据的方法进行调用。不仅仅如此,数据帧也必须提供一个方法调用的异常表,当JVM在方法中抛出异常而非正常结束的时候,该异常表就用来存放异常信息。

  3)内存堆(Heap):

  当一个Java应用程序在运行的时候在程序中创建一个对象或者一个数组的时候,JVM会针对该对象和数组分配一个新的内存堆空间。但是在JVM实例内部,只存在一个内存堆实例,所有的依赖该JVM的Java应用程序都需要共享该堆实例,而Java应用程序本身在运行的时候它自己包含了一个由JVM虚拟机实例分配的自己的堆空间,而在应用程序启动的时候,任何一个Java应用程序都会得到JVM分配的堆空间,而且针对每一个Java应用程序,这些运行Java应用程序的堆空间都是相互独立的。这里所提及到的共享堆实例是指JVM在初始化运行的时候整体堆空间只有一个,这个是Java语言平台直接从操作系统上能够拿到的整体堆空间,所以的依赖该JVM的程序都可以得到这些内存空间,但是针对每一个独立的Java应用程序而言,这些堆空间是相互独立的,每一个Java应用程序在运行最初都是依靠JVM来进行堆空间的分配的。即使是两个相同的Java应用程序,一旦在运行的时候处于不同的操作系统进程(一般为java.exe)中,它们各自分配的堆空间都是独立的,不能相互访问,只是两个Java应用进程初始化拿到的堆空间来自JVM的分配,而JVM是从最初的内存堆实例里面分配出来的。在同一个Java应用程序里面如果出现了不同的线程,则是可以共享每一个Java应用程序拿到的内存堆空间的,这也是为什么在开发多线程程序的时候,针对同一个Java应用程序必须考虑线程安全问题,因为在一个Java进程里面所有的线程是可以共享这个进程拿到的堆空间的数据的。但是Java内存堆有一个特性,就是JVM拥有针对新的对象分配内存的指令,但是它却不包含释放该内存空间指令,当然开发过程可以在Java源代码中显示释放内存或者说在JVM字节码中进行显示的内存释放,但是JVM仅仅只是检测堆空间中是否有引用不可达(不可以引用)的对象,然后将接下来的操作交给垃圾回收器来处理。

  对象表示:

  JVM规范里面并没有提及到Java对象如何在堆空间中表示和描述,对象表示可以理解为设计JVM的工程师在最初考虑到对象调用以及垃圾回收器针对对象的判断而独立的一种Java对象在内存中的存储结构,该结构是由设计最初考虑的。针对一个创建的类实例而言,它内部定义的实例变量以及它的超类以及一些相关的核心数据,是必须通过一定的途径进行该对象内部存储以及表示的。当开发过程给定了一个对象引用的时候,JVM必须能够通过这个引用快速从对象堆空间中去拿到该对象能够访问的数据内容。也就是说,堆空间内对象的存储结构必须为外围对象引用提供一种可以访问该对象以及控制该对象的接口使得引用能够顺利地调用该对象以及相关操作。因此,针对堆空间的对象,分配的内存中往往也包含了一些指向方法区的指针,因为从整体存储结构上讲,方法区似乎存储了很多原子级别的内容,包括方法区内最原始最单一的一些变量:比如类字段、字段数据、类型数据等等。而JVM本身针对堆空间的管理存在两种设计结构:

  【1】设计一:

  堆空间的设计可以划分为两个部分:一个处理池和一个对象池,一个对象的引用可以拿到处理池的一个本地指针,而处理池主要分为两个部分:一个指向对象池里面的指针以及一个指向方法区的指针。 这种结构的优势在于JVM在处理对象的时候,更加能够方便地组合堆碎片以使得所有的数据被更加方便地进行调用。当JVM需要将一个对象移动到对象池的时 候,它仅仅需要更新该对象的指针到一个新的对象池的内存地址中就可以完成了,然后在处理池中针对该对象的内部结构进行相对应的处理工作。不过这样的方法也 会出现一个缺点就是在处理一个对象的时候针对对象的访问需要提供两个不同的指针,这一点可能不好理解,其实可以这样讲,真正在对象处理过程存在一个根据时间戳有区别的对象状态,而对象在移动、更新以及创建的整个过程中,它的处理池里面总是包含了两个指针,一个指针是指向对象内容本身,一个指针是指向了方法区,因为一个完整的对外的对象是依靠这两部分被引用指针引用到的,而我们开发过程是不能够操作处理池的两个指针的,只有引用指针我们可以通过外围编程拿到。如果Java是按照这种设计进行对象存储,这里的引用指针就是平时提及到的“Java的引用”,只是JVM在引用指针还做了一定的封装,这种封装的规则是JVM本身设计的时候做的,它就通过这种结构在外围进行一次封装,比如Java引用不具备直接操作内存地址的能力就是该封装的一种限制规则。这种设计的结构图如下:

 

  【2】设计二:

  另外一种堆空间设计就是使用对象引用拿到的本地指针,将该指针直接指向绑定好的对象的实例数据,这些数据里面仅仅包含了一个指向方法区原子级别的数据去拿到该实例相关数据,这种情况下只需要引用一个指针来访问对象实例数据,但是这样的情况使得对象的移动以及对象的数据更新变得更加复杂。当JVM需要移动这些数据以及进行堆内存碎片的整理的时候,就必须直接更新该对象所有运行时的数据区,这种情况可以用下图进行表示:

  JVM需要从一个对象引用来获得该引用能够引用的对象数据存在多个原因,当一个程序试图将一个对象的引用转换成为另外一个类型的时候,JVM就会检查两个引用指向的对象是否存在父子类关系,并且检查两个引用引用到的对象是否能够进行类型转换,而且所有这种类型的转换必须执行同样的一个操作:instanceof操作,在上边两种情况下,JVM都必须要去分析引用指向的对象内部的数据。当一个程序调用了一个实例方法的时候,JVM就必须进行动态绑定操作,它必须选择调用方法的引用类型,是一个基于类的方法调用还是一个基于对象的方法调用,要做到这一点,它又要获取该对象的唯一引用才可以。不管对象的实现是使用什么方式来进行对象描述,都是在针对内存中关于该对象的方法表进行操作,因为使用这样的方式加快了实例针对方法的调用,而且在JVM内部实现的时候这样的机制使得其运行表现比较良好,所以方法表的设计在JVM整体结构中发挥了极其重要的作用。关于方法表的存在与否,在JVM规范里面没有严格说明,也有可能真正在实现过程只是一个抽象概念物理层它根本不存在,针对放发表实现对于一个创建的实例而言,它本身具有不太高的内存需要求,如果该实现里面使用了方法表,则对象的方法表应该是可以很快被外围引用访问到的。

  有一种办法就是通过对象引用连接到方法表的时候,如下图:

  该图表明,在每个指针指向一个对象的时候,实际上是使用的一个特殊的数据结构,这些特殊的结构包括几个部分:

  • 一个指向该对象类所有数据的指针
  • 该对象的方法表

  实际上从图中可以看出,方法表就是一个指针数组,它的每一个元素包含了一个指针,针对每个对象的方法都可以直接通过该指针在方法区中找到匹配的数据进行相关调用,而这些方法表需要包括的内容如下:

  • 方法内存堆栈段空间中操作栈的大小以及局部变量
  • 方法字节码
  • 一个方法的异常表

   这些信息使得JVM足够针对该方法进行调用,在调用过程,这种结构也能够方便子类对象的方法直接通过指针引用到父类的一些方法定义,也就是说指针在内存 空间之内通过JVM本身的调用使得父类的一些方法表也可以同样的方式被调用,当然这种调用过程避免不了两个对象之间的类型检查,但是这样的方式就使得继承 的实现变得更加简单,而且方法表提供的这些数据足够引用对对象进行带有任何OO特征的对象操作。

  另外一种数据在上边的途中没有显示出来,也是从逻辑上讲内存堆中的对象的真实数据结构——对象的锁。这一点可能需要关联到JMM模型中讲的进行理解。JVM中的每一个对象都是和一个锁(互斥)相关联的,这种结构使得该对象可以很容易支持多线程访问,而且该对象的对象锁一次只能被一个线程访问。 当一个线程在运行的时候具有某个对象的锁的时候,仅仅只有这个线程可以访问该对象的实例变量,其他线程如果需要访问该实例的实例变量就必须等待这个线程将 它占有的对象锁释放过后才能够正常访问,如果一个线程请求了一个被其他线程占有的对象锁,这个请求线程也必须等到该锁被释放过后才能够拿到这个对象的对象 锁。一旦这个线程拥有了一个对象锁过后,它自己可以多次向同一个锁发送对象的锁请求,但是如果它要使得被该线程锁住的对象可以被其他锁访问到的话就需要同样的释放锁的次数,比如线程A请求了对象B的对象锁三次,那么A将会一直占有B对象的对象锁,直到它将该对象锁释放了三次。

  很多对象也可能在整个生命周期都没有被对象锁锁住过,在这样的情况下对象锁相关的数据是不需要对象内部实现的,除非有线程向该对象请求了对象锁,否则这个对象就没有该对象锁的存储结构。所以上边的实现图可以知道,很多实现不包括指向对象锁的“锁数据”,锁数据的实现必须要等待某个线程向该对象发送了对象锁请求过后,而且是在第一次锁请求过后才会被实现。 这个结构中,JVM却能够间接地通过一些办法针对对象的锁进行管理,比如把对象锁放在基于对象地址的搜索树上边。实现了锁结构的对象中,每一个Java对 象逻辑上都在内存中成为了一个等待集,这样就使得所有的线程在锁结构里面针对对象内部数据可以独立操作,等待集就使得每个线程能够独立于其他线程去完成一 个共同的设计目标以及程序执行的最终结果,这样就使得多线程的线程独享数据以及线程共享数据机制很容易实现。

  不仅仅如此,针对内存堆对象还必须存在一个对象的镜像,该镜像的主要目的是提供给垃圾回收器进行监控操作,垃圾回收器是通过对象的状态来判断该对象是否被应用,同样它需要针对堆内的对象进行监控。而当监控过程垃圾回收器收到对象回收的事件触发的时候,虽然使用了不同的垃圾回收算法,不论使用什么算法都需要通过独有的机制来判断对象目前处于哪种状态, 然后根据对象状态进行操作。开发过程程序员往往不会去仔细分析当一个对象引用设置成为null了过后虚拟机内部的操作,但实际上Java里面的引用往往不 像我们想像中那么简单,Java引用中的虚引用、弱引用就是使得Java引用在显示提交可回收状态的情况下对内存堆中的对象进行的反向监控,这些引用可以监视到垃圾回收器回收该对象的过程。垃圾回收器本身的实现也是需要内存堆中的对象能够提供相对应的数据的。其实这个位置到底JVM里面是否使用了完整的Java对象的镜像还是使用的一个镜像索引我没有去仔细分析过,总之是在堆结构里面存在着堆内对象的一个类似拷贝的镜像机制,使得垃圾回收器能够顺利回收不再被引用的对象。

  4)内存栈和内存堆的实现原理探测【该部分为不确定概念】:

  实际上不论是内存栈结构、方法区还是内存堆结构,归根到底使用的是操作系统的内存,操作系统的内存结构可以理解为内存块,常用的抽象方式就是一个内存堆栈,而JVM在OS上边安装了过后,就在启动Java程序的时候按照配置文件里面的内容向操作系统申请内存空间,该内存空间会按照JVM内部的方法提供相应的结构调整。

  内存栈应该是很容易理解的结构实现,一般情况下,内存栈是保持连续的,但是不绝对, 内存栈申请到的地址实际上很多情况下都是连续的,而每个地址的最小单位是按照计算机位来算的,该计算机位里面只有两种状态1和0,而内存栈的使用过程就是 典型的类似C++里面的普通指针结构的使用过程,直接针对指针进行++或者--操作就修改了该指针针对内存的偏移量,而这些偏移量就使得该指针可以调用不 同的内存栈中的数据。至于针对内存栈发送的指令就是常见的计算机指令,而这些指令就使得该指针针对内存栈的栈帧进行指令发送,比如发送操作指令、变量读取等等,直接就使得内存栈的调用变得更加简单,而且栈帧在接受了该数据过后就知道到底针对栈帧内部的哪一个部分进行调用,是操作帧、数据帧还是局部变量。

   内存堆实际上在操作系统里面使用了双向链表的数据结构,双向链表的结构使得即使内存堆不具有连续性,每一个堆空间里面的链表也可以进入下一个堆空间,而 操作系统本身在整理内存堆的时候会做一些简单的操作,然后通过每一个内存堆的双向链表就使得内存堆更加方便。而且堆空间不需要有序,甚至说有序不影响堆空间的存储结构,因为它归根到底是在内存块上边进行实现的,内存块本身是一个堆栈结构,只是该内存堆栈里面的块如何分配不由JVM决定,是由操作系统已经最开始分配好了,也就是最小存储单位。然后JVM拿到从操作系统申请的堆空间过后,先进行初始化操作,然后就可以直接使用了。

  常见的对程序有影响的内存问题主要是两种:溢出和内存泄漏,上边已经讲过了内存泄漏,其实从内存的结构分析,泄漏这种情况很难甚至说不可能发生在栈空间里面,其主要原因是栈空间本身很难出现悬停的内存,因为栈空间的存储结构有可能是内存的一个地址数组,所以在访问栈空间的时候使用的都是索引或者下标或者就是最原始的出栈和入栈的操作,这些操作使得栈里面很难出现像堆空间一样的内存悬停(也就是引用悬挂)问 题。堆空间悬停的内存是因为栈中存放的引用的变化,其实引用可以理解为从栈到堆的一个指针,当该指针发生变化的时候,堆内存碎片就有可能产生,而这种情况 下在原始语言里面就经常发生内存泄漏的情况,因为这些悬停的堆空间在系统里面是不能够被任何本地指针引用到,就使得这些对象在未被回收的时候脱离了可操作 区域并且占用了系统资源。

  栈溢出问题一直都是计算机领域里面的一个安全性问题,这里不做深入讨论,说多了就偏离主题了,而内存泄漏是程序员最容易理解的内存问题,还有一个问题来自于我一个黑客朋友就是:堆溢出现象,这种现象可能更加复杂。

   其实Java里面的内存结构,最初看来就是堆和栈的结合,实际上可以这样理解,实际上对象的实际内容才存在对象池里面,而有关对象的其他东西有可能会存 储于方法区,而平时使用的时候的引用是存在内存栈上的,这样就更加容易理解它内部的结构,不仅仅如此,有时候还需要考虑到Java里面的一些字段和属性到 底是对象域的还是类域的,这个也是一个比较复杂的问题。

  二者的区别简单总结一下:

  • 管理方式:JVM自己可以针对内存栈进行管理操作,而且该内存空间的释放是编译器就可以操作的内容,而堆空间在Java中JVM本身执行引擎不会对其进行释放操作,而是让垃圾回收器进行自动回收
  • 空间大小:一般情况下栈空间相对于堆空间而言比较小,这是由栈空间里面存储的数据以及本身需要的数据特性决定的,而堆空间在JVM堆实例进行分配的时候一般大小都比较大,因为堆空间在一个Java程序中需要存储太多的Java对象数据
  • 碎片相关:针 对堆空间而言,即使垃圾回收器能够进行自动堆内存回收,但是堆空间的活动量相对栈空间而言比较大,很有可能存在长期的堆空间分配和释放操作,而且垃圾回收 器不是实时的,它有可能使得堆空间的内存碎片主键累积起来。针对栈空间而言,因为它本身就是一个堆栈的数据结构,它的操作都是一一对应的,而且每一个最小 单位的结构栈帧和堆空间内复杂的内存结构不一样,所以它一般在使用过程很少出现内存碎片。
  • 分配方式:一 般情况下,栈空间有两种分配方式:静态分配和动态分配,静态分配是本身由编译器分配好了,而动态分配可能根据情况有所不同,而堆空间却是完全的动态分配 的,是一个运行时级别的内存分配。而栈空间分配的内存不需要我们考虑释放问题,而堆空间即使在有垃圾回收器的前提下还是要考虑其释放问题。
  • 效率:因 为内存块本身的排列就是一个典型的堆栈结构,所以栈空间的效率自然比起堆空间要高很多,而且计算机底层内存空间本身就使用了最基础的堆栈结构使得栈空间和 底层结构更加符合,它的操作也变得简单就是最简单的两个指令:入栈和出栈;栈空间针对堆空间而言的弱点是灵活程度不够,特别是在动态管理的时候。而堆空间 最大的优势在于动态分配,因为它在计算机底层实现可能是一个双向链表结构,所以它在管理的时候操作比栈空间复杂很多,自然它的灵活度就高了,但是这样的设 计也使得堆空间的效率不如栈空间,而且低很多。

 

3.本机内存[部分内容来源于IBM开发中心]

  Java堆空间是在编写Java程序中被我们使用得最频繁的内存空间,平时开发过程,开发人员一定遇到过OutOfMemoryError,这种结果有可能来源于Java堆空间的内存泄漏,也可能是因为堆的大小不够而导致的,有时候这些错误是可以依靠开发人员修复的,但是随着Java程序需要处理越来越多的并发程序,可能有些错误就不是那么容易处理了。有些时候即使Java堆空间没有满也可能抛出错误,这种情况下需要了解的就是JRE(JavaRuntime Environment)内部到底发生了什么。Java本身的运行宿主环境并不是操作系统,而是Java虚拟机,Java虚拟机本身是用C编写的本机程序,自然它会调用到本机资源,最常见的就是针对本机内存的调用。本机内存是可以用于运行时进程的,它和Java应用程序使用的Java堆内存不一样,每一种虚拟化资源都必须存储在本机内存里面,包括虚拟机本身运行的数据,这样也意味着主机的硬件和操作系统在本机内存的限制将直接影响到Java应用程序的性能。

  i.Java运行时如何使用本机内存:

  1)堆空间和垃圾回收

  Java运行时是一个操作系统进程(Windows下一般为java.exe),该环境提供的功能会受一些位置的用户代码驱动,这虽然提高了运行时在处理资源的灵活性,但是无法预测每种情况下运行时环境需要何种资源,这一点Java堆空间讲解中已经提到过了。在Java命令行可以使用-Xmx和-Xms来控制堆空间初始配置,mx表示堆空间的最大大小,ms表示初始化大小,这也是上提到的启动Java的配置文件可以配置的内容。尽管逻辑内存堆可以根据堆上的对象数量和在GC上花费的时间增加或者减少,但是使用本机内存的大小是保持不变的,而且由-Xms的值指定,大部分GC算法都是依赖被分配的连续内存块的堆空间,因此不能在堆需要扩大的时候分配更多本机内存,所有的堆内存必须保留下来,请注意这里说的不是Java堆内存空间是本机内存。

  本机内存保留本机内存分配不一样,本机内存被保留的时候,无法使用物理内存或者其他存储器作为备用内存,尽管保留地址空间块不会耗尽物理资源,但是会阻止内存用于其他用途,由保留从未使用过的内存导致的泄漏和泄漏分配的内存造成的问题其严重程度差不多,但使用的堆区域缩小时,一些垃圾回收器会回收堆空间的一部分内容,从而减少物理内存的使用。对于维护Java堆的内存管理系统,需要更多的本机内存来维护它的状态,进行垃圾收集的时候,必须分配数据结构来跟踪空闲存储空间和进度记录,这些数据结构的确切大小和性质因实现的不同而有所差异。

  2)JIT

  JIT编译器在运行时编译Java字节码来优化本机可执行代码,这样极大提高了Java运行时的速度,并且支持Java应用程序与本地代码相当的速度运行。字节码编译使用本机内存,而且JIT编译器的输入(字节码)和输出(可执行代码)也必须存储在本机内存里面,包含了多个经过JIT编译的方法的Java程序会比一些小型应用程序使用更多的本机内存。

  3)类和类加载器

  Java 应用程序由一些类组成,这些类定义对象结构和方法逻辑。Java 应用程序也使用 Java 运行时类库(比如 java.lang.String)中的类,也可以使用第三方库。这些类需要存储在内存中以备使用。存储类的方式取决于具体实现。Sun JDK 使用永久生成(permanent generation,PermGen)堆区域,从最基本的层面来看,使用更多的类将需要使用更多内存。(这可能意味着您的本机内存使用量会增加,或者您必须明确地重新设置 PermGen 或共享类缓存等区域的大小,以装入所有类)。记住,不仅您的应用程序需要加载到内存中,框架、应用服务器、第三方库以及包含类的 Java 运行时也会按需加载并占用空间。Java 运行时可以卸载类来回收空间,但是只有在非常严酷的条件下才会这样做,不能卸载单个类,而是卸载类加载器,随其加载的所有类都会被卸载。只有在以下情况下才能卸载类加载器

  • Java 堆不包含对表示该类加载器的 java.lang.ClassLoader 对象的引用。
  • Java 堆不包含对表示类加载器加载的类的任何 java.lang.Class 对象的引用。
  • 在 Java 堆上,该类加载器加载的任何类的所有对象都不再存活(被引用)。

  需要注意的是,Java 运行时为所有 Java 应用程序创建的 3 个默认类加载器 bootstrapextension 和 application 都不可能满足这些条件,因此,任何系统类(比如 java.lang.String)或通过应用程序类加载器加载的任何应用程序类都不能在运行时释放。 即使类加载器适合进行收集,运行时也只会将收集类加载器作为 GC 周期的一部分。一些实现只会在某些 GC 周期中卸载类加载器,也可能在运行时生成类,而不去释放它。许多 Java EE 应用程序使用JavaServer Pages (JSP) 技术来生成 Web 页面。使用 JSP 会为执行的每个 .jsp 页面生成一个类,并且这些类会在加载它们的类加载器的整个生存期中一直存在 —— 这个生存期通常是 Web应用程序的生存期。另一种生成类的常见方法是使用 Java 反射。反射的工作方式因Java 实现的不同而不同,当使用 java.lang.reflect API 时,Java 运行时必须将一个反射对象(比如 java.lang.reflect.Field)的方法连接到被反射到的对象或类。这可以通过使用 Java 本机接口(Java Native Interface,JNI)访 问器来完成,这种方法需要的设置很少,但是速度缓慢,也可以在运行时为您想要反射到的每种对象类型动态构建一个类。后一种方法在设置上更慢,但运行速度更 快,非常适合于经常反射到一个特定类的应用程序。Java 运行时在最初几次反射到一个类时使用 JNI 方法,但当使用了若干次 JNI 方法之后,访问器会膨胀为字节码访问器,这涉及到构建类并通过新的类加载器进行加载。执行多次反射可能导致创建了许多访问器类和类加载器,保持对反射对象的引用会导致这些类一直存活,并继续占用空间,因为创建字节码访问器非常缓慢,所以 Java 运行时可以缓存这些访问器以备以后使用,一些应用程序和框架还会缓存反射对象,这进一步增加了它们的本机内存占用。

  4)JNI

  JNI支持本机代码调用Java方法,反之亦然,Java运行时本身极大依赖于JNI代码来实现类库功能,比如文件和网络I/O,JNI应用程序可以通过三种方式增加Java运行时对本机内存的使用:

  • JNI应用程序的本机代码被编译到共享库中,或编译为加载到进程地址空间中的可执行文件,大型本机应用程序可能仅仅加载就会占用大量进程地址空间
  • 本机代码必须与Java运行时共享地址空间,任何本机代码分配本机代码执行内存映射都会耗用Java运行时内存
  • 某些JNI函数可能在它们的常规操作中使用本机内存,GetTypeArrayElementsGetTypeArrayRegion函数可以将Java堆复制到本机内存缓冲区中,提供给本地代码使用,是否复制数据依赖于运行时实现,通过这种方式访问大量Java堆数据就可能使用大量的本机内存堆空间

  5)NIO

   JDK 1.4开始添加了新的I/O类,引入了一种基于通道和缓冲区执行I/O的新方式,就像Java堆上的内存支持I/O缓冲区一样,NIO添加了对直接ByteBuffer的支持,ByteBuffer受本机内存而不是Java堆的支持,直接ByteBuffer可以直接传递到本机操作系统库函数,以执 行I/O,这种情况虽然提高了Java程序在I/O的执行效率,但是会对本机内存进行直接的内存开销。ByteBuffer直接操作和非直接操作的区别如 下:

   对于在何处存储直接 ByteBuffer 数据,很容易产生混淆。应用程序仍然在 Java 堆上使用一个对象来编排I/O 操作,但持有该数据的缓冲区将保存在本机内存中,Java 堆对象仅包含对本机堆缓冲区的引用。非直接 ByteBuffer 将其数据保存在 Java堆上的 byte[] 数组中。直接ByteBuffer对象会自动清理本机缓冲区,但这个过程只能作为Java堆GC的一部分执行,它不会自动影响施加 在本机上的压力。GC仅在Java堆被填满,以至于无法为堆分配请求提供服务的时候,或者在Java应用程序中显示请求它发生。

  6)线程:

  应用程序中的每个线程都需要内存来存储器堆栈(用于在调用函数时持有局部变量并维护状态的内存区域)。每个 Java 线程都需要堆栈空间来运行。根据实现的不同,Java 线程可以分为本机线程和 Java 堆栈。除了堆栈空间,每个线程还需要为线程本地存储(thread-local storage)和内部数据结构提供一些本机内存。尽管每个线程使用的内存量非常小,但对于拥有数百个线程的应用程序来说,线程堆栈的总内存使用量可能非常大。如果运行的应用程序的线程数量比可用于处理它们的处理器数量多,效率通常很低,并且可能导致糟糕的性能和更高的内存占用。

  ii.本机内存耗尽:

  Java运行时善于以不同的方式来处理Java堆空间的耗尽本机堆空间的耗尽,但是这两种情形具有类似症状,当Java堆空间耗尽的时候,Java应用程序很难正常运行,因为Java应用程序必须通过分配对象来完成工作,只要Java堆被填满,就会出现糟糕的GC性能,并且抛出OutOfMemoryError。相反,一旦 Java 运行时开始运行并且应用程序处于稳定状态,它可以在本机堆完全耗尽之后继续正常运行,不一定会发生奇怪的行为,因为需要分配本机内存的操作比需要分配 Java 堆的操作少得多。尽管需要本机内存的操作因 JVM 实现不同而异,但也有一些操作很常见:启动线程加载类以及执行某种类型的网络和文件 I/O。本机内存不足行为与Java 堆内存不足行为也不太一样,因为无法对本机堆分配进行控制,尽管所有 Java 堆分配都在 Java 内存管理系统控制之下,但任何本机代码(无论其位于 JVM、Java类库还是应用程序代码中)都可能执行本机内存分配,而且会失败。尝试进行分配的代码然后会处理这种情况,无论设计人员的意图是什么:它可能通过 JNI 接口抛出一个 OutOfMemoryError,在屏幕上输出一条消息,发生无提示失败并在稍后再试一次,或者执行其他操作。

  iii.例子:

  这篇文章一致都在讲概念,这里既然提到了ByteBuffer,先提供一个简单的例子演示该类的使用:

  ——[$]使用NIO读取txt文件——

package org.susan.java.io;

 

import java.io.FileInputStream;

import java.io.IOException;

import java.nio.ByteBuffer;

import java.nio.channels.FileChannel;

 

publicclass ExplicitChannelRead {

   public static void main(String args[]){

       FileInputStream fileInputStream;

       FileChannel fileChannel;

       long fileSize;

       ByteBuffer byteBuffer;

       try{

           fileInputStream = newFileInputStream("D:\\read.txt");

           fileChannel =fileInputStream.getChannel();

           fileSize = fileChannel.size();

           byteBuffer =ByteBuffer.allocate((int)fileSize);

           fileChannel.read(byteBuffer);

           byteBuffer.rewind();

           for( int i = 0; i <fileSize; i++ )

              System.out.print((char)byteBuffer.get());

           fileChannel.close();

           fileInputStream.close();

       }catch(IOException ex){

           ex.printStackTrace();

       }

   }

}

  在读取文件的路径放上该txt文件里面写入:HelloWorld,上边这段代码就是使用NIO的方式读取文件系统上的文件,这段程序的输入就为:

Hello World

  ——[$]获取ByteBuffer上的字节转换为Byte数组——

package org.susan.java.io;

 

import java.nio.ByteBuffer;

 

publicclass ByteBufferToByteArray {

   public static void main(String args[]) throws Exception{

       // 从byte数组创建ByteBuffer

       byte[] bytes = new byte[10];

       ByteBuffer buffer = ByteBuffer.wrap(bytes);

 

       // 在position和limit,也就是ByteBuffer缓冲区的首尾之间读取字节

       bytes = new byte[buffer.remaining()];

       buffer.get(bytes, 0, bytes.length);

 

       // 读取所有ByteBuffer内的字节

       buffer.clear();

       bytes = new byte[buffer.capacity()];

       buffer.get(bytes, 0, bytes.length);

   }

}

  上边代码就是从ByteBuffer到byte数组转换过程,有了这个过程在开发过程中可能更加方便,ByteBuffer的详细讲解我保留到IO部分,这里仅仅是涉及到了一些,所以提供两段实例代码。

  iv.共享内存:

  在Java语言里面,没有共享内存的概念,但是在某些引用中,共享内存却很受用,例如Java语言的分布式系统,存着大量的Java分布式共享对象,很多时候需要查询这些对象的状态,以查看系统是否运行正常或者了解这些对象目前的一些统计数据和状态。如果使用的是网络通信的方式,显然会增加应用的额外开销,也增加了不必要的应用编程,如果是共享内存方式,则可以直接通过共享内存查看到所需要的对象的数据和统计数据,从而减少一些不必要的麻烦。

  1)共享内存特点:

  • 可以被多个进程打开访问
  • 读写操作的进程在执行读写操作的时候其他进程不能进行写操作
  • 多个进程可以交替对某一个共享内存执行写操作
  • 一个进程执行了内存写操作过后,不影响其他进程对该内存的访问,同时其他进程对更新后的内存具有可见性
  • 在进程执行写操作时如果异常退出,对其他进程的写操作禁止自动解除
  • 相对共享文件,数据访问的方便性和效率  

  2)出现情况:

  • 独占的写操作,相应有独占的写操作等待队列。独占的写操作本身不会发生数据的一致性问题;
  • 共享的写操作,相应有共享的写操作等待队列。共享的写操作则要注意防止发生数据的一致性问题;
  • 独占的读操作,相应有共享的读操作等待队列;
  • 共享的读操作,相应有共享的读操作等待队列;

  3)Java中共享内存的实现:

  JDK 1.4里面的MappedByteBuffer为开发人员在Java中实现共享内存提供了良好的方法,该缓冲区实际上是一个磁盘文件的内存映象, 二者的变化会保持同步,即内存数据发生变化过后会立即反应到磁盘文件中,这样会有效地保证共享内存的实现,将共享文件和磁盘文件简历联系的是文件通道 类:FileChannel,该类的加入是JDK为了统一外围设备的访问方法,并且加强了多线程对同一文件进行存取的安全性,这里可以使用它来建立共享内 存用,它建立了共享内存和磁盘文件之间的一个通道。打开一个文件可使用RandomAccessFile类的getChannel方法,该方法直接返回一个文件通道,该文件通道由于对应的文件设为随机存取,一方面可以进行读写两种操作,另外一个方面使用它不会破坏映象文件的内容。这里,如果使用FileOutputStream和FileInputStream则不能理想地实现共享内存的要求,因为这两个类同时实现自由读写很困难。

  下边代码段实现了上边提及的共享内存功能

// 获得一个只读的随机存取文件对象

RandomAccessFileRAFile = new RandomAccessFile(filename,"r");

// 获得相应的文件通道

FileChannel fc =RAFile.getChannel();

// 取得文件的实际大小

int size =(int)fc.size();

// 获得共享内存缓冲区,该共享内存只读 
MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);

// 获得一个可读写的随机存取文件对象 
RAFile = new RandomAccessFile(filename,"rw");

// 获得相应的文件通道 
fc = RAFile.getChannel();

// 取得文件的实际大小,以便映像到共享内存 
size = (int)fc.size();

// 获得共享内存缓冲区,该共享内存可读写 
mapBuf = fc.map(FileChannel.MAP_RW,0,size);

// 获取头部消息:存取权限 

mode =mapBuf.getInt(); 

   如果多个应用映象使用同一文件名的共享内存,则意味着这多个应用共享了同一内存数据,这些应用对于文件可以具有同等存取权限,一个应用对数据的刷新会更 新到多个应用中。为了防止多个应用同时对共享内存进行写操作,可以在该共享内存的头部信息加入写操作标记,该共享文件的头部基本信息至少有:

  • 共享内存长度
  • 共享内存目前的存取模式

  共享文件的头部信息是私有信息,多个应用可以对同一个共享内存执行写操作,执行写操作和结束写操作的时候,可以使用如下方法:

public boolean startWrite()

{

   if(mode == 0) // 这里mode代表共享内存的存取模式,为0代表可写

   {

       mode = 1; // 意味着别的应用不可写

       mapBuf.flip();

       mapBuf.putInt(mode);    //写入共享内存的头部信息

       return true;

   }

   else{

       return false; //表明已经有应用在写该共享内存了,本应用不能够针对共享内存再做写操作

   }
}

 

publicboolean stopWrite()

{

   mode = 0; // 释放写权限

   mapBuf.flip();

   mapBuf.putInt(mode);    //写入共享内存头部信息

    returntrue;
}

  【*:上边提供了对共享内存执行写操作过程的两个方法,这两个方法其实理解起来很简单,真正需要思考的是一个针对存取模式的设置,其实这种机制和最前面提到的内存的锁模式有点类似,一旦当mode(存取模式)设置称为可写的时候,startWrite才能返回true,不仅仅如此,某个应用程序在向共享内存写入数据的时候还会修改其存取模式,因为如果不修改的话就会导致其他应用同样针对该内存是可写的,这样就使得共享内存的实现变得混乱,而在停止写操作stopWrite的时候,需要将mode设置称为1,也就是上边注释段提到的释放写权限。】

  关于锁的知识这里简单做个补充【*:上边代码的这种模式可以理解为一种简单的锁模式】:一般情况下,计算机编程中会经常遇到锁模式,在整个锁模式过程中可以将锁分为两类(这里只是辅助理解,不是严格的锁分类)——共享锁排他锁(也称为独占锁),锁的定位是定位于针对所有与计算机有关的资源比如内存、文件、存储空间等,针对这些资源都可能出现锁模式。在上边堆和栈一节讲到了Java对象锁,其实不仅仅是对象,只要是计算机中会出现写入和读取共同操作的资源,都有可能出现锁模式。

  共享锁——当应用程序获得了资源的共享锁的时候,那么应用程序就可以直接访问该资源,资源的共享锁可以被多个应用程序拿到,在Java里面线程之间有时候也存在对象的共享锁,但是有一个很明显的特征,也就是内存共享锁只能读取数据,不能够写入数据,不论是什么资源,当应用程序仅仅只能拿到该资源的共享锁的时候,是不能够针对该资源进行写操作的。

  独占锁——当应用程序获得了资源的独占锁的时候,应用程序访问该资源在共享锁上边多了一个权限就是写权限,针对资源本身而言,一个资源只有一把独占锁,也就是说一个资源只能同时被一个应用或者一个执行代码程序允许写操作,Java线程中的对象写操作也是这个道理,若某个应用拿到了独占锁的时候,不仅仅可以读取资源里面的数据,而且可以向该资源进行数据写操作。

  数据一致性——当资源同时被应用进行读写访问的时候,有可能会出现数据一致性问题,比如A 应用拿到了资源R1的独占锁,B应用拿到了资源R1的共享锁,A在针对R1进行写操作,而两个应用的操作——A的写操作和B的读操作出现了一个时间 差,s1的时候B读取了R1的资源,s2的时候A写入了数据修改了R1的资源,s3的时候B又进行了第二次读,而两次读取相隔时间比较短暂而且初衷没有考 虑到A在B的读取过程修改了资源,这种情况下针对锁模式就需要考虑到数据一致性问题。独占锁的排他性在这里的意思是该锁只能被一个应用获取,获取过程只能由这个应用写入数据到资源内部,除非它释放该锁,否则其他拿不到锁的应用是无法对资源进行写入操作的。

  按照上边的思路去理解代码里面实现共享内存的过程就更加容易理解了。

   如果执行写操作的应用异常中止,那么映像文件的共享内存将不再能执行写操作。为了在应用异常中止后,写操作禁止标志自动消除,必须让运行的应用获知退出 的应用。在多线程应用中,可以用同步方法获得这样的效果,但是在多进程中,同步是不起作用的。方法可以采用的多种技巧,这里只是描述一可能的实现:采用文 件锁的方式。写共享内存应用在获得对一个共享内存写权限的时候,除了判断头部信息的写权限标志外,还要判断一个临时的锁文件是否可以得到,如果可以得到,则即使头部信息的写权限标志为1(上述),也可以启动写权限,其实这已经表明写权限获得的应用已经异常退出,这段代码如下:

// 打开一个临时文件,注意统一共享内存,该文件名必须相同,可以在共享文件名后边添加“.lock”后缀

RandomAccessFilefiles = new RandomAccessFile("memory.lock","rw");

// 获取文件通道

FileChannellockFileChannel = files.getChannel();

// 获取文件的独占锁,该方法不产生任何阻塞直接返回

FileLock fileLock =lockFileChannel.tryLock();

// 如果为空表示已经有应用占有了

if( fileLock== null ){

   // ...不可写

}else{

   // ...可以执行写操作

}

  4)共享内存的应用:

  在Java中,共享内存一般有两种应用:

  [1]永久对象配置——在 java服务器应用中,用户可能会在运行过程中配置一些参数,而这些参数需要永久 有效,当服务器应用重新启动后,这些配置参数仍然可以对应用起作用。这就可以用到该文 中的共享内存。该共享内存中保存了服务器的运行参数和一些对象运行特性。可以在应用启动时读入以启用以前配置的参数。

  [2]查询共享数据——一个应用(例 sys.java)是系统的服务进程,其系统的运行状态记录在共享内存中,其中运行状态可能是不断变化的。为了随时了解系统的运行状态,启动另一个应用(例 mon.java),该应用查询该共享内存,汇报系统的运行状态。

  v.小节:

  提 供本机内存以及共享内存的知识,主要是为了让读者能够更顺利地理解JVM内部内存模型的物理原理,包括JVM如何和操作系统在内存这个级别进行交互,理解 了这些内容就让读者对Java内存模型的认识会更加深入,而且不容易遗忘。其实Java的内存模型远不及我们想象中那么简单,而且其结构极端复杂,看过 《Inside JVM》的朋友应该就知道,结合JVM指令集去写点小代码测试.class文件的里层结构也不失为一种好玩的学习方法。

  

4.防止内存泄漏

  Java中会有内存泄漏,听起来似乎是很不正常的,因为Java提供了垃圾回收器针对内存进行自动回收,但是Java还是会出现内存泄漏的。

  i.什么是Java中的内存泄漏:

  在Java语言中,内存泄漏就是存在一些被分配的对象,这些对象有两个特点:这些对象可达,即在对象内存的有向图中存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象了。如果对象满足这两个条件,该对象就可以判定为Java中的内存泄漏,这些对象不会被GC回收,然而它却占用内存,这就是Java语言中的内存泄漏。 Java中的内存泄漏和C++中的内存泄漏还存在一定的区别,在C++里面,内存泄漏的范围更大一些,有些对象被分配了内存空间,但是却不可达,由于 C++中没有GC,这些内存将会永远收不回来,在Java中这些不可达对象则是被GC负责回收的,因此程序员不需要考虑这一部分的内存泄漏。二者的图如 下:

  因此按照上边的分析,Java语言中也是存在内存泄漏的,但是其内存泄漏范围比C++要小很多,因为Java里面有个特殊程序回收所有的不可达对象:垃圾回收器。对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低,JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。 但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突 然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提 供的HotSpot JVM就支持这一特性。

  举个例子:

  ——[$]内存泄漏的例子——

package org.susan.java.collection;

 

import java.util.Vector;

 

publicclass VectorMemoryLeak {

   public static void main(String args[]){

       Vector<String> vector = newVector<String>();

       for( int i = 0; i < 1000; i++ ){

           String tempString= new String();

           vector.add(tempString);

           tempString = null;

       }

   }

}

   从上边这个例子可以看到,循环申请了String对象,并且将申请的对象放入了一个Vector中,如果仅仅是释放对象本身,因为Vector仍然引用 了该对象,所以这个对象对CG来说是不可回收的,因此如果对象加入到Vector后,还必须从Vector删除才能够回收,最简单的方式是将Vector引用设置成null。实际上这些对象已经没有用了,但是还是被代码里面的引用引用到了,这种情况GC拿它就没有了任何办法,这样就可以导致了内存泄漏。

  【*:Java 语言因为提供了垃圾回收器,照理说是不会出现内存泄漏的,Java里面导致内存泄漏的主要原因就是,先前申请了内存空间而忘记了释放。如果程序中存在对无 用对象的引用,这些对象就会驻留在内存中消耗内存,因为无法让GC判断这些对象是否可达。如果存在对象的引用,这个对象就被定义为“有效的活动状态”,同 时不会被释放,要确定对象所占内存被回收,必须要确认该对象不再被使用。典型的做法就是把对象数据成员设置成为null或者中集合中移除,当局部变量不需 要的情况则不需要显示声明为null。】

  ii.常见的Java内存泄漏

  1)全局集合:

  在大型应用程序中存在各种各样的全局数据仓库是很普遍的,比如一个JNDI树或者一个Sessiontable(会话表),在这些情况下,必须注意管理存储库的大小,必须有某种机制从存储库中移除不再需要的数据。

  [$]解决:

  [1]常用的解决方法是周期运作清除作业,该作业会验证仓库中的数据然后清楚一切不需要的数据

  [2]另外一种方式是反向链接计数,集合负责统计集合中每个入口的反向链接数据,这要求反向链接告诉集合合适会退出入口,当反向链接数目为零的时候,该元素就可以移除了。

  2)缓存:

   缓存一种用来快速查找已经执行过的操作结果的数据结构。因此,如果一个操作执行需要比较多的资源并会多次被使用,通常做法是把常用的输入数据的操作结果 进行缓存,以便在下次调用该操作时使用缓存的数据。缓存通常都是以动态方式实现的,如果缓存设置不正确而大量使用缓存的话则会出现内存溢出的后果,因此需 要将所使用的内存容量与检索数据的速度加以平衡。

  [$]解决:

  [1]常用的解决途径是使用java.lang.ref.SoftReference类坚持将对象放入缓存,这个方法可以保证当虚拟机用完内存或者需要更多堆的时候,可以释放这些对象的引用。

  3)类加载器:

  Java类装载器的使用为内存泄漏提供了许多可乘之机。一般来说类装载器都具有复杂结构,因为类装载器不仅仅是只与"常规"对象引用有关,同时也和对象内部的引用有关。比如数据变量方法各种类。这意味着只要存在对数据变量,方法,各种类和对象的类装载器,那么类装载器将驻留在JVM中。既然类装载器可以同很多的类关联,同时也可以和静态数据变量关联,那么相当多的内存就可能发生泄漏。

  iii.Java引用【摘录自前边的《Java引用总结》】

  Java中的对象引用主要有以下几种类型:

  1)强可及对象(stronglyreachable):

  可以通过强引用访问的对象,一般来说,我们平时写代码的方式都是使用的强引用对象,比如下边的代码段:

  StringBuilder builder= new StringBuilder();

  上边代码部分引用obj这个引用将引用内存堆中的一个对象,这种情况下,只要obj的引用存在,垃圾回收器就永远不会释放该对象的存储空间。这种对象我们又成为强引用(Strong references),这种强引用方式就是Java语言的原生的Java引用,我们几乎每天编程的时候都用到。上边代码JVM存储了一个StringBuilder类型的对象的强引用在变量builder呢。强引用和GC的交互是这样的,如果一个对象通过强引用可达或者通过强引用链可达的话这种对象就成为强可及对象,这种情况下的对象垃圾回收器不予理睬。如果我们开发过程不需要垃圾回器回收该对象,就直接将该对象赋为强引用,也是普通的编程方法。

  2)软可及对象(softlyreachable):

  不通过强引用访问的对象,即不是强可及对象,但是可以通过软引用访问的对象就成为软可及对象,软可及对象就需要使用类SoftReference(java.lang.ref.SoftReference)。此种类型的引用主要用于内存比较敏感的高速缓存,而且此种引用还是具有较强的引用功能,当内存不够的时候GC会回收这类内存,因此如果内存充足的时候,这种引用通常不会被回收的。不仅仅如此,这种引用对象在JVM里面保证在抛出OutOfMemory异常之前,设置成为null。通俗地讲,这种类型的引用保证在JVM内存不足的时候全部被清除,但是有个关键在于:垃圾收集器在运行时是否释放软可及对象是不确定的,而且使用垃圾回收算法并不能保证一次性寻找到所有的软可及对象。当垃圾回收器每次运行的时候都可以随意释放不是强可及对象占用的内存,如果垃圾回收器找到了软可及对象过后,可能会进行以下操作:

  • 将SoftReference对象的referent域设置成为null,从而使该对象不再引用heap对象。
  • SoftReference引用过的内存堆上的对象一律被生命为finalizable。
  • 当内存堆上的对象finalize()方法被运行而且该对象占用的内存被释放,SoftReference对象就会被添加到它的ReferenceQueue,前提条件是ReferenceQueue本身是存在的。

  既然Java里面存在这样的对象,那么我们在编写代码的时候如何创建这样的对象呢?创建步骤如下:

  先创建一个对象,并使用普通引用方式【强引用】,然后再创建一个SoftReference来引用该对象,最后将普通引用设置为null, 通过这样的方式,这个对象就仅仅保留了一个SoftReference引用,同时这种情况我们所创建的对象就是SoftReference对象。一般情况 下,我们可以使用该引用来完成Cache功能,就是前边说的用于高速缓存,保证最大限度使用内存而不会引起内存泄漏的情况。下边的代码段:

  public staticvoid main(String args[])

  {

    //创建一个强可及对象

    A a = new A();

    //创建这个对象的软引用SoftReference

    SoftReference sr= new SoftReference(a);

    //将强引用设置为空,以遍垃圾回收器回收强引用

    a = null;

    //下次使用该对象的操作

    if( sr != null ){

      a = (A)sr.get();

    }else{

      //这种情况就是由于内存过低,已经将软引用释放了,因此需要重新装载一次

      a = new A();

      sr = new SoftReference(a);

    }

  }

  软引用技术使得Java系统可以更好地管理内存,保持系统稳定,防止内存泄漏,避免系统崩溃,因此在处理一些内存占用大而且生命周期长使用不频繁的对象可以使用该技术。

  3)弱可及对象(weaklyreachable):

  不是强可及对象同样也不是软可及对象,仅仅通过弱引用WeakReference(java.lang.ref.WeakReference)访问的对象,这种对象的用途在于规范化映射(canonicalized mapping),对于生存周期相对比较长而且重新创建的时候开销少的对象,弱引用也比较有用,和软引用对象不同的是,垃圾回收器如果碰到了弱可及对象,将释放WeakReference对象的内存,但是垃圾回收器需要运行很多次才能够找到弱可及对象。弱引用对象在使用的时候,可以配合ReferenceQueue类使用,如果弱引用被回收,JVM就会把这个弱引用加入到相关的引用队列中去。最简单的弱引用方法如以下代码:

  WeakReference weakWidget= new WeakReference(classA);

  在上边代码里面,当我们使用weakWidget.get()来获取classA的时候,由于弱引用本身是无法阻止垃圾回收的,所以我们也许会拿到一个null为返回。【*:这里提供一个小技巧,如果我们希望取得某个对象的信息,但是又不影响该对象的垃圾回收过程,我们就可以使用WeakReference来记住该对象,一般我们在开发调试器和优化器的时候使用这个是很好的一个手段。】

  如果上边的代码部分,我们通过weakWidget.get()返回的是null就证明该对象已经被垃圾回收器回收了,而这种情况下弱引用对象就失去了使用价值,GC就会定义为需要进行清除工作。这种情况下弱引用无法引用任何对象,所以在JVM里面就成为了一个死引用,这就是为什么我们有时候需要通过ReferenceQueue类来配合使用的原因,使用了ReferenceQueue过后,就使得我们更加容易监视该引用的对象,如果我们通过一ReferenceQueue类来构造一个弱引用,当弱引用的对象已经被回收的时候,系统将自动使用对象引用队列来代替对象引用,而且我们可以通过ReferenceQueue类的运行来决定是否真正要从垃圾回收器里面将该死引用(Dead Reference)清除

  弱引用代码段:

  //创建普通引用对象

  MyObject object = new MyObject();

  //创建一个引用队列

  ReferenceQueue rq= new ReferenceQueue();

  //使用引用队列创建MyObject的弱引用

  WeakReference wr= new WeakReference(object,rq);

  这里提供两个实在的场景来描述弱引用的相关用法:

  [1]你 想给对象附加一些信息,于是你用一个 Hashtable 把对象和附加信息关联起来。你不停的把对象和附加信息放入 Hashtable 中,但是当对象用完的时候,你不得不把对象再从 Hashtable 中移除,否则它占用的内存变不会释放。万一你忘记了,那么没有从 Hashtable 中移除的对象也可以算作是内存泄漏。理想的状况应该是当对象用完时,Hashtable 中的对象会自动被垃圾收集器回收,不然你就是在做垃圾回收的工作。

  [2]你 想实现一个图片缓存,因为加载图片的开销比较大。你将图片对象的引用放入这个缓存,以便以后能够重新使用这个对象。但是你必须决定缓存中的哪些图片不再需 要了,从而将引用从缓存中移除。不管你使用什么管理缓存的算法,你实际上都在处理垃圾收集的工作,更简单的办法(除非你有特殊的需求,这也应该是最好的办 法)是让垃圾收集器来处理,由它来决定回收哪个对象。 

  当Java回收器遇到了弱引用的时候有可能会执行以下操作:

  • 将WeakReference对象的referent域设置成为null,从而使该对象不再引用heap对象。
  • WeakReference引用过的内存堆上的对象一律被生命为finalizable。
  • 当内存堆上的对象finalize()方法被运行而且该对象占用的内存被释放,WeakReference对象就会被添加到它的ReferenceQueue,前提条件是ReferenceQueue本身是存在的。

  4)清除:

  当引用对象的referent域设置为null,并且引用类在内存堆中引用的对象声明为可结束的时候,该对象就可以清除,清除不做过多的讲述

  5)虚可及对象(phantomlyreachable):

  不是强可及对象,也不是软可及对象,同样不是弱可及对象,之所以把虚可及对象放到最后来讲,主要也是因为它的特殊性,有时候我们又称之为“幽灵对象”,已经结束的,可以通过虚引用来访问该对象。我们使用类PhantomReference(java.lang.ref.PhantomReference)来访问,这个类只能用于跟踪被引用对象进行的收集,同样的,可以用于执行per-mortern清除操作。PhantomReference必须与 ReferenceQueue类一起使用。需要使用ReferenceQueue是因为它能够充当通知机制,当垃圾收集器确定了某个对象是虚可及对象的时 候,PhantomReference对象就被放在了它的ReferenceQueue上,这就是一个通知,表明PhantomReference引用的对象已经结束,可以收集了,一般情况下我们刚好在对象内存在回收之前采取该行为。这种引用不同于弱引用和软引用,这种方式通过get()获取到的对象总是 返回null,仅仅当这些对象在ReferenceQueue队列里面的时候,我们可以知道它所引用的哪些对对象是死引用(Dead Reference)。而这种引用和弱引用的区别在于:

  弱引用(WeakReference)是在对象不可达的时候尽快进入ReferenceQueue队列的,在finalization方法执行和垃圾回收之前是确实会发生的,理论上这类对象是不正确的对象,但是WeakReference对象可以继续保持Dead状态,

  虚引用(PhantomReference)是在对象确实已经从物理内存中移除过后才进入的ReferenceQueue队列,而且get()方法会一直返回null

  当垃圾回收器遇到了虚引用的时候将有可能执行以下操作:

  • PhantomReference引用过的heap对象声明为finalizable;
  • 虚引用在堆对象释放之前就添加到了它的ReferenceQueue里面,这种情况使得我们可以在堆对象被回收之前采取操作【*:再次提醒,PhantomReference对象必须经过关联的ReferenceQueue来创建,就是说必须和ReferenceQueue类配合操作】

  看似没有用处的虚引用,有什么用途呢?

  • 首先,我们可以通过虚引用知道对象究竟什么时候真正从内存里面移除的,而且这也是唯一的途径。
  • 虚引用避过了finalize()方法,因为对于此方法的执行而言,虚引用真正引用到的对象是异常对象,若在该方法内要使用对象只能重建。一般情况垃圾回收器会轮询两次,一次标记为finalization,第二次进行真实的回收,而往往标记工作不能实时进行,或者垃圾回收其会等待一个对象去标记finalization。这种情况很有可能引起MemoryOut,而使用虚引用这种情况就会完全避免。因为虚引用在引用对象的过程不会去使得这个对象由Dead复活,而且这种对象是可以在回收周期进行回收的。

   在JVM内部,虚引用比起使用finalize()方法更加安全一点而且更加有效。而finaliaze()方法回收在虚拟机里面实现起来相对简单,而 且也可以处理大部分工作,所以我们仍然使用这种方式来进行对象回收的扫尾操作,但是有了虚引用过后我们可以选择是否手动操作该对象使得程序更加高效完美。

  iv.防止内存泄漏[来自IBM开发中心]:

  1)使用软引用阻止泄漏:

  [1]在Java语言中有一种形式的内存泄漏称为对象游离(Object Loitering):

  ——[$]对象游离——

// 注意,这段代码属于概念说明代码,实际应用中不要模仿

publicclass LeakyChecksum{

   private byte[] byteArray;

    publicsynchronized int getFileCheckSum(String filename)

   {

       int len = getFileSize(filename);

       if( byteArray == null || byteArray.length< len )

           byteArray= new byte[len];

       readFileContents(filename,byteArray);

       // 计算该文件的值然后返回该对象

   }

}

  上边的代码是类LeakyChecksum用来说明对象游离的概念,里面有一个getFileChecksum()方法用来计算文件内容校验和,getFileCheckSum方法将文件内容读取到缓冲区中计算校验和,更加直观的实现就是简单地将缓冲区作为getFileChecksum中的本地变量分配,但是上边这个版本比这种版本更加“聪明”,不是将缓冲区缓冲在实例中字段中减少内存churn。该“优化”通常不带来预期的好处,对象分配比很多人期望的更加便宜。(还要注意,将缓冲区从本地变量提升到实例变量,使得类若不带有附加的同步,就不再是线程安全的了。直观的实现不需要将 getFileChecksum() 声明为 synchronized,并且会在同时调用时提供更好的可伸缩性。)

  这个类存在很多的问题,但是我们着重来看内存泄漏。缓存缓冲区的决定很可能是根据这样的假设得出的,即该类将在一个程序中被调用许多次,因此它应该更加有效,以重用缓冲区而不是重新分配它。但 是结果是,缓冲区永远不会被释放,因为它对程序来说总是可及的(除非LeakyChecksum对象被垃圾收集了)。更坏的是,它可以增长,却不可以缩 小,所以 LeakyChecksum 将永久保持一个与所处理的最大文件一样大小的缓冲区。退一万步说,这也会给垃圾收集器带来压力,并且要求更频繁的收集;为计算未来的校验和而保持一个大型 缓冲区并不是可用内存的最有效利用。LeakyChecksum 中问题的原因是,缓冲区对于getFileChecksum() 操作来说逻辑上是本地的,但是它的生命周期已经被人为延长了,因为将它提升到了实例字段。因此,该类必须自己管理缓冲区的生命周期,而不是让 JVM 来管理。

  这里可以提供一种策略就是使用Java里面的软引用:

  弱引用如何可以给应用程序提供当对象被程序使用时另一种到达该对象的方法,但是不会延长对象的生命周期。Reference 的另一个子类——软引用——可满足一个不同却相关的目的。其中弱引用允许应用程序创建不妨碍垃圾收集的引用,软引用允许应用程序通过将一些对象指定为 “expendable” 而利用垃圾收集器的帮助。尽管垃圾收集器在找出哪些内存在由应用程序使用哪些没在使用方面做得很好,但是确定可用内存的最适当使用还是取决于应用程序。如果应用程序做出了不好的决定,使得对象被保持,那么性能会受到影响,因为垃圾收集器必须更加辛勤地工作,以防止应用程序消耗掉所有内存。高速缓存是 一种常见的性能优化,允许应用程序重用以前的计算结果,而不是重新进行计算。高速缓存是 CPU 利用和内存使用之间的一种折衷,这种折衷理想的平衡状态取决于有多少内存可用。若高速缓存太少,则所要求的性能优势无法达到;若太多,则性能会受到影响, 因为太多的内存被用于高速缓存上,导致其他用途没有足够的可用内存。因为垃圾收集器比应用程序更适合决定内存需求,所以应该利用垃圾收集器在做这些决定方 面的帮助,这就是件引用所要做的。如果一个对象惟一剩下的引用是弱引用或软引用,那么该对象是软可及的(softly reachable)。垃圾收集器并不像其收集弱可及的对象一样尽量地收集软可及的对象,相反,它只在真正 “需要” 内存时才收集软可及的对象。软引用对于垃圾收集器来说是这样一种方式,即 “只要内存不太紧张,我就会保留该对象。但是如果内存变得真正紧张了,我就会去收集并处理这个对象。” 垃圾收集器在可以抛出OutOfMemoryError 之前需要清除所有的软引用。通过使用一个软引用来管理高速缓存的缓冲区,可以解决 LeakyChecksum中的问题,如上边代码所示。现在,只要不是特别需要内存,缓冲区就会被保留,但是在需要时,也可被垃圾收集器回收:

  ——[$]使用软引用修复上边代码段——

publicclass CachingChecksum

{

   private SoftReference<byte[]> bufferRef;

   public synchronized int getFileChecksum(String filename)

   {

       int len = getFileSize(filename);

       byte[] byteArray = bufferRef.get();

       if( byteArray == null || byteArray.length< len )

       {

           byteArray = new byte[len];

           bufferRef.set(byteArray);

       }

       readFileContents(filename,byteArray);

   }

}

  一种廉价缓存:

  CachingChecksum使用一个软引用来缓存单个对象,并让 JVM 处理从缓存中取走对象时的细节。类似地,软引用也经常用于 GUI 应用程序中,用于缓存位图图形。是否可使用软引用的关键在于,应用程序是否可从大量缓存的数据恢复。如果需要缓存不止一个对象,您可以使用一个 Map,但是可以选择如何使用软引用。您可以将缓存作为 Map<K, SoftReference<V>> 或SoftReference<Map<K,V>> 管理。后一种选项通常更好一些,因为它给垃圾收集器带来的工作更少,并且允许在特别需要内存时以较少的工作回收整个缓存。弱引用有时会错误地用于取代软引 用,用于构建缓存,但是这会导致差的缓存性能。在实践中,弱引用将在对象变得弱可及之后被很快地清除掉——通常是在缓存的对象再次用到之前——因为小的垃 圾收集运行得很频繁。对于在性能上非常依赖高速缓存的应用程序来说,软引用是一个不管用的手段,它确实不能取代能够提供灵活终止期复制事务型高速缓存的复杂的高速缓存框架。但是作为一种 “廉价(cheapand dirty)” 的高速缓存机制,它对于降低价格是很有吸引力的。正如弱引用一样,软引用也可创建为具有一个相关的引用队列,引用在被垃圾收集器清除时进入队列。引用队列对于软引用来说,没有对弱引用那么有用,但是它们可以用于发出管理警报,说明应用程序开始缺少内存

  2)垃圾回收对引用的处理:

  弱引用和软引用都扩展了抽象的 Reference 类虚引用(phantom references),引 用对象被垃圾收集器特殊地看待。垃圾收集器在跟踪堆期间遇到一个 Reference 时,不会标记或跟踪该引用对象,而是在已知活跃 的 Reference 对象的队列上放置一个 Reference。在跟踪之后,垃圾收集器就识别软可及的对象——这些对象上除了软引用外,没有任何强 引用。垃圾收集器然后根据当前收集所回收的内存总量和其他策略考虑因素,判断软引用此时是否需要被清除。将被清除的软引用如果具有相应的引用队列,就会进 入队列。其余的软可及对象(没有清除的对象)然后被看作一个根集(root set),堆跟踪继续使用这些新的根,以便通过活跃的软引用而可及的对象能够被标记。处理软引用之后,弱可及对象的集合被识别 —— 这样的对象上不存在强引用或软引用。这些对象被清除和加入队列。所有 Reference 类型在加入队列之前被清除,所以处理事后检查(post-mortem)清除的线程永远不会具有referent 对象的访问权,而只具有Reference 对象的访问权。因此,当 References 与引用队列一起使用时,通常需要细分适当的引用类型,并将它直接用于您的设计中(与 WeakHashMap 一样,它的 Map.Entry 扩展了 WeakReference)或者存储对需要清除的实体的引用。

  3)使用弱引用堵住内存泄漏:

  [1]全局Map造成的内存泄漏:

  无意识对象保留最常见的原因是使用 Map 将元数据与临时对象(transient object)相 关联。假定一个对象具有中等生命周期,比分配它的那个方法调用的生命周期长,但是比应用程序的生命周期短,如客户机的套接字连接。需要将一些元数据与这个 套接字关联,如生成连接的用户的标识。在创建 Socket 时是不知道这些信息的,并且不能将数据添加到 Socket 对象上,因为不能控 制 Socket 类或者它的子类。这时,典型的方法就是在一个全局 Map 中存储这些信息:

publicclass SocketManager{

   private Map<Socket,User> m= new HashMap<Socket,User>();

   public void setUser(Socket s,User u)

   {

       m.put(s,u);

   }

   public User getUser(Socket s){

       return m.get(s);

   }

   public void removeUser(Socket s){

       m.remove(s);

   }

}

 

SocketManagersocketManager;

//...

socketManager.setUser(socket,user);

   这种方法的问题是元数据的生命周期需要与套接字的生命周期挂钩,但是除非准确地知道什么时候程序不再需要这个套接字,并记住从 Map 中删除相应的映射,否则,Socket和 User 对象将会永远留在 Map 中,远远超过响应了请求和关闭套接字的时间。这会阻止 Socket 和User 对象被垃圾收集,即使应用程序不会再使用它们。这些对象留下来不受控制,很容易造成程序在长时间运行后内存爆满。除了最简单的情况,在几乎所有情况下找出 什么时候 Socket 不再被程序使用是一件很烦人和容易出错的任务,需要人工对内存进行管理。

  [2]弱引用内存泄漏代码:

  程序有内存泄漏的第一个迹象通常是它抛出一个 OutOfMemoryError,或者因为频繁的垃圾收集而表现出糟糕的性能。幸运的是,垃圾收集可以提供能够用来诊断内存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 选 项调用 JVM,那么每次 GC 运行时在控制台上或者日志文件中会打印出一个诊断信息,包括它所花费的时间、当前堆使用情况以及恢复了多少内存。记录 GC 使用情况并不具有干扰性,因此如果需要分析内存问题或者调优垃圾收集器,在生产环境中默认启用 GC 日志是值得的。有工具可以利用GC 日志输出并以图形方式将它显示出来,JTune 就是这样的一种工具。观察 GC 之后堆大小的图,可以看到程序内存使用的趋势。对于大多数程序来说,可以将内存使用分为两部分:baseline 使用和 currentload 使用。对于服务器应用程序,baseline 使用就是应用程序在没有任何负荷、但是已经准备好接受请求时的内存使用,current load 使 用是在处理请求过程中使用的、但是在请求处理完成后会释放的内存。只要负荷大体上是恒定的,应用程序通常会很快达到一个稳定的内存使用水平。如果在应用程 序已经完成了其初始化并且负荷没有增加的情况下,内存使用持续增加,那么程序就可能在处理前面的请求时保留了生成的对象。

publicclass MapLeaker{

   public ExecuteService exec = Executors.newFixedThreadPool(5);

   public Map<Task,TaskStatus> taskStatus

       =Collections.synchronizedMap(new HashMap<Task,TaskStatus>());

   private Random random = new Random();

    privateenum TaskStatus { NOT_STARTEDSTARTEDFINISHED };

    privateclass Task implements Runnable{

       private int[] numbers = newint[random.nextInt(200)];

       public void run()

       {

           int[] temp = newint[random.nextInt(10000)];

           taskStatus.put(this,TaskStatus.STARTED);

           doSomework();

           taskStatus.put(this,TaskStatus.FINISHED);

       }

   }

   public Task newTask()

   {

       Task t = new Task();

       taskStatus.put(t,TaskStatus.NOT_STARTED);

       exec.execute(t);

       return t;

   }

}

  [3]使用弱引用堵住内存泄漏:

  SocketManager 的问题是Socket-User 映射的生命周期应当与 Socket 的生命周期相匹配,但是语言没有提供任何容易的方法实施这项规则。这使得程序不得不使用人工内存管理的老技术。幸运的是,从 JDK 1.2 开始,垃圾收集器提供了一种声明这种对象生命周期依赖性的方法,这样垃圾收集器就可以帮助我们防止这种内存泄漏——利用弱引用。弱引用是对一个对象(称为 referent)的引用的持有者。使用弱引用后,可以维持对 referent 的引用,而不会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,如果对一个对象的引用只有弱引用,那么这个 referent 就会成为垃圾收集的候选对象,就像没有任何剩余的引用一样,而且所有剩余的弱引用都被清除。(只有弱引用的对象称为弱可及(weakly reachable))WeakReference 的 referent 是在构造时设置的,在没有被清除之前,可以用 get() 获取它的值。如果弱引用被清除了(不管是 referent 已经被垃圾收集了,还是有人调用了 WeakReference.clear()),get() 会返回 null。相应地,在使用其结果之前,应当总是检查get() 是否返回一个非 null 值,因为 referent 最终总是会被垃圾收集的。用一个普通的(强)引用拷贝一个对象引用时,限制 referent 的生命周期至少与被拷贝的引用的生命周期一样长。如果不小心,那么它可能就与程序的生命周期一样——如果将一个对象放入一个全局集合中的话。另一方面,在创建对一个对象的弱引用时,完全没有扩展 referent 的生命周期,只是在对象仍然存活的时候,保持另一种到达它的方法。弱引用对于构造弱集合最有用,如那些在应用程序的其余部分使用对象期间存储关于这些对象的元数据的集合——这就是 SocketManager类所要做的工作。因为这是弱引用最常见的用法,WeakHashMap 也被添加到 JDK 1.2 的类库中,它对键(而不是对值)使用弱引用。如果在一个普通 HashMap 中用一个对象作为键,那么这个对象在映射从 Map 中删除之前不能被回收,WeakHashMap 使您可以用一个对象作为 Map 键,同时不会阻止这个对象被垃圾收集。下边的代码给出了 WeakHashMap 的 get() 方法的一种可能实现,它展示了弱引用的使用:

publicclass WeakHashMap<K,V> implements Map<K,V>

{

   private staticclass Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V>

   {

       private V value;

       private final int hash;

       private Entry<K,V> next;

       // ...

   }

 

   public V get(Object key)

   {

       int hash = getHash(key);

       Entry<K,V> e = getChain(hash);

       while(e != null)

       {

           k eKey = e.get();

           if( e.hash == hash && (key== eKey || key.equals(eKey)))

              return e.value;

           e = e.next;

       }

       return null;

   }

}

  调用 WeakReference.get() 时,它返回一个对 referent 的强引用(如果它仍然存活的话),因此不需要担心映射在 while 循环体中消失,因为强引用会防止它被垃圾收集。WeakHashMap 的实现展示了弱引用的一种常见用法——一些内部对象扩展 WeakReference。其原因在下面一节讨论引用队列时会得到解释。在向 WeakHashMap 中添加映射时,请记住映射可能会在以后“脱离”,因为键被垃圾收集了。在这种情况下,get() 返回 null,这使得测试 get() 的返回值是否为 null 变得比平时更重要了。

  [4]使用WeakHashMap堵住泄漏

  在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如下边代码所示。(如果 SocketManager 需要线程安全,那么可以用 Collections.synchronizedMap() 包装 WeakHashMap)。当映射的生命周期必须与键的生命周期联系在一起时,可以使用这种方法。不过,应当小心不滥用这种技术,大多数时候还是应当使用普通的 HashMap 作为 Map 的实现。

publicclass SocketManager{

   private Map<Socket,User> m= new WeakHashMap<Socket,User>();

   public void setUser(Socket s, User s)

   {

       m.put(s,u);

   }

   public User getUser(Socket s)

   {

       return m.get(s);

   }

}

  引用队列:

  WeakHashMap 用弱引用承载映射键,这使得应用程序不再使用键对象时它们可以被垃圾收集,get() 实现可以根据 WeakReference.get() 是否返回 null 来区分死的映射和活的映射。但是这只是防止 Map 的内存消耗在应用程序的生命周期中不断增加所需要做的工作的一半,还需要做一些工作以便在键对象被收集后从 Map 中删除死项。否则,Map 会充满对应于死键的项。虽然这对于应用程序是不可见的,但是它仍然会造成应用程序耗尽内存,因为即使键被收集了,Map.Entry 和值对象也不会被收集。可以通过周期性地扫描 Map,对每一个弱引用调用 get(),并在 get() 返回 null 时删除那个映射而消除死映射。但是如果 Map 有许多活的项,那么这种方法的效率很低。如果有一种方法可以在弱引用的 referent 被垃圾收集时发出通知就好了,这就是引用队列的作用。引用队列是垃圾收集器向应用程序返回关于对象生命周期的信息的主要方法。弱引用有两个构造函数:一个只取 referent 作为参数,另一个还取引用队列作为参数。如果用关联的引用队列创建弱引用,在 referent 成为 GC 候选对象时,这个引用对象(不是referent)就在引用清除后加入 到引用队列中。之后,应用程序从引用队列提取引用并了解到它的 referent 已被收集,因此可以进行相应的清理活动,如去掉已不在弱集合中的对象的项。(引用队列提供了与 BlockingQueue 同样的出列模式 ——polled、timedblocking 和 untimed blocking。)WeakHashMap 有一个名为 expungeStaleEntries() 的私有方法,大多数 Map 操作中会调用它,它去掉引用队列中所有失效的引用,并删除关联的映射。

  4)关于Java中引用思考:

  先观察一个列表:

级别

回收时间

用途

生存时间

强引用

从来不会被回收

对象的一般状态

JVM停止运行时终止

软引用

在内存不足时

在客户端移除对象引用过后,除非再次激活,否则就放在内存敏感的缓存中

内存不足时终止

弱引用

在垃圾回收时,也就是客户端已经移除了强引用,但是这种情况下内存还是客户端引用可达的

阻止自动删除不需要用的对象

GC运行后终止

虚引用[幽灵引用]

对象死亡之前,就是进行finalize()方法调用附近

特殊的清除过程

不定,当finalize()函数运行过后再回收,有可能之前就已经被回收了。

  可以这样理解:
  SoftReference
: 假定垃圾回收器确定在某一时间点某个对象是软可到达对象。这时,它可以选择自动清除针对该对象的所有软引用,以及通过强引用链,从其可以到达该对象的针对 任何其他软可到达对象的所有软引用。在同一时间或晚些时候,它会将那些已经向引用队列注册的新清除的软引用加入队列。 软可到达对象的所有软引用都要保证在虚拟机抛出 OutOfMemoryError 之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。 此 类的直接实例可用于实现简单缓存;该类或其派生的子类还可用于更大型的数据结构,以实现更复杂的缓存。只要软引用的指示对象是强可到达对象,即正在实际使 用的对象,就不会清除软引用。例如,通过保持最近使用的项的强指示对象,并由垃圾回收器决定是否放弃剩余的项,复杂的缓存可以防止放弃最近使用的项。一般 来说,WeakReference我们用来防止内存泄漏,保证内存对象被VM回收。

  WeakReference:弱引用对象,它们并不禁止其指示对象变得可终结,并被终结,然后被回收。弱引用最常用于实现规范化的映射。假定垃圾回收器确定在某一时间点上某个对象是弱可到达对象。这时,它将自动清除针对此对象的所有弱引用,以及通过强引用链和软引用,可以从其到达该对象的针对任何其他弱可到达对象的所有弱引用。同时它将声明所有以前的弱可到达对象为可终结的。在同一时间或晚些时候,它将那些已经向引用队列注册的新清除的弱引用加入队列。 SoftReference多用作来实现cache机制,保证cache的有效性。

  PhantomReference:虚引用对象,在回收器确定其指示对象可另外回收之后,被加入队列。虚引用最常见的用法是以某种可能比使用 Java 终结机制更灵活的方式来指派pre-mortem 清除操作。如果垃圾回收器确定在某一特定时间点上虚引用的指示对象是虚可到达对象,那么在那时或者在以后的某一时间,它会将该引用加入队列。为了确保可回 收的对象仍然保持原状,虚引用的指示对象不能被检索:虚引用的 get 方法总是返回 null。与软引用和弱引用不同,虚引用在加入队列时并没有通过垃圾回收器自动清除。通过虚引用可到达的对象将仍然保持原状,直到所有这类引用都被清除,或者它们都变得不可到达

  以下是不确定概念

  【*:Java引用的深入部分一直都是讨论得比较多的话题,上边大部分为摘录整理,这里再谈谈我个人的一些看法。从整个JVM框架结构来看,Java的引用垃圾回收器形成了针对Java内存堆的一个对象的“闭包管理集”,其中在基本代码里面常用的就是强引用,强引用主要使用目的是就是编程的正常逻辑,这是所有的开发人员最容易理解的,而弱引用和软引用的作用是比较耐人寻味的。按照引用强弱,其排序可以为:强引用——软引用——弱引用——虚引用,为什么这样写呢,实际上针对垃圾回收器而言,强引用是它绝对不会随便去动的区域,因为在内存堆里面的对象,只有当前对象不是强引用的时候,该对象才会进入垃圾回收器的目标区域

  软引用又可以理解为“内存应急引用”,也就是说它和GC是完整地配合操作的,为了防止内存泄漏,当GC在回收过程出现内存不足的时候,软引用会被优先回收,从垃圾回收算法上讲,软引用在设计的时候是很容易被垃圾回收器发现的。为什么软引用是处理告诉缓存的优先选择的,主要有两个原因:第一,它对内存非常敏感,从抽象意义上讲,我们甚至可以任何它和内存的变化紧紧绑定到一起操作的,因为内存一旦不足的时候,它会优先向垃圾回收器报警以提示内存不足;第二,它会尽量保证系统在OutOfMemoryError之前将对象直接设置成为不可达,以保证不会出现内存溢出的情况;所以使用软引用来处理Java引用里面的高速缓存是很不错的选择。其实软引用不仅仅和内存敏感,实际上和垃圾回收器的交互也是敏感的,这点可以这样理解,因为当内存不足的时候,软引用会报警,而这种报警会提示垃圾回收器针对目前的一些内存进行清除操作,而在有软引用存在的内存堆里面,垃圾回收器会第一时间反应,否则就会MemoryOut了。按照我们正常的思维来考虑,垃圾回收器针对我们调用System.gc()的时候,是不会轻易理睬的,因为仅仅是收到了来自强引用层代码的请求,至于它是否回收还得看JVM内部环境的条件是否满足,但是如果是软引用的方式去申请垃圾回收器会优先反应,只是我们在开发过程不能控制软引用对垃圾回收器发送垃圾回收申请,而JVM规范里面也指出了软引用不会轻易发送申请到垃圾回收器。这里还需要解释的一点的是软引用发送申请不是说软引用像我们调用System.gc()这样直接申请垃圾回收,而是说软引用会设置对象引用为null,而垃圾回收器针对该引用的这种做法也会优先响应,我们可以理解为是软引用对象在向垃圾回收器发送申请。反应快并不代表垃圾回收器会实时反应,还是会在寻找软引用引用到的对象的时候遵循一定的回收规则,反应快在这里的解释是相对强引用设置对象为null,当软引用设置对象为null的时候,该对象的被收集的优先级比较高

  弱引用是一种比软引用相对复杂的引用,其实弱引用和软引用都是Java程序可以控制的,也就是说可以通过代码直接使得引用针对弱可及对象以及软可及对象是可引用的,软引用和弱引用引用的对象实际上通过一定的代码操作是可重新激活的,只是一般不会做这样的操作,这样的用法违背了最初的设计。弱引用和软引用在垃圾回收器的目标范围有一点点不同的就是,使用垃圾回收算法是很难找到弱引用的,也就是说弱引用用来监控垃圾回收的整个流程也是一种很好的选择,它不会影响垃圾回收的正常流程,这样就可以规范化整个对象从设置为null了过后的一个生命周期的代码监控。而且因为弱引用是否存在对垃圾回收整个流程都不会造成影响,可以这样认为,垃圾回收器找得到弱引用,该引用的对象就会被回收,如果找不到弱引用,一旦等到GC完成了垃圾回收过后,弱引用引用的对象占用的内存也会自动释放,这就是软引用在垃圾回收过后的自动终止。

  最后谈谈虚引用,虚引用应该是JVM里面最厉害的一种引用,它的厉害在于它可以在对象的内存物理内存中清除掉了过后再引用该对象,也就是说当虚引用引用到对象的时候,这个对象实际已经从物理内存堆清除掉了,如果我们不用手动对对象死亡或者濒临死亡进行处理的话,JVM会默认调用finalize函数,但是虚引用存在于该函数附近的生命周期内,所以可以手动对对象的这个范围的周期进行监控。它之所以称为“幽灵引用”就是因为该对象的物理内存已经不存在的,我个人觉得JVM保存了一个对象状态的镜像索引,而这个镜像索引里面包含了对象在这个生命周期需要的所有内容,这里的所需要就是这个生命周期内需要的对象数据内容,也就是对象死亡和濒临死亡之前finalize函数附近,至于强引用所需要的其他对象附加内容是不需要在这个镜像里面包含的,所以即使物理内存不存在,还是可以通过虚引用监控到该对象的,只是这种情况是否可以让对象重新激活为强引用我就不敢说了。因为虚引用在引用对象的过程不会去使得这个对象由Dead复活,而且这种对象是可以在回收周期进行回收的。

 

5.总结:

  本章节主要涵盖了Java里面比较底层的一个章节,主要是以JVM内存模型为基础包括JVM针对内存的线程模型的探讨以及针对Java里面内存堆和栈的详细分析。特别感谢白远方同学提供的汇编方面关于操作系统以及内存发展的资料提供。

 

 

Java 动态代理的机制和特点

本文通过分析 Java 动态代理的机制和特点,解读动态代理类的源代码,并且模拟推演了动态代理类的可能实现,向读者阐述了一个完整的 Java 动态代理运作过程,希望能帮助读者加深对 Java 动态代理的理解和应用。

引言

Java 动态代理机制的出现,使得 Java 开发人员不用手工编写代理类,只要简单地指定一组接口及委托类对象,便能动态地获得代理类。代理类会负责将所有的方法调用分派到委托对象上反射执行,在分派执行的过程中,开发人员还可以按需调整委托类对象及其功能,这是一套非常灵活有弹性的代理框架。通过阅读本文,读者将会对 Java 动态代理机制有更加深入的理解。本文首先从 Java 动态代理的运行机制和特点出发,对其代码进行了分析,推演了动态生成类的内部实现。

 

代理:设计模式

代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。


图 1. 代理模式

为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。Java 动态代理机制以巧妙的方式近乎完美地实践了代理模式的设计理念。

 

相关的类和接口

要了解 Java 动态代理的机制,首先需要了解以下相关的类或接口:

java.lang.reflect.Proxy:这是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。

清单 1. Proxy 的静态方法
                      

// 方法 1: 该方法用于获取指定代理对象所关联的调用处理器

static InvocationHandler getInvocationHandler(Object proxy)

 

// 方法 2:该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象

static Class getProxyClass(ClassLoader loader, Class[] interfaces)

 

// 方法 3:该方法用于判断指定类对象是否是一个动态代理类

static boolean isProxyClass(Class cl)

 

// 方法 4:该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例

static Object newProxyInstance(ClassLoader loader, Class[] interfaces,

    InvocationHandler h)

 

 

java.lang.reflect.InvocationHandler:这是调用处理器接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。

清单 2. InvocationHandler 的核心方法
                     

// 该方法负责集中处理动态代理类上的所有方法调用。第一个参数既是代理类实例,第二个参数是被调用的方法对象

// 第三个方法是调用参数。调用处理器根据这三个参数进行预处理或分派到委托类实例上发射执行

Object invoke(Object proxy, Method method, Object[] args)

 


每次生成动态代理类对象时都需要指定一个实现了该接口的调用处理器对象(参见 Proxy 静态方法 4 的第三个参数)。

             java.lang.ClassLoader:这是类装载器类,负责将类的字节码装载到 Java 虚拟机(JVM)中并为其定义类对象,然后该类才能被使用。Proxy 静态方法生成动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别就是其字节码是由 JVM 在运行时动态生成的而非预存在于任何一个 .class 文件中。

每次生成动态代理类对象时都需要指定一个类装载器对象(参见 Proxy 静态方法 4 的第一个参数)

代理机制及其特点

首先让我们来了解一下如何使用 Java 动态代理。具体有如下四步骤:

1        通过实现 InvocationHandler 接口创建自己的调用处理器;

1        通过为 Proxy 类指定ClassLoader 对象和一组 interface 来创建动态代理类;

1        通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;

1        通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。

             清单 3. 动态代理对象创建过程

             

// InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发

// 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用

InvocationHandler handler = new InvocationHandlerImpl(..);

 

// 通过 Proxy 为包括 Interface 接口在内的一组接口动态创建代理类的类对象

Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, ... });

 

// 通过反射从生成的类对象获得构造函数对象

Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class });

 

// 通过构造函数对象创建动态代理类实例

Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler });

 

 

实际使用过程更加简单,因为 Proxy 的静态方法newProxyInstance 已经为我们封装了步骤 2 到步骤 4 的过程,所以简化后的过程如下


清单 4. 简化的动态代理对象创建过程

             

// InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发

InvocationHandler handler = new InvocationHandlerImpl(..);

 

// 通过 Proxy 直接创建动态代理类实例

Interface proxy = (Interface)Proxy.newProxyInstance( classLoader,

     new Class[] { Interface.class },

     handler );

 

 

接下来让我们来了解一下 Java 动态代理机制的一些特点。

首先是动态生成的代理类本身的一些特点。1)包:如果所代理的接口都是public 的,那么它将被定义在顶层包(即包路径为空),如果所代理的接口中有非 public 的接口(因为接口不能被定义为protect 或 private,所以除 public 之外就是默认的 package 访问级别),那么它将被定义在该接口所在包(假设代理了 com.ibm.developerworks 包中的某非 public接口 A,那么新生成的代理类所在的包就是 com.ibm.developerworks),这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题而无法被成功定义并访问;2)类修饰符:该代理类具有final 和 public 修饰符,意味着它可以被所有的类访问,但是不能被再度继承;3)类名:格式是“$ProxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表 Proxy 类第 N 次生成的动态代理类,值得注意的一点是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。4)类继承关系:该类的继承关系如图:


图 2. 动态代理类的继承图

由图可见,Proxy 类是它的父类,这个规则适用于所有由 Proxy 创建的动态代理类。而且该类还实现了其所代理的一组接口,这就是为什么它能够被安全地类型转换到其所代理的某接口的根本原因。

接下来让我们了解一下代理类实例的一些特点。每个实例都会关联一个调用处理器对象,可以通过 Proxy 提供的静态方法getInvocationHandler 去获得代理类实例的调用处理器对象。在代理类实例上调用其代理的接口中所声明的方法时,这些方法最终都会由调用处理器的 invoke 方法执行,此外,值得注意的是,代理类的根类 java.lang.Object 中有三个方法也同样会被分派到调用处理器的 invoke 方法执行,它们是 hashCode,equals 和 toString,可能的原因有:一是因为这些方法为 public 且非 final 类型,能够被代理类覆盖;二是因为这些方法往往呈现出一个类的某种特征属性,具有一定的区分度,所以为了保证代理类与委托类对外的一致性,这三个方法也应该被分派到委托类执行。当代理的一组接口有重复声明的方法且该方法被调用时,代理类总是从排在最前面的接口中获取方法对象并分派给调用处理器,而无论代理类实例是否正在以该接口(或继承于该接口的某子接口)的形式被外部引用,因为在代理类内部无法区分其当前的被引用类型。

接着来了解一下被代理的一组接口有哪些特点。首先,要注意不能有重复的接口,以避免动态代理类代码生成时的编译错误。其次,这些接口对于类装载器必须可见,否则类装载器将无法链接它们,将会导致类定义失败。再次,需被代理的所有非 public 的接口必须在同一个包中,否则代理类生成也会失败。最后,接口的数目不能超过 65535,这是 JVM 设定的限制。

最后再来了解一下异常处理方面的特点。从调用处理器接口声明的方法中可以看到理论上它能够抛出任何类型的异常,因为所有的异常都继承于 Throwable 接口,但事实是否如此呢?答案是否定的,原因是我们必须遵守一个继承原则:即子类覆盖父类或实现父接口的方法时,抛出的异常必须在原方法支持的异常列表之内。所以虽然调用处理器理论上讲能够,但实际上往往受限制,除非父接口中的方法支持抛 Throwable 异常。那么如果在invoke 方法中的确产生了接口方法声明中不支持的异常,那将如何呢?放心,Java 动态代理类已经为我们设计好了解决方法:它将会抛出 UndeclaredThrowableException 异常。这个异常是一个 RuntimeException 类型,所以不会引起编译错误。通过该异常的 getCause 方法,还可以获得原来那个不受支持的异常对象,以便于错误诊断。

代码是最好的老师

机制和特点都介绍过了,接下来让我们通过源代码来了解一下 Proxy 到底是如何实现的。

首先记住 Proxy 的几个重要的静态变量:


清单 5.Proxy 的重要静态变量

             

// 映射表:用于维护类装载器对象到其对应的代理类缓存

private static Map loaderToCache = new WeakHashMap();

 

// 标记:用于标记一个动态代理类正在被创建中

private static Object pendingGenerationMarker = new Object();

 

// 同步表:记录已经被创建的动态代理类类型,主要被方法 isProxyClass 进行相关的判断

private static Map proxyClasses = Collections.synchronizedMap(new WeakHashMap());

 

// 关联的调用处理器引用

protected InvocationHandler h;

 

 

然后,来看一下 Proxy 的构造方法:


清单 6.Proxy 构造方法

             

// 由于 Proxy 内部从不直接调用构造函数,所以 private 类型意味着禁止任何调用

private Proxy() {}

 

// 由于 Proxy 内部从不直接调用构造函数,所以 protected 意味着只有子类可以调用

protected Proxy(InvocationHandler h) {this.h = h;}

 

 

接着,可以快速浏览一下 newProxyInstance 方法,因为其相当简单:


清单 7.Proxy 静态方法newProxyInstance

             

public static Object newProxyInstance(ClassLoader loader,

            Class<?>[] interfaces,

            InvocationHandler h)

            throws IllegalArgumentException {

   

    // 检查 h 不为空,否则抛异常

    if (h == null) {

        throw new NullPointerException();

    }

 

    // 获得与制定类装载器和一组接口相关的代理类类型对象

    Class cl = getProxyClass(loader, interfaces);

 

    // 通过反射获取构造函数对象并生成代理类实例

    try {

        Constructor cons = cl.getConstructor(constructorParams);

        return (Object) cons.newInstance(new Object[] { h });

    } catch (NoSuchMethodException e) { throw new InternalError(e.toString());

    } catch (IllegalAccessException e) { throw new InternalError(e.toString());

    } catch (InstantiationException e) { throw new InternalError(e.toString());

    } catch (InvocationTargetException e) { throw new InternalError(e.toString());

    }

}

 

 

由此可见,动态代理真正的关键是在 getProxyClass 方法,该方法负责为一组接口动态地生成代理类类型对象。在该方法内部,您将能看到 Proxy 内的各路英雄(静态变量)悉数登场。有点迫不及待了么?那就让我们一起走进 Proxy 最最神秘的殿堂去欣赏一番吧。该方法总共可以分为四个步骤:

对这组接口进行一定程度的安全检查,包括检查接口类对象是否对类装载器可见并且与类装载器所能识别的接口类对象是完全相同的,还会检查确保是 interface 类型而不是 class 类型。这个步骤通过一个循环来完成,检查通过后将会得到一个包含所有接口名称的字符串数组,记为 String[] interfaceNames。总体上这部分实现比较直观,所以略去大部分代码,仅保留留如何判断某类或接口是否对特定类装载器可见的相关代码。

清单 8. 通过 Class.forName 方法判接口的可见性
                      

try {

    // 指定接口名字、类装载器对象,同时制定 initializeBoolean 为 false 表示无须初始化类

    // 如果方法返回正常这表示可见,否则会抛出 ClassNotFoundException 异常表示不可见

    interfaceClass = Class.forName(interfaceName, false, loader);

} catch (ClassNotFoundException e) {

}

 

 

从 loaderToCache 映射表中获取以类装载器对象为关键字所对应的缓存表,如果不存在就创建一个新的缓存表并更新到 loaderToCache。缓存表是一个 HashMap 实例,正常情况下它将存放键值对(接口名字列表,动态生成的代理类的类对象引用)。当代理类正在被创建时它会临时保存(接口名字列表,pendingGenerationMarker)。标记 pendingGenerationMarke 的作用是通知后续的同类请求(接口数组相同且组内接口排列顺序也相同)代理类正在被创建,请保持等待直至创建完成。

清单 9. 缓存表的使用
                     

do {

    // 以接口名字列表作为关键字获得对应 cache 值

    Object value = cache.get(key);

    if (value instanceof Reference) {

        proxyClass = (Class) ((Reference) value).get();

    }

    if (proxyClass != null) {

        // 如果已经创建,直接返回

        return proxyClass;

    } else if (value == pendingGenerationMarker) {

        // 代理类正在被创建,保持等待

        try {

            cache.wait();

        } catch (InterruptedException e) {

        }

        // 等待被唤醒,继续循环并通过二次检查以确保创建完成,否则重新等待

        continue;

    } else {

        // 标记代理类正在被创建

        cache.put(key, pendingGenerationMarker);

        // break 跳出循环已进入创建过程

        break;

} while (true);

 

 

动态创建代理类的类对象。首先是确定代理类所在的包,其原则如前所述,如果都为 public 接口,则包名为空字符串表示顶层包;如果所有非 public 接口都在同一个包,则包名与这些接口的包名相同;如果有多个非 public 接口且不同包,则抛异常终止代理类的生成。确定了包后,就开始生成代理类的类名,同样如前所述按格式“$ProxyN”生成。类名也确定了,接下来就是见证奇迹的发生 —— 动态生成代理类:

清单 10. 动态生成代理类
                     

// 动态地生成代理类的字节码数组

byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces);

try {

    // 动态地定义新生成的代理类

    proxyClass = defineClass0(loader, proxyName, proxyClassFile, 0,

        proxyClassFile.length);

} catch (ClassFormatError e) {

    throw new IllegalArgumentException(e.toString());

}

 

// 把生成的代理类的类对象记录进 proxyClasses 表

proxyClasses.put(proxyClass, null);

 


由此可见,所有的代码生成的工作都由神秘的 ProxyGenerator 所完成了,当你尝试去探索这个类时,你所能获得的信息仅仅是它位于并未公开的 sun.misc 包,有若干常量、变量和方法以完成这个神奇的代码生成的过程,但是 sun 并没有提供源代码以供研读。至于动态类的定义,则由 Proxy 的 native 静态方法 defineClass0 执行。

1        代码生成过程进入结尾部分,根据结果更新缓存表,如果成功则将代理类的类对象引用更新进缓存表,否则清楚缓存表中对应关键值,最后唤醒所有可能的正在等待的线程。

走完了以上四个步骤后,至此,所有的代理类生成细节都已介绍完毕,剩下的静态方法如 getInvocationHandler 和 isProxyClass 就显得如此的直观,只需通过查询相关变量就可以完成,所以对其的代码分析就省略了。

代理类实现推演

分析了 Proxy 类的源代码,相信在读者的脑海中会对 Java 动态代理机制形成一个更加清晰的理解,但是,当探索之旅在 sun.misc.ProxyGenerator 类处嘎然而止,所有的神秘都汇聚于此时,相信不少读者也会对这个 ProxyGenerator 类产生有类似的疑惑:它到底做了什么呢?它是如何生成动态代理类的代码的呢?诚然,这里也无法给出确切的答案。还是让我们带着这些疑惑,一起开始探索之旅吧。

事物往往不像其看起来的复杂,需要的是我们能够化繁为简,这样也许就能有更多拨云见日的机会。抛开所有想象中的未知而复杂的神秘因素,如果让我们用最简单的方法去实现一个代理类,唯一的要求是同样结合调用处理器实施方法的分派转发,您的第一反应将是什么呢?“听起来似乎并不是很复杂”。的确,掐指算算所涉及的工作无非包括几个反射调用,以及对原始类型数据的装箱或拆箱过程,其他的似乎都已经水到渠成。非常地好,让我们整理一下思绪,一起来完成一次完整的推演过程吧。

1        
清单 11. 代理类中方法调用的分派转发推演实现

             

// 假设需代理接口 Simulator

public interface Simulator {

    short simulate(int arg1, long arg2, String arg3) throws ExceptionA, ExceptionB;

}

 

// 假设代理类为 SimulatorProxy, 其类声明将如下

final public class SimulatorProxy implements Simulator {

   

    // 调用处理器对象的引用

    protected InvocationHandler handler;

   

    // 以调用处理器为参数的构造函数

    public SimulatorProxy(InvocationHandler handler){

        this.handler = handler;

    }

   

    // 实现接口方法 simulate

    public short simulate(int arg1, long arg2, String arg3)

        throws ExceptionA, ExceptionB {

 

        // 第一步是获取 simulate 方法的 Method 对象

        java.lang.reflect.Method method = null;

        try{

            method = Simulator.class.getMethod(

                "simulate",

                new Class[] {int.class, long.class, String.class} );

        } catch(Exception e) {

            // 异常处理 1(略)

        }

       

        // 第二步是调用 handler 的 invoke 方法分派转发方法调用

        Object r = null;

        try {

            r = handler.invoke(this,

                method,

                // 对于原始类型参数需要进行装箱操作

                new Object[] {new Integer(arg1), new Long(arg2), arg3});

        }catch(Throwable e) {

            // 异常处理 2(略)

        }

        // 第三步是返回结果(返回类型是原始类型则需要进行拆箱操作)

        return ((Short)r).shortValue();

    }

}

 

模拟推演为了突出通用逻辑所以更多地关注正常流程,而淡化了错误处理,但在实际中错误处理同样非常重要。从以上的推演中我们可以得出一个非常通用的结构化流程:第一步从代理接口获取被调用的方法对象,第二步分派方法到调用处理器执行,第三步返回结果。在这之中,所有的信息都是可以已知的,比如接口名、方法名、参数类型、返回类型以及所需的装箱和拆箱操作,那么既然我们手工编写是如此,那又有什么理由不相信 ProxyGenerator 不会做类似的实现呢?至少这是一种比较可能的实现。

接下来让我们把注意力重新回到先前被淡化的错误处理上来。在异常处理 1 处,由于我们有理由确保所有的信息如接口名、方法名和参数类型都准确无误,所以这部分异常发生的概率基本为零,所以基本可以忽略。而异常处理 2 处,我们需要思考得更多一些。回想一下,接口方法可能声明支持一个异常列表,而调用处理器 invoke 方法又可能抛出与接口方法不支持的异常,再回想一下先前提及的 Java 动态代理的关于异常处理的特点,对于不支持的异常,必须抛 UndeclaredThrowableException 运行时异常。所以通过再次推演,我们可以得出一个更加清晰的异常处理 2 的情况:


清单 12. 细化的异常处理 2

             

Object r = null;

 

try {

    r = handler.invoke(this,

        method,

        new Object[] {new Integer(arg1), new Long(arg2), arg3});

 

} catch( ExceptionA e) {

 

    // 接口方法支持 ExceptionA,可以抛出

    throw e;

 

} catch( ExceptionB e ) {

    // 接口方法支持 ExceptionB,可以抛出

    throw e;

 

} catch(Throwable e) {

    // 其他不支持的异常,一律抛 UndeclaredThrowableException

    throw new UndeclaredThrowableException(e);

}

 

这样我们就完成了对动态代理类的推演实现。推演实现遵循了一个相对固定的模式,可以适用于任意定义的任何接口,而且代码生成所需的信息都是可知的,那么有理由相信即使是机器自动编写的代码也有可能延续这样的风格,至少可以保证这是可行的。

美中不足

诚然,Proxy 已经设计得非常优美,但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持 interface 代理的桎梏,因为它的设计注定了这个遗憾。回想一下那些动态生成的代理类的继承关系图,它们已经注定有一个共同的父类叫 Proxy。Java 的继承机制注定了这些动态代理类们无法实现对 class 的动态代理,原因是多继承在Java 中本质上就行不通。

有很多条理由,人们可以否定对 class 代理的必要性,但是同样有一些理由,相信支持 class 动态代理会更美好。接口和类的划分,本就不是很明显,只是到了 Java 中才变得如此的细化。如果只从方法的声明及是否被定义来考量,有一种两者的混合体,它的名字叫抽象类。实现对抽象类的动态代理,相信也有其内在的价值。此外,还有一些历史遗留的类,它们将因为没有实现任何接口而从此与动态代理永世无缘。如此种种,不得不说是一个小小的遗憾。

但是,不完美并不等于不伟大,伟大是一种本质,Java 动态代理就是佐例。

 

基于Spring+Ibatis的安全线程实现

过去做过一些基于spring、hibernate整合应用的实例,本人感觉spring与hibernate最好的结合就是泛型Dao的实现,代码量节省了一半,而且业务逻辑一目了然。
    后来做别的系统时候考虑过这样的框架,但是数据库结构如果不固定,动态生成的东西比较多这个时候只好放弃了hibernate而选择了同样具有orm性能的ibatis,下面就spring与ibatis的结合相关配置做下说明(如有不同意见,希望交流)

       首先spring和ibatis具体下载和安装就不多说了。直接切入正题

       Spring框架下的ibatis应用,特别是在容器事务管理模式下的ibatis应用开发

       部署如下:

       首先spring配置文件:

   view plaincopy to clipboardprint?
      Spring_base.xml

       <?xml version="1.0" encoding="UTF-8" ?>

       <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

       <beans default-lazy-init="true">

              <!-- 配置数据源 -->

              <bean id="dataSource"class="org.apache.commons.dbcp.BasicDataSource"destroy-method="close">

                     <property name="driverClassName">

                            <value>net.sourceforge.jtds.jdbc.Driver</value>

                     </property>

                     <property name="url">

                            <value>jdbc:jtds:sqlserver://localhost:1433/test</value>

                     </property>

                     <property name="username">

                            <value>sa</value>

                     </property>

                     <property name="password">

                            <value>sa</value>

                     </property>

                     <property name="maxActive">

                            <value>10</value>

                     </property>

                     <property name="maxIdle">

                            <value>2</value>

                     </property>

                     <property name="maxWait">

                            <value>300</value>

                     </property>

              </bean>

       /// dataSource:配置你的数据源连接

              <bean id="sqlMapClient"

                     class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">

                     <property name="configLocation">

                            <value>SqlMap_config.xml</value>

                     </property>

                     <property name="dataSource"><!-- 从指定dataSource中获取数据源 亦可把该定义放到每个自定义Dao中-->

                            <ref bean="dataSource" />

                     </property>

              </bean>

        sqlMapClient:集成ibatis配置文件和把数据源与ibatis相关联

              <!-- 配置事务管理 -->

              <bean id="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

                     <property name="dataSource">

                            <ref local="dataSource" />

                     </property>

              </bean>

       / transactionManager:配置事务管理

              <!--公共组件-->

              <import resource="spring_other.xml" />

       把用户自定义Bean与基本bean分开,集成进去spring_other.xml文件

       </beans>

       以上是spring 把一些ibatis相关配置集成到自己的配置文件里面

       Spring_other.xml

       <?xml version="1.0" encoding="UTF-8"?>

       <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

       <beans>

           <bean id="userService"class="com.user.UserServiceImpl">

              <property name="transactionManager">

                  <ref bean="transactionManager" />

              </property>

              <property name="userDao">

                  <ref local="userDao" />

              </property>

           </bean>

       使用service管理所有用户自定义bean和Dao操作,用来设置事务回滚,线程安全等。

           <bean id="userDao" class="com.user.dao.UserDaoImpl">

               <property name="sqlMapClient" ref="sqlMapClient"/>

           </bean>

       ///用户自定义Dao操作,因spring_base.xml中sqlMapClient已经把dataSource包含,故dataSource不再声明,如果该操作需要别的数据连接,可加入例如:

       <property name=”dataSource1”ref=”dataSource1”/>//

       </beans>

       Spring_other.xml存放用户自定义bean

       SqlMap_config.xml

       <?xml version="1.0" encoding="UTF-8" ?>

       <!DOCTYPE sqlMapConfig

           PUBLIC "-//ibatis.apache.org//DTD <A title=sql href="http://www.google.cn/search?sbi=sql&q=sql&sbb=搜索&sa=搜索&client=pub-6430022987645146&forid=1&prog=aff&ie=GB2312&oe=GB2312&hl=zh-CN"target=_blank>sql</A> Map Config 2.0//EN"

           "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">

       <sqlMapConfig>

           <settings lazyLoadingEnabled="true"

              useStatementNamespaces="true"

              enhancementEnabled="true"

              errorTracingEnabled="true"

              />

      定义ibatis相关操作参数,例如延迟加载,命名空间是否生效,是否打开缓存,调试阶段出错信息反馈等等

           <sqlMap resource="com/user/user.xml" />

       //包含用户的相关操作xml文件

       </sqlMapConfig>

       User.xml

       <?xml version="1.0" encoding="UTF-8" ?>

       <!DOCTYPE sqlMap

           PUBLIC "-//ibatis.apache.org//DTD <A title=sql href="http://www.google.cn/search?sbi=sql&q=sql&sbb=搜索&sa=搜索&client=pub-6430022987645146&forid=1&prog=aff&ie=GB2312&oe=GB2312&hl=zh-CN"target=_blank>sql</A> Map 2.0//EN"

           "http://ibatis.apache.org/dtd/sql-map-2.dtd">

       <sqlMap namespace="User">

           <typeAlias alias="user" type="com.user.User"/><!-- obj -->

           <!--  get user -->

           <select id="getUser"

           parameterClass="java.lang.String"

           resultClass="user">

          <![CDATA[

               select

               id

               name,

               sex

               from

               t_user

               where name like #name#

          ]]>

           </select>

           <!-- update user -->

           <update id="updateUser" parameterClass="user">

              <![CDATA[

                  update t_user

                  set

                     name=#name#,

                     sex=#sex#

  where id =#id#

              ]]>

           </update>

           <insert id="insertUser" parameterClass="user">

              <![CDATA[

                  insert into t_user(

                         id,

                         name,

                         sex)

                     values(

                         #id#,

                         #name#,

                         #sex#

                     )

              ]]>

           </insert>

           <delete id="deleteUser"parameterClass="java.lang.String">

              <![CDATA[

                  delete from t_user

                  where id = #value#

              ]]>

           </delete>

           <select id="selectUser"  resultClass="user">

              <![CDATA[

                  select * from t_user order by id desc

              ]]>

           </select>

       </sqlMap>

       该配置文件属性就不多了。用户可在网上搜一堆够看了。

       针对spring_other.xml 里面的用户自定义bean如下

       UserDao.java 接口

              package com.user.dao;

                  import java.util.List;

                  import com.user.User;

              public interface UserDao {

             public User getUser(String name);

          public void updateUser(User user);

          public List selectUser();

          public void insertUser(User user);

          }

       UserDaoImpl.java 实现类

          package com.user.dao;

          import java.util.List;

          import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

          import com.user.User;

          public class UserDaoImpl extends SqlMapClientDaoSupport implements UserDao {

          //private static Logger log = Logger.getLogger(UserDaoImpl.class);

          public User getUser(String name) {

                  return (User)this.getSqlMapClientTemplate().queryForObject("User.getUser","name");

          }

          public void updateUser(User user) {

                  this.getSqlMapClientTemplate().update("User.updateUser", user);

          }

          public List selectUser() {

                  returnthis.getSqlMapClientTemplate().queryForList("User.selectUser","");

          }

          public void insertUser(User user) {

                  this.getSqlMapClientTemplate().insert("User.insertUser", user);

          }

          }
       现在大家也许看到这里觉得就差不多了。该Dao方法差不多全了,可以进行操作了。其实不然,下面我载自官方的一段:

     Spring_base.xml

       <?xml version="1.0" encoding="UTF-8" ?>

       <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

       <beans default-lazy-init="true">

              <!-- 配置数据源 -->

              <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"destroy-method="close">

                     <property name="driverClassName">

                            <value>net.sourceforge.jtds.jdbc.Driver</value>

                     </property>

                     <property name="url">

                            <value>jdbc:jtds:sqlserver://localhost:1433/test</value>

                     </property>

                     <property name="username">

                            <value>sa</value>

                     </property>

                     <property name="password">

                            <value>sa</value>

                     </property>

                     <property name="maxActive">

                            <value>10</value>

                     </property>

                     <property name="maxIdle">

                            <value>2</value>

                     </property>

                     <property name="maxWait">

                            <value>300</value>

                     </property>

              </bean>

       /// dataSource:配置你的数据源连接

              <bean id="sqlMapClient"

                     class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">

                     <property name="configLocation">

                            <value>SqlMap_config.xml</value>

                     </property>

                    <property name="dataSource"><!-- 从指定dataSource中获取数据源 亦可把该定义放到每个自定义Dao中-->

                            <ref bean="dataSource" />

                     </property>

              </bean>

        sqlMapClient:集成ibatis配置文件和把数据源与ibatis相关联

              <!-- 配置事务管理 -->

              <bean id="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

                     <property name="dataSource">

                            <ref local="dataSource" />

                     </property>

              </bean>

       / transactionManager:配置事务管理

              <!--公共组件-->

              <import resource="spring_other.xml" />

       把用户自定义Bean与基本bean分开,集成进去spring_other.xml文件

       </beans>

 

       以上是spring 把一些ibatis相关配置集成到自己的配置文件里面

       Spring_other.xml

       <?xml version="1.0" encoding="UTF-8"?>

       <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

       <beans>

           <bean id="userService" class="com.user.UserServiceImpl">

              <property name="transactionManager">

                  <ref bean="transactionManager" />

              </property>

              <property name="userDao">

                  <ref local="userDao" />

              </property>

           </bean>

       使用service管理所有用户自定义bean和Dao操作,用来设置事务回滚,线程安全等。

           <bean id="userDao" class="com.user.dao.UserDaoImpl">

               <property name="sqlMapClient" ref="sqlMapClient"/>

           </bean>

       ///用户自定义Dao操作,因spring_base.xml中sqlMapClient已经把dataSource包含,故dataSource不再声明,如果该操作需要别的数据连接,可加入例如:

       <property name=”dataSource1”ref=”dataSource1”/>//

       </beans>

       Spring_other.xml存放用户自定义bean

       SqlMap_config.xml

       <?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPEsqlMapConfig

           PUBLIC "-//ibatis.apache.org//DTD sql Map Config 2.0//EN"

           "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">

       <sqlMapConfig>

           <settings lazyLoadingEnabled="true"

              useStatementNamespaces="true"

              enhancementEnabled="true"

              errorTracingEnabled="true"

              />

      /定义ibatis相关操作参数,例如延迟加载,命名空间是否生效,是否打开缓存,调试阶段出错信息反馈等等

           <sqlMap resource="com/user/user.xml" />

       //包含用户的相关操作xml文件

       </sqlMapConfig>

       User.xml

       <?xml version="1.0" encoding="UTF-8" ?>

       <!DOCTYPE sqlMap

           PUBLIC "-//ibatis.apache.org//DTD sql Map 2.0//EN"

           "http://ibatis.apache.org/dtd/sql-map-2.dtd">

       <sqlMap namespace="User">

           <typeAlias alias="user" type="com.user.User"/><!-- obj -->

           <!--  get user -->

           <select id="getUser"

           parameterClass="java.lang.String"

           resultClass="user">

          <![CDATA[

               select

               id,

               name,

               sex

               from

               t_user

               where name like #name#

          ]]>

           </select>

           <!-- update user -->

           <update id="updateUser" parameterClass="user">

              <![CDATA[

                  update t_user

                  set

                     name=#name#,

                     sex=#sex#

                  where id = #id#

              ]]>

           </update>

 

           <insert id="insertUser" parameterClass="user">

              <![CDATA[

                  insert into t_user(

                         id,

                         name,

                         sex)

                     values(

                         #id#,

                         #name#,

                         #sex#

                     )

              ]]>

           </insert>

           <delete id="deleteUser"parameterClass="java.lang.String">

              <![CDATA[

                  delete from t_user

                  where id = #value#

              ]]>

           </delete>

           <select id="selectUser"  resultClass="user">

              <![CDATA[

                  select * from t_user order by id desc

              ]]>

           </select>

       </sqlMap>

       该配置文件属性就不多了。用户可在网上搜一堆够看了。

       针对spring_other.xml 里面的用户自定义bean如下

       UserDao.java 接口

              package com.user.dao;

                  import java.util.List;

                  import com.user.User;

              public interface UserDao {

             public User getUser(String name);

          public void updateUser(User user);

          public List selectUser();

          public void insertUser(User user);

          }

       UserDaoImpl.java 实现类

          package com.user.dao;

          import java.util.List;

          import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

          import com.user.User;

          public class UserDaoImpl extends SqlMapClientDaoSupport implements UserDao {

          //private static Logger log = Logger.getLogger(UserDaoImpl.class);

          public User getUser(String name) {

                  return (User) this.getSqlMapClientTemplate().queryForObject("User.getUser","name");

          }

          public void updateUser(User user) {

                  this.getSqlMapClientTemplate().update("User.updateUser", user);

          }

          public List selectUser() {

                  return this.getSqlMapClientTemplate().queryForList("User.selectUser","");

          }

          public void insertUser(User user) {

                  this.getSqlMapClientTemplate().insert("User.insertUser", user);

          }

          }
       现在大家也许看到这里觉得就差不多了。该Dao方法差不多全了,可以进行操作了。其实不然,下面我载自官方的一段:
    Spring提供两种方式的编程式事务管理,分别是:使用TransactionTemplate和直接使用PlatformTransactionManager。
      ⅰ.
    TransactionTempale采用和其他Spring模板,如JdbcTempalte和HibernateTemplate一样的方法。它使用回调方法,把应用程序从处理取得和释放资源中解脱出来。如同其他模板,TransactionTemplate是线程安全的
    view plaincopy to clipboardprint?
      所以我们下面我们要再封装一层以实现线程是安全的。这就是我们在spirng_other.xml里面的那段配置实现

     所以我们下面我们要再封装一层以实现线程是安全的。这就是我们在spirng_other.xml里面的那段配置实现view plaincopy to clipboardprint?
    <PRE class=csharpname="code">baseService.java

       package com.base;

       import org.springframework.transaction.support.TransactionTemplate;

       /**

        * 工厂的基础类.

        * @author 刘玉华

        * @time <A title=2007 href="http://www.google.cn/search?sbi=基金&amp;amp;q=基金&amp;amp;sbb=搜索&amp;amp;sa=搜索&amp;amp;client=pub-6430022987645146&amp;amp;forid=1&amp;amp;prog=aff&amp;amp;ie=GB2312&amp;amp;oe=GB2312&amp;amp;hl=zh-CN"target=_blank>2007</A>-12-14

        */

       public class BaseService extends TransactionTemplate{

           private static final long serialVersionUID = 1L;

       }

       serviceFactory.java

          package com.base;

          import org.springframework.beans.factory.BeanFactory;

          import org.springframework.context.support.ClassPathXmlApplicationContext;

          import com.user.dao.UserDao;

          /**

* 数据操作工厂,所有的数据操作都从该工厂中获得。

           * @author 刘玉华

           * @time <A title=2007 href="http://www.google.cn/search?sbi=基金&amp;amp;q=基金&amp;amp;sbb=搜索&amp;amp;sa=搜索&amp;amp;client=pub-6430022987645146&amp;amp;forid=1&amp;amp;prog=aff&amp;amp;ie=GB2312&amp;amp;oe=GB2312&amp;amp;hl=zh-CN"target=_blank>2007</A>-12-14

           */

          public class ServiceFactory {

          private static BeanFactory factory = null;

          static {

                  ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(

                                new String[] {"spring_base.xml"});

                  factory = (BeanFactory) context;

          }

          /**

           * 获得用户服务类

           * @return 用户服务

           */

          public static UserDao getUserService(){

                  return (UserDao) factory.getBean("userService");

          }

          }

       我们所需要做的就是继承Baseservice.java 以实现tx管理

       UserService.java

       package com.user;

       import java.util.List;

       public interface UserService {

           public User getUser(final String name);

           public void updateUser(final User user);

           public List selectUser();

           public void insertUser(final User user);

       }

       UserServiceImpl.java 用户服务实现类

          package com.user;

          import java.util.List;

          import org.springframework.transaction.TransactionStatus;

          import org.springframework.transaction.support.TransactionCallback;

          import com.base.BaseService;

          import com.user.dao.UserDao;

          public class UserServiceImpl extends BaseService implements UserDao {

         private UserDao userDao;

          public UserDao getUserDao() {

                  return userDao;

          }

          public void setUserDao(UserDao userDao) {

                  this.userDao = userDao;

          }

          public List selectUser() {

                  Object obj = execute(new TransactionCallback(){

                         public Object doInTransaction(TransactionStatus status) {

                                return userDao.selectUser();

                         }

                  });

                       return (List)obj;

                }

                public User getUser(final String name){

                       Object obj = execute(new TransactionCallback(){

                              public Object doInTransaction(TransactionStatus status) {

                                     // TODO Auto-generated method stub

                                     return userDao.getUser(name);

                              }

                       });

                       return (User) obj;

                }

                public void insertUser(final User user) {

                       Object obj = execute(new TransactionCallback(){

                              public Object doInTransaction(TransactionStatus status) {

                                     userDao.insertUser(user);

                                     return null;

                              }

                       });

                }

                public void updateUser(final User user) {

                       Object obj = execute(new TransactionCallback(){

        public Object doInTransaction(TransactionStatus arg0) {

                                     userDao.updateUser(user);

                                     return null;

                              }

                       });

                }

         }

       这样我们就把相关操作实现事务控制了。

       数据表建立:

           create <A title=table href="http://www.alimama.com/membersvc/buyadzone/buy_ad_zone.htm?adzoneid=892989" target=_blank>table</A> t_user(

               "id" int null,

              "name" varchar(50) null,

              "sex" int null

           )

       这样我们在以后调用方式为:

       测试类

         package com.user.junit;

         import java.util.List;

         import com.base.ServiceFactory;

         import com.user.User;

         import com.user.dao.UserDao;

         import common.Logger;

         public class UserTest {

                private static Logger log = Logger.getLogger(UserTest.class);

                public static void main(String args[]){

                       UserDao service = ServiceFactory.getUserService();

                       User user=null;

                       int i = 4;

   switch(i) {

                       case 0:

                              user.setId(1);

                              user.setName("444");

                              user.setSex(2);

                              service.updateUser(user);

                              <A title=system href="http://www.alimama.com/membersvc/buyadzone/buy_ad_zone.htm?adzoneid=892989" target=_blank>system</A>.out.println(user.getName()+""+user.getSex());

                              break;

                       case 1:

                              try {

                                     user = service.getUser("2");

                              } catch (Exception e) {

                                     log.debug("出错了"+e.getMessage());

                              }

                              <A title=system href="http://www.alimama.com/membersvc/buyadzone/buy_ad_zone.htm?adzoneid=892989" target=_blank>system</A>.out.println(user.getId());

                       case 3:

                              List<User> ls = service.selectUser();

                              for (int j = 0; j < ls.size(); j++) {

                                     <A title=system href="http://www.alimama.com/membersvc/buyadzone/buy_ad_zone.htm?adzoneid=892989"target=_blank>system</A>.out.println(ls.get(j).getId()+"===="+ls.get(j).getName());

                              }

                       case 4:

                              List<User> ls1 = service.selectUser();

                              for (int j = 0; j < ls1.size(); j++) {

                                     user = ls1.get(j);

                                     <A title=system href="http://www.alimama.com/membersvc/buyadzone/buy_ad_zone.htm?adzoneid=892989"target=_blank>system</A>.out.println(user.getId()+user.getName()+user.getSex()+user.getAddress());

                              }

                              for (int j = 0; j < 100; j++) {

                                     user.setId(user.getId()+1);

                                     service.insertUser(user);

                             }

                     default:

                              break;

                       }

                }

        }

   </PRE>

 

进程、线程和DLL操作技巧

前 言

本书旨在帮助读者解决在使用Visual C++的开发过程中所遇到的诸多实际问题,从中获取大量的编程技巧和代码参考。
本书涉及的内容广泛,包括了Visual C++代码的排版,开发环境的设置,数据类型的转换,Visual C++常用控件的使用技巧,对话框处理技巧,窗口和界面处理技巧,文件、文件夹及磁盘操作技巧,数据库操作技巧,进程、线程和DLL操作技巧,多媒体的处理技巧,通信的操作技巧,COM组件技术操作技巧,系统编程等Visual C++的开发技巧。
本书以技巧解答的方式进行讲解,并且配有详细的代码参考,内容涉及广泛,按照功能目的对其进行分类。本书可以作为使用Visual C++开发的程序员解决实际问题、积累编程经验的得力助手,重点突出解决问题的技巧,同时给出相应问题代码参考。
对于每个技巧的产生,首先提出了它的产生的原因,给出技巧的实际解决办法,最后给出详细的代码参考。对于文中的代码,只给出了关键代码,详细的代码可以参考实例的代码。
1.本书特点
1)重举一反三,不就事论事
本书旨在向读者传授正确学习和使用Visual C++的方法,而非为了给出某个问题的具体答案,启发读者进行相关主题的讨论。在每个问题上,本文都会告诉读者:你该怎么办?你为什么这么办?你还能怎么办?从而开拓读者的分析思路,让读者能够使用本书给出的技巧去解决更多的问题。
2)注重系统性,结构完整
本书涉及面广泛,面向不同层次的读者,其内容涉及界面开发、多媒体开发、系统通信等领域。全书力求从Visual C++的各个面进行阐述,而不是具体的知识点。各个面之间层层推进,具有较强的连贯性和系统性,从而给读者带来全面的知识感受。
3)启发、讨论为主,贴近程序员
本书从各个角度讨论Visual C++的使用技巧和编程经验,对每个问题的提出,进行技巧分析,然后根据给出解决问题的技巧和相关的代码参考,更符合Visual C++程序员的学习心理和阅读习惯。
2.本书内容
第1章 C++语法及编程技巧。本章主要讲述了C++的代码风格和排版技巧、const的使用技巧、sizeof的使用技巧、预处理的使用技巧、数据类型的转换、内存的管理及面向对象的特点等。
第2章 Visual C++环境和调试技巧。本章主要讲述了Visual C++的环境配置和Visual C++的调试技巧。
第3章 键盘和鼠标输入处理技巧。本章主要讲述了键盘和鼠标的处理技巧。
第4章 常用控件使用技巧。本章主要讲述了在Visual C++中各种控件的使用技巧,包括静态控件、按钮控件、EDIT框控件、ListBox控件、ListCtrl控件、树型控件、RichEdit控件、ProgressCtrl控件、ComBox控件、Scroll Bar控件、TabControl控件等的使用技巧。
第5章 对话框处理。包括通用对话框处理技巧、模式对话框的使用技巧、非模式对话框的使用技巧、属性页的使用技巧。
第6章 窗口、界面处理技巧。包括框架和视图的处理技巧、标题栏的处理技巧、状态栏处理技巧、工具栏的处理技巧、菜单的处理技巧、光标的处理技巧、图标的处理技巧。
第7章 文件、文件夹、磁盘操作技巧。包括对文件、文件夹的各种操作、属性的设置和获取,磁盘信息的获取。
第8章 数据库操作。包括Visual C++的访问技术,重点介绍了ADO的访问技术。
第9章 进程、线程和DLL的操作技巧。包括进程的数据通信技巧、线程的同步和互斥的技巧,以及DLL的处理技巧。
第10章 多媒体的处理技巧。包括图像的处理技巧,以及对声音和视频的处理。
第11章 通信的处理技巧。包括网络通信技术和串口操作技巧。
第12章 COM组件技术操作技巧。包括COM组件的创建和各种系统接口的使用。
第13章 系统编程技巧。包括如何获取系统的信息和如何对系统进行各种控制。
本书由郭克新编著,编写过程中得到了白乔先生的大力支持和帮助,另外,王海泉、冯吉喆、位来喜等人也参加了本书部分内容的编写及素材整理工作,在此一并表示感谢。希望读者通过对本书的学习,能够有所收获,掌握编程的方法,学会解决问题的办法。由于作者水平有限,错误之处在所难免,不足之处敬请广大读者批评指正。联系信箱:vcskills@gmail.com,详情请垂询http://book.vcer.net/skills

第9章 进程、线程和DLL操作技巧

第9章 进程、线程和DLL操作技巧
本章介绍了进程的相关概念和对进程的各种操作,以及进程间通信的方式。进程通常被定义为程序运行的实例,它一般包括两部分,即进程内核和进程地址空间。进程是不活泼的,若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,该线程负责执行包含在进程的地址空间中的代码。线程的操作要注意线程对资源的同步和互斥的情况。最后阐述了动态链接库的创建及调用方式。
9.1  进程的操作技巧在操作系统的教材中,都学习过程序和进程的概念:程序是一段代码,它是一个静态的概念,进程是程序的一次执行,它是一个动态的概念,进程具有生命周期,在其生命周期期间,具有不同的状态:新建、运行、阻塞、就绪和完成5个状态。在多任务操作系统中,可以同时运行多个进程,每个进程都有自己的虚拟地址空间。进程在自己的地址空间中修改数据不会影响其他的进程。操作系统为了识别不同的进程,为每个进程分配一个进程标识。我们如何去创建一个进程,如何在进程之间进行通信呢?下面将会解决这些疑问。

9.1.1  进程的概念进程是当前操作系统下一个被加载到内存的、正在运行的应用程序的实例。每一个进程都是由内核对象和地址空间所组成的,内核对象可以让系统在其内存放有关进程的统计信息并使系统能够以此来管理进程,而地址空间则包括了所有程序模块的代码和数据,以及线程堆栈、堆分配空间等动态分配的空间。进程仅仅是一个存在,是不能独自完成任何操作的,必须拥有至少一个在其环境下运行的线程,并由其负责执行在进程地址空间内的代码。在进程启动的同时即同时启动了一个线程,该线程被称作主线程或执行线程,由此线程可以继续创建子线程。如果主线程退出,那么进程也就没有存在的可能了,系统将自动撤销该进程并完成对其地址空间的释放。
加载到进程地址空间的每一个可执行文件或动态链接库文件的映像都会被分配一个与之相关联的全局唯一的实例句柄(Hinstance),该实例句柄实际是一个记录有进程加载位置的基本内存地址。进程的实例句柄在程序入口函数WinMain()中通过第一个参数HINSTANCEhinstExe传递,其实际值即为进程所使用的基本地址空间的地址。对于VC++链接程序所链接产生的程序,其默认的基本地址空间地址为0x00400000,如没有必要不要修改该值。在程序中,可以通过GetModuleHandle()函数得到指定模块所使用的基本地址空间。
9.1.2  创建/终止进程1.问题阐述进程的创建通过CreateProcess()函数来实现,CreateProcess()通过创建一个新的进程及在其地址空间内运行的主线程来启动并运行一个新的程序。具体地,在执行CreateProcess()函数时,首先由操作系统负责创建一个进程内核对象,初始化计数为1,并立即为新进程创建一块虚拟地址空间。随后将可执行文件或其他任何必要的动态链接库文件的代码和数据装载到该地址空间中。在创建主线程时,也是首先由系统负责创建一个线程内核对象,并初始化为1。最后启动主线程并执行进程的入口函数WinMain(),完成对进程和执行线程的创建。
2.实现技巧CreateProcess()函数的原型声明如下:
BOOL CreateProcess(
LPCTSTR lpApplicationName,                     // 可执行模块名
LPTSTR lpCommandLine,                          // 命令行字符串
LPSECURITY_ATTRIBUTES lpProcessAttributes,      // 进程的安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes,       // 线程的安全属性
BOOL bInheritHandles,                          // 句柄继承标志
DWORD dwCreationFlags,                         // 创建标志
LPVOID lpEnvironment,                          // 指向新的环境块的指针
LPCTSTR lpCurrentDirectory,                    // 指向当前目录名的指针
LPSTARTUPINFO lpStartupInfo,                   // 指向启动信息结构的指针
LPPROCESS_INFORMATION lpProcessInformation      // 指向进程信息结构的指针
);
3.实例代码在程序设计时,某一个具体的功能模块可以通过函数或线程等不同的形式来实现。对于同一进程而言,这些函数、线程都是存在于同一个地址空间下的,而且在执行时,大多只对与其相关的一些数据进行处理。如果算法存在某种错误,将有可能破坏与其同处一个地址空间的其他一些重要内容,这将造成比较严重的后果。为保护地址空间中的内容可以考虑将那些需要对地址空间中的数据进行访问的操作部分放到另外一个进程的地址空间中运行,并且只允许其访问原进程地址空间中的相关数据。具体地,可在进程中通过CreateProcess()函数去创建一个子进程,子进程在全部处理过程中只对父进程地址空间中的相关数据进行访问,从而可以保护父进程地址空间中与当前子进程执行任务无关的全部数据。对于这种情况,子进程所体现出来的作用同函数和线程比较相似,可以看成是父进程在运行期间的一个过程。为此,需要由父进程来掌握子进程的启动、执行和退出。下面这段代码即展示了此过程:
CString sCommandLine;
char cWindowsDirectory[MAX_PATH];
char cCommandLine[MAX_PATH];
DWORD dwExitCode;
PROCESS_INFORMATION pi;
STARTUPINFO si = {sizeof(si)};
// 得到Windows目录
GetWindowsDirectory(cWindowsDirectory, MAX_PATH);
// 启动“记事本”程序的命令行
sCommandLine = CString(cWindowsDirectory) + "\\NotePad.exe";
::strcpy(cCommandLine, sCommandLine);
// 启动“记事本”作为子进程
BOOL ret = CreateProcess(NULL, cCommandLine, NULL, NULL, FALSE, 0, NULL, NULL,
&si, &pi);
if (ret) {
  // 关闭子进程的主线程句柄
  CloseHandle(pi.hThread);
  // 等待子进程的退出
  WaitForSingleObject(pi.hProcess, INFINITE);
  // 获取子进程的退出码
  GetExitCodeProcess(pi.hProcess, &dwExitCode);
  // 关闭子进程句柄
  CloseHandle(pi.hProcess);
}
4.小结此段代码首先通过CreateProcess()创建Windows自带的“记事本”程序为子进程,子进程启动后父进程通过WaitForSingleObject()函数等待其执行的结束,在子进程没有退出前父进程是一直处于阻塞状态的,这里子进程的作用同单线程中的函数类似。一旦子进程退出,WaitForSingleObject()函数所等待的pi.hProcess对象将得到通知,父进程将得以继续,如有必要可以通过GetExitCodeProcess()来获取子进程的退出代码。
9.1.3  获取系统进程的技巧1.问题阐述进程的定义是为执行程序指令的线程而保留的一系列资源的集合。进程是一个可执行的程序,由私有虚拟地址空间、代码、数据和其他操作系统资源(如进程创建的文件、管道、同步对象等)组成。进程是一些所有权的集合,一个进程拥有内存、CPU运行时间等一系列资源,为线程的运行提供一个环境,每个进程都有它自己的地址空间和动态分配的内存、线程、文件和其他一些模块。
2.实现技巧系统统快照的获取可以通过Win32 API函数CreateToolhelp32Snapshot()来完成,通过该函数不仅可以获取进程的快照,同样可以获取堆、模块和线程的系统快照。函数的声明如下:
HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags,              //指定要创建包含哪一类系统信息的快照函数
DWORD th32ProcessID             //指定进程的ID号,当设定为0时表示指定当前进程
);
一旦系统得到系统快照句柄,就可以对当前的标识号进行枚举,进程号通过函数Process32First()和Procee32Next()得到,这两个函数可以用于获取系统快照中第一个和下一个系统的信息,这两个函数的声明如下:
BOOL WINAPI Process32First(
HANDLE hSnapshot,            // 系统快照句柄
LPPROCESSENTRY32 lppe        // 指向结构体PROCESSENTRY32的指针
);
BOOL WINAPI Process32Next(
HANDLE hSnapshot,            // 系统快照句柄
LPPROCESSENTRY32 lppe        // 指向结构体PROCESSENTRY32的指针
);
3.实例代码#include <tlhelp32.h>
void CTestView::OnRButtonDown(UINT nFlags, CPoint point)
{
    CString StrInfo="系统当前进程包括:\n";
    int nProcess =0;
    HANDLE snapshot=CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0);
    if(snapshot == NULL)return ;
    SHFILEINFO shSmall;
    PROCESSENTRY32 processinfo ;
    processinfo.dwSize=sizeof(processinfo) ;
    BOOL status=Process32First(snapshot,&processinfo) ;
    while (status)
    {
        ZeroMemory(&shSmall, sizeof(shSmall));     
       SHGetFileInfo(processinfo.szExeFile,0,&shSmall,sizeof(shSmall),
SHGFI_ICON|SHGFI_SMALLICON);
        StrInfo+=processinfo.szExeFile;
        StrInfo+="\n";
        status = Process32Next (snapshot,&processinfo) ;
        nProcess++;
    }
    MessageBox(StrInfo,"信息提示",MB_OK);
    CView::OnRButtonDown(nFlags, point);
}
4.小结获取当前系统所有已启动的进程,通常分为两个过程:首先获取系统进程快照,然后根据快照枚举进程。在Windows操作系统下,系统已为所有保存在系统内存中的进程、线程及模块等当前状态的信息制作了一个系统快照,用户可以通过对系统快照的访问完成对进程当前状态的检测。
9.1.4  终止指定进程的技巧1.问题阐述终止进程也就是结束进程,让进程从内存中卸载。进程的终止的原因一般有4种。
l  主线程的入口函数返回。
l  进程中的一个线程调用ExitProcess函数。
l  次进程中的所有线程结束。
l  其他进程中又有线程都结束。
2.实现技巧函数Process32First()和函数Process32Next()能够枚举系统中的所有进程,函数SHGetFileInfo()能够获得进程的信息,一旦得到进程的标识号,就可以对进程进行终止。由于被管理进程在当前进程之外,因此进程首先通过OpenProcess()函数来获取一个已经存在的进程对象的句柄,然后才可以通过该句柄对指定的进程进行管理和控制。OpenProcess()函数的声明如下:
HANDLE WINAPI OpenProcess(
DWORD dwDesiredAccess,           //访问标志
BOOL bInheritHandle,             //处理进程标志
DWORD dwProcessId               //进程标志号
);
3.实例代码#include <tlhelp32.h>
void CTestView::OnRButtonDown(UINT nFlags, CPoint point)
{   
    int nProcess =0;
    HANDLE snapshot=CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0);
    if(snapshot == NULL)
        return ;
    SHFILEINFO shSmall;
    PROCESSENTRY32 processinfo ;
    processinfo.dwSize=sizeof(processinfo) ;
    BOOL status=Process32First(snapshot,&processinfo) ;
    while (status)
    {
        ZeroMemory(&shSmall,sizeof(shSmall));      
       SHGetFileInfo(processinfo.szExeFile,0,&shSmall,sizeof(shSmall),
SHGFI_ICON|SHGFI_SMALLICON);
        CString StrInfo="是否需要终止进程:";
        StrInfo+=processinfo.szExeFile;
       if(AfxMessageBox(StrInfo,MB_YESNO)==IDYES)
        {
            DWORDdwProcessID=processinfo.th32ProcessID;
            HANDLEhProcess=::OpenProcess(PROCESS_TERMINATE,FALSE,
dwProcessID);
           ::TerminateProcess(hProcess,0);
            CloseHandle(hProcess);
        }
        status = Process32Next (snapshot,&processinfo) ;
        nProcess++;     
    }
    CView::OnRButtonDown(nFlags, point);
}
4.小结进程结束后,调用GetExitCodeProcess函数可以得到其退出代码,如果在调用这个函数时,目标进程还没有结束,此函数会返回STILL_ACTIVE,表示进程还在运行。
9.1.5  使用文件映射机制实现进程间通信的技巧1.问题阐述传统上都是用fread()、fwrite()之类的函数来存取文件,而文件映射把部分文件或全部文件映射在process的内存空间中,因此可以像存取内存一样存取文件。
2.实现技巧下面我们介绍创建文件映射的方法。在介绍CreateFileMapping()函数之前,必须先创建CreateFile()函数和OpenFile()函数打开映射到内存空间的文件,取得文件的句柄。
CreateFileMapping()的函数声明:
HANDLE CreateFileMapping(
HANDLE hFile,                                 //文件的句柄
LPSECURITY_ATTRIBUTES lpAttributes,      //与安全有关的设置
DWORD flProtect,                           //用来决定与view有关的属性
DWORD dwMaximumSizeHigh,                 //设置映射文件的尺寸
DWORD dwMaximumSizeLow,
LPCTSTR lpName                                //文件映射对象的名称
);
MapViewOfFile()函数的声明:
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,                  //文件映射的句柄
DWORD dwDesiredAccess,                   //此view的属性
DWORD dwFileOffsetHigh,                      //view文件的起点
DWORD dwFileOffsetLow,      
SIZE_T dwNumberOfBytesToMap                 //映射区的大小
);
3.实例代码//发送数据
void CTestDlg::OnBnClickedBtnsendinfo()
{
    UpdateData(TRUE);
    //创建文件映像对象
    HANDLE hMapping;   
    LPSTR StrData;   
   hMapping=CreateFileMapping((HANDLE)0xFFFFFFFF,NULL,PAGE_READWRITE,0,
0x100,"COMMUNICATION");   
    if(hMapping==NULL)   
    {   
        MessageBox("创建文件映像对象","信息提示",MB_OK);
        return;
    }
    //将文件映射到一个进程的地址空间上
   StrData=(LPSTR)MapViewOfFile(hMapping,FILE_MAP_ALL_ACCESS,0,0,0);   
    if(StrData==NULL)   
    {   
        AfxMessageBox("MapViewOfFile()failed.");
        MessageBox("文件映射失败","信息提示",MB_OK);
        return;
    }
    //向映射内存写数据
    sprintf(StrData,m_StrSendData);     
    //释放映像内存
    UnmapViewOfFile(StrData);   
}
//接收数据
void CTestDlg::OnBnClickedBtnreceiveinfo()
{
    //创建文件映像对象
    HANDLE hMapping;   
    LPSTR StrData;   
    hMapping=CreateFileMapping((HANDLE)0xFFFFFFFF,NULL,PAGE_READWRITE,0,
0x100,"COMMUNICATION");   
    if(hMapping==NULL)   
    {   
        MessageBox("创建文件映像对象","信息提示",MB_OK);
        return;
    }
    //将文件映射到一个进程的地址空间上
   StrData=(LPSTR)MapViewOfFile(hMapping,FILE_MAP_ALL_ACCESS,0,0,0);   
    if(StrData==NULL)   
    {   
        AfxMessageBox("MapViewOfFile()failed.");
        MessageBox("文件映射失败","信息提示",MB_OK);
        return;
    }
    //获取映像内存的数据量
    m_StrReceiveData.Format("%s",StrData);
    //释放映像内存
    UnmapViewOfFile(StrData);      
    UpdateData(FALSE);
}
4.小结由于各个process之间是独立的,因此彼此之间无法存取对方的内存空间,这虽然是安全的保护机制,但是如果要两个process之间进行数据交换,那就又有问题了。不过win3又另外提供了一系列的进程通信的机制,帮助process与另外一个process交换数据。
9.1.6  使用消息实现进程间通信1.问题阐述消息是Windows提供的一种驱动机制,在前面的章节中,已经多次使用消息解决问题了。使用消息进行进程通信的过程,就是使用消息激活某种操作的过程。对于进程间的通信,一般采用用户自定义的消息来完成进程间的通信,当然如果要实现的是Windows定义的消息功能,则完全可以使用已定义消息。例如完全可以在一个进程中向另一个进程中的EDIT发送WM_COPY消息,那么,如何用消息来完成进程间的通信呢?
2.实现技巧在进程间进行消息通信,那么进程之间首先应该约定唯一的确定的消息标识,这个消息标识必须是唯一的。定义了消息标识后,消息就可以通过就PostMessage,SendMessage或者PostThreadMessage函数给接收方进程的窗口发送消息。那么进程间通信还存在另外一个问题,就是消息发送给哪一个窗口,消息的发送方必须知道接收方的一个标识,比如窗口的句柄。所以在通信前,两个通信的进程之间要进行协商,确定消息的接收方的窗口标识。消息发送方可以通过FindWindow()/FindWindowEx()函数根据窗口的标题或者接收窗体的类名搜索窗口。所以在进程通信之间,进程双方将约定好窗口类名或者窗口的标题。前者只搜索顶层窗口,不搜索子窗口;而后者可以搜索子窗口,搜索的过程不区分大小写。可以用FindWindow搜索指定的窗口,然后使用FindWindowEx来搜索它的子窗口。MFC封装了FindWindow函数,没有对FindWindowEx函数进行封装。FindWindow和FindwindowEx的原型如下:
HWND  FindWindow(
    LPCTSTR lpClassName,               //窗口类名
    LPCTSTR lpWindowName               //窗口标题
)  
HWND FindwWindowEx(
    HWND hwndParent,                   //父窗口句柄
    HWND hwndChildAfter,               //开始搜索的子窗口句柄
    LPCTSTR lpszClass,             //窗口类名
    LPCTSTR lpszWindow             //窗口标题
)
3.实例代码本节编写了两个程序:传输数据(Send.exe)和接收数据(Recv.exe)。本例主要由Send.exe发送3个指令,Recv.exe接收这3个指令后,分别对这3个指令进行响应,根据指令改变窗口背景颜色。
发送端程序设计,用MFC的AppWizard(exe)创建新项目Send,设置“Project name”为“Send”,单击【确定】按钮后进入创建应用程序类型,选择“Dialog Based”类型并单击【Finish】按钮。在对话框上增加3个按钮控件,在SendDlg.h中增加3个消息标识,对控件按钮的BN_CLICKED消息进行响应,其主要代码参考如下:
void CSendDlg::OnRedBtn()
{
    CString strRecvWndName = "Receiver";
    CWnd* pWnd = CWnd::FindWindow(NULL,strRecvWndName);
    if(pWnd)
       pWnd->PostMessage(WM_RED_CONN,0,0);
}
void CSendDlg::OnGreenBtn()
{
    CString strRecvWndName = "Receiver";
    CWnd* pWnd = CWnd::FindWindow(NULL,strRecvWndName);
    if(pWnd)
    pWnd->PostMessage(WM_GREEN_CONN,0,0);
}
void CSendDlg::OnBlueBtn()
{
     CString strRecvWndName = "Receiver";
    CWnd* pWnd = CWnd::FindWindow(NULL,strRecvWndName);
    if(pWnd)
    pWnd->PostMessage(WM_BLUE_CONN,0,0);
}
接收端程序设计,用MFC的AppWizard(exe)创建新项目Receiver,设置“Project name”为“Receiver”,单击【确定】按钮后进入创建应用程序类型,选择“Dialog Based”类型并单击【Finish】按钮。在Receiver.h中增加3个消息标识,增加3个自定义消息响应函数,代码参考如下:
/************************************************************************/
/*
/************************************************************************/
// ReceiverDlg.cpp 的实现:
void CReceiverDlg::OnRed(WPARAM wParam,LPARAM lParam)
{
     m_pbbrush=CreateSolidBrush(RGB(255,0,0));//设置皮肤颜色
     Invalidate();
}
/************************************************************************/
/*
/************************************************************************/
void CReceiverDlg::OnGreen(WPARAM wParam,LPARAM lParam)
{
m_pbbrush=CreateSolidBrush(RGB(0,255,0));//设置皮肤颜色
     Invalidate();
}
/************************************************************************/
/*
/************************************************************************/
void CReceiverDlg::OnBlue(WPARAM wParam,LPARAM lParam)
{
m_pbbrush=CreateSolidBrush(RGB(0,0,255));//设置皮肤颜色
     Invalidate();
}
/************************************************************************/
/*
/************************************************************************/
HBRUSH CReceiverDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
    HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
    switch(nCtlColor)
    {
    case CTLCOLOR_LISTBOX:
    case CTLCOLOR_STATIC:
    case CTLCOLOR_DLG:
    case CTLCOLOR_MSGBOX :
        pDC->SetBkMode(TRANSPARENT);
        pDC->SetTextColor(RGB(0,0,0));
        return m_pbbrush;
        break;
    default:
        return CReceiverDlg::OnCtlColor(pDC,pWnd, nCtlColor);
        break;  
    }
    return hbr;
}
上面代码中前3个函数为消息自定义的消息响应函数,它们接收到发送者的消息后,对背景画刷进行初始化。这个背景画刷由消息响应函数OnCtrlColor()返回,以达到更改背景的作用,在每个消息响应函数中调用Invalidate()函数,刷新接收方的背景,以使更改的背景及时显现。
4.小结通过上面的实例可以看出利用消息进行进程间通信不失为一种便捷的方法,进程间的数据交换量不大却能完成一定的功能,要对上下层程序制定好规范、详尽的协议,便可编制出协调性很好的进程间的通信软件。
9.1.7  使用共享数据段实现进程间的通信的技巧1.问题阐述进程间的数据交换和共享是一种非常重要和实用的技术。大、中型软件的开发设计多是由众多程序设计人员合作完成的,通常一个程序设计人员只负责其中一个或几个模块的开发,这些模块可以是动态链接库也可以是应用程序或其他形式的程序组件。这些独立开发出来的程序模块最终需要作为一个整体来运行,即组成一个系统,在系统运行期间这些模块往往需要频繁地进行数据交换和数据共享,对于动态链接库同其主应用程序之间的数据交换是非常容易实现的,但是在两个应用程序之间或动态链接库同其主调应用程序之外的其他应用程序进行数据交换就比较困难了。尤其是在交换数据量过大、交换过于频繁的情况下更是难以实现,本文即对此展开讨论,并提出了一种通过共享内存来实现进程间大数据量快速交换的一种方法。
2.实现技巧在Windows操作系统下,任何一个进程不允许读取、写入或修改另一个进程的数据(包括变量、对象和内存分配等),但是在某个进程内创建的文件映射对象的视图却能够为多个其他进程所映射,这些进程共享的是物理存储器的同一个页面。因此,当一个进程将数据写入此共享文件映射对象的视图时,其他进程可以立即获取数据变更情况。为了进一步提高数据交换的速度,还可以采用由系统页文件支持的内存映射文件而直接在内存区域使用,显然这种共享内存的方式是完全可以满足在进程间进行大数据量数据快速传输任务要求的。下面给出在两个相互独立的进程间通过文件映射对象来分配和访问同一个共享内存块的应用实例。在本例中,由发送方程序负责向接收方程序发送数据,文件映射对象由发送方创建和关闭,并且指定一个唯一的名字供接收程序使用。接收方程序直接通过这个唯一指定的名字打开此文件映射对象,并完成对数据的接收。
在发送方程序中,首先通过CreateFileMapping()函数创建一个内存映射文件对象,如果创建成功则通过MapViewOfFile()函数将此文件映射对象的视图映射进地址空间,同时得到此映射视图的首地址。可见,共享内存的创建主要是通过这两个函数完成的,这两个函数原型声明如下:
HANDLE CreateFileMapping(HANDLE hFile,
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
LPCTSTR lpName);
LPVOID MapViewOfFile(HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap);
CreateFileMapping()函数的参数hFile指定了待映射到进程地址空间的文件句柄,如果为无效句柄则系统会创建一个使用来自页文件而非指定磁盘文件存储器的文件映射对象。很显然,在本例中为了使数据能快速交换,需要人为将此参数设定为INVALID_HANDLE_VALUE;参数flProtect设定了系统对页面采取的保护属性,由于需要进行读写操作,因此可以设置保护属性PAGE_READWRITE;双字型参数dwMaximumSizeHigh和dwMaximumSizeLow指定了所开辟共享内存区的最大字节数;最后的参数lpName用来给此共享内存设定一个名字,接收程序可以通过这个名字将其打开。MapViewOfFile()函数的参数hFileMappingObject为CreateFileMapping()返回的内存文件映像对象句柄;参数dwDesiredAccess再次指定对其数据的访问方式,而且需要同CreateFileMapping()函数所设置的保护属性相匹配。这里对保护属性的重复设置可以确保应用程序能更多地对数据的保护属性进行有效控制。
3.实例代码下面给出创建共享内存的部分关键代码:
hRecvMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE |
SEC_COMMIT, 0, 1000000, "DataMap");
if (hRecvMap != NULL)
{
lpData = (LPBYTE)MapViewOfFile(hRecvMap, FILE_MAP_WRITE, 0, 0, 0);
if (lpData == NULL)
{
CloseHandle(hRecvMap);
hRecvMap = NULL;
}
}
// 通知接收程序内存文件映射对象的视图已经打开
HWND hRecv = ::FindWindow(NULL, DECODE_PROGRAMM);
if (hRecv != NULL)
::PostMessage(hRecv, WM_MAP_OPEN, 0, 0);
数据的传送实际是将数据从发送方写到共享内存中,然后由接收程序及时从中取走即可。数据从发送方程序写到共享内存比较简单,只需用memcpy()函数将数据复制过去,关键在于能及时通知接收程序数据已写入到共享内存,并让其立即取走。在这里仍采取消息通知的方式,当数据写入共享内存后通过PostMessage()函数向接收方程序发送消息,接收方在消息响应函数中完成对数据的读取:
// 数据复制到共享内存
memcpy(lpData, RecvBuf, sizeof(RecvBuf));
// 通知接收方接收数据
HWND hDeCode = ::FindWindow(NULL, DECODE_PROGRAMM);
if (hDeCode != NULL)
::PostMessage(hDeCode, WM_DATA_READY, (WPARAM)0, (LPARAM)sizeof(RecvBuf));
当数据传输结束,即将退出程序时,需要将映射进来的内存文件映射对象视图卸载和资源释放等。这部分工作主要由UnmapViewOfFile()和CloseHandle()等函数完成:
HWND hDeCode = ::FindWindow(NULL, DECODE_PROGRAMM);
if (hDeCode != NULL)
::PostMessage(hDeCode, WM_MAP_CLOSE, 0, 0);
if (lpData != NULL)
{
UnmapViewOfFile(lpData);
lpData = NULL;
}
if (hRecvMap != NULL)
{
CloseHandle(hRecvMap);
hRecvMap = NULL;
}
在接收程序中,在收到由发送方发出的WM_MAP_OPEN消息后,由OpenFileMapping()函数打开由名字“DataMap”指定的文件映射对象,如果执行成功,继续用MapViewOfFile()函数将此文件映射对象的视图映射到接收应用程序的地址空间并得到其首地址:
m_hReceiveMap = OpenFileMapping(FILE_MAP_READ, FALSE, "DataMap");
if (m_hReceiveMap == NULL)
return;
m_lpbReceiveBuf = (LPBYTE)MapViewOfFile(m_hReceiveMap,FILE_MAP_READ,0,0,0);
if (m_lpbReceiveBuf == NULL)
当发送方程序将数据写入到共享内存后,接收方将收到消息WM_DATA_READY,在响应函数中将数据从共享内存复制到本地缓存中,再进行后续的处理。同发送程序类似,在接收程序接收数据完毕后,也需要用UnmapViewOfFile()、CloseHandle()等函数完成对文件视图等打开过的资源进行释放。
memcpy(RecvBuf, (char*)(m_lpbReceiveBuf), (int)lParam);
// 程序退出前资源的释放
UnmapViewOfFile(m_lpbReceiveBuf);
m_lpbReceiveBuf = NULL;
CloseHandle(m_hReceiveMap);
m_hReceiveMap = NULL;
4.小结经实际测试,使用共享内存在处理大数据量数据的快速交换时表现出了良好的性能,在数据可靠性等方面要远远高于发送WM_COPYDATA消息的方式。这种大容量、高速的数据共享处理方式在设计高速通信类软件中有着很好的使用效果。
9.1.8  用命名管道实现进程间的通信的技巧1.问题阐述命名管道是通过网络来完成进程间的通信的,它屏蔽了底层的网络协议细节。所以在不了解网络协议的情况下,也可以利用命名管道来实现进程间的通信。命名管道充分利用了Windows NT和Windows2000内建的安全机制。命名管道是围绕Windows文件系统设计的一种机制,采用“命名管道文件系统(Named Pipe File System,NPFS)”接口。将命名管道作为一种网络编程方案时,它实际上建立了一个客户机/服务器通信体系,并在其中可靠地传输数据。创建管道的进程称为管道服务器,连接到一个管道的进程称为管道客户机。管道服务器和一台或多台管道客户机进行单向或双向的通信。一个命名管道的所有实例共享同一个管道名,但是每一个实例均拥有独立的缓存与句柄,并且为客户——服务通信提供一个分离的管道。实例的使用保证了多个管道客户能够在同一时间使用同一个命名管道。
2.实现技巧命名管道提供了两种基本通信模式:字节模式和消息模式。在字节模式中,数据以一个连续的字节流的形式,在客户机和服务器之间流动。而在消息模式中,客户机和服务器则通过一系列不连续的数据单位,进行数据的收发,每次在管道上发出了一条消息后,它必须作为一条完整的消息读入。
由于命名管道采用“命名管道文件系统(Named Pipe File System,NPFS)”接口,因此,客户机和服务器可利用标准的Win32文件系统函数(例如ReadFile和WriteFile)来进行数据的收发,创建命名管道CreateNamedPipe的原型如下:
HANDLE CreateNamedPipe(
LPCTSTR lpName,
DWORD dwOpenMode,
DWORD dwPipeMode,
DWORD nMaxInstances,
DWORD nOutBufferSize,
DWORD nInBufferSize,
DWORD nDefaultTimeOut,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
其中,lpName为管道的名字,格式为\\.\pipe\pipename,名字的最大长度为256,管道名字对字符的大小写不敏感。
dwPipeMode是管道的打开模式,等待客户端连接函数ConnectNamedPipe的原型如下:
BOOL ConnectNamedPipe(
HANDLE hNamedPipe,
LPOVERLAPPED lpOverlapped
);
等待命名管道函数WaitNamedPipe的原型如下:
BOOL WaitNamedPipe(
LPCTSTR lpNamedPipeName,
DWORD nTimeOut
);
读取数据和写入数据的函数原型分别如下。
1)读取数据
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
2)写入数据
BOOL WriteFile(
HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);
3.实例代码下面给出创建共享命名管道的部分关键代码。
1)服务器端
void CNamedPipeSrvView::OnPipeCreate()
{
    // TODO: 在此添加代码
    hPipe=CreateNamedPipe("\\\\.\\pipe\\MyPipe",
        PIPE_ACCESS_DUPLEX |FILE_FLAG_OVERLAPPED,
        0,1,1024,1024,0,NULL);
    if(INVALID_HANDLE_VALUE==hPipe)
    {
        MessageBox("创建命名管道失败!");
        hPipe=NULL;
        return;
    }
    HANDLE hEvent;
    hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
    if(!hEvent)
    {
        MessageBox("创建事件对象失败!");
        CloseHandle(hPipe);
        hPipe=NULL;
        return;
    }
    OVERLAPPED ovlap;
    ZeroMemory(&ovlap,sizeof(OVERLAPPED));
    ovlap.hEvent=hEvent;
    if(!ConnectNamedPipe(hPipe,&ovlap))
    {
        if(ERROR_IO_PENDING!=GetLastError())
        {
            MessageBox("等待客户端连接失败!");
            CloseHandle(hPipe);
            CloseHandle(hEvent);
            hPipe=NULL;
            return;
        }
    }
    if(WAIT_FAILED==WaitForSingleObject(hEvent,INFINITE))
    {
        MessageBox("等待对象失败!");
        CloseHandle(hPipe);
        CloseHandle(hEvent);
        hPipe=NULL;
        return;
    }
    CloseHandle(hEvent);
}
void CNamedPipeSrvView::OnPipeRead()
{
    // TODO: 在此添加相关代码
    char buf[100];
    DWORD dwRead;
    if(!ReadFile(hPipe,buf,100,&dwRead,NULL))
    {
        MessageBox("读取数据失败!");
        return;
    }
    MessageBox(buf);
}
void CNamedPipeSrvView::OnPipeWrite()
{
    // TODO: 在此添加相关代码
    char buf[]="http://www.163.com";
    DWORD dwWrite;
    if(!WriteFile(hPipe,buf,strlen(buf)+1,&dwWrite,NULL))
    {
        MessageBox("写入数据失败!");
        return;
    }
}
2)客户端
void CNamedPipeCltView::OnPipeConnect()
{
    // TODO: 在此添加相关代码
   if(!WaitNamedPipe("\\\\.\\pipe\\MyPipe",NMPWAIT_WAIT_FOREVER))
    {
        MessageBox("当前没有可利用的命名管道实例!");
        return;
    }
    hPipe=CreateFile("\\\\.\\pipe\\MyPipe",GENERIC_READ |GENERIC_WRITE,
       0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
    if(INVALID_HANDLE_VALUE==hPipe)
    {
        MessageBox("打开命名管道失败!");
        hPipe=NULL;
        return;
    }
}
void CNamedPipeCltView::OnPipeRead()
{
    // TODO: 在此添加相关代码
    char buf[100];
    DWORD dwRead;
    if(!ReadFile(hPipe,buf,100,&dwRead,NULL))
    {
        MessageBox("读取数据失败!");
        return;
    }
    MessageBox(buf);
}
void CNamedPipeCltView::OnPipeWrite()
{
    // TODO: 在此添加相关代码
    char buf[]="命名管道测试程序";
    DWORD dwWrite;
    if(!WriteFile(hPipe,buf,strlen(buf)+1,&dwWrite,NULL))
    {
        MessageBox("写入数据失败!");
        return;
    }
}
4.小结命名管道服务器和客户机的区别在于:服务器是唯一一个有权创建命名管道的进程,也只有它才能接收管道客户机的连接请求,而客户机只能同一个现成的命名管道服务器建立连接。命名管道服务器只能在Windows NT或Windows2000上创建,所以,我们无法在两台Windows 95或Windows 98计算机之间利用管道进行通信。不过,客户机可以是Windows 95或Windows 98计算机,与Windows NT或Windows2000计算机进行连接通信。
9.1.9  使用邮槽实现进程间通信的技巧1.问题阐述邮槽是基于广播通信体系设计出来的,它采用无连接的不可靠的数据传输。邮槽是一种单向通信机制,创建邮槽的服务器进程读取数据,打开邮槽的客户机进程写入数据。
2.实现技巧邮槽可以实现进程间通信,同样我们也可利用标准的Win32文件系统函数(例如:ReadFile和WriteFile)来进行数据的收发,创建邮槽的函数原型如下:
HANDLE CreateMailslot(
LPCTSTR lpName,
DWORD nMaxMessageSize,
DWORD lReadTimeout,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
3.实例代码服务器端的代码参考如下:
void CMailslotSrvView::OnMailslotRecv()
{
    // TODO: 在此添加相关代码
    HANDLE hMailslot;
   hMailslot=CreateMailslot("\\\\.\\mailslot\\MyMailslot",0,
        MAILSLOT_WAIT_FOREVER,NULL);
    if(INVALID_HANDLE_VALUE==hMailslot)
    {
        MessageBox("创建邮槽失败!");
        return;
    }
    char buf[100];
    DWORD dwRead;
    if(!ReadFile(hMailslot,buf,100,&dwRead,NULL))
    {
        MessageBox("读取数据失败!");
        CloseHandle(hMailslot);
        return;
    }
    MessageBox(buf);
    CloseHandle(hMailslot);
}
客户端的代码参考如下:
void CMailslotCltView::OnMailslotSend()
{
    // TODO: 在此添加相关代码
    HANDLE hMailslot;
   hMailslot=CreateFile("\\\\.\\mailslot\\MyMailslot",GENERIC_WRITE,
        FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
    if(INVALID_HANDLE_VALUE==hMailslot)
    {
        MessageBox("打开邮槽失败!");
        return;
    }
    char buf[]="http://www.sunxin.org";
    DWORD dwWrite;
    if(!WriteFile(hMailslot,buf,strlen(buf)+1,&dwWrite,NULL))
    {
        MessageBox("写入数据失败!");
        CloseHandle(hMailslot);
        return;
    }
    CloseHandle(hMailslot);
}
3.小结为保证邮槽在各种Windows平台下都能够正常工作,我们传输消息的时候,应将消息的长度限制在424字节以下。

9.2 线程的操作技巧

Windows是一种多任务的操作系统,在Windows的一个进程内包含一个或多个线程。在32位Windows环境下的Win32 API提供了多线程应用程序开发所需要的接口函数,而利用VC++中提供的标准C库也可以开发多线程应用程序,相应的MFC类库封装了多线程编程的类,用户在开发时可根据应用程序的需要和特点选择相应的工具。为了使大家能全面地了解Windows多线程编程技术,本文将重点介绍在Win32 API和MFC两种方式下如何编制多线程程序。
多线程编程在Win32方式下和在MFC类库支持下的原理是一致的,进程的主线程在任何需要的时候都可以创建新的线程。当线程执行完后,自动终止线程;当进程结束后,所有的线程都终止。所有活动的线程共享进程的资源,因此,在编程时需要考虑在多个线程访问同一资源时产生冲突的问题。当一个线程正在访问某进程对象,而另一个线程要改变该对象时,就可能会产生错误的结果,编程时要解决这个冲突。

9.2.1  线程的概念理解线程是非常关键的,因为每个进程至少需要一个线程。本节将更加详细地介绍线程的知识,尤其是要讲述进程与线程之间存在的差别,它们各自具有什么作用。还要介绍系统如何使用线程内核对象来管理线程,与进程内核对象一样,线程内核对象也拥有属性,我们将要观察许多用于查询和修改这些属性的函数。此外还要介绍可以在进程中创建和生成更多的线程时所用的函数。
上一节介绍的进程是由两个部分构成的,一个是进程内核对象,另一个是地址空间。同样,线程也是由两个部分组成的:
l  线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
l  线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量。
上一节中讲过,进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个生命期都在该进程中。这意味着线程在它的进程地址空间中执行代码,并且在进程的地址空间中对数据进行操作。因此,如果在单进程环境中,有两个或多个线程正在运行,那么这两个线程将共享单个地址空间。这些线程能够执行相同的代码,对相同的数据进行操作。这些线程还能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程。
9.2.2  创建/终止线程的技巧1.问题阐述线程是进程的一条执行路径,它包含独立的堆栈和CPU寄存器状态,每个线程共享所有的进程资源,包括打开的文件、信号标识及动态分配的内存等。一个进程内的所有线程使用同一个地址空间,而这些线程的执行由系统调度程序控制,调度程序决定哪个线程可执行及什么时候执行线程,线程有优先级别,优先权较低的线程必须等到优先权较高的线程执行完后再执行。在多处理器的机器上,调度程序可将多个线程放到不同的处理器上去运行,这样可使处理器任务平衡,并提高系统的运行效率。
2.实现技巧创建用户界面线程有两种方法。第一种方法,首先从CWinTread类派生一个类(注意,必须要用宏DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE对该类进行声明和实现);然后调用函数AfxBeginThread创建CWinThread派生类的对象进行初始化,启动线程运行;第二种方法,先通过构造函数创建类CWinThread的一个对象,然后由程序员调用函数::CreateThread来启动线程。
调用CreateProcess函数创建新的进程,运行指定的程序。
CreateProcess的原型如下:
BOOL CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation  );
终止一个线程有两种方法:最常用的方法是调用函数ExitThread()结束线程;另一种方法是调用函数TerminateThread终止线程。在当前线程中的一个线程调用函数ExitProcess就会结束当前线程:
VOID ExitThread(DWORD dwExitCode);
这个函数用来结束当前线程,其中参数用来存放此线程的退出码,这是最正常的结束线程的方法:
BOOL  TerminateThread(
HANDLE hThread.          // 线程句柄
DWORD dwExitCode         // 线程退出码
);
3.实例代码#include <windows.h>
#include <iostream.h>
DWORD WINAPI ThreadFunc(HANDLE Thread)
{
int i;
for(i=0;i<10;i++)
{
cout<<"A new thread has created!"<<endl;
}
return 0;
}
int main(int argc,char* argv[])
{
HANDLE Thread;
DWORD dwThreadId;
Thread=::CreateThread
(NULL,0,ThreadFunc,NULL,0,&dwThreadId);
cout<<"The new thread ID is :"<<dwThreadId<<endl;
::WaitForSingleObject(Thread,INFINITE);
::CloseHandle(Thread);
return 0;
}
4.小结我们知道,要创建一个线程,必须得有一个主进程,然后由这个主进程来创建一个线程,在一般的VC程序中,主函数所在的进程就是程序的主进程。
9.2.3  工作线程实现的技巧1.问题阐述工作线程是用于处理后台工作的,我们平常接触到的后台打印就是一个工作线程的例子。下面我们看看如何创建一个工作线程。
创建一个工作线程十分简单,只需要两步:实现线程函数和开始线程。不需要由CWinThread派生类,你可以不加修改地使用CWinThread。
AfxBeginThread有两种形式,一种是用来创建用户界面线程的,另一种就是用来创建工作线程的。为了开始执行线程,只需要向AfxBeginThread提供下面的参数就可以了。
l  线程函数的地址。
l  传送到线程函数的参数。
l  (可选的)线程的优先级,默认的是平常的优先级,如果希望使用其他优先级请参阅::SetThreadPriority。
l  (可选的)线程的堆栈大小,默认的大小是和创建线程的堆栈一样大。
l  (可选的)如果用户创建的线程在开始的时候处于挂起态,而不在运行态,可以设置为CREATE_SUSPENDED。
l  (可选的)线程的安全属性,默认的是和父线程的访问权限一样,有关安全信息的格式,请参阅SECURITY_ATTRIBUTES。
2.实现技巧AfxBeginThread为用户创建并初始化一个CWinThread对象,运行这个对象,并返回它的地址,这样通过这个地址用户就可以找到它了。在这一过程中还要进行许多检查,这一切都不用你操心。AfxBeginThread函数的声明如下:
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
那么下面我们来看看线程函数怎么写。线程函数定义了线程要做什么,在进入这个函数的时候线程开始,退出的时候线程结束。这个函数必须是下面的形式:
UINT ControllingFunction( LPVOID pParam );
参数是一个32位数,这个参数是在线程对象创建时传送给对象的构造函数。至于线程函数要怎么处理这个数,那就随便了,它可能是一个人的年纪,可能是一个文件的地址,可能是一个窗口句柄,反正你想它是什么就是什么,主动权在你手里。如果参数指的是一个结构,可以用来向线程传送参数,也可以让线程把结果传回主程序,线程需要通知主程序,什么时候来取结果。
在线程函数结束时,应该返回一个UINT类型的值,说明返回原因,也就是返回代码。通常这个数为0,表示正常返回,当然你也可以定义一个错误编码指示错误了。
3.实例代码下面是一个线程函数的例子,这个例子解释如何定义线程函数,也介绍了如何从程序的其他地方控制线程:
UINT ThreadProc( LPVOID pParam )
{
return 0;                              // 线程成功完成
}
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc,               // 线程函数地址
LPVOID pParam,                          // 线程参数
int nPriority = THREAD_PRIORITY_NORMAL,  // 线程优先级
UINT nStackSize = 0,                    // 线程堆栈大小,默认为1M
DWORD dwCreateFlags = 0,                
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
4.小结工作线程通常用于程序的计算、调度等后台任务。工作线程和用户界面线程不同,它不从CWinThread类派生创建,最重要的是实现完成工作线程任务的运行控制函数,即工作线程常表现为函数,这个函数完成线程并行的任务,由其他语句调用工作线程函数将线程启动。
9.2.4  用户界面线程实现的技巧1.问题阐述MFC中有两类线程,分别称为工作者线程和用户界面线程。二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环。
工作者线程没有消息机制,通常用来执行后台计算和维护任务,如冗长的计算过程,打印机的后台打印等。用户界面线程一般用于处理独立于其他线程执行之外的用户输入,响应用户及系统所产生的事件和消息等。但对于Win32的API编程而言,这两种线程是没有区别的,它们都只需线程的启动地址即可启动线程来执行任务。
2.实现技巧当一个Windows应用程序运行时,它会自动产生一个主线程,一般的窗口处理等都由该主线程处理,在主线程中可以创建和使用其他线程。用户界面线程通常用于处理用户输入并响应各种事件和消息。
启用用户界面线程的函数AfxBeginThread()与启用工作线程的函数是同一个函数的不同重载形式,该函数的声明如下:
CWinThread* AfxBeginThread(
CRuntimeClass* pThreadClass,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
l  pParam:传递给线程函数的一个32位参数,执行函数将用某种方式解释该值。它可以是数值,或指向一个结构的指针,甚至可以被忽略。
l  nPriority:线程的优先级。如果为0,则线程与其父线程具有相同的优先级。
l  nStackSize:线程为自己分配堆栈的大小,其单位为字节。如果nStackSize被设为0,则线程的堆栈被设置成与父线程堆栈大小相同。
l  dwCreateFlags:如果为0,则线程在创建后立刻开始执行;如果为CREATE_ SUSPEND,则线程在创建后立刻被挂起。
l  lpSecurityAttrs:线程的安全属性指针,一般为NULL。
3.实例代码BOOL CMyThread::InitInstance()
{
    GetMainWnd()->SetWindowText("从线程中设置标题栏文字");
    return TRUE;
}
// CTestView 消息处理程序
#include"MyThread.h"
void CTestView::OnRButtonDown(UINT nFlags, CPoint point)
{   
    CMyThread *pThread;
    pThread=(CMyThread*)AfxBeginThread(RUNTIME_CLASS(CMyThread));
    CView::OnRButtonDown(nFlags, point);
}
4.小结在Visual C++ 6.0编程环境中,我们既可以编写C风格的32位Win32应用程序,也可以利用MFC类库编写C++风格的应用程序,二者各有其优缺点。基于Win32的应用程序执行代码小巧,运行效率高,但要求程序员编写的代码较多,且需要管理系统提供给程序所有资源;而基于MFC类库的应用程序可以快速建立起应用程序,类库为程序员提供了大量的封装类,而且Developer Studio为程序员提供了一些工具来管理用户源程序,其缺点是类库代码很庞大。由于使用类库所带来的快速、简捷和功能强大等优越性,因此除非有特殊的需要,否则Visual C++推荐使用MFC类库进行程序开发。
9.2.5  使用事件对象完成线程的同步的技巧1.问题阐述CEvent类提供了对事件的支持。事件是一个允许一个线程在某种情况发生时,唤醒另外一个线程的同步对象。例如在某些网络应用程序中,一个线程(记为A)负责监听通信端口,另外一个线程(记为B)负责更新用户数据。通过使用CEvent类,线程A可以通知线程B何时更新用户数据。每一个CEvent对象可以有两种状态:有信号状态和无信号状态。线程监视位于其中的CEvent类对象的状态,并在相应的时候采取相应的操作。
2.实现技巧在MFC中,CEvent 类对象有两种类型:人工事件和自动事件。一个自动CEvent 对象在被至少一个线程释放后会自动返回到无信号状态;而人工事件对象获得信号后,释放可利用线程,但直到调用成员函数ReSetEvent()才将其设置为无信号状态。在创建CEvent 类的对象时,默认创建的是自动事件。CEvent 类的各成员函数的原型和参数说明如下:
CEvent(BOOL bInitiallyOwn=FALSE,
BOOL bManualReset=FALSE,
LPCTSTR lpszName=NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute=NULL);
l  bInitiallyOwn:指定事件对象初始化状态,TRUE为有信号,FALSE为无信号。
l  bManualReset:指定要创建的事件是属于人工事件还是自动事件,TRUE为人工事件,FALSE为自动事件。
BOOL CEvent::SetEvent();
将CEvent类对象的状态设置为有信号状态。如果事件是人工事件,则CEvent类对象保持为有信号状态,直到调用成员函数ResetEvent()将其重新设为无信号状态时为止。如果CEvent类对象为自动事件,则在SetEvent()将事件设置为有信号状态后,CEvent类对象由系统自动重置为无信号状态。
如果该函数执行成功,则返回非零值,否则返回零。
BOOL CEvent::ResetEvent();
该函数将事件的状态设置为无信号状态,并保持该状态直至SetEvent()被调用时为止。由于自动事件是由系统自动重置的,故自动事件不需要调用该函数。如果该函数执行成功,返回非零值,否则返回零。我们一般通过调用WaitForSingleObject函数来监视事件状态。
3.实例代码#include <windows.h>
#include <iostream.h>
DWORD WINAPI Fun1Proc(
  LPVOID lpParameter            // 线程数据
);
DWORD WINAPI Fun2Proc(
  LPVOID lpParameter            // 线程数据
);
int tickets=100;
HANDLE g_hEvent;
void main()
{
    HANDLE hThread1;
    HANDLE hThread2;
    hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
    hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
    CloseHandle(hThread1);
    CloseHandle(hThread2);
    //g_hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);
    g_hEvent=CreateEvent(NULL,FALSE,FALSE,"tickets");
    if(g_hEvent)
    {
        if(ERROR_ALREADY_EXISTS==GetLastError())
        {
            cout<<"onlyinstance can run!"<<endl;
            return;
        }
    }
    SetEvent(g_hEvent);
    Sleep(4000);
    CloseHandle(g_hEvent);
}
DWORD WINAPI Fun1Proc(
  LPVOID lpParameter            // 线程数据
)
{
    while(TRUE)
    {
        WaitForSingleObject(g_hEvent,INFINITE);
        ResetEvent(g_hEvent);
        if(tickets>0)
        {
            Sleep(1);
            cout<<"thread1sell ticket : "<<tickets--<<endl;
        }
        else
            break;
        SetEvent(g_hEvent);
    }
    return 0;
}
DWORD WINAPI Fun2Proc(
  LPVOID lpParameter            // 线程数据
)
{
    while(TRUE)
    {
        WaitForSingleObject(g_hEvent,INFINITE);
        ResetEvent(g_hEvent);
        if(tickets>0)
        {
            Sleep(1);
            cout<<"thread2sell ticket : "<<tickets--<<endl;
        }
        else
            break;
        SetEvent(g_hEvent);
    }
    return 0;
}
4.小结事件对象也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。有两种不同类型的事件对象:一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。
9.2.6  使用信号量完成线程的同步的技巧1.问题阐述当需要一个计数器来限制可以使用某个线程的数目时,可以使用“信号量”对象。CSemaphore类的对象保存了对当前访问某一指定资源的线程的计数值,该计数值是当前还可以使用该资源的线程的数目。如果这个计数达到了零,则所有对这个CSemaphore类对象所控制的资源的访问尝试都被放入到一个队列中等待,直到超时或计数值不为零时为止。一个线程被释放已访问了被保护的资源时,计数值减1;一个线程完成了对被控共享资源的访问时,计数值增1。这个被CSemaphore类对象所控制的资源可以同时接收访问的最大线程数在该对象的构建函数中指定。
2.实现技巧CSemaphore类的构造函数原型及参数说明如下:
CSemaphore (LONG lInitialCount=1,
               LONGlMaxCount=1,
               LPCTSTRpstrName=NULL,
              LPSECURITY_ATTRIBUTES lpsaAttributes=NULL);
l  lInitialCount:信号量对象的初始计数值,即可访问线程数目的初始值。
l  lMaxCount:信号量对象计数值的最大值,该参数决定了同一时刻可访问由信号量保护的资源的线程最大数目。
后两个参数在同一进程中使用一般为NULL,不做过多讨论。
3.实例代码声明3个线程函数:
UINT WriteA(LPVOID pParam);
UINT WriteB(LPVOID pParam);
UINT WriteC(LPVOID pParam);
定义信号量对象和一个字符数组,为了能够在不同线程间使用,定义为全局变量:
CSemaphore semaphoreWrite(2,2);         //资源最多访问线程2个,当前可访问线程数2个
char g_Array[10];
添加3个线程函数:
UINT WriteA(LPVOID pParam)
{
    CEdit *pEdit=(CEdit*)pParam;
    pEdit->SetWindowText("");
    WaitForSingleObject(semaphoreWrite.m_hObject,INFINITE);
    CString str;
    for(int i=0;i<10;i++)
    {
        pEdit->GetWindowText(str);
        g_Array=''A'';
        str=str+g_Array;
        pEdit->SetWindowText(str);
        Sleep(1000);
    }
    ReleaseSemaphore(semaphoreWrite.m_hObject,1,NULL);
    return 0;
}
UINT WriteB(LPVOID pParam)
{
    CEdit *pEdit=(CEdit*)pParam;
    pEdit->SetWindowText("");
    WaitForSingleObject(semaphoreWrite.m_hObject,INFINITE);
    CString str;
    for(int i=0;i<10;i++)
    {
        pEdit->GetWindowText(str);
        g_Array=''B'';
        str=str+g_Array;
        pEdit->SetWindowText(str);
        Sleep(1000);
    }
    ReleaseSemaphore(semaphoreWrite.m_hObject,1,NULL);
    return 0;
}
UINT WriteC(LPVOID pParam)
{
    CEdit *pEdit=(CEdit*)pParam;
    pEdit->SetWindowText("");
    WaitForSingleObject(semaphoreWrite.m_hObject,INFINITE);
    for(int i=0;i<10;i++)
    {
        g_Array=''C'';
        pEdit->SetWindowText(g_Array);
        Sleep(1000);
    }
    ReleaseSemaphore(semaphoreWrite.m_hObject,1,NULL);
    return 0;
}
这3个线程函数不再多说,在信号量对象有信号的状态下,线程执行到WaitForSingleObject语句处继续执行,同时可用线程数减1;若线程执行到WaitForSingleObject语句时信号量对象无信号,线程就在这里等待,直到信号量对象有信号线程才往下执行。
添加其响应函数:
void CMultiThread10Dlg::OnStart()
{
    CWinThread *pWriteA=AfxBeginThread(WriteA,
        &m_ctrlA,
        THREAD_PRIORITY_NORMAL,
        0,
        CREATE_SUSPENDED);
    pWriteA->ResumeThread();
    CWinThread *pWriteB=AfxBeginThread(WriteB,
        &m_ctrlB,
        THREAD_PRIORITY_NORMAL,
        0,
        CREATE_SUSPENDED);
    pWriteB->ResumeThread();
    CWinThread *pWriteC=AfxBeginThread(WriteC,
        &m_ctrlC,
        THREAD_PRIORITY_NORMAL,
        0,
        CREATE_SUSPENDED);
    pWriteC->ResumeThread();
}
4.小结在用CSemaphore 类的构造函数创建信号量对象时要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时,则说明当前占用资源的线程数已经达到了所允许的最大数目,不能再允许其他线程进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源数加1。
9.2.7  使用互斥量完成线程的同步的技巧1.问题阐述互斥对象和事件对象属于内核对象,利用内核对象进行线程同步,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
互斥对象与临界区对象很像,互斥对象与临界区对象的不同在于:互斥对象可以在进程间使用,而临界区对象只能在同一进程的各线程间使用。当然,互斥对象也可以用于同一进程的各个线程间,但是在这种情况下,使用临界区会更节省系统资源,更有效率。
9.2.8  使用临界量完成线程的同步的技巧1.问题阐述当多个线程访问一个独占性共享资源时,可以使用“临界区”对象。任一时刻只有一个线程可以拥有临界区对象,拥有临界区的线程可以访问被保护起来的资源或代码段,其他希望进入临界区的线程将被挂起等待,直到拥有临界区的线程放弃临界区时为止,这样就保证了不会在同一时刻出现多个线程访问共享资源。
2.实现技巧待续。。。。

第12章 COM组件技术操作技巧

COM是开发组件的一种方法,组件是一些小的二进制程序,它可以为操作系统或者应用程序提供服务。COM技术的发展进一步加强了程序的模块化编程的思想,使应用程序在更容易扩展与升级,具有更好的灵活性和动态性,COM支持了分部使应用程序的开发。
12.1  COM的概念和编程技巧COM,即组件对象模型,是关于如何建立组件及如何通过组件建构应用程序的一个规范,是一种跨应用和语言共享二进制代码的方法。而C++是提倡源代码重用,源码级的复用容易产生错误,并且使得应用工程变得异常臃肿。COM组件是以Win32动态链接库(DLLs)或可执行文件(EXEs)的形式发布的可执行代码组成的。遵循COM规范编写的组件将能够满足对组件架构的所有需求。COM组件是动态链接的,COM使用DLL将组件动态链接起来,对于COM组件的封装是很容易的。COM组件按照一种标准的方式来宣布它们的存在。COM组件是一种给其他应用程序提供面向对象的API或服务的极好方法。
12.1.1  COM接口1.接口的概念在COM中,接口就是一组函数,它是不可变的。
任何一个组件都要包含一个IUnknown接口,客户同组件的交互都是通过这个接口完成的。在客户查询组件的其他接口时,也是通过这个接口完成的。IUnknown接口的定义包含在Win32SDK中的UNKNOWN.H头文件中。
interface IUnknown
{   
virtual HRESULT _stdcall QueryInterface(const IID& iid,void **ppv) = 0;
virtual ULONG _stdcall AddRef() = 0;
virtual ULONG _stdcall Release() = 0;
}
在IUnknown中定义了一个名为QueryInterface的函数。客户可以调用QueryInterface来决定组件是否支持某个特定的接口。
2.接口的注意事项所有的COM接口都需要继承IUnknown。
由于所有的COM接口都继承了IUnknown,每个接口的vtbl中的前3个函数都是QueryInterface、AddRef和Release。若某个接口的vtbl中的前3个函数不是这3个,那么它将不是一个COM接口。由于所有的接口都是从IUnknown继承的,因此所有的接口都支持QueryInterface,组件的任何一个接口都可以被客户用来获取它所支持的其他接口。
l  非虚拟继承:注意IUnknown并不是虚拟基类,所以COM接口并不能按虚拟方式继承IUnknown,这是由于会导致与COM不兼容的vtbl。若COM接口按虚拟方式继承IUnknown,那么COM接口的vtbl中的头3个函数指向的将不是IUnknown的3个成员函数。
l  QuertyInterface可以用一个简单的if-then-else语句实现,但case语句是无法用的,因为接口标识符是一个结构而不是一个数。
12.1.2  CLSID和ProgID相互转换1.问题阐述每一个COM组件都需要指定一个 CLSID,并且不能重名,CLSID表示使用一个具有16个字节的数字,每个CLSID都在系统的注册表中被注册,它表示组件的实际路径,保证了组件路径的透明性。在Localserver32中保存了组件的路径,如图12-1所示。

图12-1  组件的路径
如果使用ATL或者其他开发环境,会自动产生一个CLSID,用以标识组件,同时,组件的标识还支持字符串方式,就是ProgID。二者都可以用来标识,只是采用了不同的表示形式。
2.实现技巧通过上面的分析,两者之间的转换,可以通过查询注册表达得到,还可以通过函数CLSIDFromProgID和ProgIDFromCLSID完成转换,函数原型如下:
HRESULT CLSIDFromProgID(
LPCOLESTR lpszProgID,            // 指向ProgID的指针
LPCLSID pclsid                       // 指向CLSID的指针
);
WINOLEAPI ProgIDFromCLSID(
REFCLSID clsid,                     // CLSID 的值,已知
LPOLESTR * lplpszProgID          // 指向接收ProgID的缓冲区

);
3.实例代码本实例演示了CLSID和ProgID之间的相互转换。首先创建一个简单的组件,然后利用一个调用者程序进行二者之间的转换。
(1)建立一个ATL工程Object,选择DLL方式,如图12-2所示。
Allow merging of proxy/stub code、Support MFC和Support MTS为默认即可。
(2)添加ATL类对象Cfun,设置其类对象的属性如图12-3所示。

图12-2  组件创建                                                  图12-3  组件创建
从图12-3可以知道ProgID = OBJECT.Fun,默认为工程名+ShortName,单击Attributes选项卡,如图12-4所示。

图12-4  组件属性配置
这样,一个简单的COM组件就做好了,这个组件,没有任何功能实现。从这个COM组件中找出它的CLSID,查看idl文件。其中86A70E6F-3F1C-46B5-86F9-C21DAD69C756为CLSID。
下面写一个函数,完成CLSID和ProgID的转换。
    CLSID clsid = {0x86A70E6F,0x3F1C,0x46B5,{0x86,0xF9,0xC2,0x1D,
0xAD,0x69,0xC7,0x56}};
    CString strClsID;
   strClsID.Format("%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x",clsid.Data1,
clsid.Data2,clsid.Data3,clsid.Data4[0],clsid.Data4[1],clsid.Data4[2],
clsid.Data4[3],clsid.Data4[4],clsid.Data4[5],clsid.Data4[6],
clsid.Data4[7]);
    SetDlgItemText(IDC_CLSID_ED,strClsID);
    HRESULT hr;
    LPOLESTR lpwProgID = NULL;
    hr = ::ProgIDFromCLSID( clsid, &lpwProgID );
    if ( SUCCEEDED(hr) )
    {
        //::MessageBoxW( NULL, lpwProgID,L"ProgID", MB_OK );
        USES_CONVERSION;
        LPCTSTR lpstr =  OLE2CT(lpwProgID );
       SetDlgItemText(IDC_PROGID_ED,lpstr);   
        IMalloc * pMalloc = NULL;
        hr = ::CoGetMalloc( 1, &pMalloc);  // 取得 IMalloc
        if ( SUCCEEDED(hr) )
        {
            pMalloc->Free( lpwProgID);     // 释放ProgID内存
           pMalloc->Release();             // 释放IMalloc
        }
    }
其中OLE2CT完成了LPCOLESTR到LPCTSTR的转换,运行结果如图12-5所示。

图12-5  CLSID 转换为ProgID
12.1.3  利用IPicture接口实现显示BMP/JPG/GIF图像1.问题阐述在VB 中显示一个图像非常简单,只要将图像控件拖入到面板中,设置相应的属性即可。其实它的显示原理只是调用Windows的Ipicture接口,本节重点介绍了这一接口。
2.实现技巧如何利用IPicture显示图像呢?首先了解一下这个接口的内容和作用、IPicture接口是一个COM类,操纵着图像对象及其属性。图像对象提供对位图的抽象,而Windows负责BMP、JPG和GIF位图的标准实现。IPictrue接口支持BMP、DIB、EMF、GIF、ICO、JPG、WMF格式图片的显示,但只能保存BMP和ICO格式的图片。下面是IPicture的方法描述:
l  get_Handle 返回图像对象的Windows GDI句柄。
l  get_Hpal 返回图像对象当前使用的调色板副本。
l  get_Type返回当前图像对象的图像类型。
l  get_Width 返回当前图像对象的图像宽度。
l  get_Height 返回当前图像对象的图像高度。
l  Render在指定的位置、指定的设备上下文上绘制指定的图像部分。
l  set_Hpal设置当前图像的调色板。
l  get_CurDC返回当前选中这个图像的设备上下文。
l  SelectPicture将一个位图图像选入给定的设备上下文,返回选中图像的设备上下文和图像的GDI句柄。
l  get_KeepOriginalForma返回图像对象KeepOriginalFormat属性的当前值。
l  put_KeepOriginalFormat设置图像对象的KeepOriginalFormat属性。
l  PictureChanged通知图像对象它的图像资源改变了。
l  SaveAsFile将图像数据存储到流中,格式与存成文件格式相同。
l  get_Attributes返回图像位属性当前的设置。
l  IPicture接口和其他的接口不同,这个接口的实例不用CoCreateInstance来创建,而是采用一个专门的函数OleLoadPicture创建IPicture接口的实例。OleLoadPicture的声明和参数的含义如下所示:
STDAPI OleLoadPicture(
IStream * pStream,
LONG lSize,  
BOOL fRunmode,
REFIID riid,
VOID ppvObj  
);
第一个参数pStream指向包含有图像数据的流的指针,第二个参数lSize为从流中读取的字节数,第三个参数fRunmode为图像属性对应的初值,第四个参数riid为涉及到的接口标识描述要返回的接口指针的类型,第五个参数ppvObj为在rrid中用到的接口指针变量的地址。
3.实例代码本实例使用IPicture接口完成图像JPG和GIF等图像的显示。建立一个基于单文档的工程,工程名为PictureRend。
在PiectureRend.h的函数InitInstance()中初始化COM库,在程序退出的时候,关闭COM库,加载COM库的代码如下:
BOOL CPictureRendApp::InitInstance()
{
//--------------------------------------------------------------------------
    AfxEnableControlContainer();

*****************************
}
关闭COM库的代码如下:
int CPictureRendApp::ExitInstance()
{
CoUninitialize();   
return CWinApp::ExitInstance();
}
在PictureRendView.h中声明:
CFile m_File;
    IStream* m_pStream ;
    IPicture* m_pPicture ;
    LONG m_nWidth;
    LONG m_nHeight;
在打开菜单中,获取打开的图像的路径名,代码如下:
void CPictureRendApp::OnFileOpen()
{
     CFileDialog dlg(TRUE, NULL, NULL, OFN_HIDEREADONLY
|OFN_OVERWRITEPROMPT,
"文件 (*.jpg)|*.jpg|文件 (*.gif)|*.gif|All Files (*.*)|*.*||", NULL);
    if( IDOK != dlg.DoModal())
{
           return;
         }
    m_strPicPath = dlg.GetPathName();
PostMessage(((CMainFrame*)m_pMainWnd)->GetActiveView()->m_hWnd,
WM_ON_RENDER_PIC,0,0);//一旦打开文件,消息响应
}
注意,在响应“打开”消息的时候,要对消息映射的地方加以改变,如下:
ON_COMMAND(ID_FILE_OPEN,OnFileOpen)。
在View中添加渲染图像,代码如下:
void CPictureRendView::OnRenderPic()
{
    HRESULT hr;
    CClientDC dc(this);
    m_File.Open( theApp.m_strPicPath, CFile::modeRead |CFile::shareDenyNone );
    // 读入文件内容
    DWORD dwSize = m_File.GetLength();
    HGLOBAL hMem = ::GlobalAlloc( GMEM_MOVEABLE, dwSize );
    LPVOID lpBuf = ::GlobalLock( hMem );
    m_File.ReadHuge( lpBuf, dwSize );
    m_File.Close();
    m_pStream = NULL;
    m_pPicture = NULL;
    // 由 HGLOBAL 得到 IStream,参数 TRUE 表示释放 IStream 的同时,释放内存
    hr = ::CreateStreamOnHGlobal( hMem, TRUE, &m_pStream );
        ::GlobalUnlock( hMem );
    ASSERT ( SUCCEEDED(hr) );

    hr = ::OleLoadPicture(
m_pStream, dwSize, TRUE, IID_IPicture, ( LPVOID * )&m_pPicture );
    ASSERT(hr==S_OK);

    long nWidth,nHeight;           // 宽高,MM_HIMETRIC 模式,单位是0.01毫米
    m_pPicture->get_Width( &m_nWidth );    // 宽
    m_pPicture->get_Height( &m_nHeight );  // 高

    原大小显示//
    CSize sz( m_nWidth, m_nHeight );
    dc.HIMETRICtoDP( &sz );
// 转换 MM_HIMETRIC 模式单位为MM_TEXT 像素单位
    m_pPicture->Render(dc.m_hDC,0,0,sz.cx,sz.cy,
        0,m_nHeight,m_nWidth,-m_nHeight,NULL);
}
在程序退出时,释放数据流和接口指针:
if ( m_pPicture ) m_pPicture->Release();       // 释放 IPicture 指针
if ( m_pStream ) m_pStream->Release();      // 释放 IStream 指针,同时释放了hMem
运行效果如图12-6所示。

图12-6  IPicture接口的使用
12.1.4  创建/删除快捷方式的技巧1.问题阐述Windows的快捷方式实际上是一个带有扩展名LNK的数据文件,快捷方式中包括所指对象的大量的信息,如目标对象的路径和名称,工作目录,要传递的命令行参数,运行时的初始显示状态,图标及其快捷键等。通过在快捷方式上单击鼠标右键并在弹出菜单中选择【属性】命令可以观察该快捷方式的这些性质。
快捷方式的数据文件如果存放在C:\Windows\Desktop子目录下,这个快捷方式就会显示在桌面上,而如果存放在C:\Windows\Start Menu\Programs子目录下,这个快捷方式就会作为【开始】菜单的一个菜单项出现。
在操作系统中,通过手工操作建立这些应用程序的快捷方式并不复杂,在此不再赘述,在应用程序中如何完成上述的工作呢?
2.实现技巧Windows外壳(Shell)的快捷方式是以OLE技术的组件对象模型COM(Component ObjectModal)为基础而设计的。利用COM模型,一个应用程序可以调用另一应用程序的某些功能。创建Windows的快捷方式比较容易,首先利用OLE通过调用CoCreateInstance()函数建立一个IID_IShellLink实例,并同时得到其接口指针。利用这个接口指针可以对其各项属性进行设置。为了使这些信息以快捷方式的数据文件(*.lnk)格式保存起来,还需要从IID_IShellLink对象取得其IID_IPersistFile接口指针,以便于调用其成员函数Save()保存前面设置的信息。
3.实例代码本实例演示了如何利用IshellLink接口创建快捷方式。创建一个基于对话框的工程,工程名为ShortCut。
快捷方式生成代码:
void CShortCutDlg::OnCtreateBtn()
{
    GetDlgItemText(IDC_EDIT2,m_strLnkPath);
    if(m_strLnkPath == "")
    {
          MessageBox("请输入快捷方式的路径");
          return ;
    }
    CreateShortCut((LPCTSTR)(m_strDesPath),(LPCTSTR)m_strLnkPath);
}
创建快捷方式的代码:
/************************************************************************/
/* 作用:建立块捷方式
/* 参数 lpExeName:EXE 文件全路径名
/* 参数 lpLinkPath:快捷方式文件全路径名
/*
/************************************************************************/
void CShortCutDlg::CreateShortCut(LPCTSTR lpExeName,LPCTSTR lpLinkPath)
{
    IShellLink * psl = NULL;
    IPersistFile * ppf = NULL;
    HRESULT hr = ::CoCreateInstance(
        CLSID_ShellLink,     
        NULL,                
        CLSCTX_INPROC_SERVER,
        IID_IShellLink,      
        (LPVOID *)&psl );                  // 获取接口实例
    if (SUCCEEDED(hr))
    {
        psl->SetPath( lpExeName );          // 全路径程序名
           psl->SetIconLocation("moon_ie.ico",0);
            psl -> SetHotkey(MAKEWORD( 'X', HOTKEYF_SHIFT |HOTKEYF_CONTROL)) ;
           psl->SetDescription("create a short cut");
            hr = psl->QueryInterface(       // 查找持续性文件接口指针
               IID_IPersistFile,            // 持续性接口 IID
                (LPVOID*)&ppf
);      // 得到接口指针
        if ( SUCCEEDED(hr) )
        {
             USES_CONVERSION;      
              ppf->Save(T2COLE( lpLinkPath ), TRUE );  // 保存
        }
    }
    if ( ppf )
    ppf->Release();
    if ( psl )  
psl->Release();
}
选择快捷方式的目标文件:
void CShortCutDlg::OnFindFileBtn()
{
   CFileDialog  file(TRUE,".exe",".exe",OFN_HIDEREADONLY| OFN_
OVERWRITEPROMPT,"文件(*.exe)|*.exe||",NULL);
    if(file.DoModal() == IDOK)
    {
          m_strDesPath = file.GetPathName();
          SetDlgItemText(IDC_EDIT1,m_strDesPath);
    }
}
12.1.5  C++类对象、DLL和 COM 的区别和联系1.问题阐述C++类对象、DLL及COM都是面对向对象的,它们都实现了重用,避免了程序员重复造轮子的现象的产生。那么它们之间有什么区别呢?
2.实现技巧C++对象重用是定义在源代码级别上的,而DLL和COM是定义在二进制级别上的重用,是执行代码重用的技术。DLL和COM都实现了模块之间的通信,但是DLL对于内存的利用和数据类型使用没有一定的约束规范。而COM对数据、内存等其他的几个方面进行了规范,使得软件模块间实现调用、通信的标准。所以,COM不是接口,也不是对象,它是一种标准。符合COM标准的对象就是COM对象,其实COM对象无非是实现了很多接口的对象而已。COM对象必须实现IUnknown接口,这个接口是管理COM对象生命周期的。当COM对象不使用的时候,这个接口定义的方法负责释放内存。一个COM对象可以没有任何别的接口,但是必须要有这个接口,它也是默认实现了接口。QI,即所谓的查询接口。由于COM中有很多接口,不同的接口管理着COM的不同类型的方法,因此从一个接口可以使用的方法转到另一个接口可以使用的方法的过程称为QI,这个过程是由Idispatch接口管理的。每个组件都有一个独一无二的标识,即广泛唯一标识(GUIDs),它代表了COM的身份。一个COM对象可以有多个接口,一个接口可以被多个COM对象实现。
12.1.6  使用C++API创建COM对象1.问题阐述COM的设计目的,是要能实现跨语言的调用,既然是跨语言的,那么组件的接口描述就必须在任何语言环境中都要能够认识。如何实现模块之间的调用呢?
2.实现技巧微软使用了一个新的文件格式IDL文件(接口定义描述语言)。IDL是一个文本文件,IDL经过编译,生成二进制的等价类型库文件 TLB 提供给其他语言来使用。
调用组件程序的方法可以有#include 头文件的方法和#import类库方法。COM对象的创建由API函数CoCreateInstance完成,其函数原型声明如下:
STDAPI CoCreateInstance(
REFCLSID rclsid,     
LPUNKNOWN pUnkOuter,
DWORD dwClsContext,  
REFIID riid,         
LPVOID * ppv      
);
第一个参数是组件的CLSID,第二个参数聚合时才能用到,第三个参数为进程内的服务,第四个参数为接口的IID,第五个参数返回接口的指针。
3.实例代码本实例演示了如何调用COM组件。首先生成一个COM组件,然后再生成一个主程序调用COM组件。
在上面的建立的COM组件中增加两个函数,其中一个完成一个简单的加法运算,另一个完成一个字符串的连接任务,代码如下。
完成加法任务的函数代码:
STDMETHODIMP CFun::Add(long a, long b, long *retval)
{
// TODO: 在此添加相关代码
*retval = a+b;
return S_OK;
}
完成字符串连接的代码:
STDMETHODIMP CFun::CatString(BSTR str1, BSTR str2, BSTR *pretval)
{
      // TODO: 在此添加相关代码
int nLen1 = ::SysStringLen(str1);          //计算BSTR字符串的长度
      int nLen2 = ::SysStringLen(str2);
      *pretval = ::SysAllocStringLen(str1,nLen1+nLen2);
//申请nLen1+nLen2长度的内存区域,将字符串存进去
    if(nLen2>0)
    {
           ::memcpy(pretval+nLen1,str2,sizeof(WCHAR)*nLen2);
//将str2复制进申请的内存缓冲区
    }
    return S_OK;
}
调用组件的代码,首先将组件ID、接口ID及接口的函数集包含在工程中,代码如下:
#include "..\Object\OBJECT.h"
#include "..\Object\OBJECT_i.c"
调用过程如下所示:
/*将BSTR转换为CString*/
CString convert(BSTR b)
{
   CString s;
    if(b == NULL)
       return s; // empty for NULL BSTR
#ifdef UNICODE
    s = b;
#else
    LPSTR p = s.GetBuffer(SysStringLen(b) + 1);
    ::WideCharToMultiByte(CP_ACP,           
                         0,                
                         b,                
                         -1,              
                         p,                
                         SysStringLen(b)+1,
                         NULL,             
                         NULL);           
    s.ReleaseBuffer();
#endif
    return s;
}
void CExample1Dlg::OnExeBtn()
{
    // TODO: 在此添加相关代码
      ::CoInitialize( NULL );
      UpdateData(TRUE);
      IFun* pFun = NULL;
      IUnknown* pUn = NULL;
    try
    {
          HRESULT hr = ::CoCreateInstance(
              CLSID_Fun,
              NULL,
             CLSCTX_INPROC_SERVER,
              IID_IUnknown,
             (LPVOID*)&pUn
            );
        if(FAILED(hr))
        {
             MessageBox("请您注册组件");
              return;
        }
        hr =pUn->QueryInterface(IID_IFun,(LPVOID*)&pFun);
        if(FAILED(hr))
        {
             MessageBox("检查接口是否存在");
              return;
        }
        pFun->Add(m_add1,m_add2,&m_add3);
        BSTR  bstr1 =m_strCat1.AllocSysString();
        BSTR  bstr2 =m_strCat2.AllocSysString();
        BSTR  bstr3;
       pFun->CatString(bstr1,bstr2,&bstr3);
        m_strCat3 = convert(bstr3);
        UpdateData(FALSE);

    }
    catch ( LPCTSTR lpErr)
    {
        AfxMessageBox(lpErr);
    }
    pUn->Release();     //释放接口
    pFun->Release();        //释放接口
    ::CoUninitialize();
}
使用API创建接口实例,最后不要忘记了加接口指针Realease。大部分COM的API都是以Co开头的,这个前缀是COMObject的缩写。
12.1.7  使用智能指针创建COM对象1.问题阐述利用COM的API创建COM对象,一切的处理工作都要程序员手动完成,比如接口指针最后的释放,这样如果在任务繁重的情况下,很容易出现忘记释放指针的情况,为了解决这个问题,COM能够提供一种自动释放的机制,于是引入了智能指针。
2.实现技巧使用COM中的智能指针,使对象的创建工作更加简单化。而且它提供自动销毁生成的 COM的对象机制,使程序的精力转移到其他的方面。下面看一下ATL提供的两个智能指针—CcomPtr和CComQIPtr。
CcomPtr类实现客户端基本的COM引用计数模型,CComPtr有一个数据成员,它是一个未经过任何加工的COM接口指针。其类型被作为模板参数传递。
CComPtr<IUnknown> spUnk;
CComPtr<IFun> spFun;
默认的构造函数将这个原始指针数据成员初始化为NULL。
智能指针的参数要么是原始指针,要么是相同类型的智能参数。不论哪种情况,智能指针都调用AddRef控制引用。CComPtr的赋值操作符既可以处理原始指针,也可以处理智能指针,并且在调用新分配指针的AddRef之前自动释放保存的指针。最重要的是,CComPtr的析构函数释放保存的接口(如果非空)。
CComQIPtr对于CComPtr只增加了两个成员函数,CComQIPtr有两个模板参数:一个是被操纵的指针类型,另一个是对应于这个指针类型的GUID。例如,下列代码声明了操纵IDataObject和IPersist接口的智能指针:
CComQIPtr<IFun, &IID_IFun> spUnk;
CCom
CComQIPtr的优点是它有重载的构造函数和赋值操作符。同类版本(例如,接收相同类型的接口)仅仅进行AddRef右边的赋值/初始化操作,这实际上就是CComPtr的功能。异类版本(接收类型不一致的接口)正确调用QueryInterface来决定是否这个对象确实支持所请求的接口:
void f(IFun* spUnk) {
    CComQIPtr<IFun, &IID_IFun> p;
    // 同类赋值 - AddRef''s
    p = spUnk;
    CComQIPtr<IDataObject, &IID_IDataObject> do;
    // 异类赋值 - QueryInterface''s
   do = spUnk;
}
3.实例代码本实例的目的借助于智能指针创建COM对象,建立一个基于对话框的工程。首先初始化应用工程的COM库,在CXXXXApp的InitInstance()中添加初始化语句:
if(AfxOleInit()==FALSE)
{
    AfxMessageBox("初始化环境COM库失败!");
    return FALSE;
}
引入智能指针类,引入组件的CLSID、接口的ID及接口函数集:
#include "..\Object\Object.h"
#include "..\Object\Object_i.c"
#include <atlbase.h>
智能指针操作代码如下:
void CExample2Dlg::OnExeBtn()
{
    UpdateData(TRUE);
   CComPtr<IUnknown>  spUnk;                //定义IUnknown的智能指针
   CComPtr<IFun> spFun;                    //定义IFun的智能指针
   try
   {
       HRESULT hr = spUnk.CoCreateInstance(CLSID_Fun,NULL,
CLSCTX_INPROC_SERVER);               //启动组件
       if(FAILED(hr))
       {
           MessageBox("组件没有注册!");
               return ;
       }
       hr = spUnk.QueryInterface(&spFun);      //查找IFun的接口
       if(FAILED(hr))
       {
           MessageBox("没有接口IFun");
           return;
       }
       spFun->Add(m_add1,m_add2,&m_add3);
       CComBSTR s1(m_str1);
       CComBSTR s2(m_str2);
       CComBSTR s3;
       spFun->CatString(s1,s2,&s3);
       m_str3 = convert(s3.m_str);          //将BSTR转换为CString 同上
   }
   catch(LPCTSTR str)
   {
       MessageBox(str);
   }
   UpdateData(FALSE);  
}
上面的代码演示了使用CcomPtr智能指针,下面的代码演示了CComQIPtr的用法:
void CExample2Dlg::OnComqiBtn()
{
   UpdateData(TRUE);
   CComPtr<IUnknown>  spUnk;                //定义IUnknown的智能指针
   CComQIPtr<IFun> spFun;                      //定义IFun的智能指针
   try
   {
       HRESULT hr = spUnk.CoCreateInstance(CLSID_Fun,NULL,
CLSCTX_INPROC_SERVER);               //启动组件
       if(FAILED(hr))
       {
           MessageBox("组件没有注册!");
               return ;
       }
       spFun = spUnk;                      //会自动调用QueryInterface查找接口
       if(spFun == NULL)
       {
           MessageBox("没有接口!");
           return;
       }
       spFun->Add(m_add1,m_add2,&m_add3);
       CComBSTR s1(m_str1);
       CComBSTR s2(m_str2);
       CComBSTR s3;
       spFun->CatString(s1,s2,&s3);
       m_str3 = convert(s3.m_str);  
   }
   catch(LPCTSTR str)
   {
       MessageBox(str);
   }
   UpdateData(FALSE);  
}
12.1.8  使用智能指针的封装类创建COM对象1.问题阐述在前面就已经说过,COM之间的通讯是跨语言的,在前面都只直接包含了C接口,那么如果一个COM对象没有给C接口,应该如何调用呢?
2.实现技巧在前面提到过,为了实现跨语言,微软提供了一个新的文件格式.idl,idl经过编译后,生成二进制的等价类型库文件 TLB提供给其他的语言使用。首先要将这个文件导入到工程中:
#import "..\Object\OBJECT.tlb" no_namespace
编译后会生成.tlh和.tlh文件的智能指针包装,利用智能指针包装创建对象。
3.实例代码本实例演示了如何使用智能包装类创建COM对象,代码如下:
void CExample3Dlg::OnExeBtn()
{
    UpdateData(TRUE);
    IFunPtr  spFun;
    HRESULT hr = spFun.CreateInstance(__uuidof(Fun));
    if(FAILED(hr))
    {
        MessageBox("创建COM接口失败!");
        return;
    }
    m_add3 = spFun->Add(m_add1,m_add2);
    BSTR s1,s2,s3;
    s1 = m_str1.AllocSysString();
    s2 = m_str2.AllocSysString();
    s3 = spFun->CatString(s1,s2);
    m_str3 = convert(s3);
    UpdateData(FALSE);
}
如果使用命名空间,程序应该改动如下:
void CExample3Dlg::OnExeBtn()
{
    UpdateData(TRUE);
    OBJECTLib::IFunPtr  spFun;
    HRESULT hr = spFun.CreateInstance(__uuidof(OBJECTLib::Fun));
    if(FAILED(hr))
    {
        MessageBox("创建COM接口失败!");
        return;
    }
    m_add3 = spFun->Add(m_add1,m_add2);
    BSTR s1,s2,s3;
    s1 = m_str1.AllocSysString();
    s2 = m_str2.AllocSysString();
    s3 = spFun->CatString(s1,s2);
    m_str3 = convert(s3);
    UpdateData(FALSE);
}
12.1.9  创建一个自动化组件的技巧1.问题阐述前面创建的COM对象都是定制接口对象,也就是所谓的前绑定。编译器在编译的时候装载类型库,分别使用了 #include 方法和 #import 方法来实现。装载了类型库后,编译器就知道如何编译接口函数的调用了。脚本语言是解释执行的语言,它在执行的时候不会知道具体的函数的地址,那么在脚本语言中,如何调用COM组件呢?
2.实现技巧为了使脚本语言支持COM组件的调用,MS提供了另外一个接口,即Idispatch接口,又称为自动化接口,也被称为后绑定接口。自动化组件,即实现了Idispatch接口的组件。IDispatch接口用IDL形式说明如下:
[
    object,
    uuid(00020400-0000-0000-C000-000000000046),
//IDispatch接口的IID =IID_IDispatch
    pointer_default(unique)
]
interface IDispatch : IUnknown
{
    typedef [unique] IDispatch * LPDISPATCH; // 转定义 IDispatch * 为 LPDISPATCH
    HRESULT GetTypeInfoCount([out] UINT * pctinfo);  
    HRESULT GetTypeInfo([in] UINT iTInfo,[in] LCID lcid,
[out] ITypeInfo ** ppTInfo);
    HRESULT GetIDsOfNames(                  // 根据函数名字,取得函数序号(DISPID)
                [in] REFIIDriid,
                [in,size_is(cNames)] LPOLESTR * rgszNames,
                [in] UINTcNames,
                [in] LCIDlcid,
                [out,size_is(cNames)] DISPID * rgDispId
            );
    [local]                                //本地版函数
    HRESULT Invoke(  
                [in] DISPIDdispIdMember,
                [in] REFIIDriid,
                [in] LCIDlcid,
                [in] WORDwFlags,
                [in, out] DISPPARAMS* pDispParams,
                [out] VARIANT* pVarResult,
                [out]EXCEPINFO * pExcepInfo,
                [out] UINT *puArgErr
            );
    [call_as(Invoke)]                           //远程版函数
    HRESULT RemoteInvoke(
                [in] DISPIDdispIdMember,
                [in] REFIIDriid,
                [in] LCIDlcid,
                [in] DWORDdwFlags,
                [in]DISPPARAMS * pDispParams,
                [out] VARIANT* pVarResult,
                [out] EXCEPINFO* pExcepInfo,
                [out] UINT *pArgErr,
                [in] UINTcVarRef,
                [in,size_is(cVarRef)] UINT * rgVarRefIdx,
                [in, out,size_is(cVarRef)] VARIANTARG * rgVarRef
            );
}
IDispatch接口有4个函数,解释语言的执行器就通过仅有的4个函数来执行组件所提供的功能。
其中GetIDsOfNames将读取一个函数的名称并返回其调度ID,又称为DISPID。DISPID并不是一个GUID,而只是一个长整数,它标识的是一个函数。对于IDispatch的每一个特定的实现,DISPID是唯一的。IDispatch的每一个实现都有其自己的IID。为执行某个函数,自动化控制程序将把DISPID传给Invoke成员函数。
Invoke可以将DISPID作为函数指针数组的索引,这一点同常规COM接口是相似的。但是自动化服务并不需要按此种方式实现Invoke。一个简单的自动化服务器可以根据DISPID用一个case语句执行不同的代码。IDispatch::Invoke将实现一组按索引来访问的函数,Invoke的一个实现所实现的函数集被称为一个调度接口,而COM接口是一个指向一个函数指针数组的指针,此数组的前3个元素分别是QueryInterface、AddRef及Release。Invoke函数参数含义:第一个参数是控制程序待调用函数的DISPID;第二个参数是保留值,必须为IID_NULL;第三个参数为保存位置信息;第四个参数为所指的调用函数的类型,它的值可以是DISPATCH_METHOD、DISPATCH_PROPERTYGET和DISPATCH_PROPERTYPUT值中的一个;第五个参数是传给被调用函数的参数;第六个参数是返回值。
3.实例代码本实例演示如何创建双接口组件。启动ATL COM AppWizard,工程名为Object。选择DLL类型、不合并代理和存根代码、不支持MFC、不支持MTS。“New AtlObject”选择“Simple Object”,输入名称DFun,属性按默认设置,增加函数Add,如图12-7所示。
选择“Attributes”选项卡,设置双接口的属性如图12-8所示。

图12-7  双接口设置向导                                       图12-8  设置双接口属性
在Idl中组件描述如下:
import "oaidl.idl";
import "ocidl.idl";
    [
        object,
        uuid(DAB964C5-24E5-4648-8765-F8D8AA9D6F23),
        dual,
        helpstring("IDFun Interface"),
        pointer_default(unique)
    ]
    interface IDFun : IDispatch
    {
        [id(1), helpstring("methodAdd")] HRESULT Add([in]long n1,[in]long
n2,[out,retval]long* pVal );
    };
[
    uuid(51000335-57C3-4D25-8A05-8768264350F0),
    version(1.0),
    helpstring("Object 1.0 Type Library")
]
library OBJECTLib
{
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");
    [
       uuid(DF7E08FC-51B3-4498-9D14-63E5E704133E),
        helpstring("DFun Class")
    ]
    coclass DFun
    {
        [default] interface IDFun;
    };
增加函数,在 ClassView 选项卡中,选择接口,单击鼠标右键菜单,在弹出菜单中添加接口函数。
Add([in] VARIANT v1, [in] VARIANT v2, [out, retval] VARIANT * pVal);
Upper([in] BSTR str, [out,retval] BSTR * pVal);
函数实现的参考代码如下:
/************************************************************************/
/* 转换为小写
/************************************************************************/
STDMETHODIMP CDFun::Lower(BSTR bstr, BSTR *pVal)
{
*pVal = NULL;   
    CComBSTR s(bstr);
    s.ToLower();    // 转换为小写
    *pVal = s.Copy();
    return S_OK;
}
/************************************************************************/
/* 加法运算
/* 整数的加法运算和字符串的加法运算
/************************************************************************/
STDMETHODIMP CDFun::Add(VARIANT  v1,VARIANT  v2,VARIANT*  pVal)
{
         ::VariantInit( pVal );  
        CComVariant v_1( v1 );
        CComVariant v_2( v2 );
    if((v1.vt & VT_I4) && (v2.vt & VT_I4) ) // 如果都是整数类型
    {   
           v_1.ChangeType( VT_I4);         // 转换为整数
           v_2.ChangeType( VT_I4);         // 转换为整数
           pVal->vt = VT_I4;
           pVal->lVal = v_1.lVal +v_2.lVal; // 加法
    }
    else
    {
           v_1.ChangeType( VT_BSTR);           // 转换为字符串
           v_2.ChangeType( VT_BSTR);           // 转换为字符串
           CComBSTR bstr( v_1.bstrVal);
           bstr.AppendBSTR( v_2.bstrVal);  // 字符串连接
           pVal->vt = VT_BSTR;
           pVal->bstrVal =bstr.Detach();
    }
        return S_OK;
}
最后在脚本中调用COM接口实例,创建一个记事本,更改扩展名为vbs,脚本的参考代码如下:
Set obj = CreateObject("Object.DFun")
MsgBox obj.Lower("接口函数Lower:THIS IS  A TEST")
MsgBox obj.Add("1+2=" ,obj.Add(1,2))
Set obj = Nothing
运行执行结果如图12-9和图12-10所示。
           
图12-9  调用COM接口中的Lower函数          图12-10  调用COM接口中的Add函数
12.1.10  使用ATL创建进程外组件1.问题阐述在前面创建的组件均属于进程内组件,即组件对象和客户进程在同一个进程,客户进程在同一进程内调用组件对象提供的服务;进程外组件,组件对象和客户进程分属不同的进程,客户进程可以跨进程调用组件对象提供的服务。如何创建一个进程外的组件呢?
2.实现技巧使用ATL创建组件向导创建一个进程外组件,首先使用ATL COM AppWizard创建一个工程,如图12-11所示。
单击【OK】按钮,选择组件提供服务时所用的类型。在此因为是进程外组件,故选用Executable类型,如图12-12所示。

图12-11  创建工程                                   图12-12  选择组件提供服务器的类型
最后添加组件提供的接口,和前面介绍的就基本一致了。
3.实例代码本例的消费者和生产者的简单模型由COM来实现。
组件服务器具体创建步骤如下。
(1)用ATL COM Appwzard创建一个新的工程,工程名为ProcOut。
(2)选择组件提供服务时所用的类型(.dll或.exe)。
(3)在工程中插入一个对象。在ClassView选项页面,用鼠标右键单击工程名,在弹出的右键菜单中选择【New ATL Object】,在打开的对话框中选中SimpleObject,如图12-13所示。

图12-13  新建组件对象
(4)单击【Next】按钮,在打开对话框的ShortName中输入 Modu,其他的按默认设置,如图12-14所示。

图12-14  组件命名
(5)定义接口函数。在接口IModu上面点击鼠标右键,在弹出的快捷菜单中选择【Add Method】命令。出现添加生产函数对话框,添加接口函数HRESULT,参数Produce([in]long nProduce),如图12-15所示。

图12-15  添加生产函数
(6)添加消费函数HRESULT Customer([in]long nProduce),如图12-16所示。

图12-16  添加消费函数
(7)添加属性,在ClassView页中用鼠标右键单击接口,选择【Add Property】命令,打开的对话框如图12-17所示。

图12-17  添加属性
(8)组件服务的代码,组件服务提供了生产和消费函数及改变属性的两个函数,其代码参考如下。
在类Cobject的头文件Object.h中添加成员变量:
int m_lNum;//表示当前的数量
生产函数的参考代码如下:
STDMETHODIMP CObject::Produce(long nProduce)
{
m_lNum += nProduce;
return S_OK;
}
消费函数的参考代码如下:
STDMETHODIMP CObject::Customer(long nCustomer)
{
        m_lNum -= nCustomer;
        if(m_lNum<0)
        {
                MessageBox(NULL,"消耗没了","提示",MB_OK);
                m_lNum = 0;
        }
        return S_OK;
}
接口属性函数的参考代码如下:
STDMETHODIMP CObject::get_CurrentNum(long *pVal)
{
    *pVal = m_lNum;
        return S_OK;
}
STDMETHODIMP CObject::put_CurrentNum(long newVal)
{
    m_lNum = newVal;
        return S_OK;
}
(9)在客户端创建一个基于对话框的工程ProcOutTest,放置两个文本框分别表示生产或消耗的数量和当前现存的数量。放置两个按钮,用于响应生产和消费事件。参考代码如下。
在ProcOutTest的InitInstance中添加AfxOleInit( )初始化应用程序COM环境。
在stdafx.h中引入ProcOut.tlb库:
#import "ProcOut.tlb" no_namespace
在ProcOutTestDlg.h中声明接口的智能指针:
IobjectPtr  m_Iobject
在ProcOutTest.cpp中的OinitDialog添加创建接口实例的代码:
    HRESULT hr =m_IObject.CreateInstance(L"ProcOut.Object");
    if(FAILED(hr))
    {
           MessageBox("创建接口实例失败!");
           return FALSE;
    }
    m_IObject->put_CurrentNum(100);//初始化数量为100
    m_IObject->get_CurrentNum(&m_Cur);
响应生产和消费两个按钮事件的代码:
void CProcOutTestDlg::OnProduceBtn()
{
    UpdateData(TRUE);
   m_IObject->Produce(m_Num);
   m_IObject->get_CurrentNum(&m_Cur);
   UpdateData(FALSE);
}
void CProcOutTestDlg::OnCustomerBtn()
{
   UpdateData(TRUE);
   m_IObject->Customer(m_Num);
   m_IObject->get_CurrentNum(&m_Cur);
   UpdateData(FALSE);
}
运行效果如图12-18所示。

图12-18  生产和消费

12.2 MS Office 中的COM应用

MS Office中的COM是应用最广泛的自动化技术,该功能所描述的是利用Visual C++应用程序控制MicrosoftOffice组件。自动化(OLE自动化)技术允许将现有的程序的功能合并到VC++的应用程序中。自动化技术建立在组件对象模型(COM)的基础上,系统的COM随操作系统一起安装的动态链接库(DLL)提供一组服务。例如,应用程序中使用Microsoft Word的拼写和语法检查功能,而不让用户看到Microsoft Word:可以使用Office的OCR技术,自动化也可以使用Microsoft Excel的所有图表、打印和数据分析工具等。该技术的特点大大简化了开发过程,加快了开发的进度。

12.2.1  VC++实现Office自动化1.问题阐述熟悉VBA的开发者对于这一操作都非常熟悉,在VC++中读取MS Word系统并没有显示的这一功能,但是在Microsoft 的 Office 产品中,都提供了OLE Automation 自动化程序的接口,在VC++中如何实现Office的自动化技术呢?
2.实现技巧VC++实现Office自动化,通常可以采取以下3种方式实现。
1)利用Visual C++的类向导机制,从Office类型库生成包装类
生成的这些类,以及诸如 COleVariant、COleSafeArray和 COleException 之类的其他 MFC类可简化自动化任务,操作更加简单,所以建议采用该方法。
2)通过#import指令引入Office类型库,创建智能指针的方式
智能指针的功能非常强大,但不建议使用它,因为它与 Microsoft Office 应用程序一起使用时,经常会出现引用计数问题。
3)利用C++直接调用COM服务
C++相比上面两种方式实现起来比较困难,但是有时为了避免采用MFC造成的资源开销过大或避免使用#import方式所带来的问题,通常采用此种方式。
类型库与C/C++头文件类似,它包含服务器发布的接口、方法和属性。Visual C++附带的OLE/COM对象查看器(Oleview.exe)用来查看类型库。表12-1列出了Microsoft Office 95、MicrosoftOffice 97、Microsoft Office 2000、Microsoft Office 2002和MicrosoftOffice 2003的类型库文件名。
表12-1  Office类型库

Office版本和类型

类型库文件

Office版本和类型

类型库文件

Access 97

Msacc8.olb

PowerPoint 2000

Msppt9.olb

Jet Database

3.5 DAO350.dll

Word 2000

Msword9.olb

Binder 97

Msbdr8.olb

Access 2002

Msacc.olb

Excel 97

Excel8.olb

Excel 2002

Excel.exe

Graph 97

Graph8.olb

Graph 2002

Graph.exe

Office 97

Mso97.dll

Office 2002

MSO.dll

Outlook 97

Msoutl97.olb

Outlook 2002

MSOutl.olb

PowerPoint 97

Msppt8.olb

PowerPoint 2002

MSPpt.olb

Word 97

Msword8.olb

Word 2002

MSWord.olb

Access 2000

Msacc9.olb

Office Access 2003

Msacc.olb

Jet Database 3.51

DAO360.dll

Office Excel 2003

Excel.exe

Binder 2000

Msbdr9.olb

Graph 2003

Graph.exe

Excel 2000

Excel9.olb

Office 2003

MSO.dll

Graph 2000

Graph9.olb

Office Outlook 2003

MSOutl.olb

Office 2000

Mso9.dll

Office PowerPoint 2003

MSPpt.olb

Outlook 2000

Msoutl9.olb

Office Word 2003

MSWord.olb

MFC引入类型库的一般步骤如下。
(1)启动MFC的ClassWizard,出现如图12-19所示的对话框。

图12-19  添加类型库向导1
(2)在Office的目录中找到适合版本的类型库,如图12-20所示。

图12-20  添加类型库向导2
(3)选择类型文件后,根据所实现的功能选择不同的类,当然,也可以全部选择,如图12-21所示。

图12-21  添加类型库向导3
(4)单击【OK】按钮,将所选择的类_Application添加到创建应用程序中,如图12-22所示。

图12-22  添加类型库向导4
在MS Word中,_Application的一个对象代表是Word应用程序本身,在VC++中通过自动化技术控制Word时,则可用_Application声名一个类对象,通过CreateDispatch实例化该对象,代码参考如下:
_Application  wordApp;
wordApp.CreateDispatch(_T(Word.Applcation));
实例化对象后,应用程序就启动了应用程序,通过对象实例wordApp可以获得Word版本属性、修改Word标题等,可以参考_Application 方法和属性。
3.实例代码本实例演示了利用VC++创建的应用程序打开Word,并且获取Word的版本号,关闭Word文档。
(1)创建一个基于对话框的工程AutoMation,在窗口上放置一个进度条和两个按钮,用于启动和关闭Word。启动VC++的ClassWizard,添加MS Word的类型库(MSWord.olb),选择要生成的类_Application。添加后,在源文件文件夹中增加了msword.h和msword.cpp两个文件。
(2)在CAutoMationApp的InitInstance中添加初始化COM的代码:
HRESULT hr = AfxOleInit();
    if(FAILED(hr))
    {
           AfxMessageBox("初始化COM失败!!");
    }
(3)为了处理VARIANT类型方便,在stdafx.h中包含头文件atlbase.h。
(4)添加按钮的处理消息和定时器处理代码。
打开按钮的响应代码:
void CAutoMationDlg::OnOpenBtn()
{
       if(!m_App.CreateDispatch(CLSID_Application))    //可以采用CLSID启动
       {
             AfxMessageBox(_T("请检查是否安装了Office"));
               return;
       }
      if(!m_App.CreateDispatch(_T("Word.Application"))) //启动Word
{
             AfxMessageBox(_T("请检查是否安装了Office"));
               return;
        }
        else
        {
              SetTimer(1,500,NULL);
             MessageBox("Word启动");
            MessageBox(m_App.GetVersion());            //获取Word版本
       m_App.SetCaption("this is a test");                  //设置Word的标题
               m_nCurStep =0;
               m_ProgressCtrl.SetStep(10);
             m_ProgressCtrl.SetRange(0,40);
             m_App.SetVisible(TRUE);
        }   
}
关闭按钮的响应代码:
/************************************************************************/
/* 关闭Word
/************************************************************************/
void CAutoMationDlg::OnCloseBtn()
{
    //定义调用QUIT时使用的参数
        VARIANTVarIsSave,VarInit,VarRoute;  
    //退出Word时候的不保存参数
        VarIsSave.vt=VT_BOOL;   
        VarIsSave.boolVal=VARIANT_FALSE;

    //初始化VARIANT变量
       ::VariantInit(&VarInit);    
       ::VariantInit(&VarRoute);     
       //VarRoute.vt=VT_EMPTY;
       //VarInit.vt = VT_EMPTY;
       //调用Quit,退出Word应用程序
      m_App.Quit(&VarIsSave,&VarInit,&VarRoute);
       MessageBox("退出Word编辑!");
       m_ProgressCtrl.SetPos(0);
       //释放对象指针
       m_App.ReleaseDispatch();     //一定要释放
}
定时器处理代码:
void CAutoMationDlg::OnTimer(UINT nIDEvent)
{
        m_nCurStep+=10;
        m_ProgressCtrl.SetPos(m_nCurStep);
    int n = m_ProgressCtrl.GetPos();
        if(n > 40)
        {
          KillTimer(1);
        }
       CDialog::OnTimer(nIDEvent);
}
运行效果如图12-23所示。

图12-23  VC++启动Word
12.2.2  VC++读/写Word文档1.问题阐述在第7章阐述了读/写文本文件、ini 文件、.inf等非复合文档文件,这些文件系统给出了相应的API函数完成了文件的读写,对Word的读取/写入系统没有提供API函数,VC++如何完成Word的读写呢?
2.实现技巧 写Word文档,首先要从Word应用程序获取文档的类的对象,然后设置文档中接收文字位置,最后将文字写入Word文档。
在上一节加入_Application应用程序类,本节继续加入两个类,即Document类和Selection类,按照以前添加类的步骤,将Documents类和Selection类添加进应用程序。
Documents类是文档对象集类,是所有Document对象的集合。使用Documents对象集合的Add方法可以新建一篇空白文档并将返回的Document 对象添至Documents对象集合之中。
Add方法的原型声明如下:
LPDISPATCH  Add(
VARIANT* Template,
VARIANT* NewTemplate,
VARIANT* DocumentType,
VARIANT* Visible)
l  参数Template,指定新文档使用的模板名,如果忽略此参数则使用Normal模板。
l  参数NewTemplate,如果此属性设置为True 则将文档作为模板打开。默认值为 False。
l  参数DocumentType其值可取下列WdNewDocumentType常量之一。
Ø  WdNewBlankDocument 创建一个空白文档,默认值。
Ø  WdNewEmailMessage 新建一个电子邮件信息。
Ø  wdNewWebPage 新建一个Web页。
l  参数Visible,如果此参数为True,Microsoft Word将在可见窗口打开文档。如果此参数为False,Word仍会打开此文档但文档窗口的Visible属性变为False,默认值为True。
Selection类对象用于选定文档的文字,然后对选定部分进行操作,如设置文字的格式或键入文字。Selection 对象代表窗体中的选定内容,每个窗体中只能有一个Selection 对象而且只能激活一个Selection 对象。Selection 对象代表的选定内容既可以是文档中的一个区域也可以仅仅是一个插入点。通过Selection对象的TypeText和GetText可对Word文档进行读写操作。
TypeText的函数作用为写Word文档,其函数的声明如下:
void TypeText(LPCTSTR Text);
GetText的函数的作用是读文档,其函数的声明如下:
CString GetText();
3.实例代码本实例演示了如何读写Word文档。
创建一个基于对话框的工程ReadWriteWord。通过上面的方法引入类型库,增加_Application类、Documents类、Selection类。在对话框上添加两个文本控件,两个按钮。文本控件分别用于输入和接收文本。
在CreadWriteWord中的InitInstance初始化COM库添加代码如下:
HRESULT hr;
hr = AfxOleInit( );
if(FAILED(hr))
{
    AfxMessageBox("初始化COM失败");
    return FALSE;
}
在ReadWriteWordDlg.h中包含头文件:
#include "msword.h"
#include "atlbase.h"
在stdafx.h中声明自定义消息标识:
#define WM_SEND WM_USER+1
声明3个_Application、Documents、Selection的对象,参考代码如下:
//Attribute
    _Application m_App;                               //Word应用程序对象
    Documents m_Doc;                                      //Word文档对象
    Selection m_Slection;                             //文档选择对象
声明一个CEdit类的派生类CMyEdit,在 CMyEdit中响应WM_CHAR消息,代码如下:
void CMyEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    GetParent()->PostMessage(WM_SEND,(WPARAM)nChar,0);  //向父窗口发送字符。
        CEdit::OnChar(nChar, nRepCnt, nFlags);
}
在ReadWriteWordDlgl类的OnInitDialog函数中实例化Word应用程序,新建Word文档,参考代码如下:
BOOL CReadWriteWordDlg::OnInitDialog( )
{
//**************************************************************
try
    {
         if(!m_App.CreateDispatch(_T("Word.Application"))) //启动Word
         {
                AfxMessageBox(_T("请检查是否安装了Office"));
                return FALSE;
         }
          m_App.SetVisible(true);
          m_Doc = m_App.GetDocuments();
          CComVariant Template(_T(""));
          CComVariantNewTemplate(false),DocumentType(0),Visible;
         m_Doc.Add(&Template,&NewTemplate,&DocumentType,&Visible);
    }
    catch(_com_error &e)
    {
          MessageBox(e.ErrorMessage());
    }
//*********************************************************
return TRUE;
}
在ReadWriteWordDlg类中定义WM_SEND的消息体,在ReadWriteWordDlg.h中添加消息函数声明,在ReadWriteWordDlg.cpp中函数的实现参考代码如下:
/************************************************************************/
/* 消息体函数
/************************************************************************/
void CReadWriteWordDlg::SendMsgToWord(WPARAM wParam,LPARAM lParam)
{
       CString strTmp = "";
       UINT  nChar = wParam;
       strTmp.Format("%c",wParam);
   m_Slection=m_App.GetSelection();
       m_Slection.TypeText(strTmp);
}
写Word按钮的响应参考代码如下:
void CReadWriteWordDlg::OnWriteBtn()
{
m_Slection=m_App.GetSelection();//Word文档光标输入点
}
读Word按钮的响应参考代码如下:
/************************************************************************/
/* 读Word
/************************************************************************/
void CReadWriteWordDlg::OnReadBtn()
{
   m_Slection.GetSections();              //获取光标点
       m_Slection.WholeStory();            //对当前文档全选
       SetDlgItemText(IDC_READ_ED,m_Slection.GetText());
}
释放对象的参考代码如下:
/**************************************************************************/
/* 最后释放声明的对象
/**************************************************************************/
void CReadWriteWordDlg::OnDestroy()
{
      CDialog::OnDestroy();
      m_Slection.ReleaseDispatch();        //释放selection对象
      m_Doc.ReleaseDispatch();             //释放Document对象
      CComVariantSaveChanges(false),OriginalFormat,RouteDocument;
     m_App.Quit(&SaveChanges,&OriginalFormat,&RouteDocument);
      m_App.ReleaseDispatch();             //释放应用程序类对象。
}
程序的运行效果如图12-24所示。

图12-24  读/写Word
12.2.3            VC++修改Word字体样式的技巧 待续。。。。。。

 

 

编写高效的线程安全类

线程安全性和效率 现在您可以兼得

 

在语言级支持锁定对象和线程间发信使编写线程安全类变得简单本文使用简单的编程示例来说明开发高效的线程安全类是多么有效而直观

 

Java 编程语言为编写多线程应用程序提供强大的语言支持但是编写有用的没有错误的多线程程序仍然比较困难本文试图概述几种方法程序员可用这几种方法来创建高效的线程安全类

并发性

只有当要解决的问题需要一定程度的并发性时程序员才会从多线程应用程序中受益例如如果打印队列应用程序仅支持一台打印机和一台客户机则不应该将它编写为多线程的一般说来包含并发性的编码问题通常都包含一些可以并发执行的操作同时也包含一些不可并发执行的操作例如为多个客户机和一个打印机提供服务的打印队列可以支持对打印的并发请求但向打印机的输出必须是串行形式的多线程实现还可以改善交互式应用程序的响应时间

Synchronized 关键字

虽然多线程应用程序中的大多数操作都可以并行进行但也有某些操作如更新全局标志或处理共享文件不能并行进行在这些情况下必须获得一个锁来防止其他线程在执行此操作的线程完成之前访问同一个方法在 Java 程序中这个锁是通过synchronized 关键字提供的清单 1 说明了它的用法

清单 1. 使用synchronized 关键字来获取锁

public classMaxScore {

int max;

public MaxScore() {

max = 0;

}

public synchronizedvoid currentScore(int s) {

if(s> max) {

max = s;

}

}

public int max() {

return max;

}

}

这里两个线程不能同时调用currentScore() 方法当一个线程工作时另一个线程必须阻塞但是可以有任意数量的线程同时通过 max() 方法访问最大值因为max() 不是同步方法因此它与锁定无关

试考虑在 MaxScore 类中添加另一个方法的影响该方法的实现如清单 2 所示

清单 2. 添加另一个方法

public synchronizedvoid reset() {

max = 0;

}

这个方法当被访问时不仅将阻塞reset() 方法的其他调用而且也将阻塞 MaxScore 类的同一个实例中的currentScore() 方法因为这两个方法都访问同一个锁如果两个方法必须不彼此阻塞则程序员必须在更低的级别使用同步清单 3 是另一种情况其中两个同步的方法可能需要彼此独立

清单 3. 两个独立的同步方法

import java.util.*;

public class Jury {

Vector members;

Vector alternates;

public Jury() {

members = newVector(12, 1);

alternates = newVector(12, 1);

}

public synchronizedvoid addMember(String name) {

members.add(name);

}

public synchronizedvoid addAlt(String name) {

alternates.add(name);

}

public synchronizedVector all() {

Vector retval = newVector(members);

retval.addAll(alternates);

return retval;

}

}

此处两个不同的线程可以将members 和 alternates 添加到 Jury 对象中请记住synchronized 关键字既可用于方法更一般地也可用于任何代码块清单 4 中的两段代码是等效的

清单 4. 等效的代码

synchronized voidf() { void f() {

// 执行某些操作 synchronized(this) {

} // 执行某些操作

}

}

所以为了确保 addMember() 和 addAlt() 方法不彼此阻塞可按清单 5 所示重写 Jury类

清单 5. 重写后的 Jury 类

import java.util.*;

public class Jury {

Vector members;

Vector alternates;

public Jury() {

members = newVector(12, 1);

alternates = newVector(12, 1);

}

public voidaddMember(String name) {

synchronized(members){

members.add(name);

}

}

public voidaddAlt(String name) {

synchronized(alternates){

alternates.add(name);

}

}

public Vector all(){

Vector retval;

synchronized(members){

retval = newVector(members);

}

synchronized(alternates){

retval.addAll(alternates);

}

return retval;

}

}

请注意我们还必须修改all() 方法因为对 Jury 对象同步已没有意义在改写后的版本中addMember() addAlt() 和 all() 方法只访问与members 和 alternates 对象相关的锁因此锁定Jury 对象毫无用处另请注意all() 方法本来可以写为清单 6所示的形式

清单 6. 将 members 和 alternates 用作同步的对象

public Vector all(){

synchronized(members){

synchronized(alternates){

Vector retval;

retval = newVector(members);

retval.addAll(alternates);

}

}

return retval;

}

但是因为我们早在需要之前就获得 members 和 alternates 的锁所以这效率不高清单5 中的改写形式是一个较好的示例因为它只在最短的时间内持有锁并且每次只获得一个锁这样就完全避免了当以后增加代码时可能产生的潜在死锁问题

同步方法的分解

正如在前面看到的那样同步方法获取对象的一个锁如果该方法由不同的线程频繁调用则此方法将成为瓶颈因为它会对并行性造成限制从而会对效率造成限制这样作为一个一般的原则应该尽可能地少用同步方法尽管有这个原则但有时一个方法可能需要完成需要锁定一个对象几项任务同时还要完成相当耗时的其他任务在这些情况下可使用一个动态的锁定-释放-锁定-释放方法例如清单 7 和清单 8 显示了可按这种方式变换的代码

清单 7. 最初的低效率代码

public synchonizedvoid doWork() {

unsafe1();

write_file();

unsafe2();

}

清单 8. 重写后效率较高的代码

public voiddoWork() {

synchonized(this) {

unsafe1();

}

write_file();

synchonized(this) {

unsafe2();

}

}

清单 7 和清单 8 假定第一个和第三个方法需要对象被锁定而更耗时的 write_file() 方法不需要对象被锁定如您所见重写此方法以后对此对象的锁在第一个方法完成以后被释放然后在第三个方法需要时重新获得这样当 write_file() 方法执行时等待此对象的锁的任何其他方法仍然可以运行将同步方法分解为这种混合代码可以明显改善性能但是您需要注意不要在这种代码中引入逻辑错误

嵌套类

内部类在 Java 程序中实现了一个令人关注的概念它允许将整个类嵌套在另一个类中嵌套类作为包含它的类的一个成员变量如果定期被调用的的一个特定方法需要一个类就可以构造一个嵌套类此嵌套类的唯一任务就是定期调用所需的方法这消除了对程序的其他部分的相依性并使代码进一步模块化清单 9 一个图形时钟的基础使用了内部类

清单 9. 图形时钟示例

public class Clock{

protected classRefresher extends Thread {

int refreshTime;

publicRefresher(int x) {

super("Refresher");

refreshTime = x;

}

public void run() {

while(true) {

try {

sleep(refreshTime);

}

catch(Exception e){}

repaint();

}

}

}

public Clock() {

Refresher r = newRefresher(1000);

r.start();

}

private voidrepaint() {

// 获取时间的系统调用

// 重绘时钟指针

}

}

清单 9 中的代码示例不靠任何其他代码来调用 repaint() 方法这样将一个时钟并入一个较大的用户界面就相当简单

事件驱动处理

当应用程序需要对事件或条件内部的和外部的作出反映时有两种方法或用来设计系统在第一种方法称为轮询中系统定期确定这一状态并据此作出反映这种方法虽然简单也效率不高因为您始终无法预知何时需要调用它

第二种方法称为事件驱动处理效率较高但实现起来也较为复杂在事件驱动处理的情况下需要一种发信机制来控制某一特定线程何时应该运行在 Java 程序中您可以使用wait()notify() 和 notifyAll() 方法向线程发送信号这些方法允许线程在一个对象上阻塞直到所需的条件得到满足为止然后再次开始运行这种设计减少了 CPU 占用因为线程在阻塞时不消耗执行时间并且可在 notify() 方法被调用时立即唤醒与轮询相比事件驱动方法可以提供更短的响应时间

创建高效的线程安全类的步骤

编写线程安全类的最简单的方法是用 synchronized 声明每个方法虽然这种方案可以消除数据损坏但它同时也会消除您预期从多线程获得的任何收益这样您就需要分析并确保在 synchronized 块内部仅占用最少的执行时间您必须格外关注访问缓慢资源 文件目录网络套接字和数据库 的方法这些方法可能降低您的程序的效率尽量将对这类资源的访问放在一个单独的线程中最好在任何 synchronized 代码之外

一个线程安全类的示例被设计为要处理的文件的中心储存库它与使用 getWork() 和finishWork()与 WorkTable 类对接的一组线程一起工作本例旨在让您体验一下全功能的线程安全类该类使用了 helper 线程和混合同步请注意继续添加要处理的新文件的Refresher helper 线程的用法本例没有调整到最佳性能很明显有许多地方可以改写以改善性能比如将 Refresher 线程改为使用wait()/notify() 方法事件驱动的改写populateTable() 方法以减少列出磁盘上的文件这是高成本的操作所产生的影响

小结

通过使用可用的全部语言支持Java程序中的多线程编程相当简单但是使线程安全类具有较高的效率仍然比较困难为了改善性能您必须事先考虑并谨慎使用锁定功能

 

Java servlet多线程

Servlet 体系结构是建立在Java 多线程机制之上的,它的生命周期是由Web 容器负责的。

当客户端第一次请求某个Servlet 时,Servlet 容器将会根据web.xml 配置文件实例化这个Servlet 类。当有新的客户端请求该Servlet时,一般不会再实例化该 Servlet 类,也就是有多个线程在使用这个实例。 这样,当两个或多个线程同时访问同一个Servlet 时,可能会发生多个线程同时访问同一资源的情况,数据可能会变得不一致。所以在用Servlet 构建的Web应用时如果不注意线程安全的问题,会使所写的Servlet 程序有难以发现的错误。

实例变量不正确的使用是造成Servlet 线程不安全的主要原因。下面针对该问题给出了三种解决方案并对方案的选取给出了一些参考性的建议。

1、实现 SingleThreadModel 接口

该接口指定了系统如何处理对同一个Servlet 的调用。如果一个Servlet被这个接口指定,那么在这个Servlet 中的service 方法将不会有两个线程被同时执行,当然也就不存在线程安全的问题。这种方法只要将前面的Concurrent Test 类的类头定义更改为:

Public classConcurrent Test extends HttpServlet implements SingleThreadModel {

…………

}

2、同步对共享数据的操作

使用synchronized 关键字能保证一次只有一个线程可以访问被保护的区段,在本论文中的Servlet 可以通过同步块操作来保证线程的安全。同步后的代码如下:

…………

Public classConcurrent Test extends HttpServlet { …………

Username =request.getParameter ("username");

Synchronized(this){

Output =response.getWriter ();

Try {

Thread. Sleep(5000);

} Catch (InterruptedException e){}

output.println("用户名:"+Username+"

");

}

}

}

3、避免使用实例变量

本实例中的线程安全问题是由实例变量造成的,只要在Servlet 里面的任何方法里面都不使用实例变量,那么该Servlet 就是线程安全的。

修正上面的Servlet 代码,将实例变量改为局部变量实现同样的功能,代码如下:

……

Public classConcurrent Test extends HttpServlet {public void service (HttpServletRequest

request,HttpServletResponse

Response) throwsServletException, IOException {

Print Writeroutput;

String username;

Response.setContentType("text/html; charset=gb2312");

……

}

}

对上面的三种方法进行测试,可以表明用它们都能设计出线程安全的Servlet 程序。但是,如果一个Servlet实现了 SingleThreadModel 接口,Servlet 引擎将为每个新的请求创建一个单独的Servlet 实例,这将引起大量的系统开销。 SingleThreadModel 在Servlet2.4 中已不再提倡使用;同样如果在程序中使用同步来保护要使用的共享的数据,也会使系统的性能大大下降。这是因为被同步的代码块在同一时刻只能有一个线程执行它,使得其同时处理客户请求的吞吐量降低,而且很多客户处于阻塞状态。另外为保证主存内容和线程的工作内存中的数据的一致性,要频繁地刷新缓存,这也会大大地影响系统的性能。所以在实际的开发中也应避免或最小化 Servlet 中的同步代码;在Serlet中避免使用实例变量是保证Servlet线程安全的最佳选择。从Java 内存模型也可以知道,方法中的临时变量是在栈上分配空间,而且每个线程都有自己私有的栈空间,所以它们不会影响线程的安全。

--------------------------------------------------------------------------------

补充:

servlet 存在的多线程问题

实例变量: 实例变量是在堆中分配的,并被属于该实例的所有线程共享,所以不是线程安全的.

JSP 系统提供的8 个类变量:

JSP 中用到的OUT,REQUEST,RESPONSE,SESSION,CONFIG,PAGE,PAGECONXT 是线程安全的,APPLICATION 在整个系统内被使用,所以不是线程安全的.

局部变量: 局部变量在堆栈中分配,因为每个线程都有它自己的堆栈空间,所以是线程安全的.

静态类: 静态类不用被实例化,就可直接使用,也不是线程安全的.

外部资源: 在程序中可能会有多个线程或进程同时操作同一个资源(如:多个线程或进程同时对一个文件进行写操作).

此时也要注意同步问题. 使它以单线程方式执行,这时,仍然只有一个实例,所有客户端的请求以串行方式执行。这样会降低系统的性能

对于存在线程不安全的类,如何避免出现线程安全问题:

1、采用synchronized 同步。缺点就是存在堵塞问题。

2、使用ThreadLocal(实际上就是一个HashMap),这样不同的线程维护自己的对象,线程之间相互不干扰。

ThreadLocal 的设计

首先看看ThreadLocal 的接口:

Object get() ; // 返回当前线程的线程局部变量副本 protected Object

initialValue(); // 返回该线程局部变量的当前线程的初始值

void set(Objectvalue); // 设置当前线程的线程局部变量副本的值

ThreadLocal 有3 个方法,其中值得注意的是initialValue(),该方法是一个protected的方法,显然是为了子类重写而特意实现的。该方法返回当前线程在该线程局部变量的初始值,这个方法是一个延迟调用方法,在一个线程第1 次调用get()或者set(Object)时才执行,并且仅执行1 次。ThreadLocal 中的确实实现直接返回一个null:protected Object initialValue() { return null; }

ThreadLocal 是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal 类中有一个Map,用于存储每一个线程的变量的副本。比如下面的示例实现:

public classThreadLocal

{

private Map values= Collections.synchronizedMap(new HashMap());

public Object get()

{

Thread curThread =Thread.currentThread();

Object o =values.get(curThread);

if (o == null&& !values.containsKey(curThread))

{

o = initialValue();

values.put(curThread,o);

}

return o;

}

public voidset(Object newValue)

{

values.put(Thread.currentThread(),newValue);

}

public ObjectinitialValue()

{

return null;

}

}

当然,这并不是一个工业强度的实现,但JDK 中的ThreadLocal的实现总体思路也类似于此。

ThreadLocal 的使用

如果希望线程局部变量初始化其它值,那么需要自己实现ThreadLocal 的子类并重写该方法,通常使用一个内部匿名类对ThreadLocal 进行子类化,比如下面的例子,SerialNum类为每一个类分配一个序号:

public classSerialNum

{

// The next serialnumber to be assigned

private static intnextSerialNum = 0;

private staticThreadLocal serialNum = new ThreadLocal()

{

protectedsynchronized Object initialValue()

{

return new Integer(nextSerialNum++);

}

};

public static intget()

{

return ((Integer)(serialNum.get())).intValue();

}

}

SerialNum 类的使用将非常地简单,因为get()方法是static 的,所以在需要获取当前线程的序号时,简单地调用:

int serial =SerialNum.get(); 即可。

在线程是活动的并且ThreadLocal 对象是可访问的时,该线程就持有一个到该线程局部变量副本的隐含引用,当该线程运行结束后,该线程拥有的所以线程局部变量的副本都将失效,并等待垃圾收集器收集。

ThreadLocal 与其它同步机制的比较

ThreadLocal 和其它同步机制相比有什么优势呢?ThreadLocal 和其它所有的同步机制都是为了解决多线程中的对同一变量的访问冲突,在普通的同步机制中,是通过对象加锁来实现多个线程对同一变量的安全访问的。这时该变量是多个线程共享的,使用这种同步机制需要很细致地分析在什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放该对象的锁等等很多。所有这些都是因为多个线程共享了资源造成的。ThreadLocal 就从另一个角度来解决多线程的并发访问,ThreadLocal 会为每一个线程维护一个和该线程绑定的变量的副本,从而隔离了多个线程的数据,每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal 提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的整个变量封装进ThreadLocal,或者把该对象的特定于线程的状态封装进ThreadLocal。

由于ThreadLocal 中可以持有任何类型的对象,所以使用ThreadLocal get 当前线程的值是需要进行强制类型转换。但随着新的Java 版本(1.5)将模版的引入,新的支持模版参数的ThreadLocal<T>类将从中受益。也可以减少强制类型转换,并将一些错误检查提前到了编译期,将一定程度地简化ThreadLocal 的使用。

总结

当然ThreadLocal 并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;而ThreadLocal 是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源(变量),这样当然不需要对多个线程进行同步了。所以,如果你需要进行多个线程之间进行通信,则使用同步机制;如果需要隔离多个线程之间的共享冲突,可以使用ThreadLocal,这将极大地简化你的程序,使程序更加易读、简洁。

ThreadLocal 常见用途:

存放当前session 用户

存放一些context 变量,比如webwork的ActionContext

存放session,比如Springhibernate orm 的session

例子:用 ThreadLocal 实现每线程 Singleton

线程局部变量常被用来描绘有状态“单子”(Singleton) 或线程安全的共享对象,或者是通过把不安全的整个变量封装进 ThreadLocal,或者是通过把对象的特定于线程的状态封装进ThreadLocal。例如,在与数据库有紧密联系的应用程序中,程序的很多方法可能都需要访问数据库。在系统的每个方法中都包含一个 Connection 作为参数是不方便的 —用“单子”来访问连接可能是一个虽然更粗糙,但却方便得多的技术。然而,多个线程不能安全地共享一个 JDBC Connection。如清单 3 所示,通过使用“单子”中的 ThreadLocal,我们就能让我们的程序中的任何类容易地获取每线程 Connection 的一个引用。这样,我们可以认为ThreadLocal 允许我们创建每线程单子。

例:把一个 JDBC 连接存储到一个每线程Singleton 中

public classConnectionDispenser {

private staticclass ThreadLocalConnection extends ThreadLocal {

public ObjectinitialValue() {

returnDriverManager.getConnection(ConfigurationSingleton.getDbUrl());

}

}

privateThreadLocalConnection conn = new ThreadLocalConnection();

public staticConnection getConnection() {

return (Connection)conn.get();

}

}

注意:

理论上来说,ThreadLocal 是的确是相对于每个线程,每个线程会有自己的ThreadLocal。但是上面已经讲到,一般的应用服务器都会维护一套线程池。因此,不同用户访问,可能会接受到同样的线程。因此,在做基于TheadLocal 时,需要谨慎,避免出现ThreadLocal变量的缓存,导致其他线程访问到本线程变量。

--------------------------------------------------------------------------------

一,servlet 容器如何同时处理多个请求。

Servlet 采用多线程来处理多个请求同时访问,Servelet 容器维护了一个线程池来服务请求。

线程池实际上是等待执行代码的一组线程叫做工作者线程(Worker Thread),Servlet 容器使用一个调度线程来管理工作者线程(Dispatcher Thread)。

当容器收到一个访问Servlet 的请求,调度者线程从线程池中选出一个工作者线程,将请求传递给该线程,然后由该线程来执行Servlet 的service 方法。

当这个线程正在执行的时候,容器收到另外一个请求,调度者线程将从池中选出另外一个工作者线程来服务新的请求,容器并不关系这个请求是否访问的是同一个Servlet 还是另外一个Servlet。

当容器同时收到对同一Servlet 的多个请求,那这个Servlet的service 方法将在多线程中并发的执行。

二,Servlet 容器默认采用单实例多线程的方式来处理请求,这样减少产生Servlet 实例的开销,提升了对请求的响应时间。对于Tomcat 可以在server.xml中通过<Connector>元素设置线程池中线程的数目。

就实现来说:调度者线程类所担负的责任如其名字,该类的责任是调度线程,只需要利用自己的属性完成自己的责任。所以该类是承担了责任的,并且该类的责任又集中到唯一的单体对象中。而其他对象又依赖于该特定对象所承担的责任,我们就需要得到该特定对象。那该类就是一个单例模式的实现了。

三,如何开发线程安全的 Servlet

1,变量的线程安全:这里的变量指字段和共享数据(如表单参数值)。

a,将参数变量 本地化。多线程并不共享局部变量.所以我们要尽可能的在servlet中使

用局部变量。

例如:String user = "";

user =request.getParameter("user");

b,使用同步块Synchronized,防止可能异步调用的代码块。这意味着线程需要排队处理。

在使用同板块的时候要尽可能的缩小同步代码的范围,不要直接在sevice 方法和响应方法上使用同步,这样会严重影响性能。

2,属性的线程安全:ServletContext,HttpSession,ServletRequest 对象中属性

ServletContext:(线程是不安全的)ServletContext 是可以多线程同时读/写属性的,线程是不安全的。要对属性的读写进行同步处理或者进行深度Clone()。所以在Servlet 上下文中尽可能少量保存会被修改(写)的数据,可以采取其他方式在多个Servlet 中共享,比方我们可以使用单例模式来处理共享数据。

HttpSession:(线程是不安全的)HttpSession 对象在用户会话期间存在,只能在处理属于同一个Session 的请求的线程中被访问,因此Session 对象的属性访问理论上是线程安全的。

当用户打开多个同属于一个进程的浏览器窗口,在这些窗口的访问属于同一个Session,会出现多次请求,需要多个工作线程来处理请求,可能造成同时多线程读写属性。这时我们需要对属性的读写进行同步处理:使用同步块Synchronized 和使用读/写器来解决。

ServletRequest:(线程是安全的)对于每一个请求,由一个工作线程来执行,都会创建有一个新的ServletRequest 对象,所以ServletRequest 对象只能在一个线程中被访问。ServletRequest是线程安全的。

注意:ServletRequest 对象在service 方法的范围内是有效的,不要试图在service 方法结束后仍然保存请求对象的引用。

3,使用同步的集合类:使用Vector 代替ArrayList,使用Hashtable代替HashMap。

4,不要在Servlet 中创建自己的线程来完成某个功能。

Servlet 本身就是多线程的,在Servlet 中再创建线程,将导致执行情况复杂化,出现多线程安全问题。

5,在多个servlet 中对外部对象(比方文件)进行修改操作一定要加锁,做到互斥的访问。

 

四,SingleThreadModel 接口

javax.servlet.SingleThreadModel接口是一个标识接口,如果一个Servlet 实现了这个接口,那Servlet 容器将保证在一个时刻仅有一个线程可以在给定的servlet 实例的service 方法中执行。将其他所有请求进行排队。

服务器可以使用多个实例来处理请求,代替单个实例的请求排队带来的效益问题。服务器创建一个Servlet 类的多个Servlet 实例组成的实例池,对于每个请求分配Servlet 实例进行响应处理,之后放回到实例池中等待下此请求。这样就造成并发访问的问题。

此时,局部变量(字段)也是安全的,但对于全局变量和共享数据是不安全的,需要进行同步处理。而对于这样多实例的情况SingleThreadModel 接口并不能解决并发访问问题。

SingleThreadModel 接口在servlet 规范中已经被废弃了。

 

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值