线程生命周期图
一、基础概念
1.进程
- 进程就是在某种程度上相互隔离的、独立运行的程序
- 进程,通俗的讲是一个具有独立功能的程序的一次运行活动。相比于进程而言,程序是静态的,如放在磁盘上的程序;而进程是动态的,必须是运行起来的
- 进程是程序在处理机中的一次运行。一个进程既包括其所要执行的指令,也包括了执行指令所需的系统资源,不同进程所占用的系统资源相对独立。所以进程是重量搜索级的任务,它们之间的通信和转换都需要操作系统付出较大的开销
- 多进程: 在操作系统中能同时运行多个任务(程序)
2.线程
- 线程是进程中的一个实体,是被系统独立调度和分派的基本单位
- 线程自己基本上不拥有系统资源,但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。所以线程是轻量级的任务,它们之间的通信和转换只需要较小的系统开销
- 多线程: 在同一应用程序中有多个功能流同时执行
用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现
3.Java支持多线程编程,因此用Java编写的应用程序可以同时执行多个任务。Java的多线程机制使用起来非常方便,用户只需关注程序细节的实现,而不用担心后台的多任务系统
4.Java语言里,线程表现为线程类。Thread线程类封装了所有需要的线程操作控制。在设计程序时,必须很清晰地区分开线程对象和运行线程,可以将线程对象看作是运行线程的控制面板
5. 在线程对象里有很多方法来控制一个线程是否运行,睡眠,挂起或停止。线程类是控制线程行为的唯一的手段搜索。一旦一个Java程序启动后,就已经有一个线程在运行。可通过调用Thread.currentThread方法来查看当前运行的是哪一个线程
6. 每个 Java 程序都至少有一个线程 ——主线程。当一个 Java 程序启动时,JVM 会创建主线程,并在该线程中调用程序的 main() 方法。其它线程都是通过 Thread 构造器或实例化继承类 Thread 的类来创建的
7. 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力
二、线程的生命周期
1.新建状态(创建——>就绪)
1)概述
- (1)用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态
- (2)处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)
2)创建线程的方式
- (1)继承Thread类,重写run()方法,然后直接new这个对象的实例,创建一个线程的实例。然后调用start()方法启动线程
- (2)实现Runnable接口,重写run()方法,然后调用new Thread(runnable)的方式创建一个线程,然后调用start()方法启动线程
- (3)实现Runnable接口的优势
a.线程类只是实现了Runnable接口,还可以继承其他类
b.在这种方式下,可以多个线程共享一个Runnable target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想
- 注意事项 —— 能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常
2.就绪状态(就绪——运行)
1)概述
- (1)处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU
- (2)等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”
- (3)一旦获得CPU,线程就进入运行状态并自动调用自己的run方法
2)注意事项
- (1)如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程
3.运行状态
1)概述
- (1)处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态
- (2)处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务
- (3)如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源
- (4)也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态(运行——>就绪)
- (5)当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态(运行——>死亡)
2)线程会从运行状态变为阻塞状态的情况(运行——>阻塞)
- (1)线程调用sleep方法主动放弃所占用的系统资源
- (2)线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
- (3)线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
- (4)线程在等待某个通知(notify)
- (5)程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法
4.阻塞状态(阻塞——就绪)
1)概述
- (1)处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态
- (2)在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行
2)阻塞分类
- (1)等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态
- (2)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态
- (3)其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态
5.死亡状态
1)概述
- (1)当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程
- (2)线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常
三、线程常用方法
1.sleep(运行——>阻塞,达到时间以后阻塞——>就绪, 静态方法Thread.sleep(毫秒))
- 1) sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效(即就用Thread.sleep(毫秒);)
- 2) 使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒
2.yield(运行——>就绪, 静态方法Thread.yield())
- 1)yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程
- 2)和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态
- 3)yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行
- 4)实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程
3)sleep()和yield()的区别
- sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态
- 2)sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常
- 3)sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行
4)join(不是静态方法,通过线程对象调用)
1)线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时
2)重载形式
- void join() 当前线程等该加入该线程后面,等待该线程终止
- void join(long millis) 当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
- void join(long millis,int nanos) 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
四、线程旁支点
1.线程优先级
- 1)每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会
- 2)与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行
- 3)每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级
- 4)Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1到10之间
- 5)也可以使用Thread类提供的三个静态常量
a. MAX_PRIORITY =10
b. MIN_PRIORITY =1
c. NORM_PRIORITY =5
- 6) 不同的操作系统的优先级并不相同,而且也不能很好的和Java的10个优先级别对应。所以我们应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常量来设定优先级,这样才能保证程序最好的可移植性
2.守护线程
1)守护线程与普通线程写法上基本没啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程
2)守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程
3)public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出(即在普通线程都结束后)。该方法必须在启动线程前调用
4)守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。Java的垃圾回收也是一个守护线程
3.线程池
(1)系统启动一个新线程的成本是比较高的,因为它涉及到与操作系统的交互
(2)在这种情况下,使用线程池可以很好的提供性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池
(3)与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法
(4)除此之外,使用线程池可以有效地控制系统中并发线程的数量,但系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃
(5)而线程池的最大线程数参数可以控制系统中并发的线程不超过此数目
(6)在JDK1.5之前,开发者必须手动的实现自己的线程池,从JDK1.5之后,Java内建支持线程池
(7)与多线程并发的所有支持的类都在java.lang.concurrent包中。我们可以使用里面的类更加的控制多线程的执行
五、线程的同步
1.概述
1)Java中每个对象都有一个内置同步锁(监视器)。Java中可以使用synchronized关键字来取得一个对象的同步锁
2)一个对象只有一个锁,所以其他任何线程都不能访问该对象的所有由synchronized包括的代码段,直到该线程释放掉这个对象的同步锁(释放锁是指持锁线程退出了synchronized同步方法或代码块)
3)在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用
2.synchronized关键字注意事项
1)只能同步方法和代码块,而不能同步变量和类。只要保护好类中数据的安全访问和设置就可以了,不需要对类使用synchronized关键字,所以Java不允许这么做。并且想要同步数据,只需要对成员变量私有化,然后同步方法即可,不需要对成员变量使用synchronized,java也禁止这么做
2)每个对象只有一个同步锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?上面的代码中run方法使用synchronized (account)代码块,因为两个线程访问的都是同一个Account对象,所以能够锁定。但是如果是其他的一个无关的对象,就没用了。比如说synchronized (new Date())代码块,一样没有效果
3)不必同步类中所有的方法,类可以同时拥有同步和非同步方法
4)如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法
5)如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制
6)线程睡眠时,它所持的任何同步锁都不会释放(这也是为什么会死锁的一个原因吧)
7)线程可以获得多个同步锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步同步锁
8)同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块
9)编写线程安全的代码会使系统的总体效率会降低,要适量使用
10)当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块
11)虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承
12)如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以
13)在定义接口方法时不能使用synchronized关键字
14)构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步
3. synchronized修饰
1)修饰在代码块上(synchronized(){ })
a.当前对象(synchronized(this){ })——锁对象为当前对象,但是要注意是否是同一个对象,比如继承Thread是用的不同的的对象,实现runnable用的是 同一个对象(一般)
b.共享对象(synchronized(account){ })——锁对象为共享对象,比如account是线程共享的一个对象,可以是任意共享的对象
c.类名(synchronized(MyThread.class){ })——它取得的锁是对类,该类所有的对象同一把锁
Tips:还有一种Lock对象加锁的方式
2)修饰在方法上(public synchronized void method())
a.普通方法(public synchronized void method())——锁定的是调用该方法的对象,类似于(1)的当前对象(this)
b.静态方法(public synchronized static void method())—— 我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象
4.一个线程取得了同步锁,那么在什么时候才会释放掉呢?
1)同步方法或代码块正常结束
2)使用return或 break终止了执行,或者跑出了未处理的异常
3)当线程执行同步方法或代码块时,程序执行了同步锁对象的wait()方法
5.死锁(死锁例子:http://www.cnblogs.com/baizhanshi/p/5437933.html)
1)概述
- (1)线程死锁时,第一个线程等待第二个线程释放资源,而同时第二个线程又在等待第一个线程释放资源
- (2)是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去
- (3)系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
2)死锁产生的必要条件
- (1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放
- (2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放
- (3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放
- (4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源
3)处理死锁的方法
- (1)预防死锁:方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁
- (2)避免死锁:该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁
- (3)检测死锁:通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,然后采取适当措施,从系统中将已发生的死锁清除掉
- (4)解除死锁:当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行
6.注意事项
1)线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。这可真是个无聊的绕口令
2)“共享”这两个字。只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要
3)只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。
4)多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。
六、线程间的通信
1.概述
- 1)线程之间需要一些协调通信,来共同完成一件任务。关于线程的协调运行,经典的例子就是生产者和消费者的问题
2.常用方法()
1)wait()
- (1) wait()方法使得当前线程必须要等待,等到另外一个线程调用notify()或者notifyAll()方法
- (2) 当前的线程必须拥有当前对象的monitor,也即lock,就是锁
- (3) 线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行
- (4) 要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中
- (5) 当线程调用了wait()方法时,它会释放掉对象的锁(而Thread.sleep()是不会释放的)
- (6)当wait线程唤醒后并执行时,是接着上次执行到的wait()方法代码后面继续往下执行的
2)notify()
- (1)notify()方法会唤醒一个等待当前对象的锁的线程
- (2)如果多个线程在等待,它们中的一个将会选择被唤醒。这种选择是随意的,和具体实现有关。(线程等待一个对象的锁是由于调用了wait方法中的一个)
- (3)被唤醒的线程是不能被执行的,需要等到当前线程放弃这个对象的锁(先唤醒再说,我代码还没执行完,你唤醒了也拿不到锁啊)
- (4)被唤醒的线程将和其他线程以通常的方式进行竞争,来获得对象的锁。也就是说,被唤醒的线程并没有什么优先权,也没有什么劣势,对象的下一个线程还是需要通过一般性的竞争
- notify()方法应该是被拥有对象的锁的线程所调用(换句话说,和wait()方法一样,notify方法调用必须放在synchronized方法或synchronized块中)
Tips: wait()和notify()方法要求在调用时线程已经获得了对象的锁,因此对这两个方法的调用需要放在synchronized方法或synchronized块中
3) notifyAll()
- (1) 唤醒在此同步监视器(锁)上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程
参考网址
1.Java多线程总结(一)多线程基础
2.Java 线程简介
3.Java多线程学习(吐血超详细总结)
4.Java总结篇系列:Java多线程(一)
5.Java中的多线程你只要看这一篇就够了
6.网络IO之阻塞、非阻塞、同步、异步总结
7.深入理解进程和线程
8.Java 多线程(一) 基础知识与概念
9.Java中Synchronized的用法
10.什么是死锁?如何避免死锁?
注:文章是经过参考其他的文章然后自己整理出来的,有可能是小部分参考,也有可能是大部分参考,但绝对不是直接转载,觉得侵权了我会删,我只是把这个用于自己的笔记,顺便整理下知识的同时,能帮到一部分人。
ps : 有错误的还望各位大佬指正,小弟不胜感激