多线程

Posted xxMYxx

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程相关的知识,希望对你有一定的参考价值。

16 章 多线程

16.1.1 线程和进程

所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能。进程是系统进行资源分配和调度的一个独立单位

一般而言,进程包含如下3个特征

1.独立性 :进程是系统中独立存在的实体,它拥有独立的资源,每一个进程都拥有自己私有的地址空间,没有经过进程本身运行的情况下,一个用户进程不可以访问其他进程的地址空间

2.动态性 :进程与程序的区别在于,程序只是一个静态指令集合,而进程是一个正在系统中活动的指令集合,在进程中加入了时间概念

3.并发性 : 多个进程可以在单个处理器上并发执行,多个线程之间不会相互影响

 

并发 在同一时刻只能有一条指令得到执行,但多个进程指令块快速轮换执行

并行 指在多条指令在多个处理器上同步执行

线程在系统中时独立的,并发执行流,当进程被初始化后主线程就被创建了

 

线程可以用于组件的堆栈,自己的程序计数器和自己的局部变量,但不能用于系统资源,多个线程贡献父进程的全部资源

 

线程是独立的,不知道其他线程的存在,线程的执行是抢占式的

 

一个线程可以创建和撤销另一个线程

 

16.1.2 多线程的优势

多个线程之前共享的环境包括:进程代码块,进程的共有数据等

 

多线程的优势

1.进程之间不能共享内存,但线程之间共享内存非常容易

2.系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小很多,提高了效率

3.java语言内置了多线程功能的支持,而不是简单的底层操作系统调度,从而简化了多线程编程

 

16.2 线程的创建和启动

所有线程对象都必须是Thread类或子类的实例

16.2.1 继承Thread类创建线程类

通过继承Thread类创建线程类步骤如下

1.定义Thread类的子烈,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务(线程执行体)

2.创建Thread子类的实例,创建线程对象

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

 

使用Thread类方法来创建线程类时,多个线程之间无法共享线程类的实例变量

 

16.2.2 实现Runnable接口创建线程类

实现Runnable接口创建线程类步骤如下

1.定义Runnable接口的实现类,并重写该接口的run()方法

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

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

 

通过Thread获取当前线程对象比较简单,直接通过this就可以,但是通过Runnable接口获得当前线程对象,则必须使用Thread.currentThread()

 

区别

1. Thread创建的Thread子类即可代表线程对象 ,Runnable创建的对象只能作为线程对象的target

2. 采用Runnable创建的多个线程可以共享线程类的实例变量(因为在这种情况下程序创建的Runnable对象只是线程的target,而多个线程可以共享同一个线程的实例变量)

 

实现Runnable接口相对于继承Thread类来说,有如下显著的好处: 

(1)适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码,数据有效的分离,较好地体现了面向对象的设计思想。 

(2)可以避免由于Java的单继承特性带来的局限。。 

(3)有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码来自同一个类的实例时,即称它们共享相同的代码。多个线程操作相同的数据,与它们的代码无关。当共享访问相同的对象是,即它们共享相同的数据。当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runnable接口的类的实例。

 

通过实现Runnable接口创建多线程时,Thread类的作用就是把run()方法包装成线程执行体,该Thread对象才是正真的线程对象

16.2.3 使用Callable Future 创建线程

java5 开始java提供了Callable接口,该接口类似Runnable接口的增强版 Callable接口提供了一个call()方法可以作为线程执行体,call()比run()方法更强大

1.call()可以有返回值

2.call()方法可以声明抛出异常

 

因此完全可以提供一个Callable对象作为Threadtarget,线程的执行体就是该Callable接口里的call()方法,但是Callable接口并不是Runnable接口的子接口,所以Callable接口不能直接做为Threadtarget。而且call()方法还有一个返回值

为此java5 提供了Future 接口来代表Callable接口里call()方法里的返回值,Future接口提供了FutureTask实现类,该接口实现了Future接口,并实现了Runnable接口,可以作为Thread类的target

 

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

1.创建Callable接口的实现类,并实现call()方法,在创建Callable实现类的实例

2.使用FutureTask类来包装Callable对象

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

4.调用FutureTask对象的get方法来获取线程执行结束的返回值

 

get()方法有可能导致主线程被阻塞,知道call()方法结束并返回为止

16.2.4 创建线程的三种方式

采用实现RunnableCallable方式创建多线程的优点

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

2. 在这种方式下 多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而将CPU代码和数据分开,,形成较清晰的模型 体现了面向对象的思想

 

劣势

1.编程稍稍复杂,如果需要访问当前线程,就必须使用Thread.currentThread()方法,

 

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

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

劣势

1.java的单继承局限

 

16.3 线程的生命周期

当线程被创建并启动以后,它不是一启动就进去执行状态,在线程的生命周期中,它要经过 新建(New) 就绪(Runnable) 运行(Running) 阻塞(Blocked) 和死亡(Dead,线程启动以后CPU会在多个线程之间切换运行

 

16.3.1 新建和就绪状态

当程序使用new创建一个线程之后,该线程就处于新建状态,此时它和其他的java对象一样,仅仅有jvm分配内存,并初始化其成员变量。此时线程对象没有表现出任何线程的动态特征

当线程对象调用start()后线程就处于就绪状态,jvm会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,只是表示该线程可以运行了,至于何时开始运行着取决于JVM里线程调度器的调度

 

直接调用线程的run()方法,只能通过Thread.currentThread()方法来获取当前的线程

调用了线程的run()方法后,该线程就不处于新建状态了,不要再次调用线程对象的start()方法

16.3.2运行和阻塞状态

如果处于就绪装填线程就获得了CPU,开始执行run()方法的线程处于运行状态

线程调度的细节取决于底层平台所采用的策略

 

在协作调度策略中的系统中只有当一个线程调用了它的sleep()yield()方法后才能放弃所占有的资源

 

当发生如下的情况,线程将会进入阻塞状态

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

2.线程调用了一个阻塞式IO方法,在该方法返回之前,该线程都会阻塞

3.线程试图获得一个同步的监视器,但该同步监视器整备其他线程所持有

4.线程在等待某一个通知

5.程序调用了线程的suspend()方法将线程挂起(这个方法容易导致死锁)

 

线程阻塞之后其他线程就获得执行机会,被阻塞的线程会在合适的时候重新进入就绪状态

 

当发生如下情况是,线程会解除阻塞,进入就绪状态

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

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

3.线程成功的获取了同步监视器

4.线程正在等地啊某个通知,其他线程发出了一个通知

5.处于挂起的状态的线程被调用了resume()方法

 

线程从阻塞状态只能进入就绪状态,无法直接进入运行状态

 

当运行状态的线程失去处理器资源时,线程进入就绪状态,调用yield()方法可以让线程从运行状态直接进入就绪状态

 

16.3.3 线程死亡

以下三种情况方式结束,线程处于死亡状态

1. run() call()方法结束

2. 线程抛出一个未捕捉的异常或者错误

3. 直接调用线程的stop方法(可能会导致死锁)

 

当主线程结束并不会影响其他线程

不要对死亡的线程调用start()方法,会抛出异常

调用isAlive()方法是 就绪和运行 阻塞返回true 死亡和新建为false

 

16.4 控制线程

java的线程支持提供了一些便捷的工具方法,通过这些便捷的工具方法可以很好的控制线程的执行

16.4.1 join线程

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

 

join方法通常由使用线程的程序调用,以将大问题划分为许多小问题,每一个小问题在分配一个线程组,当所有的小问题都得到处理后,在调用主线程来进一步操作

16.4.2 后台线程

有一种线程,它是在后台运行的,它的任务是为其他线程提供服务,这种现场又被称为“守护线程”,

后台线程有一个特征 如果前台所有线程都死亡了,后台线程会自动死亡,JVM会自动退出

调用Thread对象的setDaemon(true)就可将指定线程设置为后台线程

16.4.3 线程休眠

如果需要当前程序暂停一段时间,并进入阻塞状态,则可以通过Thread类的静态方法sleep()方法来实现

 

16.4.4 线程让步 yield

yield()方法是Thread类提供的静态方法,它也可以让当前线程暂停一下,让系统的线程在重新调度异常

实际上 当某一线程调用了yield()方法后,只有优先级与当前线程相同,或者优先级比当前线程更好的线程处于就绪状态才有获得执行的机会

 

sheep()方法和yield()方法的区别

1.  sleep()方法暂停当前线程后,会给其他线程执行机会不会理会其他线程优先级,但yield()暂停当前线程后,只会给优先级相同或者更高的线程执行

 

线程转入阻塞状态,只是让当前线程处于就绪状态

3.  sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么抛出异常 而yield()没有异常

4. sleep()方法比yield()方法有更好的可移植性

16.4.5 改变线程优先级

每一个线程都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会

每个线程默认的优先级都与创建它的父类优先级相同,main线程具有普通优先级

 

尽量使用MAX_PRIORITY,MIN_PRIORITY 从而保证程序的可移植性

 

16.5 线程同步

16.5.1 线程安全问题

多个线程并发时,程序本身没有错误,但是可能出现和预期不一样的结果

16.5.2 同步代码块

synchronized(obj)   同步代码块(同步监视器)

通常推荐使用可能被并发访问的共享资源充当同步监视器,

 

16.5.3 同步方法

同步方法就是使用synchronized关键字来修饰某一个方法,对于synchronized关键字来修饰的实例方法而言,无需指定同步监视器,同步方法的同步监视器是this 也就是调用该方法的对象

线程安全类具有如下的特征

1. 该类的对象可以被多个线程安全的访问

2. 每一个线程调用该类的任意方法都能得到正确的结果

3. 每一个线程调用该对象的任意方法之后,该对象状态依然保持合理状态

 

synchronized可以修饰代码块和方法 但是不恩给你修饰构造器和成员变量

 

为了减少线程安全所带来的负面影响,程序可以采用如下策略

1.不要对线程安全类的所有方法都进行同步,只要对那些会改变竞争资源的方法进行同步,

2.如果可变类有两种运行环境,单线程环境和多线程环境,则应该为该可变类提供两个版本,线程安全和线程不安全

 

16.5.4 释放同步监视器的锁定

程序无法显示释放同步监视器的锁定 线程会在如下几种情况下释放对同步锁的锁定

1. 当前线程的同步方法。同步代码块执行结束

2. 当前线程在同步代码块 同步方法中遇到 break return 终止了该代码的语句

3. 当前线程在同步代码块或同步方法中出现了为处理的异常或者错误时

4. 当前线程在同步代码块或同步方法中执行了同步监视器对象的wait()方法,当前线程暂停

 

如下情况不会释放同步监视器

1. 线程执行同步方法时,程序中调用了Thread.sleep()  Thread.yield()方法来暂停当前线程

2. 线程执行同步代码块时,其他线程调用了suspend()方法将该线程挂起,

 

程序中应该尽量避免使用 suspend() 和resume()方法来控制线程

 

16.5.5 同步锁(Lock

java 5 开始 java提供了一种更强大的线程同步机制-----通过显示定义同步锁对象来实现同步,在这种机制下,同步锁有Lock对象充当。

Lock提供了比synchronized方法 和代码块更广泛的锁定操作,Lock允许实现更灵活的结构,

Lock是控制多个线程对共享资源进行访问的工具

 

16.5.6 死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,jvm没有监视,也不会采取措施来处理死锁

一旦出现死锁,整个程序既不会发生异常也不会出任何提示,只是处在阻塞中,无法继续

 

16.6 线程通信

当线程在系统内运行时,线程调度具有一定的通明性,程序通常无法准确的控制线程的轮换执行,但java也提供了一些机制来保证线程的协调执行

16.6.1 传统线程通信

Object 提供了wait() notify()   notifyall() 三个方法,但这三个方法必须由同步监视器对象来调用,这可以分为两种情况

1.对于使用synchronized修饰的同步方法,该类默认的实例this就是同步监视器,所有可以在同步方法中直接调用这三个方法

2.对于使用synchronized修饰的同步代码块,同步监视器时synchronized后括号里的对象,所以就必须使用这个对象来调用

 

wait()导致当前线程等待,直到其他线程调用该同步监视器的notify()或者notifyall()来唤醒线程

notify()唤醒在此同步监视器上等待的单个线程,如果所有线程都在这个同步监视器上则随机唤醒其中的一个

notifyall()唤醒在此同步监视器上等待的所有线程

16.6.2使用Condition控制线程通信

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

当使用Lock对象后,java提供了Condition类来保持协调,使用Condition可以让这些已经得到Lock对象却无法继续执行线程释放Lock对象,Condition也可以唤醒其他处于等待的线程

 

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

await() 类似于隐式同步监视器上的wait()方法,

signal() 唤醒此Lock对象上的单个等待线程

signalal() 唤醒Lock对象上的所有等待的线程

16.6.3 使用阻塞队列(BlockingQueue)控制线程通信

java 5 提供了一个BlockingQueue接口,(Queue子接口),当生产者线程视图向BlockingQueue线程放入元素时,如果该队列已满,则该线程就会被阻塞,当生产者线程试图从BlockingQueue中取出元素时,如果该队列为空,则线程被阻塞

程序的两个线程被交替向BlockingQueue中放入元素。取出元素,即可以很好的控制线程通信

 

16.7 线程组合未处理的异常

java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,java允许程序直接对线程组进行控制,对线程组的控制相当于控制这批线程,用户创建的所有线程都属于指定线程组,如果没有指定线程组则该线程属于默认线程组

默认情况下 子线程和父线程处于同一个线程组内,

一旦某一个线程加入了某一个线程组就不能更改该线程组了

 

java 5 开始 java增强了线程的异常处理,如果线程执行过程中抛出了一个未处理异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,JVM会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,则会调用该对象的Thread.UncaughtExceptionThread t , Throwable e)方法来处理异常

 

线程组异常处理流程

1. 如果该线程组有父线程组,则会调用父线程组的uncaughtException()方法来处理异常

2. 如果该线程组实例所属的线程类有默认的异常处理(setDefaultUncaughtExceptionHandler()),那么就调用该异常处理器来处理该异常

3. 如果该异常对象是ThreadDeath对象则不做任何处理,当线程

 

16.8 线程池

系统启动一个线程的成本比较高,它涉及与操作系统交互,所以可以考虑使用线程池来提高性能,尤其是当程序中需要创建大量的很短暂的线程时,更应该考虑使用线程池,线程池还可以有效的控制系统中并发线程数不超过此数

16.8.1 java 8 改进线程

java 5 之前,开发者必须自己手动实现自己的线程池,java 5 开始 java内建立支持线程池,java新增了一个Excecutors来产生线程池,

 

使用线程池来执行线程任务的步骤如下

1. 调用Executors类的静态工厂方法创建一个ExcecutorsService对象,该对象代表一个线程池

2. 创建Runnable实现类或Callable实现类的实例,作为线程执行任务

3. 调用ExcecutorsService对象的submit()来提交RunnableCallable实例

4. 当不想提交任何任务时,调动ExcecutorsService对象的shutdown()方法来关闭线程池

 

16.8.2 java 8 增强的ForkJoinPool

java 7 提供了ForkJoinPool将一个任务分成多个小任务并计算,在将多个小人物的结果合并成总的计算 是ExcecutorsService的实现类,

fork() 并行执行两个小任务

 

16.9 线程相关类

16.9.1 ThreadLocal

ThreadLocal是线程局部变量的意思

就是为每一个使用该变量的线程都提供一个变量的值的副本,使每一个线程都可以独立的改变自己的副本,

以上是关于多线程的主要内容,如果未能解决你的问题,请参考以下文章

在多线程 C++11 程序中未处理异常时会发生啥?

在多线程 C++11 程序中未处理异常时会发生啥?

java-多进程,多线程

Java多线程

Python:使用多线程修改pandas DataFrame时,Spyder会发生错误

多线程死锁发生情景之一:同步的嵌套