读《Java核心技术 卷I》有感之第14章 并发

continue

  一个程序同时执行多个任务,通常每一个任务被称为一个线程,可以同时运行一个以上线程的程序被称为度线程程序。

14.1 什么是线程

  Java的线程创建常规方式:new一个Thread,为这个Thread的run方法提供实现代码,最后调用这个Thread的start方法,这个方法将创建一个执行run方法的新线程(直接调用run方法只会在同一个线程中执行其中的任务,不会启动一个新的线程来执行run中的任务)
  简单一提,之所以能够这么实现是因为Thread实现了Runnable函数式接口,其中只有一个run方法。

14.2 中断线程

  Java没有强制使线程终止的办法,但是可以通过interrupt方法来请求终止线程,这个方法会将线程的中断状态置位。中断状态是每个线程都具有的boolean标志,每个线程都会不时地检查这个标志,以判断线程是否被中断(实际上时间相对较长,某种意义上就是Java将根据线程中断状态进行控制的操作权交给了开发人员,让开发人员自己根据状态来控制线程是否中断)。如果要手动即时地检查这个标志,需要通过Thread.currentThread().isInterrupted()方法来确定。

14.2.1 Interrupted Exception异常产生与处理

  Java的中断线程有很关键的处理策略:如果线程被阻塞,那么就无法检测一个线程的中断状态,这里阻塞的状态通常指waiting和Timed waiting状态,Blocked状态反而不敏感。更详细的说,在阻塞的线程上调用interrupt方法(包括sleep、wait等引起中断状态的方法)时,阻塞调用会被Interrupted Exception异常中断。此处需要详细辨析中断和终止的概念,终止是让线程完全停下来,中断是让线程暂时停下来。没有任何的语言要求一个被中断的线程就应该被终止,因为中断一个线程不过就是引起这个线程的注意,所以被中断的线程本就可以决定如何响应其他的中断,有的重要线程会选择处理完异常后继续执行而不理会中断,然而大多线程都只是将中断作为一个终止的请求,并针对这个请求施加Interrupted Exception的异常捕捉机制
  书中提到多数开发者会无视或者抑制Interrupted Exception异常,此处的建议是不采用try语句捕获Interrupted Exception异常,而是通过在产生Interrupted Exception异常的方法定义处加上”throw InterruptedException“语句,让异常传递给调用者来处理异常。

14.2.2 典型方法辨析
  • interrupted方法:通过查询中断状态来检验线程是否被中断的静态方法,会将调用线程的中断状态置为false;
  • isInterrupted方法:通过查询中断状态来检验线程是否被中断的方法,不会改变线程的中断状态。;
  • interrupt方法:只能够置线程的中断状态为true,所以不会中断一个正在运行的线程。如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将立即抛出一个InterruptedException中断异常。

14.3 线程状态

  线程有6种状态:新创建New、可运行Runnable、被阻塞Blocked、等待Waiting、计时等待Timed waiting、被终止Terminated,通过getState方法可以直接获取。

14.3.1 新创建线程

  new操作符创建一个新线程之后,程序还没有运行线程中的任何代码,属于new状态。

14.3.2 可运行线程

  调用线程的start方法之后,线程进入runnable状态,该状态下线程可能正在运行也可能没有运行(因为时间片轮转的原因,一个可运行线程在某一时刻可能拥有运行权,也可能没有)。

14.3.3 被阻塞线程和等待线程

  当线程处于被阻塞或等待状态时,它暂时不活动,不运行任何代码且消耗最少的资源。

  • 当一个线程试图获取一个内部的对象锁,而该锁被其他线程所持有,则该线程进入Blocked状态;
  • 当一个线程在等待另一个线程通知调度器一个条件时,则该线程进入Waiting状态(Object.wait和Thread.join都会触发该状态);
  • 当一个线程中执行内部的某些具有超时参数的方法时,调用它们导致线程进入Timed waiting状态(Object.wait, Thread.join和Thread.sleep等方法都可以带上超时参数使线程进入计时等待状态)。
14.3.4 被终止的线程

  线程因为以下两个原因而被终止:

  • 因为run方法正常退出而自然死亡;
  • 因为一个没有捕获的异常终止了run方法而意外死亡。
14.3.5 线程状态与中断的关系
  • New和Terminated对于中断操作几乎是屏蔽的。
  • Runnable和Blocked类似,对于中断操作只是设置中断标志位并没有强制终止线程,对于线程的终止权利依然在程序手中。
  • waiting和Timed waiting状态下的线程对于中断操作是敏感的,他们会抛出异常并清空中断标志位,即将两种状态切换为RUNNABLE状态继续执行,不过处于Blocked状态的线程仍然处于Blocked状态。

14.4 线程属性

14.4.1 线程优先级

  Java的线程有优先级的概念,为int值,最高为10最低为1默认为5,可以继承父类的优先级,不过书中不太建议过于依赖优先级。

14.4.2 守护线程

  通过调用t.setDaemon(true);将线程t转换为守护线程,不过这个操作必须在线程start之前调用。守护线程的作用其实就是为其他线程提供服务,所以其应该永远不去访问文件或数据库等固有资源。当程序中只有守护线程时,程序将会因为没有运行必要而自己退出。

14.4.3 未捕获异常处理器

  线程的run方法不能抛出任何的受查异常,但是非受查异常会使得线程终止,为了应对这种情况,设计了一种用于未捕获异常的处理器,这个处理器必须属于一个实现了”Thread.UncaughtExceptionHandler“接口的类,这个接口是个函数式接口,只有void uncaughtException(Thread t, Throwable e)这个方法。
  对于线程而言,可以通过setUncaughtExceptionHandler为某一线程安装自己的处理器,也可以通过Thread类的静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器。如果线程的默认未捕获异常处理器没有设置,为空,此时这个线程的处理器就是该线程的ThreadGroup对象书中建议尽量不要自己定义线程组来进行相关的操作,而系统默认线程组的uncaughtException方法会做如下操作

  1. 如果当前线程的线程组有父线程组,那么父线程组的uncaughtException方法被调用;
  2. 否则,如果Thread.getUncaughtExceptionHandler方法返回一个非空的处理器,则调用该处理器;
  3. 否则,如果Throwable是ThreadDeath的一个实例,什么都不做;
  4. 否则,线程的名字以及Throwable的栈轨迹被输出到System.err上(也就是经常看到的控制台中的红色栈轨迹)。

14.5 同步

  竞争条件:两个或两个以上的线程需要共享对同一数据的存取,如果两个线程存取相同的对象,并且每一个线程都调用了一个修改该对象状态的方法,将会发生的讹误等情况。

14.5.1 竞争条件的例子

  死循环内,不断的对数组中某些元素进行增加或减少操作。

14.5.2 竞争条件详解

  书里主要讲了accounts[yo] += amount;这个语句,这个语句不是一个原子操作的语句,所以该语句在操作系统内实际运行时可能被拆分为如下三个步骤:

  1. 将accounts[to]加载到寄存器;
  2. 增加account;
  3. 将结果写回accounts[to]。

  这里可以直接看出,当某个线程执行了部分步骤时被剥夺了运行权后(比如做完了1步与2步),之后的操作将会变得相当奇怪。

14.5.3 锁对象

  synchronized关键字,常规锁Lock类,公平锁ReentrantLock类。
  这里先介绍ReentranLock类与Lock类,两种用法都是经典的锁用法,正常情况下new个锁然后调用其lock方法与unlock方法即可,不过书中建议最好将unlock方法的执行放置在try catch语句的finally子句中,这种方式能够确保unlock方法会被执行,以避免不必要的麻烦。不过ReentrantLock作为公平锁相对常规锁而言会偏爱等待当前锁时间最长的线程。
  需要特别注意的是无论常规锁还是公平锁都是可重入的,也就是说被一个锁保护的代码可以调用另一个使用相同的锁的方法。

14.5.4 条件对象

  Condition类型的条件变量/条件对象用来处理那些获得了一个锁但是不能在锁的临界区内做有用工作的线程,每个锁都可以有一个或者多个条件对象。条件对象的创建如Condition suf = bankLock.newCondition();所示,其是作为某个锁的条件对象存在。
  条件对象有三个关键方法(使用条件对象普遍效率会变慢):

  1. await方法:比如一个线程调用了A锁的条件对象C的await方法时,其会进入阻塞状态,如果这个线程获得了A锁,那么该线程会放弃A锁,以供其他等待A锁的线程进入临界区执行代码。之后A锁的释放并不会使当前线程解除阻塞,必须等待在某个其他线程中对A锁的条件对象C执行signalAll方法,让当前线程继续从await的地方继续执行。这个方法常常用于lock与unlock方法所包含的临界区内部,所以await的继续执行通常需要一定的条件,比如while(!(ok to proceed))这样的while循环判断。
  2. signalAll方法:该方法用于通知所有被A锁的条件对象C所控制的线程,这些线程将解除阻塞状态(注意不是立即激活线程,每个线程还是要等自己的执行权)。这个方法的执行时机很讲究,因为如果所有线程都在执行await方法而没线程去执行signalAll方法,那么所有线程都会陷入死锁状态。所以该方法的执行时机书中归纳为”对象的状态有利于等待线程的方向改变时调用signalAll“,通俗的来说,就是最好在每个与await限制条件相关的对象发生改变时,调用signalAll方法。
  3. signal方法:能够随机解除A锁条件对象C的等待集中的某个线程,容易出现问题。

  总结来说,就是锁用来管理线程进入临界区,条件对象用于管理进入临界区后暂时不能执行的线程。书中专门为锁和条件的关键之处进行了评判:

  • 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码;
  • 锁可以管理试图进入被保护代码段的线程;
  • 锁可以拥有一个或多个相关的条件对象;
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
14.5.5 synchronized关键字

  此关键字只能被用于方法的声明中,用于表示使当前方法获得一个内部锁,这个锁将会保护整个方法,每个想要调用整个方法的线程都需要获得该方法的内部锁。这里简单描述下,其中intrinsicLock表示内置锁。
  其中内置锁只有一个相关条件(所以可以理解为intrinsicCondition),这里解释了为什么条件中的三个函数被命名为await、signalAll和signal的原因:由于wait、notifyAll和notify方法是Object的final方法,所以条件Condition中只能命名为这三个名字,实际上在条件对象中wait、notifyAll和notify三个方法与await、signalAll和signal三个方法一一对应。也正因如此,在synchronized修饰的方法中不用显示创建条件对象,直接调用wait、notifyAll和notify三个方法即可实现条件对象的相关功能。
  静态方法也可以加上synchronized关键字,这样的话静态方法所处的类将会加上内部锁,任何尝试访问这个类的线程都会走锁的流程。

public synchronized void method()
{
	method body
}

==

public void method()
{
	this.intrinsicLock.lock();
	try
	{
		method body
	}
	finally { this.intrinsicLock.unlock(); }
}

  但是内部锁和条件锁也存在一些局限性,包括:

  • 不能中断一个正在试图获得锁的线程;
  • 试图获得锁时不能设定超时;
  • 每个锁仅有单一的条件时,可能是不够的。

  书中给出了关于锁合同步的一些建议如下:

  • 建议最好不要使用Lock/Condition和synchronized关键字,而是使用java.util.concurrent包中的一种机制,它会为开发人员处理所有的加锁。
  • 如果synchronized关键字适合程序那么最好可以使用它;
  • 特别需要Lock/Condition结构独有的特性时再使用(我的理解就是可能需要多个锁或多个条件变量时,不然永远都是synchronized更好。
14.5.6 同步阻塞

  通过synchronized(obj)方式的调用,可以使这个方式所在的方法获得obj这个锁,通过这种方式实现额外的原子操作(原子性是指,一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉的过程)被称为客户端锁定。

14.5.7 监视器方法

  监视器通常是指不需要程序员加锁的情况下保证多线程的安全性,Java为每个对象都建立了一个内部锁合内部条件,当方法使用synchronized关键字时,这个方法就表现的像个监视器方法。
  不过由于这种方式的”域不要求必须是private“、”方法不要求必须是synchronized“、”内部锁对客户是可用的“三个原因,导致Java监视器的线程安全性其实不咋地。

14.5.8 Volatile域

  同步格言:“如果向一个变量写入值,而这个变量接下来可能会被另一个线程读取,或者,从一个变量读值,而这个变量可能是之前被另一个线程写入的,此时必须写入同步”。而如果对每个方法进行synchronized关键字未免也太过繁琐,所以Java设计了volatile关键字为变量的同步访问提供了免锁机制,这个关键字修饰的变量,编译器和虚拟机将会知道它会被另一个线程并发更新,从而做一些操作。
  不过必须值得一提的是,volatile变量不能提供原子性,也就是说不能通过volatile变量自己计算自己。

14.5.9 final变量

  final变量的特性就是存储在常量存储区,所以通过final修饰的变量就可以让所有线程共享的安全访问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方寸间沧海桑田

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值