Java多线程

Java多线程

多线程

多线程指的是程序(一个进程)运行时产生了不止一个线程

多线程是实现并发机制的一种有效手段。所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程。

进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。

1.线程概述

单线程编程就是只有一条顺序执行流——程序从main方法开始执行,依次向下执行每行代码,如果某行代码出现了阻塞,则程序将会停滞在该处。

单线程的程序只有一个顺序执行流,多线程的程序则可以包括多个顺序执行流,多个顺序流之间互不干扰。

1.1进程与线程

进程:当一个程序进入内存时就变成了一个进程。进程就是处于运行过程中的程序,并且具有一定的独立能力,进程是系统进行资源分配和调度的一个独立单位。

关系:线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个所属进程。线程的调度和管理是由进程本身负责完成。

线程:进程可进一步细化为线程,是一个程序内部的一条执行路径

1.2并行,并发

并行:多个CPU同时执行多个任务,比如:多个人同时做不同的事 并发:一个CPU(采用时间片)同时执行多个任务,比如秒杀平台,多个人做同件事

1.3线程生命周期

在线程的生命周期中,它要经历新建、就绪、运行、阻塞和死亡5种状态。

1.3.1新建与就绪状态

当程序使用new关键字创建一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样仅仅由Java虚拟机为其分配内存,并初始化其他成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

当线程对象调用了start方法之后,该线程就处于就绪状态,Java虚拟机会为这个线程对象分配可执行时间片段,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于什么时候开始运行,则取决于JVM里的线程调度器的调度。

注意:启动线程使用start()方法,而不是run()方法。调用线程的start方法来启动线程,系统则会把该run方法当成线程执行体来处理。

1.3.2运行与阻塞状态

1.如果处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,则该线程就处于运行状态

(如果计算机只有一个CPU,那么在任何时候只有一个线程处于运行状态。当然,在一个多处理器的机器上,将会有多个线程并行执行,当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。)

2.当一个线程开始运行后,它不可能一直处于运行状态,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略

3.线程发生阻塞状态时的情况:

(1)线程调用sleep()方法主动放弃所占用的处理器资源。

(2)线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。

(3)线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。关于同步监视器的知识,后面会进行讲解。

(4)线程在等待某个通知(notify)

(5)程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法。

4.当前正在执行的线程被阻塞后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,合适的时候就是指线程的阻塞解除后,会重新进入就绪状态,等待线程调度器的再次调度。

5.在发生如下特定情况下,可以解除线程的阻塞状态,让线程重新进入就绪状态。

(1)调用sleep()方法的线程经过了指定时间。

(2)线程调用的阻塞式IO方法已经返回。

(3)线程成功地获得了试图取得的同步监视器。

(4)线程正在等待某个通知时,其他线程发出了一个通知。

(5)处于挂起的线程被调用了resume()恢复方法。

1.3.3.线程状态转换图

 

从上图中可以看出,线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,当处于就绪状态的线程获得处理器资源时,该线程就进入运行状态,当处于运行状态的线程失去处理器资源时,该线程就进入就绪状态。如果线程调用了yield()方法,即线程让步则线程状态进入就绪状态。

1.3.4线程死亡

线程会在此3种方法下进行结束,结束后就处于死亡状态。

(1)run()或call()方法执行完成,线程正常结束。

(2)线程抛出一个未捕获的Exception或者直接Error错误。

(3)直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。

注意:当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动之后,它就拥有和主线程相同的地位。

测试某个线程是否死亡,可以调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞3中状态时,该方法将返回true;当线程处于新建、死亡2种状态时返回false。

注意:不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,该线程不可以再次作为线程执行。

1.4线程的创建与启动

1.4.1通过继承Thread类来创建并启动多线程

1、定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run方法称为线程执行体。

2、创建Thread子类的实例,即创建了线程对象。

3、调用线程对象的start()方法来启动该线程。

1.4.2实现Runnable接口创建并启动多线程

1、定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体

2、创建Runnable实现类的实例对象,并以此实例对象作为Thread的target来创建Thread类,该Thread对象才是真正的线程对象。

3、调用Thread对象的start()方法来启动该线程。

Runnable对象仅仅作为Thread对象的target目标,Runnable实现类里包含run方法,仅仅作为线程执行体,而实际的线程对象依然是Thread实例对象,只是该Thread线程负责执行其target目标的run方法而已。

1.4.3 使用Callable和Future创建线程

Java 5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runable接口——可以作为Thread类的target

V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束才会得到返回值。

创建并启动有返回值的线程的步骤

1、 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且该call()方法有返回值。

2、 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。

3、 使用FutureTask对象作为Thread对象的target创建并启动新线程。

4、 调用FutureTask对象的get()方法来获得子线程结束后的返回值。

1.4.4 创建线程的三种方式对比

通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常,Callable需要FutureTask来进行封装成Thread可识别的target目标。因此可以将实现Runnable接口和实现Callable接口归纳为一种方式。这种方式与继承Thread方式之间的主要差别如下:

采用Runnable、Callable接口的方式创建多线程:

线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况, 形成清晰的模型,较好的体现了面向对象的思想。

劣势:编程稍微复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。

采用Thread类的方式创建多线程:

编写简单,如果需要访问当前线程,直接使用this即可。

通常情况下,建议大家采用实现Runnable接口、Callable接口的方式来创建多线程。

1.5 线程控制

1.5.1 线程休眠sleep

如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。

当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间内,该线程不会获得执行的机会,即使线程中没有其他可以执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行。

1.5.2 线程join

Thread提供了一个让线程等待另一个线程完成的方法——join()方法。当在某个线程执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完成为止。

Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。

join():等待被join的线程执行完成

1.5.3线程让步yield

yield()方法是和sleep()方法相似的方法,它也是Thread类提供的一个静态的方法,同样可以让当前线程暂停,但是不阻塞线程而是让当前线程进入就绪状态。让系统的线程调度器重新调度一次,完全有可能出现的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。

1.5.4 改变线程的优先级

每个线程执行都具有一定的优先级,优先级较高的线程会获得更多的执行机会,而优先级较低的线程则获得较少的执行机会。不要假定高优先级的线程一定先于低优先级的线程执行,不要有逻辑依赖于线程优先级,否则可能产生意外结果。

每个线程默认的优先级都与创建它的父线程优先级相同,在默认的情况下main线程具有普通优先级,由main线程创建的子线程也具有普通优先级。

Thread类提供了setPriority(int newPriority),getPriority()方法来设置和获取指定线程的优先级,其中setPriority方法的参数可以是一个整数,范围在1~10之间,10最高,1最低。可以使用Thread类的三个静态常量:MAX_PRIORITY:其值为10,MIN_PRIORITY:其值为1,NORM_PRIORITY:其值为5

1.6线程同步

线程同步可以理解为线程之间按照一定的顺序执行

1.6.1同步代码块

为了解决此类问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:

synchronized(obj){

//此处的代码块为同步代码块

}

上面语法格式中的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

虽然Java程序允许使用任何对象作为同步监视器,但是我们想一下同步监视器的目的是为了:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。

1.6.2同步方法

与同步代码块对应,Java的多线程安全支持,还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则此方法被称为同步方法。对于synchornized修饰的对象方法而言,无须显示的指定它的同步监视器,同步方法的同步监视器就是this,也就是调用该方法的对象。

1.6.3同步锁Lock

Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock可以实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。

Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

某些锁可能允许对共享资源的并发访问,例如ReadWriteLock读写锁就允许并发访问。Lock、ReadWriteLock是Java5提供的两个根接口,并为Lock提供了ReentrantLock实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。

在实现线程安全的控制中,常用的是ReentrantLock,使用该Lock可以显示的加锁、释放锁。

1.6.4死锁

当两个线程互相等待对方释放同步监视器时就会发生死锁,一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

1.7线程的通信

1.7.1传统线程的通信

当程序在系统中运行时,线程的调度具有一定的随机性,程序通常无法准确的控制线程的轮换执行,我们可以通过一些机制来保证线程协调运行。线程之间的协调运行就称为通信。

为实现此功能,可以借助Object类提供的wait()、notify()和notifyAll()3个方法,这3个方法必须由同步监视器对象来调用,同步监视器可以分以下两种情况:

Ø 使用synchronized修饰的同步方法,同步监视器就是当前实例对象,所以此方法由this直接调用。

Ø 使用synchronized修饰的同步代码块,同步监视器是synchronzied后括号里的对象,所以必须使用该对象调用这3个方法。

对此3个方法的解释如下:

Ø wait():导致当前线程进入等待,直到其他线程调用该同步监视器的notify()方法或者notifyAll()方法来唤醒该线程。该wait()方法有三种形式——无时间参数的wait,一直等待,直到其他线程的通知;带毫秒参数的wait和带毫秒、微秒参数的wait,这两种方法都是等待指定时间后自动苏醒。调用wait方法的当前线程会释放对该同步监视器的锁定。

Ø notify():唤醒此同步监视器上等待的单个线程。如果所有的线程都在此同步监视器上等待着,则会唤醒其中一个线程。选择是任意的。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

Ø notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。

1.7.2 使用Condition控制线程通信

如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()、notify()、notifyAll()方法进行线程通信了。

当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。

Condition将同步监视器方法(wait、notify、notifyAll)分解成截然不同的对象,以便通过将这些对象与Lock组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。

Condition实例被绑定在一个Lock对象上。要获得特定Lock对象的Condition实例,调用Lock对象的newCondition()方法即可。Condition类提供了如下三个方法。

Ø await():类似于隐式同步监视器上的wait()方法,导致当前线程等待,直到其他线程调用该Condition的singal()方法或singalAll()方法来唤醒该线程。该await()方法有更多的变体,如long awaitNanos(long nanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline)等,可以完成更丰富的等待操作。

Ø signal():唤醒在此Lock对象上等待的单个线程。如果多有的线程都在该Lock对象上等待,则会唤醒其中一个线程。选择是任意的,只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。

Ø signalAll():唤醒在此Lock对象上等待的所有线程,只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值