Java面试总结汇总,整理了包括Java重点知识,以及经常使用开源框架,欢迎你们阅读。文章可能有错误的地方,由于我的知识有限,欢迎各位大佬指出!文章持续更新中......java
ID 标题 地址 1 设计模式面试题(总结最全面的面试题) juejin.cn/post/684490… 2 Java基础知识面试题(总结最全面的面试题) juejin.cn/post/684490… 3 Java集合面试题(总结最全面的面试题) juejin.cn/post/684490… 4 JavaIO、BIO、NIO、AIO、Netty面试题(总结最全面的面试题) juejin.cn/post/684490… 5 Java并发编程面试题(总结最全面的面试题) juejin.cn/post/684490… 6 Java异常面试题(总结最全面的面试题) juejin.cn/post/684490… 7 Java虚拟机(JVM)面试题(总结最全面的面试题) juejin.cn/post/684490… 8 Spring面试题(总结最全面的面试题) juejin.cn/post/684490… 9 Spring MVC面试题(总结最全面的面试题) juejin.cn/post/684490… 10 Spring Boot面试题(总结最全面的面试题) juejin.cn/post/684490… 11 Spring Cloud面试题(总结最全面的面试题) juejin.cn/post/684490… 12 Redis面试题(总结最全面的面试题) juejin.cn/post/684490… 13 MyBatis面试题(总结最全面的面试题) juejin.cn/post/684490… 14 MySQL面试题(总结最全面的面试题) juejin.cn/post/684490… 15 TCP、UDP、Socket、HTTP面试题(总结最全面的面试题) juejin.cn/post/684490… 16 Nginx面试题(总结最全面的面试题) juejin.cn/post/684490… 17 ElasticSearch面试题 18 kafka面试题 19 RabbitMQ面试题(总结最全面的面试题) juejin.cn/post/684490… 20 Dubbo面试题(总结最全面的面试题) juejin.cn/post/684490… 21 ZooKeeper面试题(总结最全面的面试题) juejin.cn/post/684490… 22 Netty面试题(总结最全面的面试题) 23 Tomcat面试题(总结最全面的面试题) juejin.cn/post/684490… 24 Linux面试题(总结最全面的面试题) juejin.cn/post/684490… 25 互联网相关面试题(总结最全面的面试题) 26 互联网安全面试题(总结最全面的面试题)
提高多核CPU的利用率:通常来讲一台主机上的会有多个CPU核心,咱们能够建立多个线程,理论上讲操做系统能够将多个线程分配给不一样的CPU去执行,每一个CPU执行一个线程,这样就提升了CPU的使用效率,若是使用单线程就只能有一个CPU核心被使用。linux
好比当咱们在网上购物时,为了提高响应速度,须要拆分,减库存,生成订单等等这些操做,就能够进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。程序员
简单来讲就是:面试
出现线程安全问题的缘由通常都是三个缘由:算法
线程切换带来的原子性问题 解决办法:使用多线程之间同步synchronized或使用锁(lock)。数据库
缓存致使的可见性问题 解决办法:synchronized、volatile、LOCK,能够解决可见性问题编程
编译优化带来的有序性问题 解决办法:Happens-Before 规则能够解决有序性问题windows
作一个形象的比喻:设计模式
并发 = 俩我的用一台电脑。数组
并行 = 俩我的分配了俩台电脑。
串行 = 俩我的排队使用一台电脑。
线程也是程序,因此线程须要占用内存,线程越多占用内存也越多;
多线程须要协调和管理,因此须要 CPU 时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
什么是线程和进程?
进程
一个在内存中运行的应用程序。 每一个正在系统上运行的程序都是一个进程
线程
进程中的一个执行任务(控制单元), 它负责在程序里独立执行。
一个进程至少有一个线程,一个进程能够运行多个线程,多个线程可共享数据。
进程与线程的区别
根本区别:进程是操做系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每一个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程能够看作轻量级的进程,同一类线程共享代码和数据空间,每一个线程都有本身独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:若是一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,因此线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程与进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其余进程产生影响,可是一个线程崩溃有可能致使整个进程都死掉。因此多进程要比多线程健壮。
执行过程:每一个独立的进程有程序运行的入口、顺序执行序列和程序出口。可是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,二者都可并发执行
多线程编程中通常线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能获得有效执行,CPU 采起的策略是为每一个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会从新处于就绪状态让给其余线程使用,这个过程就属于一次上下文切换。
归纳来讲就是:当前任务在执行完 CPU 时间片切换到另外一个任务以前会先保存本身的状态,以便下次再切换回这个任务时,能够再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换一般是计算密集型的。也就是说,它须要至关可观的处理器时间,在每秒几十上百次的切换中,每次切换都须要纳秒量级的时间。因此,上下文切换对系统来讲意味着消耗大量的 CPU 时间,事实上,多是操做系统中时间消耗最大的操做。
Linux 相比与其余操做系统(包括其余类 Unix 系统)有不少的优势,其中有一项就是,其上下文切换和模式切换的时间消耗很是少。
或者直接使用JDK自带的工具查看“jconsole” 、“visualVm”,这都是JDK自带的,能够直接在JDK的bin目录下找到直接使用
继承 Thread 类;
public class MyThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法正在执行..."); } 复制代码
实现 Runnable 接口;
public class MyRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法执行中..."); } 复制代码
实现 Callable 接口;
public class MyCallable implements Callable<Integer> { @Override public Integer call() { System.out.println(Thread.currentThread().getName() + " call()方法执行中..."); return 1; } 复制代码
使用匿名内部类方式
public class CreateRunnable { public static void main(String[] args) { //建立多线程建立开始 Thread thread = new Thread(new Runnable() { public void run() { for (int i = 0; i < 10; i++) { System.out.println("i:" + i); } } }); thread.start(); } } 复制代码
相同点:
主要区别:
每一个线程都是经过某个特定Thread对象所对应的方法run()来完成其操做的,run()方法称为线程体。经过调用Thread类的start()方法来启动一个线程。
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 能够重复调用,而 start() 只能调用一次。
start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,能够直接继续执行其余的代码; 此时线程是处于就绪状态,并无运行。 而后经过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。而后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 若是直接调用run(),其实就至关因而调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,因此执行路径仍是只有一条,根本就没有线程的特征,因此在多线程执行时要使用start()方法而不是run()方法。
这是另外一个很是经典的 java 多线程面试问题,并且在面试中会常常被问到。很简单,可是不少人都会答不上来!
new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片
后就能够开始运行了。 start() 会执行线程的相应准备工做,而后自动执行 run() 方法的内容,这是真正的多线程工做。
而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,因此这并非多线程工做。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,仍是在主线程里执行。
Callable 接口相似于 Runnable,从名字就能够看出来了,可是 Runnable 不会返回结果,而且没法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,能够返回值,这个返回值能够被 Future 拿到,也就是说,Future 能够拿到异步执行任务的返回值。
Future 接口表示异步任务,是一个可能尚未完成的异步任务的结果。因此说 Callable用于产生结果,Future 用于获取结果。
新建(new):新建立了一个线程对象。
就绪(可运行状态)(runnable):线程对象建立后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
运行(running):可运行状态(runnable)的线程得到了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的惟一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞(block):处于运行状态中的线程因为某种缘由,暂时放弃对 CPU的使用权,中止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
死亡(dead)(结束):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
计算机一般只有一个 CPU,在任意时刻只能执行一条机器指令,每一个线程只有得到CPU 的使用权才能执行指令。所谓多线程的并发运行,实际上是指从宏观上看,各个线程轮流得到 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。(Java是由JVM中的线程计数器来实现线程调度)
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让全部的线程轮流得到 cpu 的使用权,而且平均分配每一个线程占用的 CPU 的时间片这个也比较好理解。
Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,若是可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
线程调度器选择优先级最高的线程运行,可是,若是发生如下状况,就会终止线程的运行:
(1)线程体中调用了 yield 方法让出了对 cpu 的占用权利
(2)线程体中调用了 sleep 方法使线程进入睡眠状态
(3)线程因为 IO 操做受到阻塞
(4)另一个更高优先级线程出现
(5)在支持时间片的系统中,该线程的时间片用完
线程调度器是一个操做系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦咱们建立一个线程并启动它,它的执行便依赖于线程调度器的实现。
时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间能够基于线程优先级或者线程等待的时间。
线程调度并不受到 Java 虚拟机控制,因此由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。
(1) wait():使一个线程处于等待(阻塞)状态,而且释放所持有的对象的锁;
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
(3)notify():唤醒一个处于等待状态的线程,固然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 肯定唤醒哪一个线程,并且与优先级无关;
(4)notityAll():唤醒全部处于等待状态的线程,该方法并非将对象的锁给全部线程,而是让它们竞争,只有得到锁的线程才能进入就绪状态;
二者均可以暂停线程的执行
处于等待状态的线程可能会收到错误警报和伪唤醒,若是不在循环中检查等待条件,程序就会在没有知足结束条件的状况下退出。
wait() 方法应该在循环调用,由于当线程获取到 CPU 开始执行的时候,其余条件可能尚未知足,因此在处理前,循环检测条件是否知足会更好。下面是一段标准的使用 wait 和 notify 方法的代码:
synchronized (monitor) { // 判断条件谓词是否获得知足 while(!locked) { // 等待唤醒 monitor.wait(); } // 处理其余的业务逻辑 } 复制代码
由于Java全部类的都继承了Object,Java想让任何对象均可以做为锁,而且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并无可供任何对象使用的锁,因此任意对象调用方法必定定义在Object类中。
有的人会说,既然是线程放弃对象锁,那也能够把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不须要从新定义wait()方法的实现。然而,这样作有一个很是大的问题,一个线程彻底能够持有不少锁,你一个线程放弃锁的时候,到底要放弃哪一个锁?固然了,这种设计并非不能实现,只是管理起来更加复杂。
使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。
当前线程到了就绪状态,那么接下来哪一个线程会从就绪状态变成执行状态呢?多是当前线程,也多是其余线程,看系统的分配了。
(1) sleep()方法给其余线程运行机会时不考虑线程的优先级,所以会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
(2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
(3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
(4)sleep()方法比 yield()方法(跟操做系统 CPU 调度相关)具备更好的可移植性,一般不建议使用yield()方法来控制并发线程的执行。
interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
注意:线程中断仅仅是置线程的中断状态位,不会中止线程。须要用户本身去监视线程的状态为并作处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
interrupted:是静态方法,查看当前中断信号是true仍是false而且清除中断信号。若是一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
isInterrupted:是能够返回当前中断信号是true仍是false,与interrupt最大的差异
首先 ,wait()、notify() 方法是针对对象的,调用任意对象的 wait()方法都将致使线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它须要从新获取该对象的锁,直到获取成功才能往下执行;
其次,wait、notify 方法必须在 synchronized 块或方法中被调用,而且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 以前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将以前获取的对象锁释放。
若是线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
notifyAll() 会唤醒全部的线程,notify() 只会唤醒一个线程。
notifyAll() 调用后,会将所有线程由等待池移到锁池,而后参与锁的竞争,竞争成功则继续执行,若是不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪个线程由虚拟机控制。
通常来讲,共享变量要求变量自己是线程安全的,而后在线程内使用的时候,若是有对共享变量的复合操做,那么也得保证复合操做的线程安全性。
能够经过中断 和 共享变量的方式实现线程间的通信和协做
好比说最经典的生产者-消费者模型:当队列满时,生产者须要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。由于生产者若是不释放对临界资源的占用权,那么消费者就没法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。所以,通常状况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。而后等待消费者消费了商品,而后消费者通知生产者队列有空间了。一样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通讯的过程就是线程间的协做。
Java中线程通讯协做的最多见方式:
一.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
二.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
线程间直接的数据交换:
同步块是更好的选择,由于它不会锁住整个对象(固然你也可让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这一般会致使他们中止执行并须要等待得到这个对象上的锁。
同步块更要符合开放调用的原则,只在须要锁住的代码块锁住相应的对象,这样从侧面来讲也能够避免死锁。
请知道一条原则:同步的范围越小越好。
当一个线程对共享的数据进行操做时,应使之成为一个”原子操做“,即在没有完成相关操做以前,不容许其余线程打断它,不然,就会破坏数据的完整性,必然会获得错误的处理结果,这就是线程的同步。
在多线程应用中,考虑不一样线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会造成线程之间的死锁。为了防止死锁的发生,须要经过同步来实现线程安全。
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任什么时候刻最多只容许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥能够当作是一种特殊的线程同步。
线程间的同步方法大致可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时须要切换内核态与用户态,而用户模式就是不须要切换到内核态,只在用户态完成操做。
用户模式下的方法有:原子操做(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
实现线程同步的方法
同步代码方法:sychronized 关键字修饰的方法
同步代码块:sychronized 关键字修饰的代码块
使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具备相同的基本行为和语义
在 java 虚拟机中,监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每个监视器都和一个对象引用相关联。线程在获取锁以前不容许执行同步代码。
一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁以前不容许执行该部分的代码
另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案
有俩种可能:
(1)若是使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,不要紧,继续添加任务到阻塞队列中等待执行,由于 LinkedBlockingQueue 能够近乎认为是一个无穷大的队列,能够无限存听任务
(2)若是使用的是有界队列好比 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增长线程数量,若是增长了线程数量仍是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
线程安全是编程中的术语,指某个方法在多线程环境中被调用时,可以正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
Struts2 的 action 是多实例多线程的,是线程安全的,每一个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。
SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 相似的处理流程。
Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 须要考虑线程安全问题,可是性能能够提高不用处理太多的 gc,能够使用 ThreadLocal 来处理多线程的问题。
方法一:使用安全类,好比 java.util.concurrent 下的类,使用原子类AtomicInteger
方法二:使用自动锁 synchronized。
方法三:使用手动锁 Lock。
手动锁 Java 示例代码以下:
Lock lock = new ReentrantLock(); lock. lock(); try { System. out. println("得到锁"); } catch (Exception e) { // TODO: handle exception } finally { System. out. println("释放锁"); lock. unlock(); } 复制代码
每个线程都是有优先级的,通常来讲,高优先级的线程在运行时会具备优先权,但这依赖于线程调度的实现,这个实现是和操做系统相关的(OS dependent)。咱们能够定义线程的优先级,可是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 表明最低优先级,10 表明最高优先级。
Java 的线程优先级调度会委托给操做系统去处理,因此与具体的操做系统优先级有关,如非特别须要,通常无需设置线程优先级。
固然,若是你真的想设置优先级能够经过setPriority()方法设置,可是设置了不必定会该变,这个是不许确的
这是一个很是刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。
若是说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么:
(1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 本身调用的
(2)Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 本身调用的
Dump文件是进程的内存镜像。能够把程序的执行状态经过调试器保存到dump文件中。
在 Linux 下,你能够经过命令 kill -3 PID (Java 进程的进程 ID)来获取 Java应用的 dump 文件。
在 Windows 下,你能够按下 Ctrl + Break 来获取。这样 JVM 就会将线程的 dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。
线程的生命周期开销很是高
消耗过多的 CPU
资源若是可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,并且大量的线程在竞争 CPU资源时还将产生其余性能的开销。
下降稳定性JVM
在可建立线程的数量上存在一个限制,这个限制值将随着平台的不一样而不一样,而且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操做系统对线程的限制等。若是破坏了这些限制,那么可能抛出OutOfMemoryError 异常。
方法 名 | 描述 |
---|---|
sleep() | 强迫一个线程睡眠N毫秒 |
isAlive() | 判断一个线程是否存活。 |
join() | 等待线程终止。 |
activeCount() | 程序中活跃的线程数。 |
enumerate() | 枚举程序中的线程。 |
currentThread() | 获得当前线程。 |
isDaemon() | 一个线程是否为守护线程。 |
setDaemon() | 设置一个线程为守护线程。 |
setName() | 为线程设置一个名称。 |
wait() | 强迫一个线程等待。 |
notify() | 通知一个线程继续运行。 |
setPriority() | 设置一个线程的优先级。 |
垃圾回收是在内存中存在没有引用的对象或超过做用域的对象时进行的。
垃圾回收的目的是识别而且丢弃应用再也不使用的对象来释放和重用资源。
在并发编程中,咱们须要处理两个关键问题:线程之间如何通讯及线程之间如何同步。通讯是指线程之间以如何来交换信息。通常线程之间的通讯机制有两种:共享内存和消息传递。
Java的并发采用的是共享内存模型,Java线程之间的通讯老是隐式进行,整个通讯过程对程序员彻底透明。若是编写多线程程序的Java程序员不理解隐式进行的线程之间通讯的工做机制,极可能会遇到各类奇怪的内存可见性问题。
下面经过示意图来讲明线程之间的通讯
不会,在下一个垃圾回调周期中,这个对象将是被可回收的。
也就是说并不会当即被垃圾收集器马上回收,而是在下一次垃圾回收时才会释放其占用的内存。
1.垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法; finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throws Throwable { } 在垃圾回收器执行时会调用被回收对象的finalize()方法,能够覆盖此方法来实现对其资源的回收。注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,而且下一次垃圾回收动做发生时,才真正回收对象占用的内存空间
int a = 5; //语句1 int r = 3; //语句2 a = a + 2; //语句3 r = a*a; //语句4 复制代码
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
as-if-serial语义和happens-before这么作的目的,都是为了在不改变程序执行结果的前提下,尽量地提升程序执行的并行度。
在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 能够修饰类、方法、变量。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,由于监视器锁(monitor)是依赖于底层的操做系统的 Mutex Lock 来实现的,Java 的线程是映射到操做系统的原生线程之上的。若是要挂起或者唤醒一个线程,都须要操做系统帮忙完成,而操做系统实现线程之间的切换时须要从用户态转换到内核态,这个状态之间的转换须要相对比较长的时间,时间成本相对较高,这也是为何早期的 synchronized 效率低的缘由。庆幸的是在 Java 6 以后 Java 官方对从 JVM 层面对synchronized 较大优化,因此如今的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减小锁操做的开销。
synchronized关键字最主要的三种使用方式:
总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽可能不要使用 synchronized(String a) 由于JVM中,字符串常量池具备缓存功能!
双重校验锁实现对象单例(线程安全)
说明:
双锁机制的出现是为了解决前面同步问题和性能问题,看下面的代码,简单分析下确实是解决了多线程并行进来不会出现重复new对象,并且也实现了懒加载
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
复制代码
}
另外,须要注意 uniqueInstance 采用 volatile 关键字修饰也是颇有必要。
可是因为 JVM 具备指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,可是在多线程环境下会致使一个线程得到尚未初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,所以返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 能够禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
Synchronized的语义底层是经过一个monitor(监视器锁)的对象来完成,
每一个对象有一个监视器锁(monitor)。每一个Synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态而且尝试获取monitor的全部权 ,过程:
一、若是monitor的进入数为0,则该线程进入monitor,而后将进入数设置为1,该线程即为monitor的全部者。
二、若是线程已经占有该monitor,只是从新进入,则进入monitor的进入数加1.
三、若是其余线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再从新尝试获取monitor的全部权。
synchronized是能够经过 反汇编指令 javap命令,查看相应的字节码文件。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 以后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,若是在运行过程当中,同步锁只有一个线程访问,不存在多线程争用的状况,则线程是不须要触发同步的,减小加锁/解锁的一些CAS操做(好比等待队列的一些CAS操做),这种状况下,就会给线程加一个偏向锁。 若是在运行过程当中,遇到了其余线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的状况下,当第二个线程加入锁争用的时候,轻量级锁就会升级为重量级锁;
重量级锁是synchronized ,是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,而且在目标锁被释放的时候,唤醒这些线程。
(1)volatile 修饰变量
(2)synchronized 修饰修改变量的方法
(3)wait/notify
(4)while 轮询
(1)synchronized 是悲观锁,属于抢占式,会引发其余线程阻塞。
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS 是基于冲突检测的乐观锁(非阻塞)
synchronized 是和 if、else、for、while 同样的关键字,ReentrantLock 是类,这是两者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,能够被继承、能够有方法、能够有各类各样的类变量
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,可是在 Java 6 中对 synchronized 进行了很是多的改进。
相同点:二者都是可重入锁
二者都是可重入锁。“可重入锁”概念是:本身能够再次获取本身的内部锁。好比一个线程得到了某个对象的锁,此时这个对象锁尚未释放,当其再次想要获取这个对象的锁的时候仍是能够获取的,若是不可锁重入的话,就会形成死锁。同一个线程每次获取锁,锁的计数器都自增1,因此要等到锁的计数器降低为0时才能释放锁。
主要区别以下:
Java中每个对象均可以做为锁,这是synchronized实现同步的基础:
对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其余线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会当即被更新到主内存中,当有其余线程须要读取时,它会去内存中读取新值。
从实践角度而言,volatile 的一个重要做用就是和 CAS 结合,保证了原子性,详细的能够参见 java.util.concurrent.atomic 包下的类,好比 AtomicInteger。
volatile 经常使用于多线程环境下的单次操做(单次读或者单次写)。
volatile 变量能够确保先行关系,即写操做会发生在后续的读操做以前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操做就不是原子性的。
而 AtomicInteger 类提供的 atomic 方法可让这种操做具备原子性如getAndIncrement()方法会原子性的进行增量操做把当前值加一,其它数据类型和引用变量也能够进行类似操做。
关键字volatile的主要做用是使变量在多个线程间可见,但没法保证原子性,对于多个线程访问同一个实例变量须要加锁进行同步。
虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double能够保证其操做原子性。
因此从Oracle Java Spec里面能够看到:
synchronized 表示只有一个线程能够获取做用对象的锁,执行代码,阻塞其余线程。
volatile 表示变量在 CPU 的寄存器中是不肯定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
区别
volatile 是变量修饰符;synchronized 能够修饰类、方法、变量。
volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则能够保证变量的修改可见性和原子性。
volatile 不会形成线程的阻塞;synchronized 可能会形成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量能够被编译器优化。
volatile关键字是线程同步的轻量级实现,因此volatile性能确定比synchronized关键字要好。可是volatile关键字只能用于变量而synchronized关键字能够修饰方法以及代码块。synchronized关键字在JavaSE1.6以后进行了主要包括为了减小得到锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各类优化以后执行效率有了显著提高,实际开发中使用 synchronized 关键字的场景仍是更多一些。
不可变对象(Immutable Objects)即对象一旦被建立它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。
不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。
只有知足以下状态,一个对象才是不可变的;
它的状态不能在建立后再被修改;
全部域都是 final 类型;而且,它被正确建立(建立期间没有发生 this 引用的逸出)。
不可变对象保证了对象的内存可见性,对不可变对象的读取不须要进行额外的同步手段,提高了代码执行效率。
Lock 接口比同步方法和同步块提供了更具扩展性的锁操做。他们容许更灵活的结构,能够具备彻底不一样的性质,而且能够支持多个相关类的条件对象。
它的优点有:
(1)能够使锁更公平
(2)能够使线程在等待锁的时候响应中断
(3)可让线程尝试获取锁,并在没法获取锁的时候当即返回或者等待一段时间
(4)能够在不一样的范围,以不一样的顺序获取和释放锁
总体上来讲 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操做。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,固然,在大部分状况下,非公平锁是高效的选择。
悲观锁:老是假设最坏的状况,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。再好比 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,能够使用版本号等机制。乐观锁适用于多读的应用类型,这样能够提升吞吐量,像数据库提供的相似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。
CAS 是 compare and swap 的缩写,即咱们所说的比较交换。
cas 是一种基于锁的操做,并且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个以前得到锁的线程释放锁以后,下一个线程才能够访问。而乐观锁采起了一种宽泛的态度,经过某种方式不加锁来处理资源,好比经过给记录加 version 来获取数据,性能较悲观锁有很大的提升。
CAS 操做包含三个操做数 —— 内存位置(V)、预期原值(A)和新值(B)。若是内存地址里面的值和 A 的值是同样的,那么就将内存里面的值更新成 B。CAS是经过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程须要自旋,到下次循环才有可能机会执行。
java.util.concurrent.atomic 包下的类大可能是使用 CAS 操做来实现的(AtomicInteger,AtomicBoolean,AtomicLong)
一、ABA 问题:
好比说一个线程 one 从内存位置 V 中取出 A,这时候另外一个线程 two 也从内存中取出 A,而且 two 进行了一些操做变成了 B,而后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操做发现内存中仍然是 A,而后 one 操做成功。尽管线程 one 的 CAS 操做成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
二、循环时间长开销大:
对于资源竞争严重(线程冲突严重)的状况,CAS 自旋的几率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
三、只能保证一个共享变量的原子操做:
当对一个共享变量执行操做时,咱们能够使用循环 CAS 的方式来保证原子操做,可是对多个共享变量操做时,循环 CAS 就没法保证操做的原子性,这个时候就能够用锁。
java.util.concurrent.atomic包:是原子类的小工具包,支持在单个变量上解除锁的线程安全编程 原子变量类至关于一种泛化的 volatile 变量,可以支持原子的和有条件的读-改-写操做。
好比:AtomicInteger 表示一个int类型的值,并提供了 get 和 set 方法,这些 Volatile 类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的 compareAndSet 方法(若是该方法成功执行,那么将实现与读取/写入一个 volatile 变量相同的内存效果),以及原子的添加、递增和递减等方法。AtomicInteger 表面上很是像一个扩展的 Counter 类,但在发生竞争的状况下能提供更高的可伸缩性,由于它直接利用了硬件对并发的支持。
简单来讲就是原子类来实现CAS无锁模式的算法
死锁:是指两个或两个以上的进程(或线程)在执行过程当中,因争夺资源而形成的一种互相等待的现象,若无外力做用,它们都将没法推动下去。
活锁:任务或者执行者没有被阻塞,因为某些条件没有知足,致使一直重复尝试,失败,尝试,失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
饥饿:一个或者多个线程由于种种缘由没法得到所须要的资源,致使一直没法执行的状态。
Java 中致使饥饿的缘由:
一、高优先级线程吞噬全部的低优先级线程的 CPU 时间。
二、线程被永久堵塞在一个等待进入同步块的状态,由于其余线程老是能在它以前持续地对该同步块进行访问。
三、线程在等待一个自己也处于永久等待完成的对象(好比调用这个对象的 wait 方法),由于其余线程老是被持续地得到唤醒。
线程池是为忽然大量爆发的线程设计的,经过有限的几个固定线程为大量的操做服务,减小了建立和销毁线程所需的时间,从而提升效率。
若是一个线程所须要执行的时间很是长的话,就不必用线程池了(不是不能做长时间操做,而是不宜。原本下降线程建立和销毁,结果你那么久我还很差控制还不如直接建立线程),何况咱们还不能控制线程池中线程的开始、挂起、和停止。
下降资源消耗:重用存在的线程,减小对象建立销毁的开销。
提升响应速度。可有效的控制最大并发线程数,提升系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务能够不须要的等到线程建立就能当即执行。
提升线程的可管理性。线程是稀缺资源,若是无限制的建立,不只会消耗系统资源,还会下降系统的稳定性,使用线程池能够进行统一的分配,调优和监控。
附加功能:提供定时执行、按期执行、单线程、并发数控制等功能。
ThreadPoolExecutor就是线程池
ThreadPoolExecutor其实也是JAVA的一个类,咱们通常经过Executors工厂类的方法,经过传入不一样的参数,就能够构造出适用于不一样应用场景下的ThreadPoolExecutor(线程池)
构造参数图:
构造参数参数介绍:
corePoolSize 核心线程数量
maximumPoolSize 最大线程数量
keepAliveTime 线程保持时间,N个时间单位
unit 时间单位(好比秒,分)
workQueue 阻塞队列
threadFactory 线程工厂
handler 线程池拒绝策略
复制代码
Executors框架实现的就是线程池的功能。
Executors工厂类中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其实也只是ThreadPoolExecutor的构造函数参数不一样而已。经过传入不一样的参数,就能够构造出适用于不一样应用场景下的线程池,
Executor工厂类如何建立线程池图:
Executors 工具类的不一样方法按照咱们的需求建立了不一样的线程池,来知足业务的需求。
Executor 接口对象能执行咱们的线程任务。
ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法咱们能得到任务执行的状态而且能够获取任务的返回值。
使用 ThreadPoolExecutor 能够建立自定义线程池。
特色:newCachedThreadPool建立一个可缓存线程池,若是当前线程池的长度超过了处理的须要时,它能够灵活的回收空闲的线程,当须要增长时, 它能够灵活的添加新的线程,而不会对池的长度做任何限制
缺点:他虽然能够无线的新建线程,可是容易形成堆外内存溢出,由于它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,通常来讲机器都没那么大内存给它不断使用。固然知道可能出问题的点,就能够去重写一个方法限制一下这个最大值
总结:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
代码示例:
package com.lijie; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestNewCachedThreadPool { public static void main(String[] args) { // 建立无限大小线程池,由jvm自动回收 ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { final int temp = i; newCachedThreadPool.execute(new Runnable() { public void run() { try { Thread.sleep(100); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + ",i==" + temp); } }); } } } 复制代码
特色:建立一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。
缺点:线程数量是固定的,可是阻塞队列是无界队列。若是有不少请求积压,阻塞队列愈来愈长,容易致使OOM(超出内存空间)
总结:请求的挤压必定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()
Runtime.getRuntime().availableProcessors()方法是查看电脑CPU核心数量)
代码示例:
package com.lijie; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestNewFixedThreadPool { public static void main(String[] args) { ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { final int temp = i; newFixedThreadPool.execute(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName() + ",i==" + temp); } }); } } } 复制代码
特色:建立一个固定长度的线程池,并且支持定时的以及周期性的任务执行,相似于Timer(Timer是Java的一个定时器类)
缺点:因为全部任务都是由同一个线程来调度,所以全部任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到以后的任务(好比:一个任务出错,之后的任务都没法继续)。
代码示例:
package com.lijie; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class TestNewScheduledThreadPool { public static void main(String[] args) { //定义线程池大小为3 ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3); for (int i = 0; i < 10; i++) { final int temp = i; newScheduledThreadPool.schedule(new Runnable() { public void run() { System.out.println("i:" + temp); } }, 3, TimeUnit.SECONDS);//这里表示延迟3秒执行。 } } } 复制代码
特色:建立一个单线程化的线程池,它只会用惟一的工做线程来执行任务,若是这个惟一的线程由于异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。保证全部任务按照指定顺序(FIFO, LIFO, 优先级)执行。
缺点:缺点的话,很明显,他是单线程的,高并发业务下有点无力
总结:保证全部任务按照指定顺序执行的,若是这个惟一的线程由于异常结束,那么会有一个新的线程来替代它
代码示例:
package com.lijie; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TestNewSingleThreadExecutor { public static void main(String[] args) { ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { final int index = i; newSingleThreadExecutor.execute(new Runnable() { public void run() { System.out.println(Thread.currentThread().getName() + " index:" + index); try { Thread.sleep(200); } catch (Exception e) { } } }); } } } 复制代码
ThreadGroup 类,能够把线程归属到某一个线程组中,线程组中能够有线程对象,也能够有线程组,组中还能够有线程,这样的组织结构有点相似于树的形式。
线程组和线程池是两个不一样的概念,他们的做用彻底不一样,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减小建立销毁线程的开销。
为何不推荐使用线程组?由于使用有不少的安全隐患吧,没有具体追究,若是须要使用,推荐使用线程池。
若是当前同时运行的线程数量达到最大线程数量而且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:
先看ThreadPoolExecutor(线程池)这个类的构造参数
corePoolSize 核心线程数量
maximumPoolSize 最大线程数量
keepAliveTime 线程保持时间,N个时间单位
unit 时间单位(好比秒,分)
workQueue 阻塞队列
threadFactory 线程工厂
handler 线程池拒绝策略
复制代码
代码示例:
package com.lijie; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class Test001 { public static void main(String[] args) { //建立线程池 ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3)); for (int i = 1; i <= 6; i++) { TaskThred t1 = new TaskThred("任务" + i); //executor.execute(t1);是执行线程方法 executor.execute(t1); } //executor.shutdown()再也不接受新的任务,而且等待以前提交的任务都执行完再关闭,阻塞队列中的任务不会再执行。 executor.shutdown(); } } class TaskThred implements Runnable { private String taskName; public TaskThred(String taskName) { this.taskName = taskName; } public void run() { System.out.println(Thread.currentThread().getName() + taskName); } } 复制代码
提交一个任务到线程池中,线程池的处理流程以下:
判断线程池里的核心线程是否都在执行任务,若是不是(核心线程空闲或者还有核心线程没有被建立)则建立一个新的工做线程来执行任务。若是核心线程都在执行任务,则进入下个流程。
线程池判断工做队列是否已满,若是工做队列没有满,则将新提交的任务存储在这个工做队列里。若是工做队列满了,则进入下个流程。
判断线程池里的线程是否都处于工做状态,若是没有,则建立一个新的工做线程来执行任务。若是已经满了,则交给饱和策略来处理这个任务。
CPU密集的意思是该任务须要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能获得加速(经过多线程),而在单核CPU上,不管你开几个模拟的多线程,该任务都不可能获得加速,由于CPU总的运算能力就那样。
CPU密集型时,任务能够少配置线程数,大概和机器的cpu核数至关,这样能够使得每一个线程都在执行任务
IO密集型时,大部分线程都阻塞,故须要多配置线程数,2*cpu核数
从如下几个角度分析任务的特性:
任务的性质:CPU密集型任务、IO密集型任务、混合型任务。
任务的优先级:高、中、低。
任务的执行时间:长、中、短。
任务的依赖性:是否依赖其余系统资源,如数据库链接等。
能够得出一个结论:
答:Vector、ConcurrentHashMap、HasTable
通常软件开发中容器用的最多的就是HashMap、ArrayList,LinkedList ,等等
可是在多线程开发中就不能乱用容器,若是使用了未加锁(非同步)的的集合,你的数据就会很是的混乱。由此在多线程开发中须要使用的容器必须是加锁(同步)的容器。
Vector与ArrayList同样,也是经过数组实现的,不一样的是它支持线程的同步,即某一时刻只有一个线程可以写Vector,避免多线程同时写而引发的不一致性,但实现同步须要很高的花费,访问它比访问ArrayList慢不少
(ArrayList是最经常使用的List实现类,内部是经过数组实现的,它容许对元素进行快速随机访问。当从ArrayList的中间位置插入或者删除元素时,须要对数组进行复制、移动、代价比较高。所以,它适合随机查找和遍历,不适合插入和删除。ArrayList的缺点是每一个元素之间不能有间隔。
)
ConcurrentHashMap是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap相似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每一个HashEntry是一个链表结构的元素;每一个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先得到它对应的Segment锁。
看不懂???很正常,我也看不懂
总结:
注意:* 号表明后面是还有内容的
此方法是干什么的呢,他完彻底全的能够把List、Map、Set接口底下的集合变成线程安全的集合
Collections.synchronized * :原理是什么,我猜的话是代理模式:Java代理模式理解
ConcurrentHashMap 把实际 map 划分红若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度得到的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程状况下就能避免争用。
在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提升并发度,具体内容仍是查看源码吧。
何为同步容器:能够简单地理解为经过 synchronized 来实现同步的容器,若是有多个线程调用同步容器的方法,它们将会串行执行。好比 Vector,Hashtable,以及 Collections.synchronizedSet,synchronizedList 等方法返回的容器。能够经过查看 Vector,Hashtable 等这些同步容器的实现代码,能够看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在须要同步的方法上加上关键字 synchronized。
并发容器使用了与同步容器彻底不一样的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,能够称为分段锁,在这种锁机制下,容许任意数量的读线程并发地访问 map,而且执行读操做的线程和写操做的线程也能够并发的访问 map,同时容许必定数量的写操做线程并发地修改 map,因此它能够在并发环境下实现更高的吞吐量。
SynchronizedMap 一次锁住整张表来保证线程安全,因此每次只能有一个线程来访为 map。
ConcurrentHashMap 使用分段锁来保证在多线程下的性能。
ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等经常使用操做只锁当前须要用到的桶。
这样,原来只能一个线程进入,如今却能同时有 16 个写线程执行,并发性能的提高是显而易见的。
另外 ConcurrentHashMap 使用了一种不一样的迭代方式。在这种迭代方式中,当iterator 被建立后集合再发生改变就再也不是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程能够使用原来老的数据,而写线程也能够并发的完成改变。
CopyOnWriteArrayList 是一个并发容器。有不少人称它是线程安全的,我认为这句话不严谨,缺乏一个前提条件,那就是非复合场景下操做它是线程安全的。
CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将致使建立整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操做能够安全地执行。
消息队列不少人知道:消息队列是分布式系统中重要的组件,是系统与系统直接的通讯
并发队列是什么:并发队列多个线程以有次序共享数据的重要组件
那就有可能要说了,咱们并发集合不是也能够实现多线程之间的数据共享吗,其实也是有区别的:
队列遵循“先进先出”的规则,能够想象成排队检票,队列通常用来解决大数据量采集处理和显示的。
并发集合就是在多个线程中共享数据的
当队列阻塞队列为空的时,从队列中获取元素的操做将会被阻塞。
或者当阻塞队列是满时,往队列里添加元素的操做会被阻塞。
或者试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其余的线程往空的队列插入新的元素。
试图往已满的阻塞队列中添加新元素的线程一样也会被阻塞,直到其余的线程使队列从新变得空闲起来
非堵塞队列:
ArrayDeque, (数组双端队列)
ArrayDeque (非堵塞队列)是JDK容器中的一个双端队列实现,内部使用数组进行元素存储,不容许存储null值,能够高效的进行元素查找和尾部插入取出,是用做队列、双端队列、栈的绝佳选择,性能比LinkedList还要好。
PriorityQueue, (优先级队列)
PriorityQueue (非堵塞队列) 一个基于优先级的无界优先级队列。优先级队列的元素按照其天然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不容许使用 null 元素也不容许插入不可比较的对象
ConcurrentLinkedQueue, (基于链表的并发队列)
ConcurrentLinkedQueue (非堵塞队列): 是一个适用于高并发场景下的队列,经过无锁的方式,实现了高并发状态下的高性能。ConcurrentLinkedQueue的性能要好于BlockingQueue接口,它是一个基于连接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。该队列不容许null元素。
堵塞队列:
DelayQueue, (基于时间优先级的队列,延期阻塞队列)
DelayQueue是一个没有边界BlockingQueue实现,加入其中的元素必需实现Delayed接口。当生产者线程调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最先到期的,越日后到期时间赿晚。
ArrayBlockingQueue, (基于数组的并发阻塞队列)
ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,咱们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue是以先进先出的方式存储数据
LinkedBlockingQueue, (基于链表的FIFO阻塞队列)
LinkedBlockingQueue阻塞队列大小的配置是可选的,若是咱们初始化时指定一个大小,它就是有边界的,若是不指定,它就是无边界的。说是无边界,实际上是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。
LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,便可以从队列的两端插入和移除元素。双向队列由于多了一个操做队列的入口,在多线程同时入队时,也就减小了一半的竞争。
相比于其余阻塞队列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first结尾的方法,表示插入、获取获移除双端队列的第一个元素。以last结尾的方法,表示插入、获取获移除双端队列的最后一个元素。
LinkedBlockingDeque是可选容量的,在初始化时能够设置容量防止其过分膨胀,若是不设置,默认容量大小为Integer.MAX_VALUE。
PriorityBlockingQueue, (带优先级的无界阻塞队列)
priorityBlockingQueue是一个无界队列,它没有限制,在内存容许的状况下能够无限添加元素;它又是具备优先级的队列,是经过构造函数传入的对象来判断,传入的对象必须实现comparable接口。
SynchronousQueue (并发同步阻塞队列)
SynchronousQueue是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另外一个线程从队列中获取了队列中存储的元素。一样,若是线程尝试获取元素而且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。
将这个类称为队列有点夸大其词。这更像是一个点。
无论是那种列队,是那个类,当是他们使用的方法都是差很少的
方法名 | 描述 |
---|---|
add() | 在不超出队列长度的状况下插入元素,能够当即执行,成功返回true,若是队列满了就抛出异常。 |
offer() | 在不超出队列长度的状况下插入元素的时候则能够当即在队列的尾部插入指定元素,成功时返回true,若是此队列已满,则返回false。 |
put() | 插入元素的时候,若是队列满了就进行等待,直到队列可用。 |
take() | 从队列中获取值,若是队列中没有值,线程会一直阻塞,直到队列中有值,而且该方法取得了该值。 |
poll(long timeout, TimeUnit unit) | 在给定的时间里,从队列中获取值,若是没有取到会抛出异常。 |
remainingCapacity() | 获取队列中剩余的空间。 |
remove(Object o) | 从队列中移除指定的值。 |
contains(Object o) | 判断队列中是否拥有该值。 |
drainTo(Collection c) | 将队列中值,所有移除,并发设置到给定的集合中。 |
CountDownLatch
CountDownLatch 类位于java.util.concurrent包下,利用它能够实现相似计数器的功能。好比有一个任务A,它要等待其余3个任务执行完毕以后才能执行,此时就能够利用CountDownLatch来实现这种功能了。
CyclicBarrier (回环栅栏) CyclicBarrier它的做用就是会让全部线程都等待完成后才会继续下一步行动。
CyclicBarrier初始化时规定一个数目,而后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,全部进入等待状态的线程被唤醒并继续。
CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,全部其它线程被唤醒前被执行。
Semaphore (信号量) Semaphore 是 synchronized 的增强版,做用是控制线程的并发数量(容许自定义多少线程同时访问)。就这一点而言,单纯的synchronized 关键字是实现不了的。
Semaphore是一种基于计数的信号量。它能够设定一个阈值,基于此,多个线程竞争获取许可信号,作本身的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore能够用来构建一些对象池,资源池之类的,好比数据库链接池,咱们也能够建立计数为1的Semaphore,将其做为一种相似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。它的用法以下: