Java编程思想——第21章 并发
前言
对于某些问题,如果能够并行的执行程序中的多个部分,则回变得非常方便甚至必要,这些部分要么看起来是并发执行,要么是在多处理环境下同时执行。并行编辑可以使程序执行速度得到极大提高,或者为设计某些类型的程序提供更易用的模型。当并行执行的任务彼此开始产生互相干涉时,实际的并发问题就发生了。
一、并发的多面性
并发解决的问题答题上可以分为“速度”和“设计可管理新”两种。
1.更快的执行
想要更快的执行,需要多处理器,并发是用于多处理器编程的基本工具。这是使用强有力的多处理器Web服务器的常见情况,在为每个请求分配一个线程的程序中,它可以将大量的用户请求分布到多个CPU上。
当并发运行在单处理器时,开销可能要比顺序执行开销大,因为增加了上下文切换的代价。但是阻塞使得问题变得不同:如果程序中的某个任务因为该程序控制范围之外的某些条件(如:I/O)而导致不能继续执行,那么这个任务线程阻塞了。如果没有并发,则整个程序都将停止下来。因此,如果没有任务会阻塞,在单线程处理器机器上使用并发就没有任何意义。单线程并发一般使用在窗口操作。
Java所使用的这种并发系统会共享诸如内存和I/O这样的资源,因此编写多线程程序最基本的困难在于协调不同线程驱动的任务之间对这些资源的使用,以使得这些资源不会同时被多个任务访问。
2.改进代码设计
简单举个例子吧,游戏里面多个npc,各自走各自的。
二、基本的线程机制
并发编程是我们可以将程序划分为多个分离的、独立运行的任务。通过多线程机制,这些独立任务中每一个都将由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,因此,单进程可以拥有多个并发执行的任务,但是程序使得每个人物都想有自己的CPU。其底层机制是切分CPU时间。
1.定义任务
线程可以驱动任务,因此你需要一种描述任务的方式,这可以由Runnable接口来提供。要想定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。
public class RunnableDemo implements Runnable { int i =100; @Override public void run() { while (i-->0){ Thread.yield(); } } }
任务的run()方法总会以循环的形式使任务一直进行下去,在run()中对静态方法Thread.yield()的调用是对线程调度器(Java线程机制的一部分,可以将CPU从一个线程转移给另一个线程)的一种建议,它声明:“我已经完成生命周期中最重要的部分,此刻是切换给其他任务执行一段时间的大好时机。
当Runnable导出一个类时,它必须具有run()方法,但是这个方法并无特殊之处——它不会产生任何内在的线程能力。要实现县城行为,你必须显式地将一个任务附着到线程了。
2.Thread类
将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器:
public static void main(String[] args) { Thread t = new Thread(new RunnableDemo()); t.start();
//其他方法 }
Thread构造器只需要一个Runnable对象。调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动该任务。start()方法实际上,产生的是对Runnable.run()的调用。程序会同时运行两个方法,main()里面的其他方法和Runnable.run()是程序中与其他线程“同时”执行代码。
3.使用Executor
执行器(Excutor)将为你管理Thread对象,简化了并发编程。相当于中介。但是由于一下原因不是很推荐
推荐:ThreadPoolExecutor使用 。
4.从任务中产生返回值
Runnable是执行工作的独立任务,但是它不返回任何值。如果希望任务中返回值那么应当实现Callable接口。Callable具有泛型,它的类型参数标识从call()方法中返回的值,并且必须使用ExectorService.submit()方法调用:
public class CallableDemo { public static void main(String[] args) { ExecutorService executorService = new ThreadPoolExecutor(5, 10, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1)); List<Future<String>> results = new ArrayList<>(); for (int i = 0; i < 10; i++) { results.add(executorService.submit(new TaskWithResult(i))); } for (Future<String> fs : results) { try { //得到返回值 System.out.println(fs.get()); } catch (Exception e) { e.printStackTrace(); } finally { executorService.shutdown(); } } } } class TaskWithResult implements Callable<String> { private int id; TaskWithResult(int id) { this.id = id; } @Override public String call() { return "result of TaskWithResult" + id; } }
5.休眠
影响任务行为的一种简单方法是调用sleep(),这将使任务中止执行对应的时间。
@Override public void run() { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } }
6.优先级
线程的优先级将该线程的重要性传递给调度器,调度器倾向于让优先权最高的线程先执行。但这并不意味着优先级低的线程得不到执行(优先权高的等待不会导致死锁),优先权低的线程仅仅是执行频率较低。在绝大多数时间里,所有程序都应该是默认优先级,试图操作线程优先级通常是一种错误。
@Override public void run() { Thread.currentThread().setPriority(Thread.MIN_PRIORITY ); Thread.currentThread().getPriority(); }
最好在run方法里面设置优先级,而且最好就用那三种常用的级别 :
Thread.MAX_PRIORITY
Thread.NORM_PRIORITY
Thread.MIN_PRIORITY
7.让步
当工作做了一段时间可以让别的线程使用cpu了。此时可以使用Thread.yield()给线程调度一个暗示(只是一个暗示,不一定被采纳)。
8.后台线程
所谓后台线程,是指在程序运行时,在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。当所有非后台线程结束时,程序也就终止了,同时会杀死进程中所有的后台线程。
设置后台线程:
public static void main(String[] args) { Thread t = new Thread(new RunnableDemo()); //这句设置线程为后台线程 t.setDaemon(true); t.start(); }
9.编码的变体
在非常简单的情况下,你可能会希望使用直接哦那个Thread继承这种可替换的方式:
public class SimpleThrad extends Thread { private int countDown = 5; /** * 依然需要实现run方法 */ @Override public void run() { while (true) { System.out.println(this); if (--countDown == 0) { return; } } } }
但是不提倡还是提倡使用ThreadPoolExecutor实现线程管理。
10.术语
从上面的各种情况中你可以看到实际你没有对Thread的控制权。你创建任务,并通过某种方式将一个线程附着到任务上,以使得这个线程可以驱动任务。在Java中Thread类自身不执行任何操作,它只是驱动赋予给他的任务,将任务和线程区分开能让你更好的理解线程。
11.加入一个线程
一个线程可以在其他线程上调用join()方法,其效果是等待一段时间直到第二线程结束才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,知道目标线程t结束才恢复。
也可也在join()加上超时参数(毫秒),使得目标函数在参数时间外还未结束,join()方法依旧能返回。对join()方法的调用可以被中断,做法是在调用线程上调用interrppt()方法,并加try-catch。这里不举例子了因为在使用过程中CycliBarrier要比join更好。
三、共享受限资源
对于并发任务,你需要某种方式来防止两个任务访问相同的资源,至少在关键阶段不能出现这种情况。
1.解决共享资源竞争
防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。通常这是通过在代码前面加上以挑锁语句来实现的,这使得在一段时间内只有一个任务可以运行这段代码。因为锁语句产生了一种互相排斥的效果,所以这种机制撑场成为互斥量(mutex)。
synchronized,当代码要执行被synchronized保护的代码块时,先检查锁是否可用,再获得锁,执行代码块,释放锁。共享资源一般是以对象形式存在的内存片段,也可以是文件,I/O,打印机等。要控制对共享资源的访问,需要先把它包装进一个对象。然后把所有调用这个资源的方法标记为synchronized。如果某个任务在调用标记为synchronized的方法,那么那么在这个线程从该方法返回前,其他所有要调用类中任何标记为synchronized方法的线程都会被阻塞。
对所有对象,自动含有单一锁,当在对象上调用其任意synchronized方法的时候,此对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放锁后才能被调用。注意,在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其他任务直接访问域。
对每个类,也有一个锁。所以 synchronized static 方法可以在类的范围内防止对static数据的并发访问。