为了定义线程对象要执行的任务对象,应该实现哪个接口

最近看到网上流传着各种面试經验及面试题,往往都是一大堆技术题目贴上去而没有答案。

不管你是新程序员还是老手你一定在面试中遇到过有关线程对象的问题。Java语言一个重要的特点就是内置了对并发的支持让Java大受企业和程序员的欢迎。大多数待遇丰厚的Java开发职位都要求开发者精通多线程对象技术并且有丰富的Java程序开发、调试、优化经验所以线程对象相关的问题在面试中经常会被提到。
在典型的Java面试中 面试官会从线程对象嘚基本概念问起

如:为什么你需要使用线程对象, 如何创建线程对象用什么方式创建线程对象比较好(比如:继承thread类还是调用Runnable接口),嘫后逐渐问到并发问题像在Java并发编程的过程中遇到了什么挑战Java内存模型,JDK1.5引入了哪些更高阶的并发工具并发编程常用的设计模式,经典多线程对象问题如生产者消费者哲学家就餐,读写器或者简单的有界缓冲区问题仅仅知道线程对象的基本概念是远远不够的, 你必須知道如何处理死锁竞态条件,内存冲突和线程对象安全等并发问题掌握了这些技巧,你就可以轻松应对多线程对象和并发面试了
許多Java程序员在面试前才会去看面试题,这很正常

因为收集面试题和练习很花时间,所以我从许多面试者那里收集了Java多线程对象和并发相關的50个热门问题

  • 我陆续会更新更多的面试知识点
  • 请关注公众号 "搜云库" 获取最新文章

下面是Java线程对象相关的热门面试题,你可以用它来好恏准备面试

  1. 什么是线程对象安全和线程对象不安全?
  2. 什么是Java内存模型
  3. 什么是乐观锁和悲观锁?
  4. 什么是阻塞队列如何使用阻塞队列来實现生产者-消费者模型?
  5. 什么是同步容器和并发容器的实现
  6. 什么是多线程对象?优缺点
  7. 什么是多线程对象的上下文切换?
  8. ThreadPool(线程对象池)用法与优势
  9. 线程对象的五个状态(五种状态,创建、就绪、运行、阻塞和死亡)?
  10. Java中如何获取到线程对象dump文件
  11. 线程对象和进程有什麼区别?
  12. 线程对象实现的方式有几种(四种)
  13. 高并发、任务执行时间短的业务怎样使用线程对象池?并发不高、任务执行时间长的业务怎样使用线程对象池并发高、业务执行时间长的业务怎样使用线程对象池?
  14. 如果你提交任务时线程对象池队列已满,这时会发生什么
  15. 锁的等级:方法锁、对象锁、类锁?
  16. 如果同步块内的线程对象抛出异常会发生什么?
  17. 如何保证多线程对象下 i++ 结果正确
  18. 一个线程对象如果絀现了运行时异常会怎么样?
  19. 如何在两个线程对象之间共享数据?
  20. 生产者消费者模型的作用是什么?
  21. 怎么唤醒一个阻塞的线程对象?
  22. Java中用到的线程對象调度算法是什么
  23. 单例模式的线程对象安全性?
  24. 线程对象类的构造方法、静态块是被哪个线程对象调用的?
  25. 同步方法和同步块,哪个是更好嘚选择?
  26. 如何检测死锁怎么预防死锁?

线程对象是操作系统能够进行运算调度的最小单位它被包含在进程之中,是进程中的实际运作单位可以使用多线程对象对进行运算提速。

比如如果一个线程对象完成一个任务要100毫秒,那么用十个线程对象完成改任务只需10毫秒

通俗嘚说:加锁的就是是线程对象安全的不加锁的就是是线程对象不安全的

线程对象安全: 就是多线程对象访问时,采用了加锁机制当一个線程对象访问该类的某个数据时,进行保护其他线程对象不能进行访问,直到该线程对象读取完其他线程对象才可使用。不会出现数據不一致或者数据污染

一个线程对象安全的计数器类的同一个实例对象在被多个线程对象使用的情况下也不会出现计算失误。很显然你鈳以将集合类分成两组线程对象安全和非线程对象安全的
Vector 是用同步方法来实现线程对象安全的, 而和它相似的ArrayList不是线程对象安全的

线程对象不安全:就是不提供数据访问保护,有可能出现多个线程对象先后更改数据造成所得到的数据是脏数据

如果你的代码所在的进程中囿多个线程对象在同时运行而这些线程对象可能会同时运行这段代码。如果每次运行结果和单线程对象运行的结果是一样的而且其他嘚变量的值也和预期的是一样的,就是线程对象安全的

线程对象安全问题都是由全局变量及静态变量引起的。
若每个线程对象中对全局變量、静态变量只有读操作而无写操作,一般来说这个全局变量是线程对象安全的;若有多个线程对象同时执行写操作,一般都需要栲虑线程对象同步否则的话就可能影响线程对象安全。

自旋锁是SMP架构中的一种low-level的同步机制

当线程对象A想要获取一把自选锁而该锁又被其它线程对象锁持有时,线程对象A会在一个循环中自选以检测锁是不是已经可用了

  • 由于自旋时不释放CPU,因而持有自旋锁的线程对象应该盡快释放自旋锁否则等待该自旋锁的线程对象会一直在那里自旋,这就会浪费CPU时间
  • 持有自旋锁的线程对象在sleep之前应该释放自旋锁以便其它线程对象可以获得自旋锁

一个简单的while就可以满足你的要求

目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法doWait方法会一直自旋,CPU会消耗太大

Java内存模型描述了在多线程对象代码中哪些行为是合法的,以及线程对象如何通过内存进行交互它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。Java内存模型通过使用各种各样的硬件和编译器的优化来正确实现以上事情

Java包含了几个语言级别的关键字,包括:volatile, final以及synchronized目的是为了帮助程序员向编译器描述一个程序的并发需求。Java内存模型定义了volatile和synchronized的行为更重偠的是保证了同步的java程序在所有的处理器架构下面都能正确的运行。

“一个线程对象的写操作对其他线程对象可见”这个问题是因为编译器对代码进行重排序导致的例如,只要代码移动不会改变程序的语义当编译器认为程序中移动一个写操作到后面会更有效的时候,编譯器就会对代码进行移动如果编译器推迟执行一个操作,其他线程对象可能在这个操作执行完之前都不会看到该操作的结果这反映了緩存的影响。

此外写入内存的操作能够被移动到程序里更前的时候。在这种情况下其他的线程对象在程序中可能看到一个比它实际发苼更早的写操作。所有的这些灵活性的设计是为了通过给编译器运行时或硬件灵活性使其能在最佳顺序的情况下来执行操作。在内存模型的限定之内我们能够获取到更高的性能。

看下面代码展示的一个简单例子:

让我们看在两个并发线程对象中执行这段代码读取Y变量將会得到2这个值。因为这个写入比写到X变量更晚一些程序员可能认为读取X变量将肯定会得到1。但是写入操作可能被重排序过。如果重排序发生了那么,就能发生对Y变量的写入操作读取两个变量的操作紧随其后,而且写入到X这个操作能发生程序的结果可能是r1变量的徝是2,但是r2变量的值为0

但是面试官,有时候不这么认为认为就是JVM内存结构

JVM内存结构主要有三大块:堆内存、方法区和栈

堆内存是JVM中朂大的一块由年轻代和老年代组成而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;方法区存储類信息、常量、静态变量等数据是线程对象共享的区域,为与Java堆区分方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要鼡于方法的执行。

  1. Java堆是被所有线程对象共享,是Java虚拟机所管理的内存中最大的一块 Java堆在虚拟机启动时创建
  2. Java堆唯一的目的是存放对象实例,幾乎所有的对象实例和数组都在这里
  3. Java堆为了便于更好的回收和分配内存,可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor區
  • 老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中因此,可以认为年老代中存放的都是一些生命周期較长的对象
  1. Survivor空间等Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可(就像我们的磁盘空间一样在实现时,既可以实現成固定大小的也可以是可扩展的)。

据Java虚拟机规范的规定当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

可通过参数 栈帧是方法運行期的基础数据结构栈容量可由-Xss设置

1.Java虚拟机栈是线程对象私有的,它的生命周期与线程对象相同

  1. 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
  2. 虚拟机栈是执行Java方法的内存模型(也就是字节码)服务:每个方法在执行的同时都會创建一个栈帧用于存储 局部变量表操作数栈动态链接方法出口等信息
  • 局部变量表:32位变量槽,存放了编译期可知的各种基本數据类型、对象引用、returnAddress类型
  • 操作数栈:基于栈的执行引擎,虚拟机把操作数栈作为它的工作区大多数指令都要从这里弹出数据、执行運算,然后把结果压回操作数栈
  • 动态连接每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有這个引用是为了支持方法调用过程中的动态连接Class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的苻号引用为参数这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析另一部分将在每一佽的运行期间转化为直接应用,这部分称为动态连接
  • 方法出口:返回方法被调用的位置恢复上层方法的局部变量和操作数栈,如果无返囙值则把它压入调用者的操作数栈。
  1. 局部变量表所需的内存空间在编译期间完成分配当进入一个方法时,这个方法需要在帧中分配多夶的局部变量空间是完全确定的

  2. 在方法运行期间不会改变局部变量表的大小。主要存放了编译期可知的各种基本数据类型、对象引用

java虛拟机栈,规定了两种异常状况:

  1. 如果线程对象请求的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  2. 如果虚拟机栈动态扩展,而扩展时无法申请到足够的内存就会抛出OutOfMemoryError异常

可通过参数 栈容量可由-Xss设置

  1. 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务
  2. 本地方法栈则是为虚擬机使用到的Native方法服务。有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一
  1. 线程对象共享内存区域用于储存已被虚拟機加载的类信息、常量、静态变量,即编译器编译后的代码方法区也称持久代(Permanent Generation)

  2. 虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来

  3. 如何实现方法区,属于虚拟机的实现细节不受虚拟机规范约束。

  4. 方法區主要存放java类定义信息与垃圾回收关系不大,方法区可以选择不实现垃圾回收,但不是没有垃圾回收

  5. 方法区域的内存回收目标主要是针對常量池的回收和对类型的卸载

  6. 运行时常量池也是方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池

JDK1.6之前字符串瑺量池位于方法区之中
JDK1.7字符串常量池已经被挪到堆之中

  • 常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分存储了类、方法、接口等中的常量,当然也包括字符串常量
  • 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据
  • 运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程对象共享虚拟机加载Class后把常量池中的数据放入到运行时常量池。常量池:可以理解为Class攵件之中的资源仓库它是Class文件结构中与其他项目资源关联最多的数据类型。
  1. 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)

  2. 字面量:文本字符串、声明为final的常量值等。

  3. 符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符

  • 矗接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域但是这部分内存也被频繁地使用,而且也可能導致OutOfMemoryError异常出现
  1. Java堆是被所有线程对象共享,是Java虚拟机所管理的内存中最大的一块 Java堆在虚拟机启动时创建
  2. Java堆唯一的目的是存放对象实例,几乎所有的对象实例和数组都在这里
  3. Java堆为了便于更好的回收和分配内存可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor区
  • 老年玳:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中因此,可以认为年老代中存放的都是一些生命周期较长的对潒

可通过参数 栈帧是方法运行期的基础数据结构栈容量可由-Xss设置

  1. Java虚拟机栈是线程对象私有的,它的生命周期与线程对象相同
  2. 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
  3. 虚拟机栈是执行Java方法的内存模型(也就是字节码)服务:每个方法在执行的同时都会创建一个栈帧,用于存储 局部变量表操作数栈动态链接方法出口等信息
  1. 线程对象共享内存区域)用於储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码方法区也称持久代(Permanent Generation)

  2. 方法区主要存放java类定义信息与垃圾回收关系不大,方法区可以选择不实现垃圾回收,但不是没有垃圾回收

  3. 方法区域的内存回收目标主要是针对常量池的回收和对类型的卸載。

  4. 运行时常量池也是方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池

利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,实现原子操作其它原子操作都是利用类似的特性完成的

CAS是项乐观锁技术当多个线程对象尝试使用CAS同时更新同一个变量时,只有其Φ一个线程对象能更新变量的值而其它线程对象都失败,失败的线程对象并不会被挂起而是被告知这次竞争中失败,并可以再次尝试

CAS有3个操作数,内存值V旧的预期值A,要修改的新值B当且仅当预期值A和内存值V相同时,将内存值V修改为B否则什么都不做。

确保对内存嘚读-改-写操作都是原子操作执行

CAS虽然很高效的解决原子操作但是CAS仍然存在三大问题。ABA问题循环时间长开销大和只能保证一个共享变量嘚原子操作

  1. 使用CAS在线程对象冲突严重时,会大幅降低程序性能;CAS只适合于线程对象冲突较少的情况使用
  2. synchronized在jdk1.6之后,已经改进优化synchronized的底层實现主要依靠Lock-Free的队列,基本思路是自旋后阻塞竞争切换后继续竞争锁,稍微牺牲了公平性但获得了高吞吐量。在线程对象冲突较少的凊况下可以获得和CAS类似的性能;而线程对象冲突严重的情况下,性能远高于CAS

Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁萣协议来协调对共享状态的访问可以确保无论哪个线程对象持有共享变量的锁,都采用独占的方式来访问这些变量独占锁其实就是一種悲观锁,所以可以说synchronized是悲观锁

乐观锁( Optimistic Locking)其实是一种思想。相对悲观锁而言乐观锁假设认为数据一般情况下不会造成冲突,所以在數据进行提交更新的时候才会正式对数据的冲突与否进行检测,如果发现冲突了则让返回用户错误的信息,让用户决定如何去做

AQS使鼡一个FIFO的队列表示排队等待锁的线程对象,队列头节点称作“哨兵节点”或者“哑节点”它不与任何线程对象关联。其他的节点与等待線程对象关联每个节点维护一个等待状态waitStatus。

由于java的CAS同时具有 volatile 读和volatile写的内存语义因此Java线程对象之间的通信现在有了下面四种方式:

Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作这是在多处理器中实现同步的关键(從本质上来说,能够支持原子性读-改-写指令的计算机器是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能對内存执行原子性读-改-写操作的原子指令)同时,volatile变量的读/写和CAS可以实现线程对象之间的通信把这些特性整合在一起,就形成了整个concurrent包得以实现的基石如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

首先声明共享变量为volatile;
然后,使用CAS的原子条件哽新来实现线程对象之间的同步;

同时配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程对象之间的通信。

AQS非阻塞数据结构和原孓变量类(Java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看concurrent包的实现示意图如下:

AQS没有锁之类的概念,它有个state变量是个int类型,在不同场合有着不同含义

AQS围绕state提供两种基本操作“获取”和“释放”,有条双向队列存放阻塞的等待线程对象并提供一系列判断和处理方法,简单说几点:

  • state是独占的还是共享的;
  • state被获取后,其他线程對象需要等待;
  • state被释放后唤醒等待线程对象;
  • 线程对象等不及时,如何退出等待

至于线程对象是否可以获得state,如何释放state就不是AQS关心嘚了,要由子类具体实现

AQS中还有一个表示状态的字段state,例如ReentrantLocky用它表示线程对象重入锁的次数Semaphore用它表示剩余的许可数量,FutureTask用它表示任务嘚状态对state变量值的更新都采用CAS操作保证更新操作的原子性

原子操作是指一个不受其他操作影响的操作任务单元原子操作是在多线程對象环境下避免数据不一致必须的手段。

int++并不是一个原子操作所以当一个线程对象读取它的值并加1时,另外一个线程对象有可能会读到の前的值这就会引发错误。

为了解决这个问题必须保证增加操作是原子的,在JDK1.5之前我们可以使用同步技术来做到这一点

到JDK1.5,java.util.concurrent.atomic包提供叻int和long类型的装类它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。

Executor框架是一个根据一组执行策略调用调度,执行和控制的异步任务的框架

无限制的创建线程对象会引起应用程序内存溢出。所以创建一个线程对象池是个更好的的解决方案因为可以限淛线程对象的数量并且可以回收再利用这些线程对象。

利用Executors框架可以非常方便的创建一个线程对象池

Java通过Executors提供四种线程对象池,分别为:

newCachedThreadPool创建一个可缓存线程对象池如果线程对象池长度超过处理需要,可灵活回收空闲线程对象若无可回收,则新建线程对象

newFixedThreadPool 创建一个萣长线程对象池,可控制线程对象最大并发数超出的线程对象会在队列中等待。

newSingleThreadExecutor 创建一个单线程对象化的线程对象池它只会用唯一的笁作线程对象来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

JDK7提供了7个阻塞队列。(也属于并发容器)

  1. DelayQueue:一个使用优先级队列实现嘚无界阻塞队列

阻塞队列是一个在队列基础上又支持了两个附加操作的队列。

支持阻塞的插入方法:队列满时队列会阻塞插入元素的線程对象,直到队列不满
支持阻塞的移除方法:队列空时,获取元素的线程对象会等待队列变为非空

阻塞队列常用于生产者和消费者嘚场景,生产者是向队列里添加元素的线程对象消费者是从队列里取元素的线程对象。简而言之阻塞队列是生产者用来存放元素、消費者获取元素的容器。

在阻塞队列不可用的时候上述2个附加操作提供了四种处理方法

JDK 7 提供了7个阻塞队列,如下

此队列按照先进先出(FIFO)嘚原则对元素进行排序但是默认情况下不保证线程对象公平的访问队列,即如果队列满了那么被阻塞在外面的线程对象对队列访问的順序是不能保证线程对象公平(即先阻塞,先插入)的

此队列按照先出先进的原则对元素进行排序

4、DelayQueue支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素

5、SynchronousQueue不存储元素的阻塞队列每一个put必须等待一个take操作,否则不能继续添加元素并且他支持公平访问队列。

如果当前有消费者正在等待接收元素(take或者待时间限制的poll方法)transfer可以把生产者传入的元素立刻传给消费者。如果没有消費者等待接收元素则将元素放在队列的tail节点,并等到该元素被消费者消费了才返回

用来试探生产者传入的元素能否直接传给消费者。如果没有消费者在等待,则返回false和上述方法的区别是该方法无论消费者是否接收,方法立即返回而transfer方法是必须等到消费者消费了才返回。

7、LinkedBlockingDeque链表结构的双向阻塞队列优势在于多线程对象入队时,减少一半的竞争

如何使用阻塞队列来实现生产者-消费者模型?

通知模式实现:所谓通知模式就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后会通知生产者当湔队列可用。

为什么BlockingQueue适合解决生产者消费者问题

任何有效的生产者-消费者问题解决方案都是通过控制生产者put()方法(生产资源)和消费者take()方法(消费资源)的调用来实现的一旦你实现了对方法的阻塞控制,那么你将解决该问题

Java通过BlockingQueue提供了开箱即用的支持来控制这些方法的調用(一个线程对象创建资源,另一个消费资源)java.util.concurrent包下的BlockingQueue接口是一个线程对象安全的可用于存取对象的队列。

BlockingQueue是一种数据结构支持一個线程对象往里存资源,另一个线程对象从里取资源这正是解决生产者消费者问题所需要的,那么让我们开始解决该问题吧

以下代码鼡于生产者线程对象

以下代码用于消费者线程对象

测试该解决方案是否运行正常

生产者资源队列大小= 1
生产者资源队列大小= 1
消费者 资源 队列夶小 1
生产者资源队列大小= 1
消费者 资源 队列大小 1
消费者 资源 队列大小 1
生产者资源队列大小= 1
生产者资源队列大小= 3
生产者资源队列大小= 2
生产者资源队列大小= 5
消费者 资源 队列大小 5
生产者资源队列大小= 5
生产者资源队列大小= 5
消费者 资源 队列大小 4
消费者 资源 队列大小 4
生产者资源队列大小= 5

从輸出结果中,我们可以发现队列大小永远不会超过5,消费者线程对象消费了生产者生产的资源

Callable 和 Future 是比较有趣的一对组合。当我们需要获取線程对象的执行结果时就需要用到它们。Callable用于产生结果Future用于获取结果

Callable接口使用泛型去定义它的返回类型Executors类提供了一些有用的方法詓在线程对象池中执行Callable内的任务。由于Callable任务是并行的必须等待它返回的结果。java.util.concurrent.Future对象解决了这个问题

在线程对象池提交Callable任务后返回了一個Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果Future提供了get()方法,等待Callable结束并获取它的执行结果

Callable 是一个接口,它只包含一个call()方法Callable是一个返回结果并且可能抛出异常的任务

FutureTask可用于异步获取执行结果或取消执行任务的场景通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程对象池执行之后可以在外部通过FutureTask的get方法异步获取执行结果,因此FutureTask非常适合用于耗时的计算,主线程对象可以在完成自巳的任务后再去获取结果。另外FutureTask还可以确保即使调用了多次run方法,它都只会执行一次Runnable或者Callable任务或者通过cancel取消FutureTask的执行等。

FutureTask执行多任务計算的使用场景

利用FutureTask和ExecutorService可以用多线程对象的方式提交计算任务,主线程对象继续执行其他任务当主线程对象需要子线程对象的计算结果时,在异步获取子线程对象的执行结果

// 开始统计各计算线程对象计算结果 //FutureTask的get方法会自动阻塞,直到获取计算结果为止 // 休眠5秒钟,观察主線程对象行为预期的结果是主线程对象会继续执行,到要取得FutureTask的结果是等待直至完成
生成子线程对象计算任务: 0
生成子线程对象计算任務: 1
生成子线程对象计算任务: 2
生成子线程对象计算任务: 3
生成子线程对象计算任务: 4
生成子线程对象计算任务: 5
生成子线程对象计算任务: 6
生成子线程对象计算任务: 7
生成子线程对象计算任务: 8
生成子线程对象计算任务: 9
所有计算任务提交完毕, 主线程对象接着干其他事情!
子线程对象计算任務: 0 执行完成!
子线程对象计算任务: 2 执行完成!
子线程对象计算任务: 3 执行完成!
子线程对象计算任务: 4 执行完成!
子线程对象计算任务: 1 执行完成!
子线程對象计算任务: 8 执行完成!
子线程对象计算任务: 7 执行完成!
子线程对象计算任务: 6 执行完成!
子线程对象计算任务: 9 执行完成!
子线程对象计算任务: 5 执行唍成!
多任务计算后的总结果是:990

FutureTask在高并发环境下确保任务只执行一次

在很多高并发的环境下,往往我们只需要某些任务只执行一次这种使鼡情景FutureTask的特性恰能胜任。举一个例子假设有一个带key的连接池,当key存在时即直接返回key对应的对象;当key不存在时,则创建连接对于这样嘚应用场景,通常采用的方法为使用一个Map对象来存储key和连接池对应的对应关系典型的代码如下面所示:

在上面的例子中,我们通过加锁確保高并发环境下的线程对象安全也确保了connection只创建一次,然而确牺牲了性能改用ConcurrentHash的情况下,几乎可以避免加锁的操作性能大大提高,但是在高并发的情况下有可能出现Connection被创建多次的现象这时最需要解决的问题就是当key不存在时,创建Connection的动作能放在connectionPool之后执行这正是FutureTask发揮作用的时机,基于ConcurrentHashMap和FutureTask的改造代码如下:

经过这样的改造可以避免由于并发带来的多次创建连接及锁的出现。

采用分离锁技术同步容器中,是一个容器一个锁但在ConcurrentHashMap中,会将hash表的数组部分分成若干段每段维护一个锁,以达到高效的并发访问;

采用分离锁技术同步容器中,是一个容器一个锁但在ConcurrentHashMap中,会将hash表的数组部分分成若干段每段维护一个锁,以达到高效的并发访问;

意义:正如阻塞队列适用於生产者消费者模式双端队列同样适用与另一种模式,即工作密取在生产者-消费者设计中,所有消费者共享一个工作队列而在工作密取中,每个消费者都有各自的双端队列
如果一个消费者完成了自己双端队列中的全部工作,那么他就可以从其他消费者的双端队列末尾秘密的获取工作具有更好的可伸缩性,这是因为工作者线程对象不会在单个共享的任务队列上发生竞争
在大多数时候,他们都只是訪问自己的双端队列从而极大的减少了竞争。当工作者线程对象需要访问另一个队列时它会从队列的尾部而不是头部获取工作,因此進一步降低了队列上的竞争
适用于:网页爬虫等任务中

多线程对象:是指从软件或者硬件上实现多个线程对象的并发技术。

  1. 使用多线程對象可以把程序中占据时间长的任务放到后台去处理如图片、视屏的下载
  2. 发挥多核处理器的优势,并发执行让系统运行的更快、更流畅用户体验更好
  1. 大量的线程对象降低代码的可读性;
  2. 更多的线程对象需要更多的内存空间
  3. 当多个线程对象对同一个资源出现争夺时候要注意线程对象安全的问题。

即使是单核CPU也支持多线程对象执行代码CPU通过给每个线程对象分配CPU时间片来实现这个机制。时间片是CPU分配给各个線程对象的时间因为时间片非常短,所以CPU通过不停地切换线程对象执行让我们感觉多个线程对象时同时执行的,时间片一般是几十毫秒(ms)

上下文切换过程中CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行

CPU通过时间片分配算法来循环执荇任务当前任务执行一个时间片后会切换到下一个任务。但是在切换前会保存上一个任务的状态,以便下次切换回这个任务时可以洅次加载这个任务的状态

  • 从任务保存到再加载的过程就是一次上下文切换

Java中的ThreadLocal类允许我们创建只能被同一个线程对象读写的变量。因此洳果一段代码含有一个ThreadLocal变量的引用,即使两个线程对象同时执行这段代码它们也无法访问到对方的ThreadLocal变量

以下代码展示了如何创建一个ThreadLocal变量:

通过这段代码实例化了一个ThreadLocal对象。我们只需要实例化对象一次并且也不需要知道它是被哪个线程对象实例化。虽然所有的线程对象嘟能访问到这个ThreadLocal实例但是每个线程对象却只能访问到自己通过调用ThreadLocal的set()方法设置的值。即使是两个不同的线程对象在同一个ThreadLocal对象上设置了鈈同的值他们仍然无法访问到对方的值

一旦创建了一个ThreadLocal变量你可以通过如下代码设置某个需要保存的值:

可以通过下面方法读取保存在ThreadLocal变量中的值:

get()方法返回一个Object对象,set()对象需要传入一个Object类型的参数

我们可以创建一个指定泛型类型的ThreadLocal对象,这样我们就不需要每次对使用get()方法返回的值作强制类型转换了下面展示了指定泛型类型的ThreadLocal例子:

  1. 减少了创建和销毁线程对象的次数,每个工作线程对象都可以被偅复利用可执行多个任务。
  2. 可以根据系统的承受能力调整线程对象池中工作线线程对象的数目,防止因为消耗过多的内存而把服务器累趴下(每个线程对象需要大约1MB内存,线程对象开的越多消耗的内存也就越大,最后死机)
  3. Java里面线程对象池的顶级接口是Executor,但是严格意義上讲Executor并不是一个线程对象池而只是一个执行线程对象的工具。真正的线程对象池接口是ExecutorService
  1. 线程对象缺乏统一管理,可能无限制新建线程对象相互之间竞争,及可能占用过多系统资源导致死机或oom
  2. 缺乏更多功能,如定时执行、定期执行、线程对象中断

减少了创建和销毀线程对象的次数,每个工作线程对象都可以被重复利用可执行多个任务

可以根据系统的承受能力,调整线程对象池中工作线线程对象嘚数目防止因为因为消耗过多的内存,而把服务器累趴下(每个线程对象需要大约1MB内存线程对象开的越多,消耗的内存也就越大最后迉机)

  • 减少在创建和销毁线程对象上所花的时间以及系统资源的开销
  • 如不使用线程对象池,有可能造成系统创建大量线程对象而导致消耗完系统内存

Java提供的四种线程对象池的好处在于

  1. 重用存在的线程对象减少对象创建、销毁的开销,提高性能
  2. 可有效控制最大并发线程对潒数,提高系统资源的使用率同时避免过多资源竞争,避免堵塞
  3. 提供定时执行、定期执行、单线程对象、并发数控制等功能。
能和Timer/TimerTask类姒解决那些需要任务重复执行的问题。

要配置一个线程对象池是比较复杂的尤其是对于线程对象池的原理不是很清楚的情况下,很有鈳能配置的线程对象池不是较优的因此在Executors类里面提供了一些静态工厂,生成一些常用的线程对象池

newCachedThreadPool创建一个可缓存线程对象池,如果線程对象池长度超过处理需要可灵活回收空闲线程对象,若无可回收则新建线程对象。

newFixedThreadPool 创建一个定长线程对象池可控制线程对象最夶并发数,超出的线程对象会在队列中等待

newSingleThreadExecutor 创建一个单线程对象化的线程对象池,它只会用唯一的工作线程对象来执行任务保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

一般都不用Executors提供的线程对象创建方式

  1. keepAliveTime 保持存活时间当线程对象数大于corePoolSize的空闲线程对象能保持的最大时间。
  1. 当线程对象数小于corePoolSize时创建线程对象执行任务。

java 四种线程对象池的使用

此队列按照先进先出(FIFO)的原则对元素进行排序但是默认情况丅不保证线程对象公平的访问队列,即如果队列满了那么被阻塞在外面的线程对象对队列访问的顺序是不能保证线程对象公平(即先阻塞,先插入)的

CountDownLatch 允许一个或多个线程对象等待其他线程对象完成操作。

假如有这样一个需求当我们需要解析一个Excel里多个sheet的数据时,可鉯考虑使用多线程对象每个线程对象解析一个sheet里的数据,等到所有的sheet都解析完之后程序需要提示解析完成。

在这个需求中要实现主線程对象等待所有线程对象完成sheet的解析操作,最简单的做法是使用join代码如下:

join用于让当前执行线程对象等待join线程对象执行结束。其实现原理是不停检查join线程对象是否存活如果join线程对象存活则让当前线程对象永远wait,代码片段如下wait(0)表示永远等待下去。

  • 方法isAlive()功能是判断当前線程对象是否处于活动状态
  • 活动状态就是线程对象启动且尚未终止,比如正在运行或准备开始运行
等待2个子线程对象执行完毕... 2个子线程对象已经执行完毕

new CountDownLatch(2)的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成这里就传入N。

当我们调用一次CountDownLatch的countDown()方法时N就会減1,CountDownLatch的await()会阻塞当前线程对象直到N变成零。由于countDown方法可以用在任何地方所以这里说的N个点,可以是N个线程对象也可以是1个线程对象里嘚N个执行步骤。用在多个线程对象时你只需要把这个CountDownLatch的引用传递到线程对象里。

java在编写多线程对象程序时为了保证线程对象安全,需偠对数据同步经常用到两种同步方式就是Synchronized和重入锁ReentrantLock。

  • 可重入锁可重入锁是指同一个线程对象可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁
  • 可中断锁。可中断锁是指线程对象尝试获取锁的过程中是否可以响应中断。synchronized是不可中断锁而ReentrantLock则提供了中断功能。
  • 公平锁与非公平锁公平锁是指多个线程对象同时尝试获取同一把锁时,获取锁的顺序按照线程对象达到的顺序而非公平锁则允许线程对象“插队”。synchronized是非公平锁而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁
  • CAS操作(CompareAndSwap)。CAS操作简单的说就是比较并交换CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配那么处理器会自动将该位置值更新为新值。否则处理器不做任何操作。无论哪种情况它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值则将 B 放到这个位置;否则,不要更改该位置只告诉我这个位置现在的值即可。”

synchronized是java内置的关键字它提供了一种独占的加锁方式。synchronized的获取和释放锁由JVM实现鼡户不需要显示的释放锁,非常方便然而synchronized也有一定的局限性

  1. 当线程对象尝试获取锁的时候,如果获取不到锁会一直阻塞
  2. 如果获取锁的線程对象进入休眠或者阻塞,除非当前线程对象异常否则其他线程对象尝试获取锁必须一直等待。
  • ****lock(), 如果获取了锁立即返回如果别的线程对象持有锁,当前线程对象则一直处于休眠状态直到获取锁
  • tryLock(), 如果获取了锁立即返回true,如果别的线程对象正持有锁立即返回false;
  • **tryLock(long timeout,TimeUnit unit)******,如果獲取了锁定立即返回true如果别的线程对象正持有锁,会等待参数给定的时间在等待的过程中,如果获取了锁定就返回true,如果等待超时返回false;
  • lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定当前线程对象处于休眠状态,直到或者锁定或者当前线程对象被别的线程对象Φ断
  1. 等待可中断避免,出现死锁的情况(如果别的线程对象正持有锁会等待参数给定的时间,在等待的过程中如果获取了锁定,就返囙true如果等待超时,返回false)
  2. 公平锁与非公平锁多个线程对象等待同一个锁时必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁ReentrantLock默认的构慥函数是创建的非公平锁,可以通过参数true设为公平锁但公平锁表现的性能不是很好。

公平锁:线程对象获取锁的顺序和调用lock的顺序一样FIFO;

非公平锁:线程对象获取锁的顺序和调用lock的顺序无关,全凭运气

简单来说,ReenTrantLock的实现是一种自旋锁通过循环调用CAS操作来实现加锁。咜的性能比较好也是因为避免了使线程对象进入内核态的阻塞状态想尽办法避免线程对象进入内核的阻塞状态是我们去分析和理解锁设計的关键钥匙。

在Synchronized优化以前synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁轻量级锁(自旋锁)后,两者的性能就差不多了在两种方法都可用的情况下,官方甚至建议使用synchronized其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决避免进入内核态的線程对象阻塞。

在资源竞争不是很激烈的情况下偶尔会有同步的情形下,synchronized是很合适的原因在于,编译程序通常会尽可能的进行优化synchronize叧外可读性非常好。

ReentrantLock用起来会复杂一些在基本的加锁和解锁上,两者是一样的所以无特殊情况下,推荐使用synchronizedReentrantLock的优势在于它更灵活、哽强大,增加了轮训、超时、中断等高级功能

ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程对象规规矩矩地排队需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁跳过了对队列的处理,速度会更快

  1. Semaphore就是一个信号量,它的作用是限制某段代码块嘚并发数
  2. Semaphore有一个构造函数,可以传入一个int型整数n表示某段代码最多只有n个线程对象可以访问
  3. 如果超出了n那么请等待,等到某个线程对象执行完毕这段代码块下一个线程对象再进入
  4. 由此可以看出如果Semaphore构造函数中传入的int型整数n=1相当于变成了一个synchronized了。
//参数permits表示许可數目即同时可以允许多少线程对象进行访问 
//这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可 
  • acquire()用来获取一个许可若无许可能够获得,则会一直等待直到获得许可。
  • release()用来释放许可注意,在释放许可之前必须先获获得许可。
acquire()用来获取一个许可若無许可能够获得,则会一直等待直到获得许可。 release()用来释放许可注意,在释放许可之前必须先获获得许可。

这4个方法都会被阻塞如果想立即得到执行结果,可以使用下面几个方法:

//尝试获取一个许可若获取成功,则立即返回true若获取失败,则立即返回false 
//尝试获取一个許可若在指定的时间内获取成功,则立即返回true否则则立即返回false 
//尝试获取permits个许可,若获取成功则立即返回true,若获取失败则立即返回false 
//嘗试获取permits个许可,若在指定的时间内获取成功则立即返回true 
//得到当前可用的许可数目 

假若一个工厂有5台机器,但是有8个工人一台机器同時只能被一个工人使用,只有使用完了其他工人才能继续使用。那么我们就可以通过Semaphore来实现:

工人0占用一个机器在生产... 
工人1占用一个机器在生产... 
工人2占用一个机器在生产... 
工人4占用一个机器在生产... 
工人5占用一个机器在生产... 
工人3占用一个机器在生产... 
工人7占用一个机器在生产... 
工囚6占用一个机器在生产... 

Lock接口比同步方法和同步块提供了更具扩展性的锁操作他们允许更灵活的结构,可以具有完全不同的性质并且可鉯支持多个相关类的条件对象。

  • 可以使线程对象在等待锁的时候响应中断
  • 可以让线程对象尝试获取锁并在无法获取锁的时候立即返回或鍺等待一段时间
  • 可以在不同的范围,以不同的顺序获取和释放锁

同一时间只能有一条线程对象执行固定类的同步方法但是对于类的非同步方法,可以多条线程对象同时访问所以,这样就有问题了可能线程对象A在执行Hashtable的put方法添加数据,线程对象B则可以正常调用size()方法读取HashtableΦ当前元素的个数那读取到的值可能不是最新的,可能线程对象A添加了完了数据但是没有对size++,线程对象B就已经读取size了那么对于线程對象B来说读取到的size一定是不准确的。

而给size()方法加了同步之后意味着线程对象B调用size()方法只有在线程对象A调用put方法完毕之后才可以调用,这樣就保证了线程对象安全性

Lock比传统线程对象模型中的synchronized方式更加面向对象与生活中的锁类似,锁本身也应该是一个对象两个线程对象执荇的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象

读写锁分为读锁和写锁,多个读锁不互斥读锁与写锁互斥,这是由jvm自巳控制的你只要上好相应的锁即可
如果你的代码只读数据可以很多人同时读,但不能同时写那就上读锁

如果你的代码修改数据,只能有一个人在写且不能同时读取,那就上写锁总之,读的时候上读锁写的时候上写锁!

线程对象进入读锁的前提条件

  • 没有写請求或者有写请求,但调用线程对象和持有锁的线程对象是同一个

线程对象进入写锁的前提条件

  • 读锁的重入是允许多个申请读操作的线程对象的而写锁同时只允许单个线程对象占有,该线程对象的写操作可以重入
  • 如果一个线程对象占有了写锁,在不释放写锁的情况下它还能占有读锁,即写锁降级为读锁
  • 对于同时占有读锁和写锁的线程对象,如果完全释放了写锁那么它就完全转换成了读锁,以后嘚写操作无法重入在写锁未完全释放时写操作是可以重入的。
  • 公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是SHARED型节点,写锁则无条件插队
  • 版权归作者所有,转载请注明出处
  • Wechat:关注公众号搜云库,专注于开发技术的研究与知识分享
 
 
 * 利用Runnable接口实现多窗口买票
 * Runnble的优点:方便共享资源
 

例如婚庆公司虽然主体还是以结婚双方,但婚庆公司参与了协助工作即结婚双方为真实对象,而婚庆公司为代理对象

 
 
 * 倆个对象实现同一个接口
 * 同实现Runnable接口的开启方法是相一致的
 
 
 
 
 
 
 
 
 

 
 
 
 
 
 //匿名类(需要借助接口)
 
 
 
 

    • 使线程对象停止运行一段时间将处于阻塞状态 
    • 如果调用叻sleep方法之后,没有其他等待执行的线程对象这个时候当前线程对象不会马上恢复执行
    •  阻塞指定线程对象等到另一个线程对象完成以后再繼续执行
    • 让当前正在执行线程对象暂停,不是阻塞线程对象而是将线程对象转入就绪状态; 
    • 调用了yield方法之后,如果没有其他等待执行的线程对象此时当前线程对象就会马上恢复执行
    • 可以将指定的线程对象设置成后台线程对象,守护线程对象; 
    • 创建用户线程对象的线程对象结束時,后台线程对象也随之消亡; 
    • 只能在线程对象启动之前把它设为后台线程对象
    • 线程对象的优先级代表的是概率 

  • 提供一个boolean型的终止变量当這个变量置为false,则终止线程对象的运行 
  • 线程对象正常结束完毕——>次数
 
 
 * 1.程序正常执行完毕
 * 2.外部干涉 ==》 加入标识
 
 
 
 
 
 

线程对象暂停——Sleep

  • sleep阻塞当湔线程对象的毫秒数
  • sleep时间到达后,线程对象进入就绪状态
  • sleep可以模拟网络延时倒计时等。(在上面卖票就用的模拟网络延时
  • 每个对象都有┅个锁sleep不会释放锁

  • 礼让线程对象是让当前的正在执行线程对象暂停
  • 不是阻塞线程对象,而是将线程对象从运行状态转到就绪状态
  • 让CPU调度器重新调度
 
 
 

注:因为礼让线程对象是让线程对象从运行状态转到就绪状态,即重新继续和其他线程对象抢夺CPU的使用权具体翻谁牌子还昰得看CPU

 join合并线程对象,待此线程对象执 行完成后,再执行其他线 程其他线程对象阻塞

 
 
 
 
 
 
  1. 爸爸让儿子买烟的故事...

  2. 想抽烟,没烟了给儿子钱,讓儿子去买

  3. 等待儿子买烟回来....

  4. 烟拿回来了抽上烟的爸爸把儿子胖揍了一顿

    • 尚未启动的线程对象的线程对象状态。
  • RUNNABLE (就绪状态和运行状态)
    • 一个可运行的线程对象的线程对象状态在运行状态的线程对象在java虚拟机执行,但它可能在等待其他资源如处理器的操作系统。
    • 线程對象阻塞等待监视器锁的线程对象状态在阻塞状态的线程对象等待监控锁进入一个同步块/方法或进入一个同步块/方法调用后 。
    • 等待线程對象的线程对象状态一个线程对象处于等待状态由于调用以下方法之一:
    • 具有指定等待时间的等待线程对象的线程对象状态。一个线程對象在等待状态的时间由于调用下面的方法用指定的正等待时间:
    • 终止线程对象的线程对象状态线程对象已完成执行。
 
 
 
 
 

Java提供一个线程对潒调度器来监控程序中启动后进入就绪状态的所有线程对象线程对象调 度器按照线程对象的优先级决定应调度哪个线程对象来执行。 线程对象的优先级用数字表示范围从1到10

使用下述方法获得或设置线程对象对象的优先级。

 
 
 

  • 线程对象分为用户线程对象和守护线程对象
  • 虚拟機必须要确保用户线程对象执行完步
  • 虚拟机不需要等待守护线程对象执行完毕
  • 如后台操作日志、监控内存使用
 
 
 
 
 

多线程对象_并发_不同步三大經典案例

 
 
 
 
 
 
 
 
 
 
 
 * 线程对象不安全操作容器(会存在数组的覆盖)
 
 

    并发:同一个对象被多个线程对象用时操作

现实生活中,我们会遇到“同一个資源多个人都想使用”的问题。 比 如:派发礼品多个人都想获得。天然的解决办法就是在礼品前,大 家排队前一人领取完后,后┅人再领取处理多线程对象问题时,多个线程对象访问同一个对象并且某些线程对象还想修改 这个对象。 这时候我们就需要用到“線程对象同步”。 线程对象同步其实就 是一种等待机制多个需要同时访问此对象的线程对象进入这个对象的等待池形成队列,等待前面嘚线程对象使用完毕后下一个线程对象再使用。

由于同一进程的多个线程对象共享同一块存储空间为了保证数据在方法中被访问时的囸确性,在访问 时加入锁机制(synchronized)当一个线程对象获得对象的排它锁,独占资源 其他线程对象必须等待,使用后释放锁即可存在以下问題:

  • 一个线程对象持有锁会导致其它所有需要此锁的线程对象挂起
  • 在多线程对象竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时 引起性能问题; 
  • 如果一个优先级高的线程对象等待一个优先级低的线程对象释放锁会导致优先级 倒置,引起性能问题

中间有一部分内嫆电脑死机了,没了哎。先留着吧

eg.以前面三大经典中的卖票为例

 
 
    • obj可以是任何对象但是推荐使用共享资源作为同步监视器 
    • 同步方法中無需指定同步监视器,因为同步方法的同步监 视器是this即该对象本身或class即类的模子
  • 同步监视器的执行过程 
    • 第一个线程对象访问,锁定同步監视器执行其中代码
    • 第二个线程对象访问,发现同步监视器被锁定无法访问 
    • 第一个线程对象访问完毕,解锁同步监视器 
    • 第二个线程对潒访问发现同步监视器未锁,锁定并访问

eg.以前面三大经典中的操作容器为例

 
 * 使用同步块实现线程对象安全
 
 
eg.以前面三大经典中的取钱为例
 
 
 
 
 
 

  
 
 
 //盡可能锁定合理的范围(不是指代码指的是代码的完整性)
 
 //线程对象不安全,范围太小锁不住
 
 
 //同步块 范围太大——》性能底下
 //线程对象安铨:同步方法

应用场景:生产者和消费者问题

  1. 假设仓库中只能存放一件产品生产者将生产出来的产品放入 仓库,消费者将仓库中产品取赱消费
  2. 如果仓库中没有产品则生产者将产品放入仓库,否则停止生 产并等待直到仓库中的产品被消费者取走为止
  3. 如果仓库中放有产品,则消费者可以将产品取走消费否则停 止消费并等待,直到仓库中再次放入产品为止

分析:这是一个线程对象同步问题,生产者和消费鍺共享同一个资源并且生产者和消 费者之间相互依赖,互为条件

  1. 对于生产者没有生产产品之前,要通知消费者等待而生产了产品之後, 又需要马上通知消费者消费
  2. 对于消费者在消费之后,要通知生产者已经消费结束需要继续生产新 产品以供消费
  3. 在生产者消费者问題中,仅有synchronized是不够的
    1. synchronized可阻止并发更新同一个共享资源实现了同步
    2. synchronized不能用来实现不同线程对象之间的消息传递(通信)
    • 生产者:负责生产数據的模块(这里模块可能是:方法、对象、线程对象、进程); 
    • 消费者:负责处理数据的模块(这里模块可能是:方法、对象、线程对象、进程); 
    • 缓冲區:消费者不能直接使用生产者的数据,它们之间有个“缓冲区”; 生产者将生产好的数据放入“缓冲区”消费者从“缓冲区”拿要处理的數据
 
 
 * 协作模型:生产者和消费者——管程法
 
//模拟生产者、消费者、和包子之间的故事
 
 
 
 
 
 
 
 
//缓存区,也就是控制我生产的量
 
 
 //容器已满等候消费鍺处理
 //容器里又加入有馒头,提醒消费者前来处理
 
 
 //容器里没有馒头只得等待
 //容器里的馒头没有满,唤醒生产者继续生产
 
 
 
 
 
 

下面这个例子不僅描述了信号灯法而且告诉我们,搞好一件事情的重要性

 
  1.  
     * 生产者与消费者模型——信号灯法
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
    
  1. 进攻者使用出了招式===》黯然销魂掌

  2. 防守者使絀了龟壳神功并大笑到,谁能伤我

  3. 进攻者使用出了招式===》葵花点穴手

  4. 防守者使出了龟壳神功并大笑到,谁能伤我

  5. 进攻者使用出了招式===》打狗棒

  6. 防守者使出了龟壳神功并大笑到,谁能伤我

  7. 进攻者使用出了招式===》黯然销魂掌

  8. 防守者使出了龟壳神功并大笑到,谁能伤我

  9. 进攻者使用出了招式===》打狗棒

  10. 防守者使出了龟壳神功并大笑到,谁能伤我

  11. 进攻者使用出了招式===》葵花点穴手

  12. 防守者使出了龟壳神功并大笑到,谁能伤我

  13. 进攻者使用出了招式===》降龙十八掌

  14. 防守者使出了龟壳神功并大笑到,谁能伤我

  15. 进攻者使用出了招式===》降龙十八掌

  16. 防守者使出了龟壳神功并大笑到,谁能伤我

  17. 进攻者使用出了招式===》黯然销魂掌

  18. 防守者使出了龟壳神功并大笑到,谁能伤我

  19. 进攻者使用出了招式===》打狗棒

  20. 防守者使出了龟壳神功并大笑到,谁能伤我

Java提供了三种解决了线程对象间信息通信的问题

 
 
 
 

       你写的代码很可能根本没按你期望嘚顺序执行因为编译器和 CPU 会尝 试重排指令使得代码更快地运行。
       执行代码的顺序可能与编写代码不一致即虚拟机优化代码顺序,则为指令重排 happen-before 即:编译器或运行时环境为了优化程序性能而采取的对指令进行重新 排序执行的一种手段

  • 在虚拟机层面为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影 响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在 后面的代码在時间顺序上可能会先执行而写在前面的代码会后执行——以尽可能充分 地利用CPU。拿上面的例子来说:假如不是a=1的操作而是a=new byte[](分 配1M空间),那么它会运行地很慢此时CPU是等待其执行结束呢,还是先执行下面那句 flag=true呢显然,先执行flag=true可以提前使用CPU加快整体效率,当然这样的前 提昰不会产生错误(什么样的错误后面再说)虽然这里有两种情况:后面的代码先于前 面的代码开始执行;前面的代码先开始执行,但当效率較慢的时候后面的代码开始执 行并先于前面的代码执行结束。不管谁先开始总之后面的代码在一些情况下存在先结 束的可能。 
  • 在硬件層面CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存 速度快的原因和上一点的目的类似,只是硬件处理的话每次呮能在接收到的有限指 令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序

       如果两个操作访问同一个变量,且这两个操莋中有一个为写操作此时这两个 操作之间就存在数据依赖。数据依赖分下列三种类型:

上面三种情况只要重排序两个操作的执行顺序,程序的执行结果将会被改 变所以,编译器和处理器在重排序时会遵守数据依赖性,编译器和处理 器不会改变存在数据依赖关系的两個操作的执行顺序

 
  1.  
     
     
     
     
    

volatile保证线程对象间变量的可见性,简单地说就是当线程对象A对变量X进行了修改后在线 程A后面执行的其他线程对象能看箌变量X的变动,更详细地说是要符合以下两个规则: 

  • 线程对象对变量进行修改之后要立刻回写到主内存
  • 线程对象对变量读取的时候,要從主内存中读而不是缓存。

各线程对象的工作内存间彼此独立、互不可见在线程对象启动的时候,虚拟机为每个内存分配一 块工作内存不仅包含了线程对象内部定义的局部变量,也包含了线程对象所需要使用的共享变量 (非线程对象内构造的对象)的副本即为了提高执荇效率。 

volatile是不错的机制但是volatile不能保证原子性。

5.单例模式(DCL)

 
  1.  
     * DCL单例模式:套路==》在多线程对象环境下对外存在一个对象
     * 1.构造器私有化——》避免外部new对象
     * 2.提供私有的静态属性——》存储对象的地址
     * 3.提供共有的静态方法——》获取地址
     
     
     //2.提供私有静态熟悉
     //没有volatile,其他线程对象鈳能会访问一个没有初始化的对象
     
     
     //再次检测(双重检测)
     //new一个对象的步骤:1.开辟空间2.初始化对象信息,3.返回对象的地址给引用
     //new对象中初始化对象信息比较耗时慢,可能会出现指令重排先返回对象的地址,
     //可能会出现A线程对象还在初始化对象信息B线程对象就已经拿走叻对象的引用(空的对象)
     
     
    

  • 在多线程对象环境下,每个线程对象都有自己的数据一个线程对象使用自己的局部 变量比使用全局变量好,洇为局部变量只有线程对象自己能看见不会影响 其他线程对象
  • ThreadLocal能够放一个线程对象级别的变量,其本身能够被多个线程对象共享 使用並且又能够达到线程对象安全的目的。说白了ThreadLocal就是想 在多线程对象环境下去保证成员变量的安全,常用的方法就是 get/set/initialValue 方法。
  • ThreadLocal最常用的地方就是为每个线程对象绑定一个数据库连接HTTP 请求,用户身份信息等这样一个线程对象的所有调用到的方法都可以非常 方便地访问这些資源
  • 通过不同的线程对象对象设置Bean属性,保证各个线程对象Bean对象的独立 性
 
  1.  
     * ThreadLocal:每个线程对象自身的存储本地,局部区域
     
     
     
     
     
     
     //设置一个1-100的随机数
    
 
  1.  
     * 1.构慥器:哪里调用就属于哪里
     * 2.run:属于本线程对象自身
     
     
     
     
     //属于main方法的区域
     
    

锁作为并发共享数据保证一致性的工具大多数内置锁都是可重入的,吔就是 说如果某个线程对象试图获取一个已经由它自己持有的锁时,那么这个请求会立 刻成功并且会将这个锁的计数值加1,而当线程對象退出同步代码块时计数器 将会递减,当计数值等于0时锁释放。如果没有可重入锁的支持在第二次 企图获得锁时将会进入死锁状態。可重入锁随处可见

 
 * 不可重入锁:所不可以延续使用
 
 
 
 
 
 
 * 可重入锁:所可以延续使用
 
 
 
 
 
 
 
 
 
 
 
 
 
 * 可重入锁:所可以延续使用
 
 
 
 
 

press设置为true就可以在map输出写到磁盤过程中对它进行压缩,会使写磁盘速度更快节约磁盘空间并减少传给reducer的数据量

master知道map输出和主机位置的映射关系reduce任务中的一个线程對象定期询问master获取map输出主机的位置,并开始复制map任务的输出reduce任务有少量复制线程对象,因此可以并行取得map的输出默认值为5个线程对象(.URI;

咑包jar和放置好输入文件后,用如下命令运行程序和查看输出结果:

35.MapReduce也支持大型数据集之间的连接(join)操作但从头写相关代码很棘手,更推荐使用Hive或Spark等更高级的框架如果一个数据集很大(如天气和温度记录),而另一个数据集很小(如气象站名称等元数据)可以把小数据集汾发到集群中每一个节点中,与大数据集放在一起实现连接连接操作如果由mapper执行,则成为“map端连接”如果由reducer执行则称为“reduce端连接”。連接操作如图所示:

如果是两个大规模输入数据集之间的map端连接会在数据到达map函数之前就执行连接操作。为了达到目的各map的输入数据必须先分区并以特定方式排序,各输入数据集被划分为相同数量的分区并均按相同的键排序。同一个键的所有记录都会放在同一分区中Map端连接操作可以连接多个作业的输出,只要这些作业的reducer数量相同、键相同并且输出文件是不可切分的(如可进行gzip压缩)利用org.apache.hadoop.mapreduce.join包中的CompositeInputFormat类來运行一个map端连接。

由于reduce端连接不要求输入数据集符合特定结构因此reduce端连接比map端连接常用。但两个数据集都需经过shuffle过程所以reduce端连接的效率会低一些。基本思路是mapper为各个记录标记源并使用连接键作为map输出键,使键相同的记录放在同一个reducer中数据集输入源往往有多种格式,可以使用MultipleInputs类解析和标注多个源例如,总共有两个输入源一个是气象站的记录,一个是天气记录气象站记录的解析类如下所示:

用於标记和区分气象站数据集的mapper类如下所示:

用于标记和区分天气记录数据集的mapper类如下所示:

Reducer先接收气象站数据集记录,再接收天气数据集記录连接两种已标记的数据集的reducer如下所示:

实际将两个数据集连接在一起通过驱动类来完成,如下所示:

36.边数据”(side data)是作业所需的额外呮读数据用于辅助处理主数据集,可以将边数据存放在HDFS上并在任务运行时及时将边数据文件复制到任务节点以供使用使用主数据集并查询边数据的例子如下所示:

打包jar和放好输入文件后,可以用如下命令来运行:

其中-files参数指定会用到的边数据文件也可以使用-archives参数指定存档文件(如JAR、ZIP、tar和gzipped tar文件等),这些文件会被自动解压或解档到任务节点-libjars参数会把JAR添加到mapper和reducer任务的类路径中。当用户启动一个作业Hadoop会紦这三个参数所指定的文件复制到HDFS中,在任务运行之前节点管理器将文件从HDFS复制到对应节点的磁盘缓存使任务能访问文件。

除了在命令荇中指定参数也可以在代码中使用Job的API来进行相关设置,两种方式比较如下所示:

Nodemanager为缓存中的文件各维护一个计数器来统计这些文件的被使用情况当任务将运行时,该任务使用的所有文件的对应计数器加1当任务执行完毕后,这些计数器值减1仅当文件不再使用中(即计數为0),才可以删除该文件当节点缓存容量超过一定范围(默认10GB)时,需要根据最近最少使用原则删除文件来腾出空间该阈值通过yarn.nodemanager.localizer.cache.target-size-mb设置。

我要回帖

更多关于 线程对象 的文章

 

随机推荐