[读书笔记]多线程学习笔记

几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个执行流就是一个线程。

线程和进程

所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程时系统进行资源分配和调度的一个独立单元。

进程包含3个特征:1、独立性 2、动态性 3、并发性

多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread)也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程拥有的全部资源。

线程是独立运行的,线程的执行是抢占性的。

操作系统无需将多个线程看作多个独立的应用,对多线程实现调度和管理以及资源分配。线程的调度和管理由进程本身负责完成。

归纳:操作系统可以执行多个任务,每个任务就是进程,但至少要包含一个线程。

多线程的优势

线程在执行过程中拥有独立的内存单元,而多个线程共享内容,从而极大的提高了程序的运行效率

线程比进程拥有更高的性能,这是由于同一个进程中的线程都有共性-多个线程共享同一个进程虚拟空间。线程共享的环境包括:进程代码段、进程的公有数据等。利用这些共享的数据,线程很容易实现互相之间的通信。

当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源,但创建一个线程则简单的多,因此使用多线程来实现并发比使用多进程实现并发的性能要高得多。

使用多线程编程有如下优点:
* 进程之间不能共享内存,但线程之间共享资源非常容易
* 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小的多,因此使用多线程来实现多任务并发比多进程的效率高
* Java语言内置了多线程功能的支持,而不是单纯的作为底层操作系统的调度方式,从而简化了Java的多线程编程。

线程的创建和启动

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或者其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码),Java使用线程执行体来代表这段程序流。

1、继承Thread类来创建线程类

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法体就代表了线程需要完成的任务,因此需要把run()方法称为线程执行体
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

进行多线程编程时不要忘记了Java程序运行时默认的主线程,main()方法的方法体就是主线程的线程执行体

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

2、实现Runnable接口创建线程类

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的执行体
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象

这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类(实际上应该是线程的target类)的实例属性

使用Callable和Future创建线程

callback提供一个call方法可以作为线程执行体
* call()方法可以有返回值
* call()方法 可以声明抛出异常

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

  1. 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值。
  2. 创建Callable实现类的实例,使用FutureTask类来包装Callable对象该FutureTask对象封装了该Callable对象的call()方法的返回值
  3. 使用FutureTask对象作为Thread对象的target创建并启动新线程
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

创建线程的三种方式对比

因为实现Runnable接口与实现Callable接口的方式基本相同,只是Callback接口里定义的方法有返回值,可以声明抛出异常而已,因此可以将实现Runnable接口和实现Callable接口归为一种方式。这种方式与继承Thread方式之间的主要差别如下:

采用实现Runnable、Callback接口的方式创建多线程

  • 线程类只是实现了Runnable接口或者Callback接口,还可以继承其他类
  • 在这种方式下,多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想
  • 劣势是:编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法

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

  • 劣势是因为线程已经继承了Thread类,所以不能再继承其他父类
  • 优势是编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程

线程的生命周期

线程的生命周期中要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dend)5种状态

1、新建和就绪状态
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。

当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。

2、运行和阻塞状态
如果处于就绪状态的线程获得了CPU,开始执行run方法的执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任意时刻只有一个线程处于运行状态

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

  • 线程调用sleep()方法主动放弃所占用的处理器资源
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
  • 线程在等待某个通知(notify)
  • 程序调用了线程的suspend()方法将该线程挂起。这个方法容易导致死锁,所以应该尽量避免使用该方法

发生如下特定的情况下可以解除上面的阻塞,让该线程重新进入就绪状态

  • 调用sleep()方法的线程经过了指定时间
  • 线程调用的阻塞式IO方法已经返回
  • 线程成功的获得了试图取得的同步监视器
  • 线程正在等待某个通知时,其他线程发出了一个通知
  • 处于挂起状态的线程调用了resume()方法

注意:调用yield方法可以让运行状态的线程转入就绪状态

线程死亡

线程会以如下3种方式结束,结束后就处于死亡状态

  • run()或call()方法执行完成,线程正常结束
  • 线程抛出一个未捕获的异常
  • 直接调用该线程的stop()方法来结束该线程-该方法容易导致死锁,通常不推荐使用

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

控制线程

1、join线程

Thread提供了让一个线程等待另一个线程完成的方法-join()方法。

join()方法有如下三种重载形式:

  • join():等待被join的线程执行完成
  • join(long millis):等待被join的线程的时间最长为millis。如果在millis毫秒内被join的线程还没有执行结束,则不再等待
  • join(long millis, int nanos):等待被join的线程的时间最长为millis毫秒加nanos微毫秒

后台线程

在后台运行,它的任务是为其他的线程提供服务,这种线程被称为”后台线程(Daemon Thread)”,又称为”守护线程”或”精灵线程”。JVM的垃圾回收线程就是典型的后台线程

特征:如果所有的前台线程都死亡,后台线程会自动死亡,调用Thread对象的setDaemon(true)方法可以指定线程设置成后台线程。当整个虚拟机中只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机也就推出了。

将某个线程设置为后台线程,必须在start()方法之前调用,否则会引发IllegalThreadStateException

线程睡眠:sleep

sleep()方法有两种重载形式:

  • static void sleep(long millis):让当前正在执行的线程暂停一段时间,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度与准确度的影响
  • static void sleep(long millis, int nanos):让当前正在执行的线程暂停millis毫秒加nanos微毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度与准确度的影响

线程让步

yield()方法可以让当前正在执行的线程暂停,但是不会阻塞线程,只是将该线程转入就绪状态。实际上,当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同、或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。

sleep()方法和yield()方法的区别如下:

  • sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级,但yeild()方法只会给优先级相同,或优先级更高的线程执行机会
  • sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态,而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,继续再次获得处理器资源被执行
  • sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法要么捕获该异常,要么显式声明抛出该异常,而yield()方法则没有声明抛出任何异常
  • sleep()方法比yield()方法有更好的移植性,通常不建议使用yield()方法来控制并发线程的执行

改变线程优先级

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

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

Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中setPriority()方法的参数可以是一个整数,范围是1~10之间,也可以是使用Thread类的如下三个静态常量

  • MAX_PRIORITY:其值是10
  • MIN_PRIORITY:其值是1
  • NORM_PRIORITY:其值是5

线程同步

1、当多个线程来访问同一个数据时,很容易偶然出现线程安全问题

2、同步代码块

当两个进程并发修改同一个文件时就有可能造成异常

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

synchronised(obj)
{
    ...
    //此处的代码就是同步代码块
}


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

注意:任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成之后,该线程会释放对该同步监视器的锁定

3、同步方法

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

通过使用同步方法可以非常方便的实现线程安全的类,线程安全的类具有如下特征:

  • 该类的对象可以被多个线程安全的访问
  • 每个线程调用该对象的任意方法之后都将得到正确结果
  • 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态

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

  • 不要对线程安全类的所有方法都同步进行,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步
  • 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两个版本,即线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本

提示:JDK所提供的StringBuilder和StringBuffer就是为了照顾单线程环境和多线程环境所提供的类,在单线程环境下应该使用StringBuilder来保证较好的性能;当需要保证多线程安全时,就应该使用StringBuilder

4、释放同步监视器的锁定

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

  • 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。当前线程将会释放同步监视器
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时,当前线程将会释放同步监视器
  • 当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器

在如下所示的情况下,线程不会释放同步监视器

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。我们应当尽量避免使用suspend()和resume()方法来控制线程

5、同步锁

从Java5开始,Java提供了一种功能更加强大的线程同步机制-通过显示定义同步锁对象来实现同步,在这种机制下,同步锁使用Lock对象充当。

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

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

某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock、ReadWriteLock是Java5新提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类;为ReadWriteLock提供了ReentrantWriteLock实现类

6、死锁

当两个线程互相等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,整个程序既不会发生任何异常,只是所有线程处于阻塞状态,无法继续

线程通信

1、使用Condition控制线程通信

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

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

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

  • await():类似于隐式同步监视器上的wait方法,导致当前线程等待,直到其他线程调用该Condition的signal()方法或者signalAll()方法来唤醒该线程。该await()方法有更多变体,可以完成更丰富的等待操作
  • signal():唤醒在此Lock对象上等待的单个线程。如果所在线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该Lock对象的锁定后(使用await()方法),才可以执行被唤醒的线程
  • signalAll():唤醒在此Lock线程上等待所有的所有线程,只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的进程

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

BlockingQueue具有一个特征,当生产者线程试图向BlockingQueue中放入元素时,如果队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞

程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好的控制线程的通信。
BlockingQueue提供如下两个支持阻塞的方法

  • put(E e):尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程
  • take():尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程

BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法。

  • 在队列尾部插入元素。包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别回抛出异常,返回false、阻塞队列
  • 在队列头部删除并返回删除的元素。包括remove()、poll()和take()方法。该队列已空时,这3个方法分别会抛出异常、返回false、阻塞队列
  • 在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别跑出异常,返回false

BlockingQueue包含如下5个实现类:

  • ArrayBlockingQueue:基于数组实现的BlockingQueue队列
  • LinkedBlockingQueue:它并不是标准的阻塞队列。与前面介绍的PriorityQueue类似,该队列掉哟过remove()、poll()、take()等方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。PriorityBlockingQueue判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可以使用Comparator进行定制排序
  • SynchronousQueue:同步队列。对该队列的存取操作必须进行交替进行
  • DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现。不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法),DelayQueue根据集合元素的getDelay()方法的返回值进行排序

线程组和未处理的异常

Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。对线程组的控制相当于同时控制这批线程。用户创建的所有线程都属于指定线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内。一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组

ThreadGroup类提供了如下几个常用的方法来操作整个线程组里的所有线程

  • int activeCount():返回此线程组中活动线程的树木
  • interrupt():中断此线程中的所有线程
  • isDaemon():判断该线程组是否是后台线程组
  • setDaemon(boolean daemon):把该线程组设置成为后台线程组。后台线程组具有一个特征-当后台线程组的最后一个线程执行结束后最后一个线程被销毁后,后台线程将自动销毁
  • setMaxPriority(int pri):设置线程的最高优先级

线程池

1、当程序中需要创建大量生存期很短暂的线程时,应该考虑线程池。

与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中称为空闲状态,等待下一个Runnable对象的run()或call()方法

除此之外,使用线程池可以有效的控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程数不超过此数

Java5新增了一个Executors工厂类来创建线程池,该工厂类包含如下几个静态工厂方法来创建线程池

  • newCachedThreadPool():创建一个可重用、具有固定线程数的线程池
  • newFixedThreadPool():创建一个只有可重用的、具有固定线程数的线程池
  • newSingleThreadExecutor(): 创建一个只有单线程的线程池,它相当于调用newFixedThreadPool()方法时传入参数为1
  • newScheduledThreadPool(int corePoolSize):创建具有指定线程的线程池,它可以在指定线程延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内
  • newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务

2、Java7新增的ForkJoinPool

Java7提供了ForkJoinPool来支持将一个任务拆分为多个小任务并行计算,再把多个小任务的结果合并成总的计算结果。ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池,提供了两个常用的构造器

  • ForkJoinPool(int parallelism):创建一个包含parallelism个并行线程的ForkJoinPool
  • ForkJoinPool():以Runtime.availableProcessors()方法的返回值作为parallelism参数来创建ForkJoinPool

线程相关类

1、通过使用ThreadLocal类可以简化多线程编程时的并发访问,使用这个工具类可以很简洁的隔离多线程程序的竞争资源

线程局部变量(ThreadLocal)的功用就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程可以独立的改变自己的副本,而不会和其他线程的副本冲突。

ThreadLocal类只提供了3个public方法

  • T get():返回此线程局部变量中当前线程副本中的值
  • void remove():删除此线程局部变量中当前线程的值
  • void set(T value):设置此线程局部变量中当前副本中的值

ThreadLocal和其他所有的同步机制一样,都是为了解决多线程中对同一变量的访问冲突,在普通的访问机制中,是通过对象加锁来实现多个线程对同一变量的安全访问的。该变量是多个线程共享的。

ThreadLocal并不能代替同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式,而ThreadLoacl是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源(变量)的竞争,也就不需要对多个线程进行同步了。

如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制,如果仅仅需要隔离多个线程之间的共享冲突,则可以使用ThreadLocal

2、包装线程不安全的集合

ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的,当多个线程向这些集合中存、取元素时,就可能会破坏这些集合的数据完整性

3、线程安全的集合类

线程安全的集合类可分为如下两类:

  • 以Concurrent开头的集合类,如ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue和ConcurrentLinkedDeque
  • 以CopyOnWrite开头的集合类,如CopyOnWriteArrayList、CopyOnWriteArraySet

你可能感兴趣的:(Java)