最近看到网上流传着,各类面试经验及面试题,每每都是一大堆技术题目贴上去,而没有答案。html
无论你是新程序员仍是老手,你必定在面试中遇到过有关线程的问题。Java语言一个重要的特色就是内置了对并发的支持,让Java大受企业和程序员的欢迎。大多数待遇丰厚的Java开发职位都要求开发者精通多线程技术而且有丰富的Java程序开发、调试、优化经验,因此线程相关的问题在面试中常常会被提到。
在典型的Java面试中, 面试官会从线程的基本概念问起java
如:为何你须要使用线程, 如何建立线程,用什么方式建立线程比较好(好比:继承thread类仍是调用Runnable接口),而后逐渐问到并发问题像在Java并发编程的过程当中遇到了什么挑战,Java内存模型,JDK1.5引入了哪些更高阶的并发工具,并发编程经常使用的设计模式,经典多线程问题如生产者消费者,哲学家就餐,读写器或者简单的有界缓冲区问题。仅仅知道线程的基本概念是远远不够的, 你必须知道如何处理死锁,竞态条件,内存冲突和线程安全等并发问题。掌握了这些技巧,你就能够轻松应对多线程和并发面试了。
许多Java程序员在面试前才会去看面试题,这很正常。程序员
由于收集面试题和练习很花时间,因此我从许多面试者那里收集了Java多线程和并发相关的50个热门问题。面试
下面是Java线程相关的热门面试题,你能够用它来好好准备面试。算法
线程是操做系统可以进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运做单位,能够使用多线程对进行运算提速。编程
好比,若是一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒网页爬虫
通俗的说:加锁的就是是线程安全的,不加锁的就是是线程不安全的segmentfault
线程安全: 就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其余线程不能进行访问,直到该线程读取完,其余线程才可以使用。不会出现数据不一致或者数据污染。设计模式
一个线程安全的计数器类的同一个实例对象在被多个线程使用的状况下也不会出现计算失误。很显然你能够将集合类分红两组,线程安全和非线程安全的。
Vector 是用同步方法来实现线程安全的, 而和它类似的ArrayList不是线程安全的。数组
线程不安全:就是不提供数据访问保护,有可能出现多个线程前后更改数据形成所获得的数据是脏数据
若是你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。若是每次运行结果和单线程运行的结果是同样的,并且其余的变量的值也和预期的是同样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引发的。
若每一个线程中对全局变量、静态变量只有读操做,而无写操做,通常来讲,这个全局变量是线程安全的;如有多个线程同时执行写操做,通常都须要考虑线程同步,不然的话就可能影响线程安全。
自旋锁是SMP架构中的一种low-level的同步机制。
当线程A想要获取一把自选锁而该锁又被其它线程锁持有时,线程A会在一个循环中自选以检测锁是否是已经可用了。
自选锁须要注意:
参考
https://segmentfault.com/q/1010000000530936
一个简单的while就能够知足你的要求。
目前的JVM实现自旋会消耗CPU,若是长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大。
public class MyWaitNotify3{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
Java内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何经过内存进行交互。它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。Java内存模型经过使用各类各样的硬件和编译器的优化来正确实现以上事情。
Java包含了几个语言级别的关键字,包括:volatile, final以及synchronized,目的是为了帮助程序员向编译器描述一个程序的并发需求。Java内存模型定义了volatile和synchronized的行为,更重要的是保证了同步的java程序在全部的处理器架构下面都能正确的运行。
“一个线程的写操做对其余线程可见”这个问题是由于编译器对代码进行重排序致使的。例如,只要代码移动不会改变程序的语义,当编译器认为程序中移动一个写操做到后面会更有效的时候,编译器就会对代码进行移动。若是编译器推迟执行一个操做,其余线程可能在这个操做执行完以前都不会看到该操做的结果,这反映了缓存的影响。
此外,写入内存的操做可以被移动到程序里更前的时候。在这种状况下,其余的线程在程序中可能看到一个比它实际发生更早的写操做。全部的这些灵活性的设计是为了经过给编译器,运行时或硬件灵活性使其能在最佳顺序的状况下来执行操做。在内存模型的限定以内,咱们可以获取到更高的性能。
看下面代码展现的一个简单例子:
ClassReordering { int x = 0, y = 0; public void writer() { x = 1; y = 2; } public void reader() { int r1 = y; int r2 = x; } }
让咱们看在两个并发线程中执行这段代码,读取Y变量将会获得2这个值。由于这个写入比写到X变量更晚一些,程序员可能认为读取X变量将确定会获得1。可是,写入操做可能被重排序过。若是重排序发生了,那么,就能发生对Y变量的写入操做,读取两个变量的操做紧随其后,并且写入到X这个操做能发生。程序的结果多是r1变量的值是2,可是r2变量的值为0。
JVM内存结构主要有三大块:堆内存、方法区和栈。
堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分红三部分,Eden空间、From Survivor空间、To Survivor空间,默认状况下年轻代按照8:1:1的比例来分配;方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。
JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)
据Java虚拟机规范的规定,当方法区没法知足内存分配需求时,将抛出OutOfMemoryError异常。
可经过参数 栈帧是方法运行期的基础数据结构栈容量可由-Xss设置
1.Java虚拟机栈是线程私有的,它的生命周期与线程相同。
java虚拟机栈,规定了两种异常情况:
可经过参数 栈容量可由-Xss设置
可经过参数-XX:MaxPermSize设置
JDK1.6以前字符串常量池位于方法区之中。
JDK1.7字符串常量池已经被挪到堆之中。
可经过参数-XX:PermSize和-XX:MaxPermSize设置
可经过-XX:MaxDirectMemorySize指定,若是不指定,则默认与Java堆的最大值(-Xmx指定)同样。
java堆(Java Heap)
可经过参数 -Xms 和-Xmx设置
java虚拟机栈(stack)
可经过参数 栈帧是方法运行期的基础数据结构栈容量可由-Xss设置
方法区(Method Area)
可经过参数-XX:MaxPermSize设置
CAS(compare and swap)的缩写,中文翻译成比较并交换。
CAS 不经过JVM,直接利用java本地方 JNI(Java Native Interface为JAVA本地调用),直接调用CPU 的cmpxchg(是汇编指令)指令。
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,实现原子操做。其它原子操做都是利用相似的特性完成的。
整个java.util.concurrent都是创建在CAS之上的,所以对于synchronized阻塞算法,J.U.C在性能上有了很大的提高。
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知此次竞争中失败,并能够再次尝试。
CAS有3个操做数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。
确保对内存的读-改-写操做都是原子操做执行
CAS虽然很高效的解决原子操做,可是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操做
参考
https://blog.52itstyle.com/archives/948/
Java在JDK1.5以前都是靠synchronized关键字保证同步的,这种经过使用一致的锁定协议来协调对共享状态的访问,能够确保不管哪一个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,因此能够说synchronized是悲观锁。
乐观锁( Optimistic Locking)实际上是一种思想。相对悲观锁而言,乐观锁假设认为数据通常状况下不会形成冲突,因此在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,若是发现冲突了,则让返回用户错误的信息,让用户决定如何去作。
AbstractQueuedSynchronizer简称AQS,是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。
AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称做“哨兵节点”或者“哑节点”,它不与任何线程关联。其余的节点与等待线程关联,每一个节点维护一个等待状态waitStatus。
参考
https://blog.52itstyle.com/archives/948/
因为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,就不是AQS关心的了,要由子类具体实现。
AQS中还有一个表示状态的字段state,例如ReentrantLocky用它表示线程重入锁的次数,Semaphore用它表示剩余的许可数量,FutureTask用它表示任务的状态。对state变量值的更新都采用CAS操做保证更新操做的原子性。
AbstractQueuedSynchronizer继承了AbstractOwnableSynchronizer,这个类只有一个变量:exclusiveOwnerThread,表示当前占用该锁的线程,而且提供了相应的get,set方法。
ReentrantLock实现原理
http://www.javashuo.com/article/p-gcsvtqbz-go.html
原子操做是指一个不受其余操做影响的操做任务单元。原子操做是在多线程环境下避免数据不一致必须的手段。
int++并非一个原子操做,因此当一个线程读取它的值并加1时,另一个线程有可能会读到以前的值,这就会引起错误。
为了解决这个问题,必须保证增长操做是原子的,在JDK1.5以前咱们能够使用同步技术来作到这一点。
到JDK1.5,java.util.concurrent.atomic包提供了int和long类型的装类,它们能够自动的保证对于他们的操做是原子的而且不须要使用同步。
Executor框架同java.util.concurrent.Executor 接口在Java 5中被引入。
Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。
无限制的建立线程会引发应用程序内存溢出。因此建立一个线程池是个更好的的解决方案,由于能够限制线程的数量而且能够回收再利用这些线程。
利用Executors框架能够很是方便的建立一个线程池,
Java经过Executors提供四种线程池,分别为:
newCachedThreadPool建立一个可缓存线程池,若是线程池长度超过处理须要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 建立一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 建立一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 建立一个单线程化的线程池,它只会用惟一的工做线程来执行任务,保证全部任务按照指定顺序(FIFO, LIFO, 优先级)执行。
JDK7提供了7个阻塞队列。(也属于并发容器)
阻塞队列是一个在队列基础上又支持了两个附加操做的队列。
2个附加操做:
支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程,直到队列不满。
支持阻塞的移除方法:队列空时,获取元素的线程会等待队列变为非空。
阻塞队列经常使用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。
在阻塞队列不可用的时候,上述2个附加操做提供了四种处理方法
方法处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | 不可用 | 不可用 |
JDK 7 提供了7个阻塞队列,以下
一、ArrayBlockingQueue 数组结构组成的有界阻塞队列。
此队列按照先进先出(FIFO)的原则对元素进行排序,可是默认状况下不保证线程公平的访问队列,即若是队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。
二、LinkedBlockingQueue一个由链表结构组成的有界阻塞队列
此队列按照先出先进的原则对元素进行排序
三、PriorityBlockingQueue支持优先级的无界阻塞队列
四、DelayQueue支持延时获取元素的无界阻塞队列,便可以指定多久才能从队列中获取当前元素
五、SynchronousQueue不存储元素的阻塞队列,每个put必须等待一个take操做,不然不能继续添加元素。而且他支持公平访问队列。
六、LinkedTransferQueue由链表结构组成的无界阻塞TransferQueue队列。相对于其余阻塞队列,多了tryTransfer和transfer方法
transfer方法
若是当前有消费者正在等待接收元素(take或者待时间限制的poll方法),transfer能够把生产者传入的元素马上传给消费者。若是没有消费者等待接收元素,则将元素放在队列的tail节点,并等到该元素被消费者消费了才返回。
tryTransfer方法
用来试探生产者传入的元素可否直接传给消费者。,若是没有消费者在等待,则返回false。和上述方法的区别是该方法不管消费者是否接收,方法当即返回。而transfer方法是必须等到消费者消费了才返回。
七、LinkedBlockingDeque链表结构的双向阻塞队列,优点在于多线程入队时,减小一半的竞争。
通知模式实现:所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。
为何BlockingQueue适合解决生产者消费者问题
任何有效的生产者-消费者问题解决方案都是经过控制生产者put()方法(生产资源)和消费者take()方法(消费资源)的调用来实现的,一旦你实现了对方法的阻塞控制,那么你将解决该问题。
Java经过BlockingQueue提供了开箱即用的支持来控制这些方法的调用(一个线程建立资源,另外一个消费资源)。java.util.concurrent包下的BlockingQueue接口是一个线程安全的可用于存取对象的队列。
BlockingQueue是一种数据结构,支持一个线程往里存资源,另外一个线程从里取资源。这正是解决生产者消费者问题所须要的,那么让咱们开始解决该问题吧。
生产者
如下代码用于生产者线程
package io.ymq.example.thread; import java.util.concurrent.BlockingQueue; /** * 描述:生产者 * * @author yanpenglei * @create 2018-03-14 15:52 **/ class Producer implements Runnable { protected BlockingQueue<Object> queue; Producer(BlockingQueue<Object> theQueue) { this.queue = theQueue; } public void run() { try { while (true) { Object justProduced = getResource(); queue.put(justProduced); System.out.println("生产者资源队列大小= " + queue.size()); } } catch (InterruptedException ex) { System.out.println("生产者 中断"); } } Object getResource() { try { Thread.sleep(100); } catch (InterruptedException ex) { System.out.println("生产者 读 中断"); } return new Object(); } }
消费者
如下代码用于消费者线程
package io.ymq.example.thread; import java.util.concurrent.BlockingQueue; /** * 描述: 消费者 * * @author yanpenglei * @create 2018-03-14 15:54 **/ class Consumer implements Runnable { protected BlockingQueue<Object> queue; Consumer(BlockingQueue<Object> theQueue) { this.queue = theQueue; } public void run() { try { while (true) { Object obj = queue.take(); System.out.println("消费者 资源 队列大小 " + queue.size()); take(obj); } } catch (InterruptedException ex) { System.out.println("消费者 中断"); } } void take(Object obj) { try { Thread.sleep(100); // simulate time passing } catch (InterruptedException ex) { System.out.println("消费者 读 中断"); } System.out.println("消费对象 " + obj); } }
测试该解决方案是否运行正常
package io.ymq.example.thread; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; /** * 描述: 测试 * * @author yanpenglei * @create 2018-03-14 15:58 **/ public class ProducerConsumerExample { public static void main(String[] args) throws InterruptedException { int numProducers = 4; int numConsumers = 3; BlockingQueue<Object> myQueue = new LinkedBlockingQueue<Object>(5); for (int i = 0; i < numProducers; i++) { new Thread(new Producer(myQueue)).start(); } for (int i = 0; i < numConsumers; i++) { new Thread(new Consumer(myQueue)).start(); } Thread.sleep(1000); System.exit(0); } }
运行结果
生产者资源队列大小= 1 生产者资源队列大小= 1 消费者 资源 队列大小 1 生产者资源队列大小= 1 消费者 资源 队列大小 1 消费者 资源 队列大小 1 生产者资源队列大小= 1 生产者资源队列大小= 3 消费对象 java.lang.Object@1e1aa52b 生产者资源队列大小= 2 生产者资源队列大小= 5 消费对象 java.lang.Object@6e740a76 消费对象 java.lang.Object@697853f6 ...... 消费对象 java.lang.Object@41a10cbc 消费对象 java.lang.Object@4963c8d1 消费者 资源 队列大小 5 生产者资源队列大小= 5 生产者资源队列大小= 5 消费者 资源 队列大小 4 消费对象 java.lang.Object@3e49c35d 消费者 资源 队列大小 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是一个返回结果而且可能抛出异常的任务。
为了便于理解,咱们能够将Callable比做一个Runnable接口,而Callable的call()方法则相似于Runnable的run()方法。
public class CallableFutureTest { public static void main(String[] args) throws InterruptedException, ExecutionException { System.out.println("start main thread "); ExecutorService exec = Executors.newFixedThreadPool(2); //新建一个Callable 任务,并将其提交到一个ExecutorService. 将返回一个描述任务状况的Future. Callable<String> call = new Callable<String>() { @Override public String call() throws Exception { System.out.println("start new thread "); Thread.sleep(5000); System.out.println("end new thread "); return "我是返回的内容"; } }; Future<String> task = exec.submit(call); Thread.sleep(1000); String retn = task.get(); //关闭线程池 exec.shutdown(); System.out.println(retn + "--end main thread"); } }
控制台打印
start main thread start new thread end new thread 我是返回的内容--end main thread
FutureTask可用于异步获取执行结果或取消执行任务的场景。经过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,以后能够在外部经过FutureTask的get方法异步获取执行结果,所以,FutureTask很是适合用于耗时的计算,主线程能够在完成本身的任务后,再去获取结果。另外,FutureTask还能够确保即便调用了屡次run方法,它都只会执行一次Runnable或者Callable任务,或者经过cancel取消FutureTask的执行等。
FutureTask执行多任务计算的使用场景
利用FutureTask和ExecutorService,能够用多线程的方式提交计算任务,主线程继续执行其余任务,当主线程须要子线程的计算结果时,在异步获取子线程的执行结果。
import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; public class FutureTaskForMultiCompute { public static void main(String[] args) { FutureTaskForMultiCompute inst = new FutureTaskForMultiCompute(); // 建立任务集合 List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>(); // 建立线程池 ExecutorService exec = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { // 传入Callable对象建立FutureTask对象 FutureTask<Integer> ft = new FutureTask<Integer>(inst.new ComputeTask(i, "" + i)); taskList.add(ft); // 提交给线程池执行任务,也能够经过exec.invokeAll(taskList)一次性提交全部任务; exec.submit(ft); } System.out.println("全部计算任务提交完毕, 主线程接着干其余事情!"); // 开始统计各计算线程计算结果 Integer totalResult = 0; for (FutureTask<Integer> ft : taskList) { try { //FutureTask的get方法会自动阻塞,直到获取计算结果为止 totalResult = totalResult + ft.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } // 关闭线程池 exec.shutdown(); System.out.println("多任务计算后的总结果是:" + totalResult); } private class ComputeTask implements Callable<Integer> { private Integer result = 0; private String taskName = ""; public ComputeTask(Integer iniResult, String taskName) { result = iniResult; this.taskName = taskName; System.out.println("生成子线程计算任务: " + taskName); } public String getTaskName() { return this.taskName; } @Override public Integer call() throws Exception { // TODO Auto-generated method stub for (int i = 0; i < 100; i++) { result = +i; } // 休眠5秒钟,观察主线程行为,预期的结果是主线程会继续执行,到要取得FutureTask的结果是等待直至完成。 Thread.sleep(5000); System.out.println("子线程计算任务: " + taskName + " 执行完成!"); return result; } } }
生成子线程计算任务: 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和链接池对应的对应关系,典型的代码以下面所示:
private Map<String, Connection> connectionPool = new HashMap<String, Connection>(); private ReentrantLock lock = new ReentrantLock(); public Connection getConnection(String key) { try { lock.lock(); if (connectionPool.containsKey(key)) { return connectionPool.get(key); } else { //建立 Connection Connection conn = createConnection(); connectionPool.put(key, conn); return conn; } } finally { lock.unlock(); } } //建立Connection private Connection createConnection() { return null; }
在上面的例子中,咱们经过加锁确保高并发环境下的线程安全,也确保了connection只建立一次,然而确牺牲了性能。改用ConcurrentHash的状况下,几乎能够避免加锁的操做,性能大大提升,可是在高并发的状况下有可能出现Connection被建立屡次的现象。这时最须要解决的问题就是当key不存在时,建立Connection的动做能放在connectionPool以后执行,这正是FutureTask发挥做用的时机,基于ConcurrentHashMap和FutureTask的改造代码以下:
private ConcurrentHashMap<String, FutureTask<Connection>> connectionPool = new ConcurrentHashMap<String, FutureTask<Connection>>(); public Connection getConnection(String key) throws Exception { FutureTask<Connection> connectionTask = connectionPool.get(key); if (connectionTask != null) { return connectionTask.get(); } else { Callable<Connection> callable = new Callable<Connection>() { @Override public Connection call() throws Exception { // TODO Auto-generated method stub return createConnection(); } }; FutureTask<Connection> newTask = new FutureTask<Connection>(callable); connectionTask = connectionPool.putIfAbsent(key, newTask); if (connectionTask == null) { connectionTask = newTask; connectionTask.run(); } return connectionTask.get(); } } //建立Connection private Connection createConnection() { return null; }
通过这样的改造,能够避免因为并发带来的屡次建立链接及锁的出现。
主要表明有Vector和Hashtable,以及Collections.synchronizedXxx等。
锁的粒度为当前对象总体。
迭代器是及时失败的,即在迭代的过程当中发现被修改,就会抛出ConcurrentModificationException。
主要表明有ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap、ConcurrentSkipListSet。
锁的粒度是分散的、细粒度的,即读和写是使用不一样的锁。
迭代器具备弱一致性,便可以容忍并发修改,不会抛出ConcurrentModificationException。
JDK 7 ConcurrentHashMap
采用分离锁技术,同步容器中,是一个容器一个锁,但在ConcurrentHashMap中,会将hash表的数组部分分红若干段,每段维护一个锁,以达到高效的并发访问;
JDK 8 ConcurrentHashMap
采用分离锁技术,同步容器中,是一个容器一个锁,但在ConcurrentHashMap中,会将hash表的数组部分分红若干段,每段维护一个锁,以达到高效的并发访问;
主要表明有LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue(Comparable,Comparator)、SynchronousQueue。
提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。
适用于生产者、消费者模式(线程池和工做队列-Executor),同时也是同步容器
主要表明有ArrayDeque和LinkedBlockingDeque。
意义:正如阻塞队列适用于生产者消费者模式,双端队列一样适用与另外一种模式,即工做密取。在生产者-消费者设计中,全部消费者共享一个工做队列,而在工做密取中,每一个消费者都有各自的双端队列。
若是一个消费者完成了本身双端队列中的所有工做,那么他就能够从其余消费者的双端队列末尾秘密的获取工做。具备更好的可伸缩性,这是由于工做者线程不会在单个共享的任务队列上发生竞争。
在大多数时候,他们都只是访问本身的双端队列,从而极大的减小了竞争。当工做者线程须要访问另外一个队列时,它会从队列的尾部而不是头部获取工做,所以进一步下降了队列上的竞争。
适用于:网页爬虫等任务中
若是不须要阻塞队列,优先选择ConcurrentLinkedQueue;
若是须要阻塞队列,队列大小固定优先选择ArrayBlockingQueue,队列大小不固定优先选择LinkedBlockingQueue;
若是须要对队列进行排序,选择PriorityBlockingQueue;
若是须要一个快速交换的队列,选择SynchronousQueue;
若是须要对队列中的元素进行延时操做,则选择DelayQueue。
什么是多线程?
多线程:是指从软件或者硬件上实现多个线程的并发技术。
多线程的好处:
多线程的缺点:
即便是单核CPU也支持多线程执行代码,CPU经过给每一个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,由于时间片很是短,因此CPU经过不停地切换线程执行,让咱们感受多个线程时同时执行的,时间片通常是几十毫秒(ms)
上下文切换过程当中,CPU会中止处理当前运行的程序,并保存当前程序运行的具体位置以便以后继续运行
CPU经过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。可是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,能够再次加载这个任务的状态
Java中的ThreadLocal类容许咱们建立只能被同一个线程读写的变量。所以,若是一段代码含有一个ThreadLocal变量的引用,即便两个线程同时执行这段代码,它们也没法访问到对方的ThreadLocal变量
如何建立ThreadLocal变量
如下代码展现了如何建立一个ThreadLocal变量:
private ThreadLocal myThreadLocal = new ThreadLocal();
经过这段代码实例化了一个ThreadLocal对象。咱们只须要实例化对象一次,而且也不须要知道它是被哪一个线程实例化。虽然全部的线程都能访问到这个ThreadLocal实例,可是每一个线程却只能访问到本身经过调用ThreadLocal的set()方法设置的值。即便是两个不一样的线程在同一个ThreadLocal对象上设置了不一样的值,他们仍然没法访问到对方的值。
如何访问ThreadLocal变量
一旦建立了一个ThreadLocal变量,你能够经过以下代码设置某个须要保存的值:
myThreadLocal.set("A thread local value”);
能够经过下面方法读取保存在ThreadLocal变量中的值:
String threadLocalValue = (String) myThreadLocal.get();
get()方法返回一个Object对象,set()对象须要传入一个Object类型的参数。
为ThreadLocal指定泛型类型
public static ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();
咱们能够建立一个指定泛型类型的ThreadLocal对象,这样咱们就不须要每次对使用get()方法返回的值做强制类型转换了。下面展现了指定泛型类型的ThreadLocal例子:
ThreadLocal的设计理念与做用
http://blog.csdn.net/u0118607...://blog.csdn.net/u011860731/article/details/48733073)
public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<Integer>();
InheritableThreadLocal类是ThreadLocal类的子类。ThreadLocal中每一个线程拥有它本身的值,与ThreadLocal不一样的是,InheritableThreadLocal容许一个线程以及该线程建立的全部子线程均可以访问它保存的值。
InheritableThreadLocal 原理
Java 多线程:InheritableThreadLocal 实现原理
http://blog.csdn.net/ni357103403/article/details/51970748
减小了建立和销毁线程的次数,每一个工做线程均可以被重复利用,可执行多个任务
能够根据系统的承受能力,调整线程池中工做线线程的数目,防止由于由于消耗过多的内存,而把服务器累趴下(每一个线程须要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)
Java提供的四种线程池的好处在于:
类 | 描述 |
---|---|
ExecutorService | 真正的线程池接口。 |
ScheduledExecutorService | 能和Timer/TimerTask相似,解决那些须要任务重复执行的问题。 |
ThreadPoolExecutor | ExecutorService的默认实现。 |
ScheduledThreadPoolExecutor | 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。 |
要配置一个线程池是比较复杂的,尤为是对于线程池的原理不是很清楚的状况下,颇有可能配置的线程池不是较优的,所以在Executors类里面提供了一些静态工厂,生成一些经常使用的线程池。
newCachedThreadPool建立一个可缓存线程池,若是线程池长度超过处理须要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 建立一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 建立一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 建立一个单线程化的线程池,它只会用惟一的工做线程来执行任务,保证全部任务按照指定顺序(FIFO, LIFO, 优先级)执行。
使用ThreadPoolExecutor建立线程池
ThreadPoolExecutor的构造函数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
ThreadPoolExecutor.AbortPolicy()
直接抛出异常RejectedExecutionExceptionThreadPoolExecutor.CallerRunsPolicy()
直接调用run方法而且阻塞执行ThreadPoolExecutor.DiscardPolicy()
直接丢弃后来的任务ThreadPoolExecutor.DiscardOldestPolicy()
丢弃在队列中队首的任务固然能够本身继承 RejectedExecutionHandler 来写拒绝策略.
https://juejin.im/post/59df0c1af265da432f301c8d
阻塞队列
一、ArrayBlockingQueue 数组结构组成的有界阻塞队列。
此队列按照先进先出(FIFO)的原则对元素进行排序,可是默认状况下不保证线程公平的访问队列,即若是队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。
CountDownLatch 容许一个或多个线程等待其余线程完成操做。
应用场景
假若有这样一个需求,当咱们须要解析一个Excel里多个sheet的数据时,能够考虑使用多线程,每一个线程解析一个sheet里的数据,等到全部的sheet都解析完以后,程序须要提示解析完成。
在这个需求中,要实现主线程等待全部线程完成sheet的解析操做,最简单的作法是使用join。代码以下:
public class JoinCountDownLatchTest { public static void main(String[] args) throws InterruptedException { Thread parser1 = new Thread(new Runnable() { @Override public void run() { } }); Thread parser2 = new Thread(new Runnable() { @Override public void run() { System.out.println("parser2 finish"); } }); parser1.start(); parser2.start(); parser1.join(); parser2.join(); System.out.println("all parser finish"); } }
join用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存活,若是join线程存活则让当前线程永远wait,代码片断以下,wait(0)表示永远等待下去。
while (isAlive()) { wait(0); }
public class Test { public static void main(String[] args) { final CountDownLatch latch = new CountDownLatch(2); new Thread(){ public void run() { try { System.out.println("子线程"+Thread.currentThread().getName()+"正在执行"); Thread.sleep(3000); System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕"); latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); new Thread(){ public void run() { try { System.out.println("子线程"+Thread.currentThread().getName()+"正在执行"); Thread.sleep(3000); System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕"); latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); try { System.out.println("等待2个子线程执行完毕..."); latch.await(); System.out.println("2个子线程已经执行完毕"); System.out.println("继续执行主线程"); } catch (InterruptedException e) { e.printStackTrace(); } } }
线程Thread-0正在执行 线程Thread-1正在执行 等待2个子线程执行完毕... 线程Thread-0执行完毕 线程Thread-1执行完毕 2个子线程已经执行完毕 继续执行主线程
new CountDownLatch(2)的构造函数接收一个int类型的参数做为计数器,若是你想等待N个点完成,这里就传入N。
当咱们调用一次CountDownLatch的countDown()方法时,N就会减1,CountDownLatch的await()会阻塞当前线程,直到N变成零。因为countDown方法能够用在任何地方,因此这里说的N个点,能够是N个线程,也能够是1个线程里的N个执行步骤。用在多个线程时,你只须要把这个CountDownLatch的引用传递到线程里。
Java并发编程:CountDownLatch、CyclicBarrier和 Semaphore
http://www.importnew.com/21889.html
java在编写多线程程序时,为了保证线程安全,须要对数据同步,常常用到两种同步方式就是Synchronized和重入锁ReentrantLock。
synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由JVM实现,用户不须要显示的释放锁,很是方便。然而synchronized也有必定的局限性
例如:
ReentrantLock它是JDK 1.5以后提供的API层面的互斥锁,须要lock()和unlock()方法配合try/finally语句块来完成。
代码示例
private Lock lock = new ReentrantLock(); public void test(){ lock.lock(); try{ doSomeThing(); }catch (Exception e){ // ignored }finally { lock.unlock(); } }
ReentrantLock 一些特性
公平锁:线程获取锁的顺序和调用lock的顺序同样,FIFO;
非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。
Java并发包(java.util.concurrent)中大量使用了CAS操做,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操做。
简单来讲,ReenTrantLock的实现是一种自旋锁,经过循环调用CAS操做来实现加锁。它的性能比较好也是由于避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是咱们去分析和理解锁设计的关键钥匙。
在Synchronized优化之前,synchronized的性能是比ReenTrantLock差不少的,可是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,二者的性能就差很少了,在两种方法均可用的状况下,官方甚至建议使用synchronized,其实synchronized的优化我感受就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
synchronized:
在资源竞争不是很激烈的状况下,偶尔会有同步的情形下,synchronized是很合适的。缘由在于,编译程序一般会尽量的进行优化synchronize,另外可读性很是好。
ReentrantLock:
ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,二者是同样的,因此无特殊状况下,推荐使用synchronized。ReentrantLock的优点在于它更灵活、更强大,增长了轮训、超时、中断等高级功能。
ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程规规矩矩地排队,须要增长阻塞和唤醒的时间开销。若是直接插队获取非公平锁,跳过了对队列的处理,速度会更快。
ReentrantLock实现原理
http://www.javashuo.com/article/p-gcsvtqbz-go.html
分析ReentrantLock的实现原理(ReentrantLock和同步工具类的实现基础都是AQS)
https://www.jianshu.com/p/fe027772e156
Semaphore类位于java.util.concurrent包下,它提供了2个构造器:
//参数permits表示许可数目,即同时能够容许多少线程进行访问 public Semaphore(int permits) { sync = new NonfairSync(permits); } //这个多了一个参数fair表示是不是公平的,即等待时间越久的越先获取许可 public Semaphore(int permits, boolean fair) { sync = (fair)? new FairSync(permits) : new NonfairSync(permits); }
Semaphore类中比较重要的几个方法,首先是acquire()、release()方法: acquire()用来获取一个许可,若无许可可以得到,则会一直等待,直到得到许可。 release()用来释放许可。注意,在释放许可以前,必须先获得到许可。
这4个方法都会被阻塞,若是想当即获得执行结果,能够使用下面几个方法:
//尝试获取一个许可,若获取成功,则当即返回true,若获取失败,则当即返回false public boolean tryAcquire() { }; //尝试获取一个许可,若在指定的时间内获取成功,则当即返回true,不然则当即返回false public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取permits个许可,若获取成功,则当即返回true,若获取失败,则当即返回false public boolean tryAcquire(int permits) { }; //尝试获取permits个许可,若在指定的时间内获取成功,则当即返回true public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //获得当前可用的许可数目 public int availablePermits();
倘若一个工厂有5台机器,可是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其余工人才能继续使用。那么咱们就能够经过Semaphore来实现:
public class Test { public static void main(String[] args) { int N = 8; //工人数 Semaphore semaphore = new Semaphore(5); //机器数目 for(int i=0;i<N;i++) new Worker(i,semaphore).start(); } static class Worker extends Thread{ private int num; private Semaphore semaphore; public Worker(int num,Semaphore semaphore){ this.num = num; this.semaphore = semaphore; } @Override public void run() { try { semaphore.acquire(); System.out.println("工人"+this.num+"占用一个机器在生产..."); Thread.sleep(2000); System.out.println("工人"+this.num+"释放出机器"); semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } } } }
运行结果:
工人0占用一个机器在生产... 工人1占用一个机器在生产... 工人2占用一个机器在生产... 工人4占用一个机器在生产... 工人5占用一个机器在生产... 工人0释放出机器 工人2释放出机器 工人3占用一个机器在生产... 工人7占用一个机器在生产... 工人4释放出机器 工人5释放出机器 工人1释放出机器 工人6占用一个机器在生产... 工人3释放出机器 工人7释放出机器 工人6释放出机器
Lock接口比同步方法和同步块提供了更具扩展性的锁操做。他们容许更灵活的结构,能够具备彻底不一样的性质,而且能够支持多个相关类的条件对象。
它的优点有:
同一时间只能有一条线程执行固定类的同步方法,可是对于类的非同步方法,能够多条线程同时访问。因此,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则能够正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,可是没有对size++,线程B就已经读取size了,那么对于线程B来讲读取到的size必定是不许确的。
而给size()方法加了同步以后,意味着线程B调用size()方法只有在线程A调用put方法完毕以后才能够调用,这样就保证了线程安全性
ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时能够有16条线程操做ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优点
Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁相似,锁自己也应该是一个对象。两个线程执行的代码片断要实现同步互斥的效果,它们必须用同一个Lock对象。
读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm本身控制的,你只要上好相应的锁便可。
若是你的代码只读数据,能够不少人同时读,但不能同时写,那就上读锁;
若是你的代码修改数据,只能有一我的在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁
线程进入读锁的前提条件:
线程进入写锁的前提条件: