到目前为止,你学到的都是有关 顺序编程 的知识。即程序中的所有事物在任意时刻都只能执行一个步骤。 下面我们应该学一学并发编程吧。
并发“具有可论证的确定性,但是实际上具有不可确定性”。实际上,你可能无法编写出能够针对你的并发程序生成故障条件的测试代码。
Web系统是最常见的Java应用系统之一,而基本的Web库类,Servlet具有天生的多线程性–这很重要,因为Web服务器经常包含多个处理器,而并发是充分利用这些处理器的理想方式。
并发编程令人困惑的一个主要原因是:使用并发时需要解决的问题有多个,而实现并发的方式也有多种,而且在这两者之间没有明显的映射关系(而且通常只具有模糊的界限)。因此,必须理解所有这些问题和特例,以便有效地使用并发。
而并发解决的问题大体上可以分为两种:
1.速度
2.设计可管理性
速度问题初听起来很简单:如果你想要一个程序运行得更快,那么可以将其断开为多个片段,在单独的处理器上运行每个片段。并发是用于多处理器编程的基本工具。
如果你有一台多处理器的机器,那么就可以在这些处理器之间分布多个任务,从而可以极大地提高吞吐量。这是使用强有力的多处理器web服务器的常见情况,在为每个请求分配一个线程的程序中,它可以将大量的用户请求分布到多个CPU上。
但是,并发通常是提高运行在单处理器上的程序的性能。听起来有些违背直觉。在单处理器上运行的并发程序开销确实应该比该程序的所有部分都顺序执行的开销大,因为其中增加了所谓上下文切换的代价(从一个任务切换到另一个任务)。
但是,有些情况下,情况就不同了,比如阻塞。如果使用并发来编写程序,那么当一个任务阻塞时,程序中的其他任务还可以继续执行,因此这个程序可以保持继续向前执行。
事实上,从性能的角度看,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义。
这里需要注意这句话的前提哦,我想应该有这么几个:
1.从性能的角度看
2.在单处理器机器上
另一个例子是事件驱动的编程。考虑比如手机页面上的一个“退出”按钮。如果不使用并发,则产生可响应用户界面的唯一方式就是所有的任务都周期性地检查用户输入。通过创建单独的执行线程来响应用户的输入,即使这个线程在大多数时间里都是阻塞的,但是程序可以保证一定程度的可响应性。
实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包容的程序。
java采取了更加传统的方式,在顺序型语言的基础上提供了对线程的支持。与在多任务操作系统中分叉外部进程不同,线程机制是在由执行程序表示的单一进程中创建人物,这种方式产生的一个好处是操作系统的透明性。
线程的一大好处是可以使你从处理器层次抽身出来,即代码不必知道它是运行在具有一个还是多个CPU的机器上。
线程可以驱动任务,因此你需要一种可以描述任务的方式。要想定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。例如:
py;class LiftOff implements Runnable{
Lprotected int countDown = 10;private static int taskCount = 0;private final int id = taskCount++;public String status(){return "#"+id+"("+(countDown>0?countDown:"liftoff")+"),";}@Overridepublic void run() {// TODO Auto-generated method stubwhile(countDown-->0){System.out.print(status());Thread.yield();}}}
标识符id可以用来区分任务的多个实例,它是final的,因为它一旦初始化就不希望被修改。
run()方法中通常总会有某种形式的循环。在run()中对静态方法Thread.yield()的调用时对线程调度器(java线程机制的一部分,可以将CPU从一个线程转移给另一个线程)的一种建议(注意,仅仅是建议,不能保证一定切换),他在声明:我已经执行完生命周期中最重要的部分了,此刻正是切换给其他任务执行一段时间的大好时机。
在下面的实例中,这个任务的run()不是由单独的线程驱动的,它是在main()中直接调用的(实际上,这里仍旧使用了线程,即总是分配给main()的那个线程):
public class MultiThread{public static void main(String[] args){LiftOff lf = new LiftOff();LiftOff lf1 = new LiftOff();lf.run();lf1.run();}
}
运行结果:
#0(9),#0(8),#0(7),#0(6),#0(5),#0(4),#0(3),#0(2),#0(1),#0(liftoff),#1(9),#1(8),#1(7),#1(6),#1(5),#1(4),#1(3),#1(2),#1(1),#1(liftoff),
当从Runnable导出一个类时,它必须具有run()方法,但是这个方法并无特殊之处–它不会产生任何内在的线程能力。要实现线程行为,你必须显式地将一个任务附着到线程上。
将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器。例如:
public class MultiThread{public static void main(String[] args){Thread t = new Thread(new LiftOff());t.start();LiftOff lf = new LiftOff();lf.run();}
}
运行结果:
#1(9),#1(8),#1(7),#1(6),#1(5),#1(4),#1(3),#0(9),#1(2),#1(1),#0(8),#1(liftoff),#0(7),#0(6),#0(5),#0(4),#0(3),#0(2),#0(1),#0(liftoff),
因为main()所在的线程和t是两个不同的线程,都绑定了Runnable。Thread构造器只需要一个Runnable对象。调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动该任务。输出说明不同任务的执行在线程被换进换出时混在了一起。
当main()创建Thread对象时,它并没有捕获任何对这些对象的引用。在使用普通对象时,这对于垃圾回收俩说是一场公平的游戏,但是在使用Thread对象时,情况就不同了。每个Thread都“注册”了它自己,因此确实有一个对它的引用,而且在它的任务退出其run()并死亡之前,垃圾回收器无法清除它。你可以从输出中看到,这些任务确实运行到了结束,因此,一个线程会创建一个单独的执行线程,在对start()的调用完成之后,它仍旧会继续存在。
Java SE5中的执行器(Executor)将为你管理Thread对象,从而简化了并发编程。Executor在客户端和任务执行之间提供了一个间接层,与客户端直接执行任务不同,这个中介对象将执行任务。Executor允许你管理异步任务的执行,而无须显式地管理线程的生命周期。
Executor在Java SE5/6中是启动任务的优选方法。
我们可以使用Executor来代替显式地创建Thread对象。ExecutorService(具有服务生命周期的Executor,例如关闭)知道如何构建恰当的上下文来执行Runnable对象。在下面的例子中,CachedThreadPool将为每个任务都创建一个线程。注意,ExecutorService对象是使用静态的Executor方法创建的,这个方法可以确定其Executor类型:
public class CachedThreadPool{public static void main(String[] args){ExecutorService exec = wCachedThreadPool();for(int i=0;i<3;i++){ute(new LiftOff());}exec.shutdown();}
}
运行结果:
#0(9),#2(9),#1(9),#2(8),#0(8),#2(7),#2(6),#2(5),#1(8),#2(4),#0(7),#2(3),#2(2),#2(1),#1(7),#2(liftoff),#1(6),#0(6),#0(5),#0(4),#0(3),#0(2),#1(5),#0(1),#0(liftoff),#1(4),#1(3),#1(2),#1(1),#1(liftoff),
非常常见的情况是,单个的Executor被用来创建和管理系统中所有的任务。
shutdown()方法的调用可以防止新任务被提交给这个Executor,当前线程(在本例中,即驱动main()的线程)将继续运行在shutdown()被调用之前提交的所有任务。这个程序将在Executor中的所有任务完成之后尽快退出。
可以将CachedThreadPool替换成不同类型的Executor。例如:
Executor exec = wFixedThreadPool(Number)
有了FixedThreadPool,你就可以一次性预先执行代价高昂的线程分配,因而也就可以限制线程的数量。这可以节省时间,因为你不用为每个任务都固定地付出创建线程的开销。
注意,在任务线程池中,现有线程在可能的情况下,都会被自动复用。
Runnable是执行工作的独立任务,但是它不返回任何值。如果你希望任务在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一种具有类型参数的泛型,它的类型参数表示的是从方法call()中返回的值的类型,并且必须使用ExecutorService.submit()方法调用它。下面是示例:
class TaskWithResult implements Callable<String>{private int id;public TaskWithResult(int id){this.id = id;}public String call(){return "return of taskWithResult"+id;}
}public class CallableDemo{public static void main(String[] args){ExecutorService exec = wCachedThreadPool();ArrayList<Future<String>> results = new ArrayList<Future<String>>();for(int i=0;i<3;i++){results.add(exec.submit(new TaskWithResult(i)));}for(Future<String> fs:results){try {//get()方法会阻塞直到完成System.out.());} catch (InterruptedException | ExecutionException e) {// TODO Auto-generated catch blocke.printStackTrace();}finally{exec.shutdown();}}}
}
结果:
return of taskWithResult0
return of taskWithResult1
return of taskWithResult2
submit()方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化。你可以用isDone()方法来查询Future是否已经完成。当任务完成时,它具有一个结果,你可以调用get()方法来获取该结果。你也可以不用isDone()进行检查就直接调用get(),在这种情况下,get()将阻塞,直至结果准备就绪。
影响任务行为的一种简单方法是调用sleep(),这将使任务中止执行给定的时间。
线程的优先级将该线程的重要性传递给了调度器。并不是优先权较低的线程将得不到执行(也就是说,优先权不会导致死锁)。优先级较低的线程仅仅是执行的频率较低。
在绝大多数时间内,所有线程都应该以默认的优先级运行。试图操作线程优先级通常是一种错误。优先级代码:
class SimplePriorities implements Runnable{private int countDown = 5;private volatile double d;//no optimizationprivate int priority;public String toString(){return Thread.currentThread()+":"+countDown;}public SimplePriorities(int priority) {// TODO Auto-generated constructor stubthis.priority = priority;}@Overridepublic void run() {// TODO Auto-generated method stubThread.currentThread().setPriority(priority);while(true){//a expensive,interruptable operationfor(int i=0;i<1000;i++){d+=(Math.PI+Math.E)/(double)i;if(i%1000==0)Thread.yield();}System.out.println(this);if(--countDown==0) return;}}
}public class PriorityDemo{public static void main(String[] args){ExecutorService exec = wCachedThreadPool();for(int i=0;i<5;i++){ute(new SimplePriorities(Thread.MIN_PRIORITY));}ute(new SimplePriorities(Thread.MAX_PRIORITY));exec.shutdown();
}
}
结果:
Thread[pool-1-thread-3,1,main]:5
Thread[pool-1-thread-6,10,main]:5
Thread[pool-1-thread-5,1,main]:5
Thread[pool-1-thread-6,10,main]:4
Thread[pool-1-thread-4,1,main]:5
Thread[pool-1-thread-6,10,main]:3
Thread[pool-1-thread-2,1,main]:5
Thread[pool-1-thread-6,10,main]:2
Thread[pool-1-thread-2,1,main]:4
Thread[pool-1-thread-1,1,main]:5
Thread[pool-1-thread-2,1,main]:3
Thread[pool-1-thread-6,10,main]:1
Thread[pool-1-thread-5,1,main]:4
Thread[pool-1-thread-5,1,main]:3
Thread[pool-1-thread-3,1,main]:4
Thread[pool-1-thread-5,1,main]:2
Thread[pool-1-thread-3,1,main]:3
Thread[pool-1-thread-2,1,main]:2
Thread[pool-1-thread-3,1,main]:2
Thread[pool-1-thread-1,1,main]:4
Thread[pool-1-thread-4,1,main]:4
Thread[pool-1-thread-5,1,main]:1
Thread[pool-1-thread-3,1,main]:1
Thread[pool-1-thread-1,1,main]:3
Thread[pool-1-thread-2,1,main]:1
Thread[pool-1-thread-4,1,main]:3
Thread[pool-1-thread-1,1,main]:2
Thread[pool-1-thread-4,1,main]:2
Thread[pool-1-thread-1,1,main]:1
Thread[pool-1-thread-4,1,main]:1
如果将6的优先级也设为最低的话,结果:
Thread[pool-1-thread-1,1,main]:5
Thread[pool-1-thread-2,1,main]:5
Thread[pool-1-thread-1,1,main]:4
Thread[pool-1-thread-2,1,main]:4
Thread[pool-1-thread-3,1,main]:5
Thread[pool-1-thread-2,1,main]:3
Thread[pool-1-thread-2,1,main]:2
Thread[pool-1-thread-1,1,main]:3
Thread[pool-1-thread-3,1,main]:4
Thread[pool-1-thread-4,1,main]:5
Thread[pool-1-thread-2,1,main]:1
Thread[pool-1-thread-1,1,main]:2
Thread[pool-1-thread-1,1,main]:1
Thread[pool-1-thread-5,1,main]:5
Thread[pool-1-thread-3,1,main]:3
Thread[pool-1-thread-6,1,main]:5
Thread[pool-1-thread-4,1,main]:4
Thread[pool-1-thread-5,1,main]:4
Thread[pool-1-thread-6,1,main]:4
Thread[pool-1-thread-4,1,main]:3
Thread[pool-1-thread-5,1,main]:3
Thread[pool-1-thread-3,1,main]:2
Thread[pool-1-thread-6,1,main]:3
Thread[pool-1-thread-5,1,main]:2
Thread[pool-1-thread-4,1,main]:2
Thread[pool-1-thread-5,1,main]:1
Thread[pool-1-thread-3,1,main]:1
Thread[pool-1-thread-6,1,main]:2
Thread[pool-1-thread-4,1,main]:1
Thread[pool-1-thread-6,1,main]:1
可以看出,高的优先级并不是保证最先执行,而是一种频率问题。注意,优先级是在run()的开头部分设定的,在构造器中设置他们不会有任何好处,因为Executor在此刻还没有开始执行任务。
这个暗示将通过调用yield()方法来做出(不过这只是一个暗示,没有任何机制保证它将会被采纳)。
当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。
必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。即:
Thread daemon = new Thread(new SimpleDaemons());
daemon.setDaemon(true);//必须在start之前调用
daemon.start();
可以通过调用isDaemon()方法来确定一个线程是否是一个后台线程。如果是一个后台线程,那么它创建的任何线程都将被自动设置为后台线程。
class DaemonThreadFactory implements ThreadFactory{//重写newThread方法,使得每个创建的线程都为后台线程@Overridepublic Thread newThread(Runnable r) {// TODO Auto-generated method stubThread t = new Thread(r);t.setDaemon(true);return t;}}public class DaemonFromFactory implements Runnable{@Overridepublic void run() {// TODO Auto-generated method stubtry{while(true){TimeUnit.MILLISECONDS.sleep(100);System.out.println(Thread.currentThread()+" "+this);}}catch(InterruptedException e){System.out.println("Interrupted");}}public static void main(String[] args) throws Exception{//传入创建线程的工厂方法ExecutorService exec = wCachedThreadPool(new DaemonThreadFactory());for(int i
本文发布于:2024-02-04 11:08:34,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170705796155026.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |