任何事物都需要不同粒度的划分。 豆腐要一块一块卖。 程序要一个一个跑。 这里的一个一个跑,不是指串行,而是指资源的划分,执行程序的界定。
- 什么是进程?百度百科
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在早期面向进程设计的计算机结构中,进程是程序的基本执行实体。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。 动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。 并发性:任何进程都可以同其他进程一起并发执行。 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。 结构特征:进程由程序、数据和进程控制块三部分组成。
- 什么是线程?
线程(英语:thread)是操作系统能够进行运算调度的最小单位。 它被包含在进程之中,是进程中的实际运作单位。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。ThreadLocal
- ThreadLocal作用是存储线程局部变量。空间解决并发共享变量冲突问题,为每个线程分别存储不同的值(分身独有)。
-
常用方法:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } public T get() { Thread t = Thread.currentThread(); // 当前线程 ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } static class ThreadLocalMap{ ... static class Entry extends WeakReference>{ Object value; Entry(ThreadLocal> k, Object v) { super(k); // key 是 ThreadLocal 实例引用,value 是线程独有变量值。 value = v; } } } 备注:ThreadLocalMap 实际上 是对 其持有的 table 的控制(private Entry[] table;),包括扩容、hash计算等。table 的本质是 当前线程 独有的线程局部变量。Entry 的 key 是 ThreadLocal 实例,value 是 对应的局部变量的值。 -
源码分析:
ThreadLocal 有静态内部类 ThreadLocalMap,ThreadLocalMap 有静态内部类 Entry。 Entry 继承 WeakReference(虚引用,垃圾回收每次都清理),目的是: To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space. Thread 强引用 ThreadLocalMap,ThreadLocalMap 强引用 Entry 数组(private Entry[] table;)。 相当于每个 Thread 都独自维护了一个数组 table,key 是 ThreadLocal 实例引用,value 是线程独有变量值。
-
应用场景:
数据库连接的时候经常用。 普通的数据库连接的管理类: 有一个很严重的问题,客户端频繁的使用数据库,会导致多次建立和关闭连接,服务器可能会吃不消。 最好使用ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
-
重要思想:
简单来想,如果实现每个线程都有自己的局部变量,ThreadLocal 可以内部直接维护一个 hashMap,key 是线程引用,value 是线程对应的局部变量值。但是,此种方案,相当于 ThreadLocal 维护所有线程的局部变量,是一种集中管控的方式,效率不高。(集中管控) ThreadLocal 的本质是想 让每一个线程都独有自己的局部变量,不是分身,不是副本,不是备份,而是属于线程自己独有的局部变量。既然如此,局部变量应该由线程自己维护。(分权思想) 真实的实现,也的确如此。每个线程 持有 ThreadLocalMap,每个 ThreadLocalMap 管控 Entry 数组,每个 Entry 存储局部变量,其中 Entry 数组的下标,通过 ThreadLocal 实例引用计算映射(下标计算),每个 Entry 的 key 为 ThreadLocal 实例(此处的 key 主要是用于 将 ThreadLocal实例 声明为弱引用),每个 Entry 的本质实际上就是“局部变量值”,每个 ThreadLocal 实例 对应 一个 Entry 中的值,映射逻辑 为下标计算。 所以,ThreadLocal 获取对应线程的步骤是: 1. 获取当前线程:Thread t = Thread.currentThread(); 2. 获取当前线程持有的 ThreadLocalMap map = getMap(t); 3. 通过当前 ThreadLocal 引用 计算 table(Entry数组)下标 获得 线程独有局部变量值,ThreadLocalMap.Entry e = map.getEntry(this);
-
注意事项:由于长时间运行的线程没有从线程本地映射中清除值,我们仍然面临内存泄漏的可能性。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } private void remove(ThreadLocal> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } } 备注:ThreadLocal实例 声明为弱引用,会被JVM垃圾回收(每次,且其销毁地点必定为伊甸区),但是每个线程持有的 ThreadLocalMap 是强引用,所以需要 把对应线程 的 tab(Entry[]数组) 中,ThreadLocal实例 对应的 线程独有局部变量值清理掉。 -
table的下标是如何计算的?
预备知识:装填因子 = hash表中的数据项和表长的比例。通常hash表数组长度是一个质数:假设hash表的长度不是一个质数为15,那么对于那些映射在数组单元0的数字步长为5,就会探索在0->5->10->0->5->10->0一直循环下去,算法只能尝试着三个位置,这是达不到要求。如果数组容量是11(是一个质数),这样就可以探测所有单元,0->5->10->4->9->3->8->2->7->1->6->0。采用质数作为数组容量会保证探测到每一个单元。Hash冲突->开放地址法:1. 线性探测:顺延一个一个找。数据超过2/3满时,性能下降。-> 缺点:数据聚集。2. 二次探测:避免聚集的尝试,相隔比较远的单元进行探测,而不是线性一个一个的探测。 比如: 二次探测是过程是x+1,x+4,x+9,以此类推。二次探测的步数是原始位置相隔的步数的平方。 缺点:都映射到10,然后探测11、14、19,即还有可能导致聚集问题。3. 再哈希法:再哈希是把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长。虽然不同的关键字可能会映射到相同的数组单元,但是可能会有不一样的探测步长。ThreadLocal 中,Thread 持有 ThreadLocalMap 持有 Table(Entry数组),需要计算数组下标。数组下标的计算是通过 ThreadLocal实例 持有的 threadLocalHashCode 成员变量 取模映射的。在 hash 映射过程中,需要避免碰撞:第一种:同一个 ThreadLocal 实例,向 ThreadLocal 变量中设置多个值的时产生的碰撞,此时采用的是 线性探测(linear-probe)。第二种:多个 ThreadLocal 实例的时候,最极端的是每个线程都new一个ThreadLocal实例,此时利用特殊的哈希码0x61c88647大大降低碰撞的几率。同时利用开放地址法处理碰撞。0x61c88647这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527(黄金比例)。斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说:(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647,目的是为了让哈希码能均匀的分布在2的N次方的数组里。
-
静态内部类的设计意图?
多继承中,当多个父类有重复的属性或者方法,会导致子类调用结果含糊不清。所以,Java是单继承的,一个类只能继承另一个具体类或者抽象类,可以实现多个接口。接口解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。每个内部类都能独立地继承一个(具体类或者抽象类)实现,所以无论外围类是否已经继承了某个(具体类或者抽象类)实现,对于内部类都没有影响。非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。ThreadLocal 仅仅是提供了内部类,也就是 table,而具体的持有与引用是在每个 Thread 上。
-
-
ThreadLocal Value为什么不是弱引用?
强调一下,ThreadLocal 提供 内部类 ThreadLocalMap,ThreadLocalMap 持有 Table(Entry数组),Entry的key为ThreadLocal的引用(弱引用),Value为强引用。目的是:将Key为Null的Entity直接清除(伊甸区清理)。假如value被设计成弱引用,那么很有可能当你需要取这个value值的时候,取出来的值是一个null。无论key是强引用还是弱引用,threadLocal都必须要在代码逻辑执行完毕后调用remove()将value的强引用删掉,否则就会导致内存泄漏。也就是说,ThreadLocal不需要开发者关心key的回收问题,开发者只需要关心自己操作的value的回收问题即可。内部的归内部管理,外部的归外部管理,各司其职。
线程池就是线程的集合,线程池集中管理线程,以实现线程的重用,降低资源消耗,提高响应速度等。JDK1.5之前,线程用于执行异步任务,单个的线程既是工作单元也是执行机制。从JDK1.5开始,把工作单元与执行机制分离开,Executor框架诞生。它是一个用于统一创建与运行的接口。Executor框架实现的就是线程池的功能。Executor框架包括3个部分:1. 任务:即工作单元,包括被执行任务需要的接口,Runnable接口或者Callable接口。2. 任务的执行:把任务分派给多个线程的执行机制,3. 任务异步计算的结果:包括Future接口及实现了Future接口的FutureTask类。Future提供了三种功能:1. 判断任务是否完成。2. 能够中断任务。3. 能够获取任务执行结果。CompletionService
ExecutorService 启动多个Callable时,每个Callable返回一个Future,而当我们执行Future的get方法获取结果时,可能拿到的Future并不是第一个执行完成的Callable的Future,就会进行阻塞,从而不能获取到第一个完成的Callable结果,那么这样就造成了很严重的性能损耗问题。CompletionService会根据线程池中Task的执行结果按执行完成的先后顺序排序,任务先完成的可优先获取到。线程池
-
线程思想:并发执行,分而治之(解决效率问题)。
-
线程池思想:每个请求一个线程(thread-per-request),有线程的创建与销毁所耗费资源和时间 远大于 实际作业占用时间和资源,造成资源浪费,所以要减少线程创建和销毁的频次。
-
线程池衍生问题:切换过度、创建无度,导致资源过载。解法:估算、预分配。
设定线程个数、资源使用情况,避免线程频繁创建与删除。一般的线程池主要分为以下 4 个组成部分:1. 线程池管理器:用于创建并管理线程池。2. 工作线程:线程池中的线程。3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行。4. 任务队列:用于存放待处理的任务,提供一种缓冲机制。
-
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。
ThreadPoolExecutor 的构造方法如下:public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue
workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(),defaultHandler); } 1. corePoolSize:指定了线程池中的线程数量。2. maximumPoolSize:指定了线程池中的最大线程数量。3. keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多次时间内会被销毁。4. unit:keepAliveTime 的单位。5. workQueue:任务队列,被提交但尚未被执行的任务。6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。 -
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
-
JDK 内置的拒绝策略如下:
1. AbortPolicy : 直接抛出异常,阻止系统正常运行。2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。
newCachedThreadPool(数目上限为 Interger.MAX_VALUE,线程默认一分钟空闲则销毁)newFixedThreadPool(maxnum,队列,空闲也不会释放)newSingleThreadExecutor(始终且仅有一个工作线程,保证任务有序(FIFO, LIFO, 优先级)执行)newScheduleThreadPool(创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。)1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断: a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务; b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列; c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务; d) 如果队列满了,而且正在运行的线程数量等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。线程池具体应用?
风险:死锁、资源过载、并发错误、线程泄漏(线程没有返回池子)、请求过载(队列很长)。参数设定线程池个数:考虑CPU处理器个数 和 任务性质。一个计算性质任务队列:线程池N或N+1个线程一般会获得最大CPU利用率。(N个处理器)。涉及I/O慢操作任务:等待时间(WT)与服务时间(ST)。大约 N*(1+WT/ST) 个线程。线程特性?
原子性:一个操作不可中断,要么全做,要么不做。可见性:当一个线程修改了共享变量后,其他线程能够立即得知这个修改。有序性:锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。因此,synchronized语义就要求线程在访问读写共享变量时只能“串行”执行。如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。锁
-
自旋锁:
线程未获得锁则原地等待,不去睡眠,直到锁被释放。适合:耗时较少的逻辑中,对共享数据的保护,每个线程持有的自旋锁的时间较短。优点:等待的线程发现自旋锁被其他线程持有的时候,不必挂起自己,而是原地稍微等一会儿,直到锁被释放。所以,避免了线程切换带来的开销。缺点:等待自旋锁的线程一直占有CPU,如果长时间得不到自旋锁或者释放自旋锁,会导致CPU的浪费。另外,自旋锁可能导致死锁。
-
互斥锁:
线程未获得锁则去睡眠。
-
并行与并发:
并发:CPU不断切换,给人同时执行多个任务的感觉。并行:CPU多个实例,真正同时执行多个任务的状态。
-
interrupt 与 wait:
interrupt:中断是一个状态!interrupt()方法只是将这个状态置为true而已。所以说正常运行的程序不去检测状态,就不会终止,而 wait 等阻塞方法会去检查并抛出异常。如果在正常运行程序中添加 while(!Thread.interrupted()) ,则同样可以在中断后离开代码体。
-
https://blog.csdn.net/zqixiao_09/article/details/79265789(Linux 网络协议栈之内核锁(五)—— 自旋锁在抢占(非抢占)单核和多核中的作用)
-
https://www.iteye.com/topic/103804(正确理解ThreadLocal)
-
https://www.cnblogs.com/ilellen/p/4135266.html(ThreadLocal 和神奇的数字 0x61c88647)
-
https://www.javaspecialists.eu/archive/Issue164-Why-0x61c88647.html(Why 0x61c88647?)
-
https://www.zhihu.com/question/399087116(threadlocal value为什么不是弱引用?)
-
https://www.jianshu.com/p/cf57726e77f2(三大性质总结:原子性,有序性,可见性)
-
https://blog.csdn.net/GZHarryAnonymous/article/details/100542611?spm=1001.2014.3001.5502(AQS理解)
-
https://blog.csdn.net/GZHarryAnonymous/article/details/100164649?spm=1001.2014.3001.5502(计算机基础知识复习)
-
https://docs.oracle.com/javase/8/docs/api/(Java™ Platform, Standard Edition 8 API Specification)
-
https://blog.csdn.net/c10WTiybQ1Ye3/article/details/78098544(线程池,这一篇或许就够了)



