JAVA基础
2020-09-04 10:21:10 0 举报
AI智能生成
JAVA基础-知识体系梳理
作者其他创作
大纲/内容
Lock
乐观锁
CAS(Compare And Swap 比较并且替换)是一种实现方式,JUC的很多工具类都是基于CAS
线程在读取时不进行加锁,在准备写回时先查询原值,如果原值未被其他线程修改则写回,若已经被修改,则重新执行读取流程
存在问题
存在ABA问题,即其他线程在当前线程写回前,修改了值,又修改回去了,当前线程不能识别出被修改过
解决ABA问题
1、版本号version
2、用时间戳
如果操作长时间不成功,会导致一直自旋,相当于死循环 ,CPU压力会很大
只能保证一个共享变量的原子操作
项目开发中的实践
用乐观锁思想实现mysql中的sequence
更新时带原状态更新,并指定状态流转顺序
悲观锁
synchronized
java语言的关键字
定义:当前线程运行到加锁的模块时,需检查有无其他线程正在使用这个模块,有的话要等正在使用synchronized方法的其他线程运行完成之后再运行当前线程,没有的话,就锁定调用者为当前线程,然后直接运行
锁定范围
对象
对象头 Header
以HotSpot为例
Mark Word 标记字段
默认存储对象的HashCode,分代年龄和锁标志位信息,数据会随着锁标志位的变化而变化
Mointor
_EntryList
存放已经获取锁的线程
Owner
指向当前持有Monitor对象的线程
_WaitList
存放等待获取锁的线程
Kiass Pointer 类型指针
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
实例数据 Instance Data
对齐填充 Padding
方法
在字节码中通过ACC_SYNCHRONIZED标志位来实现,其他线程在执行时看是否有标志位在方法前,有的话就不执行
代码块
和同步方法一样都是通过monitor来实现同步的
区别在于具体实现方式是通过monitorenter和monitorexit
每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor
每个monitor维护着一个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,当一个线程获得monitor(执行monitorenter)后,该计数器自增变为 1 。
当同一个线程再次获得该monitor的时候,计数器再次自增;
当不同线程想要获得该monitor的时候,就会被阻塞。
当同一个线程释放 monitor(执行monitorexit指令)的时候,计数器再自减,当计数器为0的时候,monitor将被释放,其他线程便可以获得monitor
锁升级过程
1.6以后引入锁升级机制
只能升级,不能降级
ObjectMonitor.hpp
无锁
偏向锁
对象头是由Mark Word和Klass pointer 组成,锁争夺也就是对象头指向的Monitor对象的争夺,一旦有线程持有了这个对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中。这个过程是采用了CAS乐观锁操作的,每次同一线程进入,虚拟机就不进行任何同步的操作了,对标志位+1就好了,不同线程过来,CAS会失败,也就意味着获取锁失败
轻量级锁,拿不到会进行短暂自旋
如果这个对象是无锁的,jvm就会在当前线程的栈帧中建立一个叫锁记录(Lock Record)的空间,用来存储锁对象的Mark Word 拷贝,然后把Lock Record中的owner指向当前对象,JVM接下来会利用CAS尝试把对象原本的Mark Word 更新会Lock Record的指针,成功就说明加锁成功,改变锁标志位,执行相关同步操作
如果失败了,就会判断当前对象的Mark Word是否指向了当前线程的栈帧,是则表示当前的线程已经持有了这个对象的锁,否则说明被其他线程持有了,继续锁升级,修改锁的状态,之后等待的线程也阻塞,默认十次
重量级锁
思考:对象级锁是一种粗糙的加锁方法,如果一个对象拥有多个资源,不需要只为了让一个线程使用其中一部分资源,就将所有线程都所在外面,可以通过方法级锁来共享资源
特性
有序性
不管编译器和CPU如何重排序,必须保证在单线程情况下程序的结果是正确的,还有就是有数据依赖的也是不能重排序的
可见性
分支主题
原子性
确保同一时间只有一个线程能拿到锁,能够进入代码块
可重入性
synchronized锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执行完对应的代码块之后,计数器就会-1,直到计数器清零,就释放锁了
不可中断性
不可中断就是指,一个线程获取锁之后,另外一个线程处于阻塞或者等待状态,前一个不释放,后一个也一直会阻塞或者等待,不可以被中断
可重入锁和不可重入锁
不可重入锁:只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待,实现简单
可重入锁:不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一
ReenTrantLock
jdk1.5以后提供的API层面的互斥锁,基于AQS和CAS实现
需要lock()和unlock()方法配合try/finally语句块来完成
公平锁
多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁
默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁
优点:所有的线程都能得到资源,不会饿死在队列中
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大
hasQueuedPredecessors
非公平锁
多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量
缺点:这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死
ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。
FairSync
NonfairSync
ReentrantReadWriteLock
ReadLock
WriteLock
AQS
AbstractQueuedSynchronizer
是ReenTrantLock实现的基础,双向链表结构
AQS 有一个 state 标记位,值为1 时表示有线程占用,其他线程需要进入到同步队列等待,同步队列是一个双向链表
当获得锁的线程需要等待某个条件时,会进入 condition 的等待队列,等待队列可以有多个
当 condition 条件满足时,线程会从等待队列重新进入同步队列进行获取锁的竞争
Semaphore
也是基于 AQS 的,差别在于 ReentrantLock 是独占锁,Semaphore 是共享锁
1、synchronized是JDK关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成
2、synchronized会自动释放锁,而Lock必须手动释放锁
3、synchronized是不可中断的,ReentrantLock可以中断也可以不中断
4、通过Lock可以知道线程有没有拿到锁,而synchronized不能
5、synchronized能锁住方法和代码块,而Lock只能锁住代码块
6、synchronized是非公平锁,ReentrantLock可以控制是否是公平锁,并可以锁绑定多个条件
7、锁的细粒度和灵活度,ReenTrantLock优
java.util.concurrent
不同集合类的区别
java.util包下的集合类都是快速失败(fail-fast)的,不能在多线程下发生并发修改(迭代过程中被修改)
java.util.concurrent包下的容器都是安全失败(fail-safe),可以在多线程下并发使用,并发修改
提供了比 synchronized 更加高级的各种同步结构,包括 CountDownLatch、CyclicBarrier、Semaphore 等,可以实现更加丰富的多线程操作,比如利用 Semaphore 作为资源控制器,限制同时进行工作的线程数量。
CountDownLatch:减数器
await
调用await()方法的线程会被挂起
等待直到count值为0才继续执行
等待一定的时间后count值还没变为0的话就会继续执行
countDown
CyclicBarrier:等多个线程完成后汇总
await
通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了
Semaphore :限流
acquire
release
Semaphore翻译成字面意思为信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可
各种线程安全的容器,比如最常见的 ConcurrentHashMap、有序的 ConcurrentSkipListMap,或者通过类似快照机制,实现线程安全的动态数组 CopyOnWriteArrayList 等。
各种并发队列实现,如各种 BlockingQueue 实现,比较典型的 ArrayBlockingQueue、 SynchronousQueue 或针对特定场景的 PriorityBlockingQueue 等。
强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运行等,绝大部分情况下,不再需要自己从头实现线程池和任务调度器。
Collection
List
ArrayList
基于数组结构,查找访问速度快,根据数据下标,增删效率低,涉及到数组重排,线程不安全,底层实现是Object[] elementData
日常使用场景中,查询居多,增删不频繁,所以使用ArrayList,如果增删频繁,就使用LinkedList,如果需要线程安全,就使用Vector
扩容机制
通过无参构造方法的方式ArrayList()初始化,则赋值底层数Object[] elementData为一个默认空数组Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}所以数组容量为0,只有真正对数据进行添加add时,才分配默认DEFAULT_CAPACITY = 10的初始容量,也就是说默认大小是10
数组的长度是有限制的,而ArrayList是可以存放任意数量对象
发现容量满了之后,会重新定义一个10+10/2的,长度为15的数组
定义完成后,把原数据的数据,原封不动地复制到新数组中,再把指向原数的地址换到新数组
1.7和1.8的区别
1.7以前会调用this(10)才会真正将容量改为10,1.8以后默认是走了空数组,只有第一次add的时候容量才会变成10
新增之前,会有ensureCapacityInternal判断,长度不够就扩容
8之后的效率更高了,采用了位运算,右移一位,其实就是除以2这个操作。
1.7的时候3/2+1 ,1.8直接就是3/2
ArrayList(int initialCapacity)会不会初始化数组大小
会初始化大小,但是list的大小没有变,list大小是返回size的
ArrayList插入删除一定慢么
取决于删除的元素离数组末端有多远,ArrayList作为堆栈来用比较合适,push和pop操作完全不涉及数据移动操作
删除也是通过copy数组实现的
线程安全吗
不是,可以通过Collections.synchronizedList,原理同Vector,给所有的方法加上了synchronized
不适合做队列
队列一般是FIFO(先入先出)的,如果用ArrayList做队列,就需要在数组尾部追加数据,数组头部删除数组,反过来也可以。
但是无论如何总会有一个操作会涉及到数组的数据搬迁,这个是比较耗费性能的
数组适合做队列
ArrayBlockingQueue
ArrayList的遍历和LinkedList遍历性能比较
遍历ArrayList要比LinkedList快得多,ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销
LinkedList
基于链表结构,查找访问速度慢,插入删除快,适合插入删除频繁的情况,内部维护了链表的长度
Vector
Object数组,线程安全,所有的方法都加上了synchronized
Map
HashMap
数据结构
1.7
数组+链表
头插法
1.8之前是头插法,新来的值会取代原有的值,原有的值就顺推到链表中去
数组中是 Key-Value,叫Entry
Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系
1.8
数组+链表+红黑树,单链表长度大于8 & hash桶的大小大于等于64时转红黑树,红黑树的节点的数量小于等于6时, 重新转成单链表
根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表
尾插法
1.8之后是尾插法,同一位置上新元素总会被放在链表的尾部位置,因为头插法有可能出现环形链表问题
引入红黑树,将时间复杂度由O(n)降为O(logn)
使用尾插法,扩容时会保持链表元素原本的顺序,就不会出现环形链表的问题
数组中是 Key-Value,叫Node
Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系,但是无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证
put
根据key的hash值去计算index值
在数组的有限长度中,使用hash,有可能会形成hash冲突,两个key的值hash到一个节点上,就形成了链表
扩容机制
Capacity:HashMap当前长度
LoadFactor:负载因子,默认值0.75f
步骤
扩容:创建一个新的Entry空数组,长度是原数组的2倍
ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组
为什么要reHash,因为长度扩大以后,Hash的规则也随之改变index = HashCode(Key) & (Length - 1)
线程不安全
1.7多线程情况下环形链表
多线程数据覆盖
想要线程安全可以使用
Collections.synchronizedMap(Map)
普通Map
互斥锁mutex
对方法上锁
HashTable
效率较低
在数据操作的时候都会上锁,synchronized
HashTable不允许键或值为null,HashMap键和值都可以为null
HashTable put空值的时候会直接抛空指针异常
HashMap做了特殊处理
HashTable使用的是fail-safe,安全失败机制
如果使用null值,就会使得其无法判断对应的key是不存在还是为空,因为无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理
不同点
Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类
HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75
现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1
HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的
当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,而 Hashtable 则不会
快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。
集合在被遍历期间如果内容发生变化,就会改变modCount的值。
每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
Tip:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出
ConcurrentHashMap
2的次幂
初始化大小16
初始化时最好赋初值,最好是2的次幂,1<<4
使用不是2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。
只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的,设置16是为了实现均匀分布
重写equals必须重写hashcode
在java中,所有的对象都是继承于Object类,Ojbect类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的
未重写equals方法是继承了object的equals方法,那里的 equals是比较两个对象的内存地址,new了2个对象内存地址肯定不一样,对于值对象,==比较的是两个对象的值,对于引用对象,比较的是两个对象的地址
重写requals,一定要重写hashcode,目的就是为了两个key的值形成链表之后,保证相通的对象返回相同的hash值,不同的对象返回不同的hash值
ConcurrentHashMap
数据结构
1.7
由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表
HashEntry使用volatile去修饰了数据value和下一个结点next
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
采取了分段锁技术,Segment继承了ReenTrantLock,支持CurrencyLevel(Segment数量)的线程并发,不可以存放空值
put时第一步会尝试获取锁,如果失败就使用scanAndLockForPut()自旋获取锁
1、尝试自旋获取锁。
2、如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功
get时需要将key通过hash之后定位到具体的Segment,再通过一次hash定位到具体的元素上,因为HashEntry中的value属性是用volatile修饰的,保证了内存可见性,所以每次获取都是新值,get很高效,不需要加锁
存在问题:因为是数组+链表的方式,所以查询的时候需要遍历链表, 效率较低
1.8
CAS+synchronized
类似HashMap,把HashEntry改成了Node,值和next也用volatile修饰,保证可见性,并且引入了红黑树,在链表长度大于8时会转换
put
1、根据 key 计算出 hashcode
2、判断是否需要进行初始化
3、即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功
4、如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
5、如果都不满足,则利用 synchronized 锁写入数据
6、如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树
get
1、根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值
2、如果是红黑树那就按照树的方式获取值
3、都满足那就按照链表的方式遍历获取值
查询效率(O(logn))
LinkedHashMap
HashMap基础上对所有entry加了双向循环链表,来保证有序
TreeMap
有序,红黑树
HashTable
线程安全,加了synchronized ,直接在方法上加锁,并发读很低,最多同时允许一个线程访问
Set
唯一
HashSet
无序,内部使用HashMap存储
LinkedHashSet
参考LinkedHashMap和HashMap的关系
TreeSet
有序,红黑树
Queue
PriorityQueue
二叉堆
BlockingQueue
PriorityBlockingQueue
二叉堆
LinkedBlockingQueue
链表实现
put和take两把锁
ArrayBlockingQueue
数组实现
一把锁
SynchronousQueue
轻量级BlockingQueue,保留一个元素在queue
线程安全的集合
ConcurrentHashMap
Collections.synchronizedSet(Sets.newHashSet())
CopyOnWriteArraySet
SynchronizedMap
SynchronizedList
泛型
优点
与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨
当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误
泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为 Cache<String>这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型
规范
T代表表一般的任何类
E 代表 Element 的意思,或者 Exception 异常的意思。
K 代表 Key 的意思。
V 代表 Value 的意思,通常与 K 一起配合使用。
S 代表 Subtype 的意思,文章后面部分会讲解示意
泛型擦除
泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除
在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>则类型参数就被替换成类型上限
线程
线程同步的几种方式
同步方法或同步代码块synchronized
特殊域变量volatile
可重入锁 ReenTrantLock
局部变量ThreadLocal
阻塞队列LinkedBlockingQueue
原子变量 AtomicInteger
线程同步机制
互斥:互斥的机制,保证同一时间只有一个线程可以操作共享资源 synchronized,Lock等。
临界值:让多线程串行话去访问资源
事件通知:通过事件的通知去保证大家都有序访问共享资源
信号量:多个任务同时访问,同时限制数量,比如发令枪CDL,Semaphore等
线程安全
在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险,即堆内存空间在没有保护机制的情况下,对多线程来说是不安全的地方,因为你放进去的数据,可能被别的线程“破坏”
线程状态
初始(NEW)
新创建了一个线程对象,但还没有调用start()方法
运行(RUNNABLE)
ready
程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)
running
就绪状态的线程在获得CPU时间片后变为运行中状态(running)
阻塞(BLOCKED)
表示线程阻塞于锁
等待(WAITING)
进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
超时等待(TIMED_WAITING)
该状态不同于WAITING,它可以在指定的时间后自行返回
终止(TERMINATED)
表示该线程已经执行完毕
方法
Thread.sleep(long millis)
一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式
Thread.yield()
一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间
thread.join()/thread.join(long millis)
当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)
obj.wait()
当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒
obj.notify()
唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程
LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines)
当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒
线程通信
Object的wait()、notify() 和 notifyAll()
两个线程通过对同一对象调用等待 wait() 和通知 notify() 方法来进行通讯,持有某个对象的 Monitor 锁,调用 wait 会让当前线程处于等待状态,直到其他线程 notify 或者 notifyAll
volatile 共享内存
CountDownLatch 并发工具
会让当前线程进入等待状态,直到 latch 被减少为 0,基于 AQS(AbstractQueuedSynchronizer) 实现
Reentrantlock中的Condition类, signal/await 方法的组合
CyclicBarrier
1、首先初始化线程参与者
2、调用 await() 将会在所有参与者线程都调用之前等待
3、直到所有参与者都调用了 await() 后,所有线程从 await() 返回继续后续逻辑
线程池 awaitTermination() 方法
管道通信PipeStream
线程池
为什么用
降低资源消耗
通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗
提高响应速度
任务到达时,无需等待线程创建即可立即执行
提高线程的可管理性
线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控
提供更多更强大的功能
线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行
并发可能存在的问题
频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大
对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险
系统无法合理管理内部的资源分布,会降低系统的稳定性
基本组成
线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务
工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务
任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等
任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制
线程池创建
ThreadPoolExecutor
corePoolSize
表示核心线程池的大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到corePoolSize,则会创建新的线程来执行所提交的任务,
如果线程个数已经达到了corePoolSize,即使当前核心线程池有空闲的线程,也不再重新创建线程。如果调用了prestartCoreThread()或者 prestartAllCoreThreads(),线程池创建的时候所有的核心线程都会被创建并且启动
maximumPoolSize
表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过maximumPoolSize的话,就会创建新的线程来执行任务
keepAliveTime
空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗
unit
时间单位,为keepAliveTime指定时间单位
workQueue
阻塞队列,用于保存任务的阻塞队列,可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue
threadFactory
创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因
handler
饱和策略,当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况
AbortPolicy
直接拒绝所提交的任务,并抛出RejectedExecutionException异常
CallerRunsPolicy
只用调用者所在的线程来执行任务
DiscardPolicy
不处理直接丢弃掉任务
DiscardOldestPolicy
丢弃掉阻塞队列中存放时间最久的任务,执行当前任务
线程池执行
分支主题
第一步要判断线程池状态,不为running状态则拒绝
先判断线程池中核心线程池所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第2步
判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第3步
判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,则交给饱和策略进行处理
分支主题
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务
如果当前运行的线程少于corePoolSize,则会创建新的线程来执行新的任务(workerCount < corePoolSize)
如果运行的线程个数等于或者大于corePoolSize,则会将提交的任务存放到阻塞队列workQueue中(workerCount >= corePoolSize)
如果当前workQueue队列已满的话,则会创建新的线程来执行任务(workerCount >= corePoolSize && workerCount < maximumPoolSize)
如果线程个数已经超过了maximumPoolSize,则会使用饱和策略RejectedExecutionHandler来进行处理(workerCount >= maximumPoolSize)
线程池关闭
shutdownNow
首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表
shutdown
只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程
修改线程池参数
考虑角度
任务的性质:CPU密集型任务,IO密集型任务和混合型任务
CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数
任务的优先级:高,中和低
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行
任务的执行时间:长,中和短
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行
任务的依赖性:是否依赖其他系统资源,如数据库连接
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU
自己实现线程池思路
RUNNING的作用是标记线程池的整体状态是否在工作
lock是为了在线程池内部的一些操作上加上并发锁,来保证程序不出错。
workers是一个工作集,用来存放工人。而且是hashSet类型,这就代表是一个没有重复worker的集合。
queue 阻塞队列,用来存放线程池将用执行的任务。使用的是并发包下的阻塞队列,可以保证在任务的存取上是线程安全的。
threads是一个简易的线程工厂,源码中相对复杂。用来存放生成的线程
poolsize代表核心线程数 就是这个线程池中主要大部分情况下有多少线程
coreSize 代表正在线程池中工作的线程数
shutdown是标记线程池停止运行的标记
Spring
ThreadPoolTaskExecutor
(基于juc的ThreadPoolExecutor)
核心线程数
最大线程数
cpu密集
n
io密集
(io时间+cpu时间)/cpu时间 * n
队列
线程闲置时间
拒绝策略
丢弃
抛异常
丢弃队列最前面的任务
交给调用主线程执行
守护线程
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出,因此,JVM退出时,不必关心守护线程是否已结束
Thread t = new MyThread();
t.setDaemon(true);
t.start();
垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源
ThreadLocal
ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置
1、对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。
2、对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。
Synchronized是通过线程等待,牺牲时间来解决访问冲突
ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值
//Entry为ThreadLocalMap静态内部类,对ThreadLocal的弱引用
//同时让ThreadLocal和储值形成key-value的关系
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//ThreadLocalMap构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//内部成员数组,INITIAL_CAPACITY值为16的常量
table = new Entry[INITIAL_CAPACITY];
//位运算,结果与取模相同,计算出需要存放的位置
//threadLocalHashCode比较有趣
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
数据类型
基本类型
整数类型byte、short、int、long
浮点类型float、double
字符型 char
布尔型 boolean
引用类型
类
接口
数组
设计模式
分类
创建型
单例模式
工厂模式
构建器模式
结构型
代理模式
适配器模式
门面模式
装饰者模式
行为型
策略模式
模板方式模式
观察者模式
工作流模式
应用
spring
1、BeanFactory和ApplicationContext应用了工厂模式
2、Bean创建,应用单例和原型等模式实现
3、AOP 领域则是使用了代理模式、装饰器模式、适配器模式等
4、各种事件监听器,是观察者模式的典型应用
5、类似 JdbcTemplate 等则是应用了模板模式
mybatis
构建器Builder模式 :
例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;
工厂模式 :
例如SqlSessionFactory、ObjectFactory、MapperProxyFactory;
单例模式 :例如ErrorContext和LogFactory
代理模式 :Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk的动态代理;还有executor.loader包使用了cglib或者javassist达到延迟加载的效果;
模板方法模式 : 例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler;
适配器模式 : 例如Log的Mybatis接口和它对jdbc、log4j等各种日志框架的适配实现
装饰者模式 : 例如cache包中的cache.decorators子包中等各个装饰者的实现;
0 条评论
下一页