线程入门(概念+实例)

线程入门

文章目录

一、线程、进程基本概念

1、操作系统中的线程与进程

1.1 进程

  指正在运行的程序实例。一个进程包含程序代码、数据、堆栈等资源。操作系统将每个进程看作是一个独立的实体,它们可以占用计算机的内存、CPU资源等,每个进程都有自己独立的地址空间,进程之间的通信需要使用进程间通信机制(IPC)。

1.1.1 进程控制块及其作用
1.1.1.1 进程控制块

  操作系统核心中一种数据结构,主要表示进程状态。

1.1.1.2 进程控制块的作用

  使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位或与其它进程并发执行的进程。

具体来说:

  1. 作为独立运行基本单位的标志
      当一个程序配置了PCB后,就表示它已是一个能在多道程序环境下独立运行的、合法的基本单位。PCB已经成为了进程存在于系统中的唯一标志。
  2. 能实现间断性运行方式
      有了PCB后,系统可以将终端进程的CPU现场信息存放在PCB中,供该进程在被调度时恢复CPU现场信息。
  3. 提供进程管理所需要的信息
      当进程调度时、需要访问文件或I/O设备时,都需要借助PCB中资源清单的信息。
  4. 提供进程调度所需要的信息
      进程调度时,需要提供优先级、等待时间和已执行时间等。
  5. 实现与其他进程的同步与通信
      进程同步,需要相应的用于同步的信号量,还需要具有用于实现进程通信的区域或通信队列指针等。
1.1.1.3 进程控制块中的信息
  1. 进程标识符:用于唯一标识一个进程。分为两种标识符:
  • 外部标识符:为了方便用户(进程)对进程的访问,
  • 内部标识符:为了方便系统对进程的使用,唯一的数字标识符
  1. 处理机状态:也称为处理机的上下文,主要是由处理机的各种寄存器中的内容组成的。这些寄存器主要包括:
  • 通用寄存器:又称为用户可视寄存器,是用户程序可以访问的,用于暂存信息,在大多数处理机中,由8~32个通用寄存器,在RISC结构的计算机中可超过100个。
  • 指令计数器:其中存放要访问的下一条指令的地址。
  • 程序状态字PSW:含有状态信息,如:条件码、执行方式、中断屏蔽标志等
  • 用户栈指针:指每个用户进程都有一个或若干个与之相关的系统栈,用于存放过程和系统调用参数及调用地址。栈指针指向该栈的栈顶。
  1. 进程调度信息 :包括进程状态、进程优先级、进程调度所需的其他信息、事件。
  2. 进程控制信息:程序和数据的地址、进程同步和通信机制、资源清单、链接指针。
1.1.1.4 进程控制块的组织方式

  线性方式、链接方式、索引方式

1.1.2 进程七状态模型

在这里插入图片描述

1.1.3 进程创建
1.1.3.1 引起进程创建的事件

  用户登录、作业调度、提供服务、应用请求

1.1.3.2 进程如何创建
  1. 申请空白PCB
  2. 为新进程分配其运行所需的资源
  3. 初始化进程控制块:初始化标识信息、初始化处理机状态信息、初始化处理机控制信息。
  4. 如果进程就绪队列能够接纳新进程,便将新进程插入就绪队列。
1.1.4 进程终止
1.1.4.1 引起进程终止的事件

  正常结束、异常结束、越界错、保护错、非法指令、特权指令错、运行超时、等待超时、算术运算错、I/O故障、外界干扰、操作员或操作系统干预、父进程请求、因父进程终止。

1.1.4.2 进程的终止过程
  1. 根据被终止进程的标识符,从PCB集合中检索出该进程的PCB,从中读出该进程的状态。
  2. 若处于执行状态,终止执行,调度标志改为真。
  3. 若还有子孙进程,应将子孙进程都终止,以防他们称为不可控的进程。
  4. 将资源归还给父进程,或系统。
  5. 将被终止进程的PCB从所在队列中移除,等待其他程序调用。
1.1.5 进程的阻塞与唤醒
1.1.5.1 引起进程堵塞和唤起的事件

  向系统请求共享资源失败、等待某种操作的完成、新数据尚未到达、等待新任务的到达

1.1.5.2 进程堵塞的过程

  正在执行的进程,如果发生了上述某事件,进程便通过调用阻塞原语block将自己阻塞。可见,阻塞是进程自身的一种主动行为
  进入block过程后,由于该进程还处于执行状态,所以应先立即停止执行,把进程控制块中的状态由"执行"改为阻塞,并将PCB插入阻塞队列。
  如果系统中设置了因不同事件而堵塞的多个堵塞队列,则应将本进程插入到具有相同事件的堵塞队列。
  最后,转调度程序进行重新调度,将处理机分配给另一就绪进程,并进行切换,保留被阻塞进程的处理机状态,按新进程的PCB中的处理机状态设置CPU的环境。

1.1.5.3 进程唤醒过程

  当被阻塞进程所期待的事件发生时,由有关进程调用唤醒原语wakeup,将等待该事件的进程唤醒。

  wakeup执行的过程是:首先把阻塞的进程从等待该事件的阻塞队列中移出,将其PCB中的现行状态由阻塞改为就绪,然后再将该PCB插入到就绪队列中。

1.1.6 进程的挂起与激活
1.1.6.1 进程的挂起过程
  • 当系统中出现了引起进程挂起的事件时,OS将利用挂起原语suspend将指定进程或处于阻塞状态的进程挂起。
  • suspend的执行过程:首先检查被挂起进程的状态,若处于活动就绪状态,便将其改为静止就绪。
  • 对于活动阻塞状态的进程,则将之改为静止阻塞。
  • 为了方便用户或父进程考察该进程的运行情况,而把该进程的PCB复制到某指定的内存区域。
  • 最后,若被挂起进程正在执行,则转向调度程序重新调度。
1.1.6.2 进程的激活过程

当系统中发生激活过程的事件时,OS将利用激活原语active,将指定进程激活。
激活原语先将进程从外存调入内存,检查该进程的现行状态。
若是静止就绪,改为活动就绪。
若是静止阻塞,改为活动阻塞。

1.2 线程

  也被称为轻量型的进程,是进程内部的一条执行路径。一个进程可以包含多个线程,且至少包含一个线程,每个线程共享进程的资源。线程之间的切换比进程之间的切换更快,因为线程共享相同的地址空间,切换时只需要切换一些寄存器和堆栈即可。

1.2.1 多线程模型

  多对一模型、一对一模型、多对多模型

1.2.1.1 线程分类(用户/内核)
  • 用户级线程
      用户级线程在内核之上,线程的管理不需要内核的支持,而是由应用层面的线程库来进行生成和管理,不属于内核层次。对于用户级线程的存在,内核是无法感知的。由于线程在进程内部的切换规则要比进程的调度简单,所以不需要用户态到内核态切换的开销,速度快。但缺点是却无法做到真正意义上的并发,如果一个进程内部有一个线程发生了阻塞,会导致这个进程(包括它的所有线程)都阻塞。

特点:

  • 用户级线程存在于用户空间
  • 内核无法感知用户线程
  • 内核资源的分配是根据进程分配的,用户级线程所在的进程可以竞争系统的资源,而每个用户线程只能竞争该进程内部的资源。对于一个进程,可能有成千上万个用户级线程,但是它们对系统的资源没有影响。
  • 内核级线程
      内核线程建立和销毁都是由操作系统负责、通过系统调用完成的。在内核的支持下运行,无论是用户进程的线程,或者是系统进程的线程,他们的创建、撤销、切换都是依靠内核实现的,但是调度开销要比用户线程更大。

特点:

  • 内核级线程可以在全系统内进行资源的竞争
  • 内核空间内为每一个内核支持线程设置了一个线程控制块(TCB),内核根据该控制块,感知线程的存在,并进行控制。
1.2.1.2 多对一模型

  多个用户线程对应一个内核级线程,无法在多核处理器中实现并行化。


在这里插入图片描述

Java的多对一模型(Green Threads)
  多对一模型的实现(一个内核线程的多个用户线程)允许应用程序创建任意数量的可并发执行的线程。在多对一(用户级线程)实现中,所有线程活动都限制为用户空间。此外,一次只有一个线程可以访问内核,因此操作系统只知道一个可调度实体。因此,此多线程模型提供有限的并发性,并且不利用多处理器。Java 线程在 Solaris 系统上的初始实现是多对一的,如下图所示。


在这里插入图片描述

1.2.1.3 一对一模型

  一个用户级别线程对应一个内核级别线程,即创建一个用户级线程就需要创建一个对应的内核级线程,可能会影响系统的性能。但和多对一模型不同的是,即使一个用户线程发生阻塞,也不会影响该进程的执行。


在这里插入图片描述

Java中的一对一模型
  一对一模型(一个用户线程到一个内核线程)是真正多线程的最早实现之一。在此实现中,应用程序创建的每个用户级线程都是内核已知的,并且所有线程可以同时访问内核。此模型的主要问题是它限制了您对线程的谨慎和节俭,因为每个额外的线程都会为该过程增加更多的“权重”。因此,此模型的许多实现(如 Windows NT 和 OS/2 线程包)限制了系统上支持的线程数。


在这里插入图片描述

1.2.1.4 多对多模型

  多对多模型允许多个用户级线程复用到更小或者数量相同内核级线程上,摒弃了多对一和一对一的缺点,开发人员可以创建任意多的用户级线程,而相应的内核级线程可以在多核处理器上正常运行。而多对多模型的另外一个分支:两级模型。虽然三种模型中,多对多模型可能是最好的,但是其实现较为困难。而目前来看,随着处理器核心数的提升,开发人员不太需要考虑内核级线程的开销,因此大部分的操作系统现在使用一对一模型。


在这里插入图片描述

在这里插入图片描述

Java中的多对多模型(Java on Solaris–Native Threads)
  多对多模型(许多用户级线程到许多内核级线程)避免了一对一模型的许多限制,同时进一步扩展了多线程功能。多对多模型(也称为两级模型)最大限度地减少了编程工作,同时降低了每个线程的成本和重量。
  在多对多模型中,程序可以具有适当数量的线程,而不会使进程过于繁重或繁琐。在此模型中,用户级线程库提供对内核线程之上的用户级线程的复杂调度。内核只需要管理当前处于活动状态的线程。用户级别的多对多实现减少了编程工作,因为它解除了对应用程序中可有效使用的线程数的限制。
  因此,多对多线程实现为每个进程提供了标准接口、更简单的编程模型和最佳性能。Java on Solaris 操作环境是 MT 操作系统上 Java 的第一个多对多商业实现。


在这里插入图片描述

1.2.1.5 三者并发性对比
  • 多对一模型: 用户创建了多个用户级线程,但实际上只有一个内核级线程在运行,并没有实现并行。
  • 一对一模型: 该模型可以提供更高的并发性,用户需要注意一个应用中不能创建太多的线程。(在一些系统中,它所能创建的线程有限)
  • 多对多模型: 该模型摒弃了以上的缺点,用户可以创建尽可能多的用户级线程,对应的内核级线程可以运行在多核心处理器上。当一个线程被阻塞,内核也会调度其他的线程来处理。但是整个模型实现起来较为困难
1.3 线程与进程的对比
  • 根本区别:
      进程是操作系统资源分配的基本单位;
      线程是处理器任务调度和执行的基本单位。
  • 资源开销:
      每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;
      线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
  • 包含关系:
      如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;
      线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
  • 内存分配:
      同一进程的线程共享本进程的地址空间和资源;
      而进程之间的地址空间和资源是相互独立的。
  • 影响关系:
      一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 执行过程:
      每个独立的进程有程序运行的入口、顺序执行序列和程序出口。
      但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
      两者均可并发执行

   如果以工厂为例的话,进程就是工厂中的一个个厂房,线程就是厂房中的流水线。 总之,进程是资源分配的基本单位,而线程是CPU调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源,但每个线程都有自己独立的执行路径。线程之间的通信比进程之间的通信更快、更高效。

1.4 线程同步

线程同步: 每个线程之间按预定的先后次序进行运行,协同、协助、互相配合。可以理解成“你说完,我再做”。有了线程同步,每个线程才不是自己做自己的事情,而是协同完成某件大事。
线程互斥: 当有若干个线程访问同一块资源时,规定同一时间只有一个线程可以得到访问权,其它线程需要等占用资源者释放该资源才可以申请访问。线程互斥可以看成是一种特殊的线程同步。
  很显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。

1.4.1 线程同步的机制

  线程同步过程中要用到四个组件:临界区(Critical Section)、互斥对象(Mutex)、信号量(Semaphore)、事件对象(Event)

  • 临界区(Critical Section)、互斥对象(Mutex):主要用于互斥控制;都具有拥有权的控制方法,只有拥有该对象的线程才能执行任务,所以拥有,执行完任务后一定要释放该对象。
  • 信号量(Semaphore)、事件对象(Event):事件对象是以通知的方式进行控制,主要用于同步控制!
1.4.1.1 临界区(Critical Section)

  通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。它并不是核心对象,不是属于操作系统维护的,而是属于进程维护的。

  1. 关键段共初始化化、销毁、进入和离开关键区域四个函数。
  2. 关键段可以解决线程的互斥问题,但因为具有“线程所有权”,所以无法解决同步问题。
  3. 推荐关键段与旋转锁配合使用。
1.4.1.2 互斥对象(Mutex)

  互斥对象和临界区很像,采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程同时访问。当前拥有互斥对象的线程处理完任务后必须将线程交出,以便其他线程访问该资源。

  1. 互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。
  2. 互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。
1.4.1.3 信号量(Semaphore)

  信号量也是内核对象。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目
  在Java中调用 Semaphore() 方法创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最 大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减 1 ,只要当前可用资源计数是大于 0 的,就可以发出信号量信号。但是当前可用计数减小到 0 时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时将当前可用资源计数加1 。在任何时候当前可用资源计数决不可能大于最大资源计数。

1.4.1.3 事件对象(Event)

  通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。

  1. 事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。
  2. 事件可以解决线程间同步问题,因此也能解决互斥问题。
1.4.2 线程同步的五个方法
1.4.2.1 互斥锁

  当有多个线程需要访问同一个资源时,如果不做处理,有时候就会出现问题,比如有两个线程需要使用打印机,进程A正在使用,而进程B也要使用打印机,此时打印出来的东西就是错乱的。互斥锁就是控制对共享资源的使用。互斥锁只有两种状态:加锁、解锁。

特点:

  • 原子性: 把互斥量锁定位一个原子操作,这就保证了如果同一时间只会有一个线程锁定共享资源
  • 唯一性: 如果一个线程锁定了某个互斥量,那么只有该线程可以使用这个被锁定的互斥量
  • 非繁忙等待: 如果一个线程锁定了某互斥量,另一个线程又来访问该互斥量,则第二个线程会被挂起,当第一个线程解锁该互斥量后唤醒第二个线程对该互斥量进行访问。
1.4.2.2 条件变量

  与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来锁定一个线程,直到某个特殊的条件发生为止。通常条件变量和互斥锁同时发生。
  条件变量可以是我们睡眠等待某种情况的发生。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

  • 一个线程等待“条件变量的条件成立”而挂起
  • 另一个线程使条件成立

  条件的检测是在互斥锁的保护下进行的。线程在改变条件变量的状态之前必须先锁定互斥量。如果一个条件为假,则线程自动阻塞,并释放等待状态改变的互斥锁。如果另外一个线程改变了条件,它发信号给相关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。

条件变量的操作流程如下:

  • 初始化条件变量
  • 等待条件成立
  • 激活条件变量
  • 清除条件变量
1.4.2.3 信号量

  使用信号量首先我们要清楚临界资源和临界区的概念,临界资源就是同一时刻只允许一个线程(或进程)访问的资源,临界区就是访问临界资源的代码段。
  信号量是一种特殊的变量,用来控制对临界资源的使用,在多个进程或线程都要访问临界资源的时候,就需要控制多个进行或线程对临界资源的使用。
  信号量机制通过p、v操作实现。p操作:原子减1,申请资源,当信号量为0时,p操作阻塞;v操作:原子加1,释放资源。

1.4.2.4 读写锁

  读写锁和互斥锁类似,不过读写锁允许更改的并行性。互斥锁要么是加锁状态,要么是不加锁状态。而读写锁可以有三种状态:读模式下的加锁、写模式下的加锁、不加锁 状态。一次只有一个线程可以占有写模式下的读写锁,但是可以有多个线程占有读模式下的读写锁。

读写锁的特点:

  • 如果有线程读数据,则允许其他线程读数据,但不允许写
  • 如果有线程写数据,则不允许其他线程进行读和写
1.4.2.5 自旋锁

  自旋锁和互斥锁的功能一样,但是互斥锁在线程阻塞时会让出cpu,而自旋锁则不会让出cpu,一直等待,直到得到锁。

1.5 线程死锁

  多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而为了保证同步,也会引入锁机制,并发执行也带来了新的问题–死锁。
  所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

1.5.1 线程死锁的四个必要条件

  以下这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

  1. 互斥条件
      进程要求对所分配的资源(如打印机)进行排他性控制(当一个线程请求打印机时其他线程不能操作的),即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
  2. 不可剥夺条件
      进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
  3. 请求与保持条件
      进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  4. 循环等待条件
      存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等待的资源被P(i+1)占有(i=0,1, …, n-1),Pn等待的资源被P0占有,如图所示。

在这里插入图片描述在这里插入图片描述

1.5.2 破坏线程死锁的方法
  • 预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
  • 避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
  • 检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
  • 解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。

具体操作:

1.5.2.1 预防死锁

  破坏“互斥”条件:“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。
- 破坏“请求并等待”条件:就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。
- 方法一:一次性分配资源,即创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它。
  需要提前知道哪些资源是这些进程或线程需要的,很多情况下不知道需要什么资源,所以这种方式可行性不大,适合知道所有资源使用情况的时候。
- 方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,须先把它先前占有的资源R释放掉,然后才能提出对S的申请,即使它可能很快又要用到资源R。
  适合每个资源之间互不依赖的情况,如果在拥有一个资源时还要拥有另一个资源时不能使用此方法。
- 破坏“不可剥夺”条件:是允许对资源实行抢夺。
- 方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
- 方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。
- 破坏“循环等待”条件:是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。

1.5.2.2 避免死锁

  避免死锁不严格限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。

  • 有序资源分配法
      该算法实现步骤如下: 必须为所有资源统一编号,例如打印机为1、传真机为2、磁盘为3等等。同类资源必须一次申请完,例如打印机和传真机一般为同一个机器,必须同时申请,不同类资源必须按顺序申请。
      例如:

    • 有两个进程P1和P2,有两个资源R1和R2。
    • P1请求资源:R1、R2
    • P2请求资源:R1、R2
      这样就破坏了环路条件,避免了死锁的发生。
  • 银行家算法
      银行家算法(Banker’s Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。流程图如下:


    在这里插入图片描述

  银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。它是最具有代表性的避免死锁的算法。 设进程i提出请求REQUEST [i],则银行家算法按如下规则进行判断:
  1、如果REQUEST [i]<= NEED[i,j],则转(2);否则,出错。
  2、如果REQUEST [i]<= AVAILABLE[i],则转(3);否则,等待。
  3、系统试探分配资源,修改相关数据:
    AVAILABLE[i]-=REQUEST[i];//可用资源数-请求资源数
    ALLOCATION[i]+=REQUEST[i];//已分配资源数+请求资源数
    NEED[i]-=REQUEST[i];//需要资源数-请求资源数
  4、系统执行安全性检查,如安全,则分配成立;否则试探险性分配作废,系统恢复原状,进程等待。

  • 顺序加锁
      当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。 例如以下两个线程就会死锁:
    • Thread 1:
      lock A (when C locked)
      lock B (when C locked)
      wait for C
    • Thread 2:
      wait for A
      wait for B
      lock C (when A locked)
        如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。 例如以下两个线程就不会死锁:
    • Thread 1:
      lock A
      lock B
      lock C
    • Thread 2:
      wait for A
      wait for B
      wait for C
        按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要事先知道所有可能会用到的锁,但总有些时候是无法预知的,所以该种方式只适合特定场景。
1.5.2.3 限时加锁

  限时加锁是线程在尝试获取锁的时候加一个超时时间,若超过这个时间则放弃对该锁请求,并回退并释放所有已经获得的锁,然后等待一段随机的时间再重试

  以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:
    Thread 1: locks A
    Thread 2: locks B
    Thread 1: attempts to lock B but is blocked
    Thread 2:attempts to lock A but is blocked
线程1等待了一段时间超时了Thread 1’s lock attempt on B times out
所以超时后线程1回退并释放了A资源 Thread 1 backs up and releases A as well
等待一段随机的时间,因为等待时间不一样,所以迟早有机会获取到资源
Thread 1 waits randomly (e.g. 257 millis) before retrying.
Thread 2’s lock attempt on A times out Thread 2 backs up and releases B as well Thread 2
waits randomly (e.g. 43 millis) before retrying.

  在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁。
这种方式有两个缺点:
  1、当线程数量少时,该种方式可避免死锁,但当线程数量过多,这些线程的加锁时限相同的概率就高很多,可能会导致超时后重试的死循环
  2、Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。

1.5.2.4 死锁检测

  预防和避免死锁系统开销大且不能充分利用资源,更好的方法是不采取任何限制性措施,而是提供检测和解脱死锁的手段,这就是死锁检测和恢复。

  • 死锁检测数据结构:资源向量(数组),矩阵(多维数组)
    • E是现有资源向量(existing resource vector),代码每种已存在资源的总数
    • A是可用资源向量(available resource vector),那么Ai表示当前可供使用的资源数(即没有被分配的资源)
    • C是当前分配矩阵(current allocation matrix),C的第i行代表Pi当前所持有的每一种类型资源的资源数 R是请求矩阵(request matrix),
    • R的每一行代表P所需要的资源的数量

      在这里插入图片描述

死锁检测步骤:

  1. 寻找一个没有结束标记的进程Pi,对于它而言R矩阵的第i行向量小于或等于A。
  2. 如果找到了这样一个进程,执行该进程,然后将C矩阵的第i行向量加到A中,标记该进程,并转到第1步
  3. 如果没有这样的进程,那么算法终止
  4. 算法结束时,所有没有标记过的进程都是死锁进程。
1.5.2.5 死锁恢复
  1. 利用抢占恢复: 临时将某个资源从它的当前所属进程转移到另一个进程。
    这种做法很可能需要人工干预,主要做法是否可行需取决于资源本身的特性。
  2. 利用回滚恢复: 周期性的将进程的状态进行备份,当发现进程死锁后,根据备份将该进程复位到一个更早的,还没有取得所需的资源的状态,接着就把这些资源分配给其他死锁进程。
  3. 通过杀死进程恢复(不太推荐使用,风险较大): 最直接简单的方式就是杀死一个或若干个进程。尽可能保证杀死的进程可以从头再来而不带来副作用。
1.6 线程异步

  异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
  异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事情。实现异步可以采用多线程技术或则交给另外的进程来处理。

2、JVM的线程与进程

2.1 概述

  Java编写的程序都运行在在Java虚拟机(JVM)中,每用java命令启动一个java应用程序,就会启动一个JVM进程。在同一个JVM进程中,有且只有一个进程,就是它自己。
  在写Java程序时,通常我们管只有一个main函数而没有别的Thread或Runnable的程序叫单线程程序。但是我们写的这个所谓的单线程程序只是JVM这个程序中的一个线程,JVM本身是一个多线程的程序,至少得有一个垃圾收集器线程吧。
  使用Java分析包,检验了一下JVM一启动,里面会有几个线程:

结果:

  • 除了main线程以外,还有四个线程:
  • Finalizer 线程:在垃圾回收之前执行“对象完成”的Java系统线程
  • Signal Dispatcher 线程:为JVM处理本地操作系统信号的Java系统线程
  • Reference Handler 线程:将挂起的对象放到队列中的高优先级Java系统线程。
  • Attach Listener 线程:用户线程

be like this:


在这里插入图片描述

  简单来说,在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。   Java支持多线程,当Java程序执行main方法的时候,就是在执行一个名字叫做main的线程,可以在main方法执行时,开启多个线程A,B,C,多个线程 main,A,B,C同时执行,相互抢夺CPU,Thread类是java.lang包下的一个常用类,每一个Thread类的对象,就代表一个处于某种状态的线程。
2.2 jvm中线程的内存分配

  一个进程可以有多个线程,多个线程共享进程的堆和方法区(JDK 1.8 之后的元空间)资源。但是每个线程有自己的程序计数器虚拟机栈本地方法栈


在这里插入图片描述

2.2.1 程序计数器为什么是私有的?

  首先明确程序计数器的作用:
  字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制。如:顺序执行、选择、循环、异常处理。
  在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程运行到哪了。
  需要注意的是:如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
  所以,程序计数器私有主要是为了线程切换后能够恢复到正确的执行位置。

2.2.2 虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈: 每个Java 方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至完成的过程,就对应一个帧栈在 Java 虚拟机中入栈和出栈的过程。
本地方法栈: 和虚拟机的作用非常相似。区别是:虚拟机为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 native 方法服务。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
  所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

2.2.3 堆和方法区

  堆和方法区是所有线程共享的资源。
  其中堆是进程中最大的一块内存,主要用来存放新创建的对象(所有的对象都在这里分配内存);
  方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。

2.2.4 Java内存分配中的栈

  在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。
  当在一段代码块定义一个变量时,Java就在栈中 为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。栈中的数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会消失。

  1. 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中。
  2. 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
  3. 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

    在这里插入图片描述

  每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧(Stack Frame),对应一次次方法的调用,栈的生命周期和线程一致,在一个时间点上,只有一个活动的栈帧,即当前栈帧,jvm对java栈的操作只有入栈、出栈。

作用: 主管java程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回。

  java虚拟机规范允许java栈的大小是动态或者固定不变的:
  如果采用固定大小的java栈,那每一个线程的栈容量在线程创建的时候独立选定,如果线程请求分配的栈容量超过栈的最大容量,会抛出 StackOverflowError 异常。
  如果java栈是动态扩展的,在尝试扩展时无法申请到足够内存,或创建线程时没有足够的内存去创建对应的栈,会抛出 OutOfMemoryError 异常。

2.3 jvm中线程安全问题
2.3.1 线程安全定义

  当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。
  所以线程安全代码的特征就是代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程下的调用问题,更无须自己实现任何措施来保证多线程环境下的正确调用。

2.3.2 Java中的线程安全

  线程执行代码过程中会使用到堆当作的数据对象,读取并复制一个存在本地变量表中

  • 在执行代码过程中使用的实际上是本地变量中的副本,操作完成之后,在将副本更新到堆内存中
  • 同时有多个线程操作同一个堆中的变量,因为重复操作,或者先后顺序的问题导致线程数据安全问题
    • int i=0;
    • 线程1与线程2同时复制一个副本,并++,再写会堆内存中。导致两次++操作的结果只加了一次
  • 解决办法: 当一个线程在操作一个共享变量时,其他线程不会去操作这个变量
    具体的实现方式就是加锁
    • 原子操作:八个原子操作 CPU在执行过程中不会被打断的操作

      • lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
      • unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后 的变量才可以被其他线程锁定
      • read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存 中,以便随后的load动作使用
      • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工 作内存的变量副本中
      • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
      • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内 存的变量
      • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存 中,以便随后的write的操作
      • write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值 传送到主内存的变量中
    • synchronized 实际上一个java内置的隐式锁 ,加锁与解锁的操作是不可见的

      • 需要一个对象为单位 ,所有线程都可以访问
          Java中的线程安全将以多个线程之间存在共享数据访问为前提来讨论。且为了更好地理解,将线程安全按其的“安全程度”由强至弱来排序,而不是把线程安全当作一个非真即假的事情来看待。因此这里会将Java语言中各种操作共享的数据分为以下五类:不可变绝对线程安全相对线程安全线程兼容线程对立
2.3.2.1 不可变

  在Java语言里面,不可变(Immut able)的对象一定是线程安全的。
  对于不可变对象,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施。
  在之前“final关键字的可见性”时曾经提到过: 只要一个不可变的对象被正确地构建出来(即没有发生this引用逃逸的情况),那其外部的可见状态永远都不会改变,永远都不会看到它在多个线程之中处于不一致的状态。
  “不可变”带来的安全性是最直接、 最纯粹的。
  在Java中,如果多线程共享的数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。
  如果共享数据是一个对象,由于Java语言目前暂时还没有提供值类型的支持,那就需要对象自行保证其行为不会对其状态产生任何影响才行。
  比如java.lang.String类的对象实例,它是一个典型的不可变对象,用户调用它的substring( ) 、replace( )和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
  保证对象行为不影响自己状态的途径有很多种,最简单的一种就是把对象里面带有状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的。
  例如下面代码中所示的java.lang.Integer 构造函数,它通过将内部状态变量value定义为final来保障状态不变:

/**
* The value of the <code>Integer</code>. * @serial
*/ 
private final int value; 
/** 

*  Constructs a newly allocated <code>Integer</code> object that  						
*  represents the specified <code>int</code> value. *  						
*  @param value the value to be represented by the  						
*  <code>Integer</code> object. */  							
public Integer(int value) { 
    this.value = value;  					
}  			

  在Java类库API中符合不可变要求的类型,除了上面提到的String之外,常用的还有枚举类型及java.lang.Number的部分子类,如Long和Double等数值包装类型、BigInteger和BigDecimal等大数据类型。
  而同为Number子类型的原子类AtomicInteger和AtomicLong则是可变的,读者不妨看看这两个原子类的源码,想一想为什么它们要设计成可变的。

2.3.2.2 绝对线程安全

  一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”可能需要付出非常高昂的,甚至不切实际的代价。
在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。
  我们可以通过Java API中一个不是“绝对线程安全”的“线程安全类型”来看看这个语境里的“绝对”究竟是什么意思。
  如果说java.util.Vector是一个线程安全的容器,相信所有的Java程序员对此都不会有异议,因为它的add()、get()和size()等方法都是被synchronized修饰的,尽管这样效率不高,但保证了具备原子性、 可见性和有序性。
  不过,即使它所有的方法都被修饰成synchronized,也不意味着调用它的时候就永远都不再需要同步手段了,看下面的代码:

private static Vector<Integer> vector = new Vector<Integer>();

public static void main(String[] args) { 
    while (true) {
        for (int i = 0; i < 10; i++) { 
            vector.add(i);
        }
        Thread removeThread = new Thread(new Runnable() { 
            @Override
            public void run() {
            	for (int i = 0; i < vector.size(); i++) {
            		vector.remove(i); 
                }
            } 
        });
        
        Thread printThread = new Thread(new Runnable() { 
            @Override
            public void run() {
            	for (int i = 0; i < vector.size(); i++) {
            		System.out.println((vector.get(i))); 
                }
            } 
        });
        removeThread.start(); 
        printThread.start();
        
        //不要同时产生过多的线程,否则会导致操作系统假死
        while (Thread.activeCount() > 20); 
    }
}

结果如下:

Exception in thread "Thread-132" java.lang.ArrayIndexOutOfBoundsException:
Array index out of range: 17
    at java.util.Vector.remove(Vector.java:777)
    at org.fenixsoft.mulithread.VectorTest$1.run(VectorTest.java:21) at java.lang.Thread.run(Thread.java:662)

  很明显 ,尽管这里使用到的Vector的get()、remove()和size()方法都是同步的,但是在多线程的环境中,如果不在方法调用端做额外的同步措施,使用这段代码仍然是不安全的。
  因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号i已经不再可用,再用 i 访问数组就会抛出一个ArrayIndexOutOfBoundsException异常。如果要保证这段代码能正确执行下去,我们不得不把removeThread和printThread的定义添加synchronize来保持同步。
  假如Vector一定要做到绝对的线程安全,那就必须在它内部维护一组一致性的快照访问才行,每次对其中元素进行改动都要产生新的快照,这样要付出的时间和空间成本都是非常大的。

2.3.2.3 相对线程安全

  相对线程安全就是我们通常意义上所讲的线程安全。Java语言中的一些现场安全的容器均是相对线程安全的。
  它需要保证对这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施。
  但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
  在Java语言中,大部分声称线程安全的类都属于这种类型,例如Vector、HashTable、Collections的 synchronizedCollection()方法包装的集合等。

2.3.2.4 线程兼容

  线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对 象在并发环境中可以安全地使用。
  我们平常说一个类不是线程安全的,通常就是指这种情况。Java类库API中大部分的类都是线程兼容的 ,如前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。

2.3.2.5 线程对立

  线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。
  由于Java 语言天生就支持多线程的特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。
一个线程对立的例子是Thread类的suspend()和resume()方法。
  如果有两个线程同时持有一个线程对象,一个尝试去中断线程,一个尝试去恢复线程, 在并发进行的情况下,无论调用时是否进行了同步,目标线程都存在死锁风险——假如suspend()中断的线程就是即将要执行resume()的那个线程,那就 肯定要产生死锁了。
  也正是这个原因,suspend()和resume()方法都已经被声明废弃了。
  常见的线程对立的操作还有System.setIn()、Sytem.setOut()和System.runFinalizersOnExit()等。

2.3.3线程安全的实现方法

  实现线程安全与代码编写有很大的关系,但虚拟机提供的同步和锁机制也起到了至关重要的作用。
  弄明白了Java虚拟机线程安全措施的原理与运作过程,再去思考代码如何编写就不是一件困难的事情了。

2.3.3.1 互斥同步

  互斥同步(Mutual Exclusion & Synchronization)是一种最常见也是最主要的并发正确性保障手段
  同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些, 当使用信号量的时候)线程使用。
  而互斥只是实现同步的一种手段,临界区(Critical Section)、互斥量 (Mutex)和信号量(Semaphore)都是常见的互斥实现方式。
因此在“互斥同步”这四个字里面,互斥是因,同步是果;互斥是方法,同步是目的。

2.3.3.2 synchronized

  在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。
  synchronized关键字经过Javac编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。
  这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。
  如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作为reference;如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。

在执行monitorenter指令时:

  • 首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行monitorexit指令时会将锁计数器的值减一。一旦计数器的值为0 ,锁随即就被释放了。
  • 如果获取锁对象失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。

使用synchronized的注意事项:
  被synchronized修饰的同步块对同一条线程来说是可重入的。这意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况。
  被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。这意味着无法像处理某些数据库中的锁那样,无法强制已获取锁的线程释放锁;也无法强制正在等待锁的线程中断等待或超时退出。
执行成本:
  从执行成本的角度看,持有锁是一个**重量级(Heavy -Weight)**的操作。
  之前我们了解到,Java的线程是映射到操作系统的原生内核线程之上的,如果要阻塞或唤醒一条线程,则需要操作系统来帮忙完成,这就不可避免地陷入用户态到核心态的转换中,进行这种状态转 换需要耗费很多的处理器时间。
  尤其是对于代码特别简单的同步块(譬如被synchronized修饰的getter()或setter()方法),状态转换消耗的时间甚至会比用户代码本身执行的时间还要长。
  因此才说, synchronized是Java语言中一个重量级的操作,有经验的程序员都只会在确实必要的情况下才使用这种操作。而虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程, 以避免频繁地切入核心态之中。

2.3.3.3 JUC

  此外Java类库中还提供了java.util.concurrent包(下文称J.U.C包),其中的java.ut il.concurrent .locks.Lock接口便成了Java的另一种全新的互斥同步手段。
  基于Lock接口,用户能够以非块结构(Non-Block Structured)来实现互斥同步,从而摆脱了语言特性的束缚,改为在类库层面去实现同步,这也为日后扩展出不同调度算法、不同特征、不同性能、不同语义的各种锁提供了广阔的空间。

2.3.3.4 可重入锁ReentrantLock

  重入锁(ReentrantLock)是Lock接口最常见的一种实现,顾名思义,它与synchronized一样是可重入的。
  在基本用法上,ReentrantLock也与synchronized很相似,只是代码写法上稍有区别而已。不过,ReentrantLock与synchronized相比增加了一些高级功能,主要有以下三项:等待可中断、可实现公 平锁及锁可以绑定多个条件

  • 等待可中断: 是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。
  • 公平锁: 是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
      synchronized中的锁是非公平的(解锁后资源是可争抢的),Reent rant Lock在默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平 锁。不过一旦使用了公平锁,将会导致ReentrantLock的性能急剧下降,会明显影响吞吐量。
  • 锁绑定多个条件: 是指一个ReentrantLock对象可以同时绑定多个Condition对象。在synchronized 中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样做,多次调用 newCondition()方法即可。如果需要使用上述功能,使用Reent rant Lock是一个很好的选择。
2.3.3.5 synchronize和ReentrantLock

  综上,ReentrantLock在功能上是synchronized的超集,在性能上又至少不会弱于 synchronized;然而在synchronized与ReentrantLock都可满足需要时,我们仍然推荐优先使用synchronized:
  synchronized是在Java语法层面的同步,足够清晰,也足够简单。每个Java程序员都熟悉 synchronized,但J.U.C中的Lock接口则并非如此。因此在只需要基础的同步功能时,更推荐 synchronized。
  Lock应该确保在finally 块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不会释放持有的锁。这一点必须由程序员自己来保证,而使用synchronized的话则可以由Java虚拟机来确保即使出现异常,锁也能被自动释放。
  从长远来看,Java虚拟机更容易针对synchronized来进行优化,因为Java虚拟机可以在线程和对象的元数据中记录synchronized中锁的相关信息,而使用J.U.C中的Lock的话,Java虚拟机是很难得知具体哪些锁对象是由特定线程锁持有的。

2.3.3.6 非阻塞同步

1. 阻塞同步
  互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,因此这种同步也被称为阻塞同步(Blocking Synchronization)。
  从解决问题的方式上看,互斥同步属于一种悲观的并发策略,其总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享的数据是否真的会出现竞争,它都会进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加 锁),这将会导致用户态到核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等开销。
2. 非阻塞同步
  随着硬件指令集的发展,已经有了另外一个选择: 基于冲突检测的乐观并发策略
  通俗地说就是不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止。
  这种乐观并发策略的实现不再需要把线程阻塞挂起,因此这种同步操作被 称为非阻塞同步(Non-Blocking Synchronization),使用这种措施的代码也常被称为无锁(Lock-Free) 编程。

2.3.3.7 原子指令

  使用乐观并发策略需要“硬件指令集的发展”,必须要求操作和冲突检测这两个步骤具备原子性。
  如果这里再使用互斥同步来保证就完全失去意义了,所以我们只能靠硬件来实现原子性,硬件保证某些从语义上看起来需要多次操作的行为可以只通过一条处理器指令就能完成,这类指令常用的有:

  • 测试并设置 (Test-and-Set ) ;
  • 获取并增加 ( Fetch-and-Increment) ;
  • 交换(Swap);
  • 比较并交换(Compare-and-Swap,下文称CAS);
  • 加载链接/条件储存(Load-Linked/Store-Conditional,下文称LL/SC)。

  其中,前面的三条是20世纪就已经存在于大多数指令集之中的处理器指令,后面的两条是现代处理器新增的,而且这两条指令的目的和功能也是类似的。

2.3.3.7 CAS(比较并交换)

  因为Java里最终暴露出来的是CAS操作,CAS指令需要有三个操作数,分别是:

  • 内存位置(在Java中可以简单地理解为变量的内存地址,用V 表示)
  • 旧的预期值(用A表示)
  • 准备设置的新值(用B表示)。

  CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。
  但不管是否更新了V的值,都会返回V的旧值,上面处理过程是一个原子操作,执行期间不会被其他线程中断。
  在JDK 5之后,Java类库中才开始使用CAS操作,该操作由sun.misc.Unsafe类里面的 compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。
  HotSpot虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程,或者可以认为是无条件内联进去了。
  不过由于Unsafe类在设计上就不是提供给用户程序调用的类 (Unsafe::getUnsafe()的代码中限制了只有启动类加载器(Bootstrap ClassLoader)加载的Class才能访问 它),因此在JDK 9之前只有Java类库可以使用CAS,譬如J.U.C包里面的整数原子类,其中的comp areAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作来实现
  而如果用户程序也有使用CAS操作的需求,那要么就采用反射手段突破Unsafe的访问限制,要么就只能通过Java类库API来间接使用它。直到JDK 9之后,Java类库才在VarHandle类里开放了面向用户程序使用的CAS操作。

CAS例子:
AtomicInteger的incrementAndGet()方法:

/**
* Atomically increment by one the current value. * @return the updated value
*/
public final int incrementAndGet() { 
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next)) 
        	return next; 
    }
}

  incrementAndGet()方法在一个无限循环中,不断尝试将一个比当前值大一的新值赋值给自己。如果失败了,那说明在执行CAS操作的时候,旧值已经发生改变,于是再次循环进行下一次操作,直到设置成功为止。

优缺点:
  尽管CAS既简单又高效,但显然这种操作无法涵盖互斥同步的所有使用场景,并 且CAS从语义上来说并不是真正完美的,它存在逻辑漏洞(ABA问题):
  如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那就能说明它的值没有被其他线程改变过了吗?
  这是不能的,因为如果在这段期间它的值曾经被改成B,后来又被改回为A,那CAS操作就会误认为它从 来没有被改变过。这个漏洞称为CAS操作的“ABA问题”。
  JUC提供了一个带有标记的原子引用类AtomicStamp edReference,它可以通过控制变量值的版本来保证CAS的正确性。
  不过还比较鸡肋,大部分情况下ABA问题不会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更为高效。

2.3.3.8 无同步方案

  要保证线程安全,也并非一定要进行阻塞或非阻塞同步,同步与线程安全两者没有必然的联系。
  同步只是保障存在共享数据争用时正确性的手段,如果能让一个方法本来就不涉及共享数据,那它自然就不需要任何同步措施去保证其正确性,因此会有一些代码天生就是线程安全的。

可重入代码(Reentrant Code)
  这种代码又称纯代码(Pure Code),是指可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误,也不会对结果有所影响(是不是可以理解为不存在对公共变量的读写)。
  在特指多线程的上下文语境里,我们可以认为可重入代码是线程安全代码的一个真子集。
  这意味着相对线程安全来说,可重入性是更为基础的特性,它可以保证代码线程安全,即所有可重入的代码都是线程安全的,但并非所有的线程安全的代码都是可重入的。

可重入代码有一些共同特征,例如:

  1. 不依赖全局变量
  2. 存储在堆上的数据和公用的系统资源,用到的状态量都由参数中传入
  3. 不调用非可重入的方法等

我们可以通过一个比较简单的原则来判断代码是否具备可重入性:
  如果一个方法的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的

2.3.3.8 线程本地存储(Thread Local Storage)

  如果一段代码中所需要的数据必须与其他代码共享,那就要看这些共享数据的代码是否能保证在同一个线程中执行。
  如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
  符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程限制在一个线程中消费完。
  其中最重要的一种应用实例就是经典Web交互模型中的“一个请求对应一个服务器线程(Thread-per-Request)"的处理方式。
  这种处理方式的广泛应用使得很多Web服务端应用都可以使用线程本地存储来解决线程安全问题。

ThreadLocal:
  Java语言中,如果一个变量要被多线程访问,可以使用volatile关键字将它声明为“易变的”;同时java中可以通过java.lang.ThreadLocal类来实现线程本地存储的功能。
  每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口。
  每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值,使用这个值就可以在线程K -V值对 中找回对应的本地线程变量。

2.4 Java线程实现
2.4.1 创建一个线程

Java 提供了三种创建线程的方法:

  • 通过实现 Runnable 接口;
  • 通过继承 Thread 类本身;
  • 通过 Callable 和 Future 创建线程。
2.4.2 通过实现 Runnable 接口来创建线程

创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。为了实现 Runnable,一个类只需要执行一个方法调用 run(),声明如下:

public void run()

也可以重写该方法, run() 可以调用其他方法,使用其他类,并声明变量,就像主线程一样。

在创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。

Thread 定义了几个构造方法,经常使用的:

Thread(Runnable threadOb,String threadName);

这里,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。

新线程创建之后,你调用它的 start() 方法它才会运行。

void start();

创建线程并执行实例:

class RunnableDemo implements Runnable {
   private Thread t;
   private String threadName;
   
   RunnableDemo( String name) {
      threadName = name;
      System.out.println("Creating " +  threadName );
   }
   
   public void run() {
      System.out.println("Running " +  threadName );
      try {
         for(int i = 4; i > 0; i--) {
            System.out.println("Thread: " + threadName + ", " + i);
            // 让线程睡眠一会
            Thread.sleep(50);
         }
      }catch (InterruptedException e) {
         System.out.println("Thread " +  threadName + " interrupted.");
      }
      System.out.println("Thread " +  threadName + " exiting.");
   }
   
   public void start () {
      System.out.println("Starting " +  threadName );
      if (t == null) {
         t = new Thread (this, threadName);
         t.start ();
      }
   }
}
 
public class TestThread {
 
   public static void main(String args[]) {
      RunnableDemo R1 = new RunnableDemo( "Thread-1");
      R1.start();
      
      RunnableDemo R2 = new RunnableDemo( "Thread-2");
      R2.start();
   }   
}

编译以上程序运行结果如下:

Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.
2.4.3 通过继承Thread来创建线程

创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。

继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。

该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。

实例

class ThreadDemo extends Thread {
   private Thread t;
   private String threadName;
   
   ThreadDemo( String name) {
      threadName = name;
      System.out.println("Creating " +  threadName );
   }
   
   public void run() {
      System.out.println("Running " +  threadName );
      try {
         for(int i = 4; i > 0; i--) {
            System.out.println("Thread: " + threadName + ", " + i);
            // 让线程睡眠一会
            Thread.sleep(50);
         }
      }catch (InterruptedException e) {
         System.out.println("Thread " +  threadName + " interrupted.");
      }
      System.out.println("Thread " +  threadName + " exiting.");
   }
   
   public void start () {
      System.out.println("Starting " +  threadName );
      if (t == null) {
         t = new Thread (this, threadName);
         t.start ();
      }
   }
}
 
public class TestThread {
 
   public static void main(String args[]) {
      ThreadDemo T1 = new ThreadDemo( "Thread-1");
      T1.start();
      
      ThreadDemo T2 = new ThreadDemo( "Thread-2");
      T2.start();
   }   
}

编译以上程序运行结果如下:

Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.
2.4.4 Thread 方法

下表列出了Thread类的一些重要方法:

方法描述:

  1. public void start()
    使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
  2. public void run()
    如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
  3. public final void setName(String name)
    改变线程名称,使之与参数 name 相同。
  4. public final void setPriority(int priority)
    更改线程的优先级。
  5. public final void setDaemon(boolean on)
    将该线程标记为守护线程或用户线程。
  6. public final void join(long millisec)
    等待该线程终止的时间最长为 millis 毫秒。
  7. public void interrupt()
    中断线程。
  8. public final boolean isAlive()
    测试线程是否处于活动状态。

上述方法是被 Thread 对象调用的,下面表格的方法是 Thread 类的静态方法。

方法描述:

  1. public static void yield()
    暂停当前正在执行的线程对象,并执行其他线程。
  2. public static void sleep(long millisec)
    在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
  3. public static boolean holdsLock(Object x)
    当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
  4. public static Thread currentThread()
    返回对当前正在执行的线程对象的引用。
  5. public static void dumpStack()
    将当前线程的堆栈跟踪打印至标准错误流。

实例,如下的ThreadClassDemo 程序演示了Thread类的一些方法:

DisplayMessage.java 文件代码:

// 文件名 : DisplayMessage.java
// 通过实现 Runnable 接口创建线程
public class DisplayMessage implements Runnable {
   private String message;
   
   public DisplayMessage(String message) {
      this.message = message;
   }
   
   public void run() {
      while(true) {
         System.out.println(message);
      }
   }
}

GuessANumber.java 文件代码:

// 文件名 : GuessANumber.java
// 通过继承 Thread 类创建线程
 
public class GuessANumber extends Thread {
   private int number;
   public GuessANumber(int number) {
      this.number = number;
   }
   
   public void run() {
      int counter = 0;
      int guess = 0;
      do {
         guess = (int) (Math.random() * 100 + 1);
         System.out.println(this.getName() + " guesses " + guess);
         counter++;
      } while(guess != number);
      System.out.println("** Correct!" + this.getName() + "in" + counter + "guesses.**");
   }
}

ThreadClassDemo.java 文件代码:

// 文件名 : ThreadClassDemo.java
public class ThreadClassDemo {
 
   public static void main(String [] args) {
      Runnable hello = new DisplayMessage("Hello");
      Thread thread1 = new Thread(hello);
      thread1.setDaemon(true);
      thread1.setName("hello");
      System.out.println("Starting hello thread...");
      thread1.start();
      
      Runnable bye = new DisplayMessage("Goodbye");
      Thread thread2 = new Thread(bye);
      thread2.setPriority(Thread.MIN_PRIORITY);
      thread2.setDaemon(true);
      System.out.println("Starting goodbye thread...");
      thread2.start();
 
      System.out.println("Starting thread3...");
      Thread thread3 = new GuessANumber(27);
      thread3.start();
      try {
         thread3.join();
      }catch(InterruptedException e) {
         System.out.println("Thread interrupted.");
      }
      System.out.println("Starting thread4...");
      Thread thread4 = new GuessANumber(75);
      
      thread4.start();
      System.out.println("main() is ending...");
   }
}

运行结果如下,每一次运行的结果都不一样。

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

实例:

public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的线程").start();  
            }  
        }  
        try  
        {  
            System.out.println("子线程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  
  
    }
    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
}
2.4.6 创建线程的三种方式的对比
  1. 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。

  2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。

二、Java线程编程实操

实例一

需求 :

  对num进行求和,三个独立线程求和,得到稳定的结果。

代码 :
public class TreadManage_v1 {

    static int num = 0;
    static Object obj = new Object ();

    public static void main(String[] args){
        ACount a1 = new ACount ();
        BCount a2 = new BCount ();
        CCount a3 = new CCount ();
        ACount a4 = new ACount ();
        ACount a5 = new ACount ();
        ACount a6 = new ACount ();
        BCount a7 = new BCount ();
        CCount a8 = new CCount ();
        ACount a9 = new ACount ();
        BCount a10 = new BCount ();

        long start = System.currentTimeMillis ();
        a1.start ();
        a2.start ();
        a3.start ();
        a4.start ();
        a5.start ();
        a6.start ();
        a7.start ();
        a8.start ();
        a9.start ();
        a10.start ();
        try {
            a1.join ();
            a2.join ();
            a3.join ();
            a4.join ();
            a5.join ();
            a6.join ();
            a7.join ();
            a8.join ();
            a9.join ();
            a10.join ();
        } catch (InterruptedException e) {
            throw new RuntimeException (e);
        }
        long end = System.currentTimeMillis ();
        System.out.println (TreadManage_v1.num + " 耗时:" + (end - start));
    }

}

//不加锁

class ACount extends Thread{
    @Override
    public void run(){
        System.out.println ("TrheadA start...");
        for(int i = 0; i < 1000; i++){
            try {
                Thread.sleep (1);
            } catch (InterruptedException e) {
                throw new RuntimeException (e);
            }
            TreadManage_v1.num++;
        }
        System.out.println ("ThreadA end..");
    }
}

class BCount extends Thread{
    @Override
    public void run(){
        System.out.println ("TrheadB start...");
        System.out.println ("TrheadB run...");
        for(int i = 0; i < 2000; i++){
            try {
                Thread.sleep (1);
            } catch (InterruptedException e) {
                throw new RuntimeException (e);
            }
            TreadManage_v1.num++;
        }

        System.out.println ("ThreadB end..");
    }
}

class CCount extends Thread{
    @Override
    public void run(){
        System.out.println ("TrheadC start...");
        for(int i = 0; i < 500; i++){
            try {
                Thread.sleep (1);
            } catch (InterruptedException e) {
                throw new RuntimeException (e);
            }
            TreadManage_v1.num++;
        }
        System.out.println ("ThreadC end..");
    }

//synchronized锁加在循环内

//class ACount extends Thread{
//    @Override
//    public void run(){
//        System.out.println ("TrheadA start...");
//        for(int i = 0; i < 1000; i++){
//            synchronized (TreadManage_v1.obj) {
//                try {
//                    Thread.sleep (1);
//                } catch (InterruptedException e) {
//                    throw new RuntimeException (e);
//                }
//                TreadManage_v1.num++;
//            }
//        }
//        System.out.println ("ThreadA end..");
//    }
//}
//
//class BCount extends Thread{
//    @Override
//    public void run(){
//        System.out.println ("TrheadB start...");
//        System.out.println ("TrheadB run...");
//        for(int i = 0; i < 2000; i++){
//            synchronized (TreadManage_v1.obj) {
//                try {
//                    Thread.sleep (1);
//                } catch (InterruptedException e) {
//                    throw new RuntimeException (e);
//                }
//                TreadManage_v1.num++;
//            }
//        }
//
//        System.out.println ("ThreadB end..");
//    }
//}
//
//class CCount extends Thread{
//    @Override
//    public void run(){
//        System.out.println ("TrheadC start...");
//        for(int i = 0; i < 500; i++){
//            synchronized (TreadManage_v1.obj) {
//                try {
//                    Thread.sleep (1);
//                } catch (InterruptedException e) {
//                    throw new RuntimeException (e);
//                }
//                TreadManage_v1.num++;
//            }
//        }
//        System.out.println ("ThreadC end..");
//    }

    //synchronized锁加在循环外

//class ACount extends Thread{
//    @Override
//    public void run(){
//        System.out.println ("TrheadA start...");
//        System.out.println ("TrheadA run...");
//        synchronized (TreadManage_v1.obj) {
//            for(int i = 0; i < 1000; i++){
//                    try {
//                        Thread.sleep (1);
//                    } catch (InterruptedException e) {
//                        throw new RuntimeException (e);
//                    }
//                    TreadManage_v1.num++;
//            }
//        }
//        System.out.println ("ThreadA end..");
//    }
//}
//
//class BCount extends Thread{
//    @Override
//    public void run(){
//        System.out.println ("TrheadB start...");
//        System.out.println ("TrheadB run...");
//        synchronized (TreadManage_v1.obj) {
//            for(int i = 0; i < 2000; i++){
//
//                    try {
//                        Thread.sleep (1);
//                    } catch (InterruptedException e) {
//                        throw new RuntimeException (e);
//                    }
//                    TreadManage_v1.num++;
//            }
//        }
//
//        System.out.println ("ThreadB end..");
//    }
//}
//
//class CCount extends Thread{
//    @Override
//    public void run(){
//        System.out.println ("TrheadC start...");
//        System.out.println ("TrheadC run...");
//        synchronized (TreadManage_v1.obj) {
//            for(int i = 0; i < 500; i++){
//                    try {
//                        Thread.sleep (1);
//                    } catch (InterruptedException e) {
//                        throw new RuntimeException (e);
//                    }
//                    TreadManage_v1.num++;
//            }
//        }
//        System.out.println ("ThreadC end..");
//    }
}

结果 :
不加锁

结果一:

TrheadA start...
TrheadA start...
TrheadC start...
TrheadA start...
TrheadB start...
TrheadC start...
TrheadA start...
TrheadB start...
TrheadA start...
TrheadB run...
TrheadB start...
TrheadB run...
TrheadB run...
ThreadC end..
ThreadC end..
ThreadA end..
ThreadA end..
ThreadA end..
ThreadA end..
ThreadA end..
ThreadB end..
ThreadB end..
ThreadB end..
8967 耗时:3033

进程已结束,退出代码0

结果二:

TrheadA start...
TrheadA start...
TrheadA start...
TrheadC start...
TrheadA start...
TrheadC start...
TrheadB start...
TrheadB start...
TrheadB run...
TrheadA start...
TrheadB run...
TrheadB start...
TrheadB run...
ThreadC end..
ThreadC end..
ThreadA end..
ThreadA end..
ThreadA end..
ThreadA end..
ThreadA end..
ThreadB end..
ThreadB end..
ThreadB end..
8619 耗时:3094

进程已结束,退出代码0

可以发现线程不加锁,每次结果都不一样,无法保证线程安全。

加锁
加锁(循环内)
TrheadA start...
TrheadA start...
TrheadA start...
TrheadA start...
TrheadC start...
TrheadB start...
TrheadA start...
TrheadC start...
TrheadB start...
TrheadB run...
TrheadB start...
TrheadB run...
TrheadB run...
ThreadC end..
ThreadA end..
ThreadA end..
ThreadC end..
ThreadA end..
ThreadA end..
ThreadB end..
ThreadA end..
ThreadB end..
ThreadB end..
12000 耗时:15450

进程已结束,退出代码0
加锁(循环外)
TrheadA start...
TrheadC start...
TrheadC run...
TrheadA start...
TrheadA run...
TrheadB start...
TrheadB start...
TrheadB run...
TrheadA start...
TrheadA run...
TrheadA start...
TrheadA run...
TrheadA run...
TrheadC start...
TrheadC run...
TrheadB start...
TrheadB run...
TrheadA start...
TrheadA run...
TrheadB run...
ThreadC end..
ThreadB end..
ThreadA end..
ThreadB end..
ThreadC end..
ThreadA end..
ThreadA end..
ThreadA end..
ThreadB end..
ThreadA end..
12000 耗时:15515

进程已结束,退出代码0

可以看到加锁之后程序每次都可以得出正确的答案。且锁加在循环之外的运行时间是要短于循环内的。(我猜测是在循环内枷锁线程需要不断竞争num切换次数比单独完成一个线程循环的竞争切换次数要少。)

实例二

需求 :

  任务: task num=0; 1:num+=10; 2: num*=20; 3:num*=num; 保证任务顺序
  结果:40000
  每个环节用单个线程处理
  类似的任务: 500 个。

代码 :
import java.util.ArrayList;
// 任务: task num=0; 1: num+=10; 2: num*=20; 3: num*=num; // 结果:40000
// 每个环节用单个线程处理
// 类似的任务: 500 个
public class TreadManage_v2 {
    public static class TaskThread {
        public static void main(String[] args) {
            ArrayList<Task> tasks = new ArrayList<>();
            for (int i = 0; i < 500; i++) {
                tasks.add(new Task());
            }
            // 启动线程
            ThreadA ta = new ThreadA(tasks);
            ThreadB tb = new ThreadB(tasks);
            ThreadC tc = new ThreadC(tasks);

            tb.start();
            tc.start();
            ta.start();
            try {
                Thread.sleep (2000);
            } catch (InterruptedException e) {
                throw new RuntimeException (e);
            }

            // 等待结束
            try {
                ta.join();
                tb.join();
                tc.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            for (int i = 0; i < tasks.size(); i++) {
                System.out.println(tasks.get(i).num);
            }
        }
    }
}


class Task{

    int num;
    // A B C = 40000
    // A C B = 2000
    volatile boolean flagA;
    volatile boolean flagB;
    volatile boolean flagC;

    public void taskA(){

        if(!flagA){
            num += 10;
            flagA = true;
        }

    }

    public void taskB(){
//        while(!flagA){//阻塞
//
//        }
        if(flagA && !flagB){
            num *= 20;
            flagB = true;
        }

    }

    public void taskC(){
//        while(!flagB){
//        }
        if(flagB && !flagC){
            num *= num;
            flagC = true;
        }

    }

}

class ThreadA extends Thread {
    ArrayList<Task> tasks;
    // 构造方法
    public ThreadA(ArrayList<Task> tasks) {
        this.tasks = tasks;
    }

    @Override
    public void run() {
        while (true) {
            System.out.println("ThreadA");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            int count = 0;
            for (int i = 0; i < tasks.size(); i++) {
                Task task = tasks.get(i);
                task.taskA();
                if (task.flagA) {
                    count++;
                }
            }
            System.out.println("ThreadA" + count);
            if (count == 500) {
                break;// 结束当前线程
            }
        }
    }
}

class ThreadB extends Thread {
    ArrayList<Task> tasks;

    public ThreadB(ArrayList<Task> tasks) {
        this.tasks = tasks;
    }

    @Override
    public void run() {

        while (true) {
            System.out.println("ThreadB");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            int count = 0;
            for (int i = 0; i < tasks.size(); i++) {
                Task task = tasks.get(i);
                task.taskB();
                if (task.flagB) {
                    count++;
                }
            }
            System.out.println("ThreadB =" + count);
            if (count == 500) {
                break;
            }
        }
    }
}

class ThreadC extends Thread {
    ArrayList<Task> tasks;

    public ThreadC(ArrayList<Task> tasks) {
        this.tasks = tasks;
    }

    @Override
    public void run() {
        while (true) {
            System.out.println("ThreadC");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            int count = 0;
            for (int i = 0; i < tasks.size(); i++) {
                Task task = tasks.get(i);
                task.taskC();
                if (task.flagC) {
                    count++;
                }
            }
            System.out.println("ThreadC =" + count);
            if (count == 500) {
                break;
            }
        }
    }
}

结果 :
ThreadB
ThreadC
ThreadA
ThreadA500
ThreadC =0
ThreadC
ThreadB =0
ThreadB
ThreadC =0
ThreadC
ThreadB =500
ThreadC =500
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000
40000

分析 :

本实例实际上是通过代码的编写来保证线程以及线程相应顺序。


在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值