多线程知识点整理
基本概念
多线程程序在较低的层次上扩展了多任务的概念:一个程序同时执行多个任务,通常,每一个任务称为一个线程,它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序。
多进程与多线程的区别:
每个进程拥有自己的一整套变量,而线程则共享数据。
相关接口
•Runnable
◦ 方法 void run()
◦可由Runnable对象构成Thread,不要调用Thread类或者Runable对象的run方法,直接调用run方法,只会执行同一个线程中的任务,而不会启动新的线程。应该调用Thread.start方法,这个方法将创建一个执行run方法的新线程。
•Callable
◦方法 V call() throws Exception;
◦与Runnable类似,但有返回值
•Future
◦方法 V get() throws…
V get(long timeout, TimeUnit unit) throws ….
Void cancle(boolean mayInterupt) //取消计算,如果已经开始根据参数判断是否中断
boolean isCacelled()
boolean isDone() // 还在计算返回false,完成计算返回true
◦保存异步计算的结果,可以启动一个计算,将future对象交给某个线程,然后忘掉它,future对象的所有者在结果计算好之后就可以获得它。
◦第一个get方法的调用被阻塞,直到计算完成。如果在计算完成之前,第二个方法的调用超时,抛出一个timeoutException异常,如果运行该计算的线程被中断,两个方法都将抛出interruptedException,如果计算已经完成,那么get方法立即返回。
◦FutureTask 包装器是一种非常便利的机制,可将Callable转换成future和Runnable,它同时实现二者的接口
Callalbe myComputation = 。。。;
FutureTask task = new FutureTask(myComputation );
Thread t = new Thread(task);
t.start();
.….
V result = task.get();
线程的状态
要获得一个线程的状态,调用getState方法
•新生:当用new操作创建一个新线程时,该线程还没有开始运行,这意味着它的状态是新生。
•可运行:一旦调用start方法,线程处于runnable状态,记住在任何时刻,一个可运行的线程可能正在运行也可能没有运行
•阻塞/等待/计时等待:当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源,直到线程调度器重新激活它。
1.当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态,当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态
2.当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用object.wait方法或者thread.join方法,或者等待lock或condition时就会出现这种情况
3.有几个方法有一个超时参数,调用它们导致线程进入计时等待状态,这一状态将一直保持到超时期满,或者接收到适当的通知。
•终止:两个原因导致终止
1.因为run方法正常退出而自然死亡 (当线程的run方法执行方法体中最后一条语句后,并经由执行return语句返回时)
2.因为一个没有捕获的异常终止了run方法而意外死亡
没有一个状态叫做中断,中断方法interrupt只是用来申请,当调用该方法时,线程的中断状态被置位。每个线程都应该不时地检查这个标志,以判断线程是否被中断。
如果线程被阻塞,就无法检测中断状态。当一个被阻塞的线程(调用sleep或者wait)上调用interrupt方法时,阻塞调用将会被InterruptedException异常中断。被中断的线程可以决定如何响应中断。
如果在中断状态被置位时调用sleep方法,它不会休眠,相反,它将清除这一状态并抛出InterruptedException。因此如果你的循环调用sleep,不会检测中断状态,应捕获InterruptException异常
线程的属性
•优先级:每一个线程有一个优先级,默认情况下,一个线程继承他的父线程的优先级。每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。Yield 方法导致当前执行线程处于让步状态,如果有其它的可运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调用。
•守护线程:setDaemon(true)可将线程转换为守护线程。守护线程应该永远不去访问固定资源,如文件,数据库,因为它会在任何时刻甚至在一个操作的中间发生中断。
同步
•锁对象ReentrantLock
◦结构: myLock.lock();
try {}
finally{ myLock.unlock(); }
….
Lock myLock= new ReentrantLock();
◦这一结构确保任何时刻,只有一个线程进入临界区,一旦一个线程封锁锁对象,其他任何线程都无法通过lock语句,当其他线程调用locka时,它们被阻塞,直到第一个线程释放锁对象。
◦锁是可重入的,因为线程可以重复地获得已经持有的锁,锁保持一个持有计数来跟踪对lock方法的嵌套调用,线程在每一次调用lock都要调用unlock来释放锁。
•条件对象
◦结构:myLock.lock();
try{
while(! Ok to proceed)
{ myCondition.await(); } //当前线程被阻塞,并放弃了锁,进入该条件的等待集
do things
myCondition.signalAll(); //解除因为这一条件而等待的所有线程,这些线程竞争后,被激活的线程从阻塞点开始执行程序
}
finally{ myLock.unlock(); }
….
Lock myLock= new ReentrantLock();
Condition myCondition =myLock.newCondition();
◦一个锁对象可以有一个或多个相关的条件对象。
•synchronized关键字
◦结构:public synchronized void method()
{
while(! Ok to procee)
wait();
do things
notifyAll();
}
◦Java中的每一个对象都一个内部锁,如果一个方法用synchronized 声明,那么对象的锁将保护整个方法,也就是说,要调用该方法,线程必须获得内部的对象锁。
◦内部对象锁只有一个相关条件,wait方法放置一个线程到等待集中,nogifyAll方法解除等待线程的阻塞状态
相当于condition中的await和signalAll
◦将静态方法声明为synchronized 也是合法的,如果调用这种方法,该方法获得相关类对象的内部锁,因此没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。
•同步阻塞
◦结构:synchronized(obj)
{ 。。。
}
Object obj = new Object();
•Volatile域
◦结构: public boolean isDone(){return done;}
public void setDone(){done = true;}
private volatile boolean done;
◦volatile关键字为实例域的同步访问提供了一种免锁机制,如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
◦volatile变量不能提供原子性 例如:
public void flipdone() { done = !done ;} //not atomic 不能确保改变域中的值
◦在这样一种非常简单的情况下,可以使用java.util.concurrent.atomic中提供的包装器类,用于原子的整数,浮点数,数组等。该类有get和set方法,确保是原子的。该实现使用有效的机器指令,在不使用锁的情况下确保原子性。
•锁测试与超时
◦tryLock试图申请一个锁,在成功获得锁后返回true,否则立即返回false,而且线程可以立即离开做其他事情。
◦调用tryLock时可使用超时参数。lock方法不能被中断,如果出现死锁,lock方法将无法终止。然而如果调用带超时参数的tryLock,那么如果线程在等待期间被中断,将抛出InterruptedException,将允许打破死锁。
◦lockInterruptlibly相当于一个超时设为无限的tryLock方法
◦在等待一个条件await时,也可以设置超时
•读/写锁ReentrantReadWriteLock
◦结构: private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock ();
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
◦readLock:得到一个可以被多个读操作共用的读锁,但会排斥所有写操作
◦writeLock:得到一个写锁,排斥所有其他所有读操作和写操作。
阻塞队列
•对于许多线程问题,可以通过使用一个或多个队列以优雅的且安全的方式将其形式化。生产者线程向队列插入元素,消费者线程则取出它们。使用队列,可以安全地从一个线程向另一个线程传递数据。在协调多个线程之间的合作时,阻塞队列是一个有用的工具。
•阻塞队列的方法分为3类:
1.put和take:当试图向满的队列中体添加,或从空的队列中移出元素时,导致线程阻塞。用于将队列当做线程的管理工具来使用。
2.add,remove和element,当试图向满的队列中体添加,或从空的队列中移出元素时抛出异常。
3.offer,poll和peek,当试图向满的队列中体添加,或从空的队列中移出元素时如果不能完成任务,只是给出一个错误提示(返回null),而不会抛出异常。
•阻塞队列的几个变种:
1.LinkedBlockingQueue : 没有上边界
2.ArrayBlockingQueue :构造时需要指定容量
3.priorityBlockingQueue:是一个带优先级的队列,而不是先进先出队列
4.DelayQueue:包含实现Delayed接口的对象
线程安全的集合
•阻塞队列也是线程安全的集合。
•其它集合:
1.ConcurrentHashMap : 有相应的方法用于原子性的关联插入以及关联删除 (散列映像表)
2.ConcurrentSkipListSet : 有序映像表
3.ConcurrentLinkedQueue : 无边界非阻塞队列
4.ConcurrentSkipListMap: 有相应的方法用于原子性的关联插入以及关联删除
•CopyOnWriteArrayList 和 copyOnWriteArraySet 是线程安全的集合。其中所有的修改线程对底层数组进行复制。如果在集合上进行迭代的线程超过修改的线程数,这样的安排是很有用的。当构建一个迭代器的时候,它包含一个对当前数组的引用,如果数组后来被修改了,迭代器仍然引用旧数组。
•Vector和HashTable是线程安全的,但后被弃用,取而代之的是ArrayList和HashMap,它们不是线程安全的。任何集合类通过使用同步包装器变成线程安全的。
List synchArrayList = Collections.synchronizedList(new ArrayList());
Map SynchHashMap = Collections.synchronizedMap(new HashMap());
结果集合的方法使用锁加以保护,提供了线程的安全访问。
如果在另一个线程可能进行修改时,要对集合进行迭代,仍需要使用同步阻塞,因为如果迭代过程中,别的线程修改该集合,迭代器会失效,抛出ConcurrentModificationException。最好使用java.utile.concurrent中定义的集合,不使用同步包装器,有一个例外经常被修改的数组列表,在那种情况下,同步的arrayList可以胜过copyOnWriteArrayList.
执行器
构建一个新的线程是有一定代价的,因为涉及与操作系统的交互。如果程序中创建了大量的生命周期很短的线程,应该使用线程池。一个线程池中包含许多准备运行的空闲线程,将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
•执行器(Executor)类有许多静态工厂方法用来构建线程池:
1.newCachedThreadPool :对于每个任务,如果有空闲线程可用,立即让它执行任务,如果没有则创建一个新线程。
2.newFixedThreadPool :构建一个具有固定大小的线程池,如果提交的任务数多于空闲的线程数,那么把得不到服务的任务放置到队列中。
3.newSingleThreadExecutor:是一个退化了的大小为一个线程池,由一个线程执行提交的任务,一个接着一个。
4.3个方法的返回值都是实现了ExecutorService接口的ThreadPoolExecutor类的对象。
5.传递Runnable或者Callable对象的方法:
i.Future submit(Runnable task)
ii.Future submit(Runnable task,T result)
iii.Future submit(Callable task)
6.调用shutdown方法启动线程池的关闭序列,被关闭的执行器不再接受新的任务,当所有的任务都完成之后,线程池中的线程死亡。shutdownNow取消尚未开始的所有任务并试图中断正在运行的线程
•预订执行(ScheduledExecrtorService)接口具有为预订执行或重复执行任务而设计的方法,它是一种允许使用线程池机制的java.utile.timer的泛化。
1.newScheduledthreadPool
2.newSingleThreadScheduledExecutor
3.二者可以预订Runnable或Callable在初始的延迟之后之运行一次,也可以预订一个Runnable对象的周期性地运行
•控制任务组
1.invokeAny方法提交所有对象到一个Callable对象的集合中,并返回某个已经完成了的任务的结果,但是无法知道返回的究竟是哪个任务的结果(适用于多个线程处理,其中一个完成,则停止所有的情况,如搜索)
2.invokeAll方法提交所有对象到一个Callable对象的集合中,并返回一个future对象的列表,代表所有任务的解决方案。
3.ExecutorCompletionService 对结果按照可获得的顺序保存起来,然后进行处理
ExecutorCompletionService service = new ExecutorCompletionService (executor);
for(Callalbe task :tasks ) service.submit(task);
for(int i=0;i<task.size();i++)
{
processFuture(service.take().get());
}
MFC多线程编程的几点经验
1) 不要在线程函数体内操作MFC控件,不要再线程里面调用UpdateData函数更新用户界面,而应该尽量采用发送消息的方式,在主线程的消息响应函数中操作控件;
2)不建议采用SendMessage往主线程发送消息,因为它是同步的,阻塞的,可以考虑采用PostMessage代替;
3)线程退出时,尽量不要使用TerminateThread函数,而尽可能的让线程自己退出;
4) 当线程退出时,必须先等待工作者线程退出,主线程才退出,但是在主线程里面不要使用WaitForSingleObject或WaitForMultiObjects等待线程结束,因为它可能造成死锁,当主线程使用这两个函数时,主线程就挂起了,尤其在第 (1), (2) 种情况下,工作者线程还在调用主线程里面的资源,这样造成死锁;
5) 为了防止退出死锁的发生,尽量使用MsgWaitForMultipleObjects函数,因为该函数等待时,可以等待线程句柄 有信号,而且还可以等待消息,不会造成死锁;