多线程编程学习总结

线程的概念和原理

为何使用多线程?html

为了更高效的完成任务和利用CPU资源,如今的操做系统设计为多任务操做系统,而多进程和多线程是实现多任务的方式。java

什么是进程和线程?编程

进程是指一个内存中运行的应用程序,每一个进程都有本身独立的一块内存空间,一个进程中能够启动多个线程。进程是OS分配资源的最小单位。 线程是指进程中的一个执行流程,一个进程中能够运行多个线程。线程老是属于某个进程,进程中的多个线程共享进程的内存。进程是OS调度的最小单位。安全

工做原理?多线程

  1. 多线程是这样一种机制,它容许在程序中并发执行多个指令流,每一个指令流都称为一个线程,彼此间互相独立。线程又称为轻量级进程,它和进程同样拥有独立的执行控制,由操做系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其它线程共享一个存储空间,这使得线程间的通讯远较进程简单。并发

  2. 具体到java内存模型,因为Java被设计为跨平台的语言,在内存管理上,显然也要有一个统一的模型。系统存在一个主内存(Main Memory), Java中全部变量都储存在主存中,对于全部线程都是共享的。每条线程都有本身的工做内存(Working Memory)——调用栈,工做内存中保存的是主存中某些变量的拷贝,线程对全部变量的操做都是在工做内存中进行,线程之间没法相互直接访问,变量传递均须要经过主存完成。函数

  3. 多个线程的执行是并发的,也就是在逻辑上“同时”,而无论是不是物理上的“同时”。若是系统只有一个CPU,那么真正的“同时”是不可能的。多线程和传统的单线程在程序设计上最大的区别在于,因为各个线程的控制流彼此独立,使得各个线程之间的代码是乱序执行的,将会带来线程调度,同步等问题。性能

 

线程状态转换

线程的状态转换是线程控制的基础。线程状态总的可分为五大状态:分别是新建、就绪、运行、等待/阻塞、死亡。用一个图来描述以下: 学习

一、新建状态:线程对象已经建立,尚未在其上调用start()方法。测试

二、就绪状态:当线程有资格运行,但调度程序尚未把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行以后或者从阻塞、等待或睡眠状态回来后,也返回到就绪状态。

三、运行状态:线程调度程序从就绪线程池中选择一个线程做为当前线程时线程所处的状态。这也是线程进入运行状态的惟一一种方式。

四、等待/阻塞/睡眠状态:线程不会被分配 CPU 时间,没法执行;可能阻塞于I/O,或者阻塞于同步锁。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,可是当前没有条件运行。换句话说,它是可运行的,可是若是某件事件出现,他可能返回到可运行状态。

五、死亡态:当线程的run()方法完成时就认为它死去,调用 stop()或 destroy() 亦有一样效果,可是不被推荐,前者会产生异常,后者是强制终止,不会释放锁。这个线程对象也许是活的,可是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 若是在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

 

锁机制

线程锁机制的本质是解决线程通讯中的互斥问题。 因为咱们能够经过 private 关键字来保证数据对象只能被方法访问,因此咱们只需针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。注意:每一个类实例对应一把锁,同步和互斥等都是相对多线程而言的

synchronized 方法

经过在方法声明中加入 synchronized关键字来声明 synchronized 方法,语法以下:

public synchronized void procData();

synchronized 方法原理:多个线程访问同一个 synchronized 方法时,必须得到调用该方法的类实例的锁才能执行,不然所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能得到该锁,从新进入可执行状态。这种机制确保了同一时刻对于每个类实例,其全部声明为 synchronized 的成员函数中至多只有一个处于可执行状态(由于至多只有一个可以得到该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要全部可能访问类成员变量的方法均被声明为 synchronized)。

在 Java 中,不光是类实例,每个类也对应一把锁,这样咱们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。

synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,因为在线程的整个生命期内它一直在运行,所以将致使它对本类任何 synchronized 方法的调用都永远不会成功。固然咱们能够经过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,可是 Java 为咱们提供了更好的解决办法,那就是 synchronized 块。

synchronized 块

经过 synchronized关键字来声明synchronized 块,语法以下:

(syncObject) {
    }

synchronized 块是这样一个代码块,其中的代码必须得到对象 syncObject (如前所述,能够是类实例或类)的锁方能执行,具体机制同前所述。因为能够针对任意代码块,且可任意指定上锁的对象,故灵活性较高。  


阻塞机制

阻塞机制的本质是为了解决线程通讯的同步问题。锁和阻塞机制解决线程通讯中的互斥和同步问题。

为了解决对共享存储区的访问冲突,引入了锁机制,考察多个线程对共享资源的访问,显然锁机制已经不够了,由于在任意时刻所要求的资源不必定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。为了解决这种状况下的访问控制问题,引入了对阻塞机制的支持。

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。Java 提供了大量方法来支持阻塞,下面让咱们逐一分析。

  1. sleep()方法:sleep()容许指定以毫秒为单位的一段时间做为参数,它使得线程在指定的时间内进入阻塞状态,不能获得CPU 时间,指定的时间一过,线程从新进入可执行状态。典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不知足后,让线程阻塞一段时间后从新测试,直到条件知足为止。

  2. suspend()和resume()方法:两个方法配套使用,suspend()使得线程进入阻塞状态,而且不会自动恢复,必须其对应的resume() 被调用,才能使得线程从新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另外一个线程产生的结果的情形:测试发现结果尚未产生后,让线程阻塞,另外一个线程产生告终果后,调用resume()使其恢复。

  1. yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,可是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另外一个线程。

  2. wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种容许指定以毫秒为单位的一段时间做为参数,另外一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程从新进入可执行状态,后者则必须对应的 notify() 被调用。

 

阻塞方法比较

  1. 2和4区别的核心在于,前面叙述的全部方法,阻塞时都不会释放占用的锁(若是占用了的话),而这一对方法则相反。上述的核心区别致使了一系列的细节上的区别。

  2. 首先,前面叙述的全部方法都隶属于Thread 类,可是这一对却直接隶属于 Object 类,也就是说,全部对象都拥有这一对方法。由于这一对方法阻塞时要释放占用的锁,而锁是任何对象都具备的,调用任意对象的 wait() 方法致使线程阻塞,而且该对象上的锁被释放。而调用任意对象的notify()方法则致使因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到得到锁后才真正可执行)。

  3. 其次,前面叙述的全部方法均可在任何位置调用,可是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁能够释放。一样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁能够释放。所以,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不知足这一条件,则程序虽然仍能编译,但在运行时会出现 IllegalMonitorStateException 异常。

 

wait() 和 notify() 方法的上述特性决定了它们常常和synchronized 方法或块一块儿使用,将它们和操做系统的进程间通讯机制做一个比较就会发现它们的类似性:synchronized方法或块提供了相似于操做系统原语的功能,它们的结合用于解决各类复杂的线程间通讯问题。

 

关于 wait() 和 notify() 方法

  1. 调用 notify() 方法致使解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,咱们没法预料哪个线程将会被选择,因此编程时要特别当心,避免因这种不肯定性而产生问题。

  2. 除了 notify(),还有一个方法 notifyAll() 也可起到相似做用,惟一的区别在于,调用 notifyAll() 方法将把因调用该对象的wait()方法而阻塞的全部线程一次性所有解除阻塞。固然,只有得到锁的那一个线程才能进入可执行状态。

谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 wait() 方法的调用均可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁的避免,咱们在编程中必须当心地避免死锁。

以上咱们对 Java 中实现线程阻塞的各类方法做了一番分析,咱们重点分析了 wait() 和 notify() 方法,由于它们的功能最强大,使用也最灵活,可是这也致使了它们的效率较低,较容易出错。实际使用中咱们应该灵活使用各类方法,以便更好地达到咱们的目的。

 

关于join()方法

join()方法可用于让当前线程阻塞,以等待特定线程(调用join的线程)的消亡。不容许线程对象在本身的可执行体中调用本身线程的join。  


线程优先级

线程的优先级表明该线程的重要程度,当有多个线程同时处于可执行状态并等待得到 CPU 时间时,线程调度系统根据各个线程的优先级来决定给谁分配 CPU 时间,优先级高的线程有更大的机会得到 CPU 时间,优先级低的线程也不是没有机会,只是机会要小一些罢了。

你能够调用 Thread 类的方法 getPriority() 和 setPriority()来存取线程的优先级,线程的优先级界于1(MIN_PRIORITY)和10(MAX_PRIORITY)之间,缺省是5(NORM_PRIORITY)。  


守护线程与用户线程

线程能够分为用户线程(User)和守护线程(Daemon): 守护线程是一类特殊的线程,它和普通线程的区别在于它并非应用程序的核心部分,当一个应用程序的全部非守护线程终止运行时,即便仍然有守护线程在运行,应用程序也将终止,反之,只要有一个非守护线程在运行,应用程序就不会终止。守护线程通常被用于在后台为其它线程提供服务。 能够经过调用方法 isDaemon() 来判断一个线程是不是守护线程,也能够调用方法 setDaemon() 来将一个线程设为守护线程。

我的对Daemon线程更直观的理解是:不管User仍是Daemon线程,都具备可执行序列,拥有本身的工做栈,区别在于:Daemon线程会随着其父线程结束而结束,它不属于程序本体。另外一层意思,父线程的结束取决于其全部子User线程,而与daemon线程无关。它们之间的不一样决定了它们用于不一样的场景,守护线程通常为其余线程提供服务,如垃圾回收器。

须要注意的是setDaemon()方法必须在线程对象没有调用start()方法以前调用,不然没效果。  


线程组机制

  1. 线程组是一个 Java 特有的概念,在 Java 中,线程组是类ThreadGroup 的对象,每一个线程都隶属于惟一一个线程组,这个线程组在线程建立时指定并在线程的整个生命期内都不能更改。你能够经过调用包含 ThreadGroup 类型参数的 Thread 类构造函数来指定线程属的线程组,若没有指定,则线程缺省地隶属于名为 system 的系统线程组。

  2. 在 Java 中,除了预建的系统线程组外,全部线程组都必须显式建立。在 Java 中,除系统线程组外的每一个线程组又隶属于另外一个线程组,你能够在建立线程组时指定其所隶属的线程组,若没有指定,则缺省地隶属于系统线程组。这样,全部线程组组成了一棵以系统线程组为根的树。

  3. Java 容许咱们对一个线程组中的全部线程同时进行操做,好比咱们能够经过调用线程组的相应方法来设置其中全部线程的优先级,也能够启动或阻塞其中的全部线程。

  4. Java 的线程组机制的另外一个重要做用是线程安全。线程组机制容许咱们经过分组来区分有不一样安全特性的线程,对不一样组的线程进行不一样的处理,还能够经过线程组的分层结构来支持不对等安全措施的采用。Java 的 ThreadGroup 类提供了大量的方法来方便咱们对线程组树中的每个线程组以及线程组中的每个线程进行操做。

 

ThreadLocal

java.lang.ThreadLocal是local variable(线程局部变量)。它为每个使用该变量的线程都提供一个变量值的副本,使每个线程均可以独立地改变本身的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每个线程都彻底拥有该变量。ThreadLocal本质是一个线程安全的hashMap,key为threadName,Value为线程内的变量。

  • Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离

  • ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题。ThreadLocal为每一个线程的中并发访问的数据对象提供一个副本,经过访问副原本运行业务,这样的结果是耗费了内存,但大大减小了线程同步所带来性能消耗,也减小了线程并发控制的复杂度。

  • ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。

  • ThreadLocal和Synchonized都用于解决多线程并发访问。可是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每个线程都提供了变量的副本,使得每一个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通讯时可以得到数据共享。

  • 固然ThreadLocal并不能替代synchronized,它们处理不一样的问题域。Synchronized用于实现锁机制,比ThreadLocal更加复杂。

 

小结

  • 咱们一块儿学习了 Java 多线程编程的方方面面,包括建立线程,以及对多个线程进行调度、管理。咱们深入认识到了多线程编程的复杂性,以及线程切换开销带来的多线程程序的低效性,这也促使咱们认真地思考一个问题:咱们是否须要多线程?什么时候须要多线程?

  • 多线程的核心在于多个代码块并发执行,本质特色在于各代码块之间的代码是乱序执行的。咱们的程序是否须要多线程,就是要看这是否也是它的内在特色。

  • 假如咱们的程序根本不要求多个代码块并发执行,那天然不须要使用多线程;假如咱们的程序虽然要求多个代码块并发执行,可是却不要求乱序,则咱们彻底能够用一个循环来简单高效地实现,也不须要使用多线程;只有当它彻底符合多线程的特色时,多线程机制对线程间通讯和线程管理的强大支持才能有用武之地,这时使用多线程才是值得的。

 

参考资料: http://programming.iteye.com/blog/158568 http://lavasoft.blog.51cto.com/62575/51926

http://www.cnblogs.com/oubo/archive/2012/01/05/2394637.html

相关文章
相关标签/搜索