疯狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 面试必备 + 面试必备 【博客园总入口 】html
疯狂创客圈 经典图书 : 《SpringCloud、Nginx高并发核心编程》 大厂必备 + 大厂必备 + 大厂必备 【博客园总入口 】java
入大厂+涨工资 必备的 高并发社群: 【博客园总入口 】linux
工欲善其事 必先利其器 |
---|
地表最强 开发环境: vagrant+java+springcloud+redis+zookeeper镜像下载(&制做详解) |
地表最强 热部署:java SpringBoot SpringCloud 热部署 热加载 热调试 |
地表最强 发请求工具(再见吧, PostMan ):IDEA HTTP Client(史上最全) |
地表最强 PPT 小工具: 屌炸天,像写代码同样写PPT |
无编程不创客,无编程不创客,一大波编程高手正在疯狂创客圈交流、学习中! 找组织,GO |
推荐阅读 |
---|
nacos 实战(史上最全) |
sentinel (史上最全+入门教程) |
springcloud + webflux 高并发实战 |
Webflux(史上最全) |
SpringCloud gateway (史上最全) |
无编程不创客,无编程不创客,一大波编程高手正在疯狂创客圈交流、学习中! 找组织,GO |
并发编程的目的就是为了能提升程序的执行效率,提升程序运行速度,可是并发编程并不老是能提升程序运行速度的,并且并发编程可能会遇到不少问题,好比:内存泄漏、上下文切换、线程安全、死锁等问题。程序员
并发编程三要素(线程的安全性问题体如今):web
原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操做要么所有执行成功要么所有执行失败。面试
可见性:一个线程对共享变量的修改,另外一个线程可以马上看到。(synchronized,volatile)redis
有序性:程序执行的顺序按照代码的前后顺序执行。(处理器可能会对指令进行重排序)算法
出现线程安全问题的缘由:spring
解决办法:数据库
作一个形象的比喻:
并发 = 两个队列和一台咖啡机。
并行 = 两个队列和两台咖啡机。
串行 = 一个队列和一台咖啡机。
多线程:多线程是指程序中包含多个执行流,即在一个程序中能够同时运行多个不一样的线程来执行不一样的任务。
多线程的好处:
能够提升 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 能够运行其它的线程而不是等待,这样就大大提升了程序的效率。也就是说容许单个程序建立多个并行执行的线程来完成各自的任务。
多线程的劣势:
一个在内存中运行的应用程序。每一个进程都有本身独立的一块内存空间,一个进程能够有多个线程,好比在Windows系统中,一个运行的xx.exe就是一个进程。
进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程能够运行多个线程,多个线程可共享数据。
线程具备许多传统进程所具备的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它至关于只有一个线程的任务。在引入了线程的操做系统中,一般一个进程都有若干个线程,至少包含一个线程。
根本区别:进程是操做系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每一个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程能够看作轻量级的进程,同一类线程共享代码和数据空间,每一个线程都有本身独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:若是一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,因此线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其余进程产生影响,可是一个线程崩溃整个进程都死掉。因此多进程要比多线程健壮。
执行过程:每一个独立的进程有程序运行的入口、顺序执行序列和程序出口。可是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,二者都可并发执行
多线程编程中通常线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能获得有效执行,CPU 采起的策略是为每一个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会从新处于就绪状态让给其余线程使用,这个过程就属于一次上下文切换。
归纳来讲就是:当前任务在执行完 CPU 时间片切换到另外一个任务以前会先保存本身的状态,以便下次再切换回这个任务时,能够再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换一般是计算密集型的。也就是说,它须要至关可观的处理器时间,在每秒几十上百次的切换中,每次切换都须要纳秒量级的时间。因此,上下文切换对系统来讲意味着消耗大量的 CPU 时间,事实上,多是操做系统中时间消耗最大的操做。
Linux 相比与其余操做系统(包括其余类 Unix 系统)有不少的优势,其中有一项就是,其上下文切换和模式切换的时间消耗很是少。
守护线程和用户线程
main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,好比垃圾回收线程。
比较明显的区别之一是用户线程结束,JVM 退出,无论这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。
注意事项:
setDaemon(true)
必须在start()
方法前执行,不然会抛出 IllegalThreadStateException
异常windows上面用任务管理器看,linux下能够用 top 这个工具看。
建立线程有四种方式:
1继承 Thread 类
步骤
public class MyThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法正在执行..."); } } 12345678 public class TheadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); System.out.println(Thread.currentThread().getName() + " main()方法执行结束"); } } 12345678910
运行结果
main main()方法执行结束 Thread-0 run()方法正在执行... 12
2实现 Runnable 接口
步骤
public class MyRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法执行中..."); } } 12345678 public class RunnableTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); System.out.println(Thread.currentThread().getName() + " main()方法执行完成"); } } 12345678910
执行结果
main main()方法执行完成 Thread-0 run()方法执行中... 12
3实现 Callable 接口
步骤
public class MyCallable implements Callable<Integer> { @Override public Integer call() { System.out.println(Thread.currentThread().getName() + " call()方法执行中..."); return 1; } } 123456789 public class CallableTest { public static void main(String[] args) { FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable()); Thread thread = new Thread(futureTask); thread.start(); try { Thread.sleep(1000); System.out.println("返回结果 " + futureTask.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " main()方法执行完成"); } } 12345678910111213141516171819
执行结果
Thread-0 call()方法执行中... 返回结果 1 main main()方法执行完成 123
4使用 Executors 工具类建立线程池
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
主要有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool,后续详细介绍这四种线程池
public class MyRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法执行中..."); } } 12345678 public class SingleThreadExecutorTest { public static void main(String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); MyRunnable runnableTest = new MyRunnable(); for (int i = 0; i < 5; i++) { executorService.execute(runnableTest); } System.out.println("线程任务开始执行"); executorService.shutdown(); } } 1234567891011121314
执行结果
线程任务开始执行 pool-1-thread-1 is running... pool-1-thread-1 is running... pool-1-thread-1 is running... pool-1-thread-1 is running... pool-1-thread-1 is running... 123456
相同点
主要区别
注:Callalbe接口支持返回执行结果,须要调用FutureTask.get()获得,此方法会阻塞主进程的继续往下执行,若是不调用不会阻塞。
每一个线程都是经过某个特定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 用于获取结果。
FutureTask 表示一个异步运算的任务。FutureTask 里面能够传入一个 Callable 的具体实现类,能够对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操做。只有当运算完成的时候结果才能取回,若是运算还没有完成 get 方法将会阻塞。一个 FutureTask 对象能够对调用了 Callable 和 Runnable 的对象进行包装,因为 FutureTask 也是Runnable 接口的实现类,因此 FutureTask 也能够放入线程池中。
新建(new):新建立了一个线程对象。
可运行(runnable):线程对象建立后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
运行(running):可运行状态(runnable)的线程得到了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的惟一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞(block):处于运行状态中的线程因为某种缘由,暂时放弃对 CPU的使用权,中止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
阻塞的状况分三种:
(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
(二). 同步阻塞:线程在获取 synchronized 同步锁失败(由于锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
(三). 其余阻塞: 经过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程从新转入就绪状态。
死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
计算机一般只有一个 CPU,在任意时刻只能执行一条机器指令,每一个线程只有得到CPU 的使用权才能执行指令。所谓多线程的并发运行,实际上是指从宏观上看,各个线程轮流得到 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让全部的线程轮流得到 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(); } // 处理其余的业务逻辑 } 12345678
Java中,任何对象均可以做为锁,而且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并无可供任何对象使用的锁,因此任意对象调用方法必定定义在Object类中。
wait(), notify()和 notifyAll()这些方法在同步代码块中调用
有的人会说,既然是线程放弃对象锁,那也能够把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不须要从新定义wait()方法的实现。然而,这样作有一个很是大的问题,一个线程彻底能够持有不少锁,你一个线程放弃锁的时候,到底要放弃哪一个锁?固然了,这种设计并非不能实现,只是管理起来更加复杂。
综上所述,wait()、notify()和notifyAll()方法要定义在Object类中。
当一个线程须要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其余线程调用这个对象上的 notify()方法。一样的,当一个线程须要调用对象的 notify()方法时,它会释放这个对象的锁,以便其余在等待的线程就能够获得这个对象锁。因为全部的这些方法都须要线程持有对象的锁,这样就只能经过同步来实现,因此他们只能在同步方法或者同步块中被调用。
使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。
当前线程到了就绪状态,那么接下来哪一个线程会从就绪状态变成执行状态呢?多是当前线程,也多是其余线程,看系统的分配了。
Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。因此在其余处于等待状态的线程上调用这些方法是没有意义的。这就是为何这些方法是静态的。它们能够在当前正在执行的线程中工做,并避免程序员错误的认为能够在其余非运行线程调用这些方法。
(1) sleep()方法给其余线程运行机会时不考虑线程的优先级,所以会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
(2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
(3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
(4)sleep()方法比 yield()方法(跟操做系统 CPU 调度相关)具备更好的可移植性,一般不建议使用yield()方法来控制并发线程的执行。
在java中有如下3种方法能够终止正在运行的线程:
interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
注意:线程中断仅仅是置线程的中断状态位,不会中止线程。须要用户本身去监视线程的状态为并作处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
interrupted:是静态方法,查看当前中断信号是true仍是false而且清除中断信号。若是一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
isInterrupted:查看当前中断信号是true仍是false
阻塞式方法是指程序会一直等待该方法完成期间不作其余事情,ServerSocket 的accept()方法就是一直等待客户端链接。这里的阻塞是指调用结果返回以前,当前线程会被挂起,直到获得结果以后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。
首先 ,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()
线程间直接的数据交换:
三.经过管道进行线程间通讯:1)字节流;2)字符流
在两个线程间共享变量便可实现共享。
通常来讲,共享变量要求变量自己是线程安全的,而后在线程内使用的时候,若是有对共享变量的复合操做,那么也得保证复合操做的线程安全性。
同步块是更好的选择,由于它不会锁住整个对象(固然你也可让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这一般会致使他们中止执行并须要等待得到这个对象上的锁。
同步块更要符合开放调用的原则,只在须要锁住的代码块锁住相应的对象,这样从侧面来讲也能够避免死锁。
请知道一条原则:同步的范围越小越好。
当一个线程对共享的数据进行操做时,应使之成为一个”原子操做“,即在没有完成相关操做以前,不容许其余线程打断它,不然,就会破坏数据的完整性,必然会获得错误的处理结果,这就是线程的同步。
在多线程应用中,考虑不一样线程之间的数据同步和防止死锁。当两个或多个线程之间同时等待对方释放资源的时候就会造成线程之间的死锁。为了防止死锁的发生,须要经过同步来实现线程安全。
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任什么时候刻最多只容许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥能够当作是一种特殊的线程同步。
线程间的同步方法大致可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时须要切换内核态与用户态,而用户模式就是不须要切换到内核态,只在用户态完成操做。
用户模式下的方法有:原子操做(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
实现线程同步的方法
在 java 虚拟机中,每一个对象( Object 和 class )经过某种逻辑关联监视器,每一个监视器和一个对象引用相关联,为了实现监视器的互斥功能,每一个对象都关联着一把锁。
一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁以前不容许执行该部分的代码
另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案
线程安全是编程中的术语,指某个方法在多线程环境中被调用时,可以正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
Struts2 的 action 是多实例多线程的,是线程安全的,每一个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。
SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 相似的处理流程。
Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 须要考虑线程安全问题,可是性能能够提高不用处理太多的 gc,可使用 ThreadLocal 来处理多线程的问题。
手动锁 Java 示例代码以下:
Lock lock = new ReentrantLock(); lock. lock(); try { System. out. println("得到锁"); } catch (Exception e) { // TODO: handle exception } finally { System. out. println("释放锁"); lock. unlock(); } 12345678910
每个线程都是有优先级的,通常来讲,高优先级的线程在运行时会具备优先权,但这依赖于线程调度的实现,这个实现是和操做系统相关的(OS dependent)。咱们能够定义线程的优先级,可是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 表明最低优先级,10 表明最高优先级。
Java 的线程优先级调度会委托给操做系统去处理,因此与具体的操做系统优先级有关,如非特别须要,通常无需设置线程优先级。
这是一个很是刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 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 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。
若是异常没有被捕获该线程将会中止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常形成线程忽然中断状况的一个内嵌接口。当一个未捕获异常将形成线程中断的时候,JVM 会使用 Thread.getUncaughtExceptionHandler()来查询线程的 UncaughtExceptionHandler 并将线程和异常做为参数传递给 handler 的 uncaughtException()方法进行处理。
线程的生命周期开销很是高
消耗过多的 CPU
资源若是可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,并且大量的线程在竞争 CPU资源时还将产生其余性能的开销。
下降稳定性JVM
在可建立线程的数量上存在一个限制,这个限制值将随着平台的不一样而不一样,而且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操做系统对线程的限制等。若是破坏了这些限制,那么可能抛出OutOfMemoryError 异常。
在 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中,字符串常量池具备缓存功能!
下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。
面试中面试官常常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”
双重校验锁实现对象单例(线程安全)
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; } } 1234567891011121314151617181920
另外,须要注意 uniqueInstance 采用 volatile 关键字修饰也是颇有必要。
uniqueInstance 采用 volatile 关键字修饰也是颇有必要的, uniqueInstance = new Singleton(); 这段代码实际上是分为三步执行:
可是因为 JVM 具备指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,可是在多线程环境下会致使一个线程得到尚未初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,所以返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 能够禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
synchronized是Java中的一个关键字,在使用的过程当中并无看到显示的加锁和解锁过程。所以有必要经过javap命令,查看相应的字节码文件。
synchronized 同步语句块的状况
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("synchronized 代码块"); } } } 1234567
经过JDK 反汇编指令 javap -c -v SynchronizedDemo
能够看出在执行同步代码块以前以后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块以后,要释放锁,释放锁就是执行monitorexit指令。
为何会有两个monitorexit呢?
这个主要是防止在同步代码块中线程因异常退出,而锁没有获得释放,这必然会形成死锁(等待的线程永远获取不到锁)。所以最后一个monitorexit是保证在异常状况下,锁也能够获得释放,避免死锁。
仅有ACC_SYNCHRONIZED这么一个标志,该标记代表线程进入该方法时,须要monitorenter,退出该方法时须要monitorexit。
synchronized可重入的原理
重入锁是指一个线程获取到该锁以后,该线程能够继续得到该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次得到该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,代表该锁未被任何线程所持有,其它线程能够竞争获取锁。
不少 synchronized 里面的代码只是一些很简单的代码,执行时间很是快,此时等待的线程都加锁多是一种不太值得的操做,由于线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得很是快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界作忙循环,这就是自旋。若是作了屡次循环发现尚未得到锁,再阻塞,这样多是一种更好的策略。
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,若是一致则能够直接使用此对象,若是不一致,则升级偏向锁为轻量级锁,经过自旋循环必定次数来获取锁,执行必定次数以后,若是尚未正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 以后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
(1)volatile 修饰变量
(2)synchronized 修饰修改变量的方法
(3)wait/notify
(4)while 轮询
不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。由于非静态方法上的 synchronized 修饰符要求执行方法时要得到对象的锁,若是已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。
(1)synchronized 是悲观锁,属于抢占式,会引发其余线程阻塞。
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS 是基于冲突检测的乐观锁(非阻塞)
synchronized 是和 if、else、for、while 同样的关键字,ReentrantLock 是类,这是两者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,能够被继承、能够有方法、能够有各类各样的类变量
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,可是在 Java 6 中对 synchronized 进行了很是多的改进。
相同点:二者都是可重入锁
二者都是可重入锁。“可重入锁”概念是:本身能够再次获取本身的内部锁。好比一个线程得到了某个对象的锁,此时这个对象锁尚未释放,当其再次想要获取这个对象的锁的时候仍是能够获取的,若是不可锁重入的话,就会形成死锁。同一个线程每次获取锁,锁的计数器都自增1,因此要等到锁的计数器降低为0时才能释放锁。
主要区别以下:
Java中每个对象均可以做为锁,这是synchronized实现同步的基础:
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争状况逐渐升级。锁能够升级但不能降级。
百度百科
:死锁是指两个或两个以上的进程(线程)在执行过程当中,因为竞争资源或者因为彼此通讯而形成的一种阻塞的现象,若无外力做用,它们都将没法推动下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
多个线程同时被阻塞,它们中的一个或者所有都在等待某个资源被释放。因为线程被无限期地阻塞,所以程序不可能正常终止。
以下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,因此这两个线程就会互相等待而进入死锁状态。
下面经过一个例子来讲明线程死锁,代码模拟了上图的死锁的状况 (代码来源于《并发编程之美》):
public class DeadLockDemo { private static Object resource1 = new Object();//资源 1 private static Object resource2 = new Object();//资源 2 public static void main(String[] args) { new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "线程 1").start(); new Thread(() -> { synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource1"); synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); } } }, "线程 2").start(); } } 123456789101112131415161718192021222324252627282930313233343536
输出结果
Thread[线程 1,5,main]get resource1 Thread[线程 2,5,main]get resource2 Thread[线程 1,5,main]waiting get resource2 Thread[线程 2,5,main]waiting get resource1 1234
线程 A 经过 synchronized (resource1) 得到 resource1 的监视器锁,而后经过Thread.sleep(1000)
;让线程 A 休眠 1s 为的是让线程 B 获得CPU执行权,而后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,而后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
产生死锁的必要条件:
一、互斥条件:所谓互斥就是进程在某一时间内独占资源。
二、请求与保持条件:一个进程因请求资源而阻塞时,对已得到的资源保持不放。
三、不剥夺条件:进程已得到资源,在末使用完以前,不能强行剥夺。
四、循环等待条件:若干进程之间造成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不知足,就不会发生死锁。
理解了死锁的缘由,尤为是产生死锁的四个必要条件,就能够最大可能地避免、预防和 解除死锁。
防止死锁能够采用如下的方法:
咱们只要破坏产生死锁的四个条件中的其中一个就能够了。
破坏互斥条件
这个条件咱们没有办法破坏,由于咱们用锁原本就是想让他们互斥的(临界资源须要互斥访问)。
破坏请求与保持条件
一次性申请全部的资源。
破坏不剥夺条件
占用部分资源的线程进一步申请其余资源时,若是申请不到,能够主动释放它占有的资源。
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
咱们对线程 2 的代码修改为下面这样就不会产生死锁了。
new Thread(() -> { synchronized (resource1) { System.out.println(Thread.currentThread() + "get resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread() + "waiting get resource2"); synchronized (resource2) { System.out.println(Thread.currentThread() + "get resource2"); } } }, "线程 2").start(); 1234567891011121314
输出结果
Thread[线程 1,5,main]get resource1 Thread[线程 1,5,main]waiting get resource2 Thread[线程 1,5,main]get resource2 Thread[线程 2,5,main]get resource1 Thread[线程 2,5,main]waiting get resource2 Thread[线程 2,5,main]get resource2 123456
咱们分析一下上面的代码为何避免了死锁的发生?
线程 1 首先得到到 resource1 的监视器锁,这时候线程 2 就获取不到了。而后线程 1 再去获取 resource2 的监视器锁,能够获取到。而后线程 1 释放了对 resource一、resource2 的监视器锁的占用,线程 2 获取到就能够执行了。这样就破坏了破坏循环等待条件,所以避免了死锁。
死锁:是指两个或两个以上的进程(或线程)在执行过程当中,因争夺资源而形成的一种互相等待的现象,若无外力做用,它们都将没法推动下去。
活锁:任务或者执行者没有被阻塞,因为某些条件没有知足,致使一直重复尝试,失败,尝试,失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
饥饿:一个或者多个线程由于种种缘由没法得到所须要的资源,致使一直没法执行的状态。
Java 中致使饥饿的缘由:
一、高优先级线程吞噬全部的低优先级线程的 CPU 时间。
二、线程被永久堵塞在一个等待进入同步块的状态,由于其余线程老是能在它以前持续地对该同步块进行访问。
三、线程在等待一个自己也处于永久等待完成的对象(好比调用这个对象的 wait 方法),由于其余线程老是被持续地得到唤醒。
池化技术相比你们已经家常便饭了,线程池、数据库链接池、Http 链接池等等都是对这个思想的应用。池化技术的思想主要是为了减小每次获取资源的消耗,提升对资源的利用率。
在面向对象编程中,建立和销毁对象是很费时间的,由于建立一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每个对象,以便可以在对象销毁后进行垃圾回收。因此提升服务程序效率的一个手段就是尽量减小建立和销毁对象的次数,特别是一些很耗资源的对象建立和销毁,这就是”池化资源”技术产生的缘由。
线程池顾名思义就是事先建立若干个可执行的线程放入一个池(容器)中,须要的时候从池中获取线程不用自行建立,使用完毕不须要销毁线程而是放回池中,从而减小建立和销毁线程对象的开销。Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤为是对于线程池的原理不是很清楚的状况下,所以在工具类 Executors 面提供了一些静态工厂方法,生成一些经常使用的线程池,以下所示:
(1)newSingleThreadExecutor:建立一个单线程的线程池。这个线程池只有一个线程在工做,也就是至关于单线程串行执行全部任务。若是这个惟一的线程由于异常结束,那么会有一个新的线程来替代它。此线程池保证全部任务的执行顺序按照任务的提交顺序执行。
(2)newFixedThreadPool:建立固定大小的线程池。每次提交一个任务就建立一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,若是某个线程由于执行异常而结束,那么线程池会补充一个新线程。若是但愿在服务器上使用线程池,建议使用 newFixedThreadPool方法来建立线程池,这样能得到更好的性能。
(3) newCachedThreadPool:建立一个可缓存的线程池。若是线程池的大小超过了处理任务所须要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增长时,此线程池又能够智能的添加新线程来处理任务。此线程池不会对线程池大小作限制,线程池大小彻底依赖于操做系统(或者说 JVM)可以建立的最大线程大小。
(4)newScheduledThreadPool:建立一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。
Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。
每次执行任务建立线程 new Thread()比较消耗性能,建立一个线程是比较耗时、耗资源的,并且无限制的建立线程会引发应用程序内存溢出。
因此建立一个线程池是个更好的的解决方案,由于能够限制线程的数量而且能够回收再利用这些线程。利用Executors 框架能够很是方便的建立一个线程池。
接收参数:execute()只能执行 Runnable 类型的任务。submit()能够执行 Runnable 和 Callable 类型的任务。
返回值:submit()方法能够返回持有计算结果的 Future 对象,而execute()没有
异常处理:submit()方便Exception处理
ThreadGroup 类,能够把线程归属到某一个线程组中,线程组中能够有线程对象,也能够有线程组,组中还能够有线程,这样的组织结构有点相似于树的形式。
线程组和线程池是两个不一样的概念,他们的做用彻底不一样,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减小建立销毁线程的开销。
为何不推荐使用线程组?由于使用有不少的安全隐患吧,没有具体追究,若是须要使用,推荐使用线程池。
《阿里巴巴Java开发手册》中强制线程池不容许使用 Executors 去建立,而是经过 ThreadPoolExecutor 的方式,这样的处理方式让写的同窗更加明确线程池的运行规则,规避资源耗尽的风险
Executors 各个方法的弊端:
ThreaPoolExecutor建立线程池方式只有一种,就是走它的构造函数,参数本身指定
建立线程池的方式有多种,这里你只须要答 ThreadPoolExecutor 便可。
ThreadPoolExecutor() 是最原始的线程池建立,也是阿里巴巴 Java 开发手册中明确规范的建立线程池的方式。
ThreadPoolExecutor 3 个最重要的参数:
ThreadPoolExecutor
其余常见参数:
corePoolSize
的时候,若是这时没有新的任务提交,核心线程外的线程不会当即销毁,而是会等待,直到等待的时间超过了 keepAliveTime
才会被回收销毁;keepAliveTime
参数的时间单位。ThreadPoolExecutor 饱和策略定义:
若是当前同时运行的线程数量达到最大线程数量而且队列也已经被放满了任时,ThreadPoolTaskExecutor
定义一些策略:
RejectedExecutionException
来拒绝新任务的处理。举个例子: Spring 经过 ThreadPoolTaskExecutor
或者咱们直接经过 ThreadPoolExecutor
的构造函数建立线程池的时候,当咱们不指定 RejectedExecutionHandler
饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy
。在默认状况下,ThreadPoolExecutor
将抛出 RejectedExecutionException
来拒绝新来的任务 ,这表明你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy
。当最大池被填满时,此策略为咱们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor
的构造函数源码就能够看出,比较简单的缘由,这里就不贴代码了)
Runnable
+ThreadPoolExecutor
线程池实现原理
为了让你们更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。
首先建立一个 Runnable
接口的实现类(固然也能够是 Callable
接口,咱们上面也说了二者的区别。)
import java.util.Date; /** * 这是一个简单的Runnable类,须要大约5秒钟来执行其任务。 */ public class MyRunnable implements Runnable { private String command; public MyRunnable(String s) { this.command = s; } @Override public void run() { System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date()); processCommand(); System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date()); } private void processCommand() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public String toString() { return this.command; } } 123456789101112131415161718192021222324252627282930313233
编写测试程序,咱们这里以阿里巴巴推荐的使用 ThreadPoolExecutor
构造函数自定义参数的方式来建立线程池。
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolExecutorDemo { private static final int CORE_POOL_SIZE = 5; private static final int MAX_POOL_SIZE = 10; private static final int QUEUE_CAPACITY = 100; private static final Long KEEP_ALIVE_TIME = 1L; public static void main(String[] args) { //使用阿里巴巴推荐的建立线程池的方式 //经过ThreadPoolExecutor构造函数自定义参数建立 ThreadPoolExecutor executor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_CAPACITY), new ThreadPoolExecutor.CallerRunsPolicy()); for (int i = 0; i < 10; i++) { //建立WorkerThread对象(WorkerThread类实现了Runnable 接口) Runnable worker = new MyRunnable("" + i); //执行Runnable executor.execute(worker); } //终止线程池 executor.shutdown(); while (!executor.isTerminated()) { } System.out.println("Finished all threads"); } } 1234567891011121314151617181920212223242526272829303132333435
能够看到咱们上面的代码指定了:
corePoolSize
: 核心线程数为 5。maximumPoolSize
:最大线程数 10keepAliveTime
: 等待时间为 1L。unit
: 等待时间的单位为 TimeUnit.SECONDS。workQueue
:任务队列为 ArrayBlockingQueue
,而且容量为 100;handler
:饱和策略为 CallerRunsPolicy
。Output:
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019 pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019 pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019 pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019 pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019 pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019 pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019 pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019 pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019 pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019 pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019 1234567891011121314151617181920
这里区分一下:
(1)若是使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,不要紧,继续添加任务到阻塞队列中等待执行,由于 LinkedBlockingQueue 能够近乎认为是一个无穷大的队列,能够无限存听任务
(2)若是使用的是有界队列好比 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增长线程数量,若是增长了线程数量仍是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
ThreadLocal 是一个本地线程副本变量工具类,在每一个线程中都建立了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的作法,每一个线程能够访问本身内部 ThreadLocalMap 对象内的 value。经过这种方式,避免资源在多线程间共享。
原理:线程局部变量是局限于线程内部的变量,属于线程自身全部,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。可是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别当心,在这种状况下,工做线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工做完成后没有释放,Java 应用就存在内存泄露的风险。
经典的使用场景是为每一个线程分配一个 JDBC 链接 Connection。这样就能够保证每一个线程的都在各自的 Connection 上进行数据库的操做,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。
ThreadLocal 使用例子:
public class TestThreadLocal { //线程本地存储变量 private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0; } }; public static void main(String[] args) { for (int i = 0; i <3; i++) {//启动三个线程 Thread t = new Thread() { @Override public void run() { add10ByThreadLocal(); } }; t.start(); } } /** * 线程本地存储变量加 5 */ private static void add10ByThreadLocal() { for (int i = 0; i <5; i++) { Integer n = THREAD_LOCAL_NUM.get(); n += 1; THREAD_LOCAL_NUM.set(n); System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n); } } } 123456789101112131415161718192021222324252627282930313233343536
打印结果:启动了 3 个线程,每一个线程最后都打印到 “ThreadLocal num=5”,而不是 num 一直在累加直到值等于 15
Thread-0 : ThreadLocal num=1 Thread-1 : ThreadLocal num=1 Thread-0 : ThreadLocal num=2 Thread-0 : ThreadLocal num=3 Thread-1 : ThreadLocal num=2 Thread-2 : ThreadLocal num=1 Thread-0 : ThreadLocal num=4 Thread-2 : ThreadLocal num=2 Thread-1 : ThreadLocal num=3 Thread-1 : ThreadLocal num=4 Thread-2 : ThreadLocal num=3 Thread-0 : ThreadLocal num=5 Thread-2 : ThreadLocal num=4 Thread-2 : ThreadLocal num=5 Thread-1 : ThreadLocal num=5 123456789101112131415
线程局部变量是局限于线程内部的变量,属于线程自身全部,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。可是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别当心,在这种状况下,工做线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工做完成后没有释放,Java 应用就存在内存泄露的风险。
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。因此,若是 ThreadLocal
没有被外部强引用的状况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap
中就会出现key为null的Entry。假如咱们不作任何措施的话,value 永远没法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种状况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal
方法后 最好手动调用remove()
方法
疯狂创客圈 经典图书 : 《Netty Zookeeper Redis 高并发实战》 面试必备 + 面试必备 + 面试必备
疯狂创客圈 - Java高并发研习社群,为你们开启大厂之门