线程基础:
进程:资源分配的最小单位
线程:CPU调度的最小单位
并行:多个CPU同时执行多个任务
并发:一个CPU同时执行多个任务(时间片轮转调度)
CPU调度算法:
先来先服务算法(FCFS)、
最短作业优先(SJF)、
时间片轮转调度算法(RR
)、
高响应比优先算法(HRRN)
、
最高优先级调度算法、
多级反馈队列调度算法
守护线程:服务于用户线程,用户线程结束后才会结束(GC线程)
用户线程:所有用户线程都结束,JVM才能结束(main线程)
如何避免死锁:
-
确保线程按照同样的顺序获取锁
-
加锁时设置超时时间,超时则放弃获取锁并释放已获取的锁
-
降低锁的粒度
-
死锁检测
实现Callable接口与实现Runnable接口的区别:
-
call()方法有返回值,run()方法没有
-
call()方法可以抛异常,外层可捕获
-
Callable支持泛型
多线程:
PS:需要平衡用多线程所带来性能的提升度与多线程环境下线程间上下文切换带来的开销以及线程安全、代码复杂度等因素,考虑是否有必要用多线程。
常见多线程使用场景:
-
常见的浏览器、Web服务(现在写的web是中间件帮你完成了线程的控制),web处理请求,各种专用服务器(如游戏服务器)
-
tomcat,tomcat内部采用多线程,上百个客户端访问同一个WEB应用,tomcat接入后就是把后续的处理扔给一个新的线程来处理,这个新的线程最后调用我们的servlet程序,比如doGet或者dpPost方法
-
FTP下载,多线程操作文件
-
分布式计算
-
后台任务:如定时向大量(100W以上)的用户发送邮件;定期更新配置文件、任务调度(如quartz),一些监控用于定期信息采集
-
自动作业处理:比如定期备份日志、定期备份数据库
-
异步处理:如发微博、记录日志
-
页面异步处理:比如大批量数据的核对工作(有10万个手机号码,核对哪些是已有用户)
-
数据库的数据分析(待分析的数据太多),数据迁移
-
多步骤的任务处理,可根据步骤特征选用不同个数和特征的线程来协作处理,多任务的分割,由一个主线程分割给多个线程完成
Synchronized:
底层
映射成字节码会增加两个指令:monitorenter、monitorexit
-
修饰非静态方法和代码块锁(this)是竞争同一把锁,都是对象锁,锁的都是调用方法的对象实例,当new两个对象实例分别去调用方法时,竞争的不是同一把锁
-
修饰静态方法和 代码块 锁(Class)是竞争同一把锁,都是类锁,锁的是调用方法的类,无论是否用不同对象实例去调用方法竞争的都是同一把锁。
-
作用于 代码块,对括号里配置的对象(同步监视器)加锁
锁升级过程: (
jdk 1.6之前
synchronized 关键
字只表示重量级锁,1.6之后区分为
偏向锁、轻量级锁、重量级锁)
默认是无锁状态,当线程第一次获取锁时(通过CAS竞争锁),锁为升级为偏向锁,对象头上会标记线程ID,表示这个对象偏向于当前线程,当偏向的线程销毁后锁会被重置为无锁状态,当产生锁竞争时,偏向锁会升级为轻量级锁,轻量级锁依赖CAS(
compare and swap)来获取锁,线程会在一定次数内通过自旋操作重复尝试获取锁,若获取失败则会升级会重量级锁。
wait()/notify()/notifyAll()
:
线程间通信的方法,只能用于同步代码块或同步方法中,且调用者只能是同步代码块或同步方法中的同步监视器
wait()/sleep():
-
都让线程进入阻塞状态
-
sleep()是Thread类的方法,wait()是Object类的方法
-
sleep()可在任何地方调用,wait()只能在同步代码块或同步方法中调用
-
sleep()不会释放锁,wait()会释放锁
Concurrent(JDK1.5之后提供)包下常用类:
volatile
:
轻量级锁,
保证修饰共享变量在多线程环境下的可
见性,会禁止JVM对指令进行重排序,但不保证原子性
当修饰共享变量被写时,变量值会立即从本地内存刷新到主内存中,当读共享变量时,会直接从主内存中读取,本地该变量会被置为无效
AtomicInteger/AtomicBoolean/AtomicLong等基本类型原子类:
既保证可见性也保证原子性:用volatile来修饰变量value保证可见性,对变量的操作通过CAS(compare and swap)保证原子性
CAS算法(乐观锁)保证原子性:CAS是一种无锁的非阻塞算法实现,
CAS 操作中包含三个操作数 —— 需要读写的内存值(V)、进行比较的预期值(A)和拟写入的新值(B)。如果内存值V与预期值A相等,那么会将值更新为新值B。否则不做任何操作。
-
用于读多的场景,即很少发生冲突,避免多次retry(减少自旋)
-
但会产生ABA问题:通过引入版本号来解决
ConcurrentHashMap:
介于HashMap和HashTable之间,内部采用“锁分段”(
DEFAULT_CONCURRENCY_LEVEL = 16,
默认分16段即支持并发数为16
) 机制代替HashTable的独占锁,性能更好。
PS:
JDK1.8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全。数据结构采用:数组+链表+红黑树,JDK1.7是Segment+HashEntry
与HashTable的区别:
- HashTable只能串行访问,效率低。ConcurrentHashMap可并行访问,不同线程可访问不同的分段(segment)
-
HashTable是用synchronized来锁,ConcurrentHashMap分段锁是用 ReentrantLock
-
CopyOnWriteArrayList:
CopyOnWriteArraySet:通过
CopyOnWriteArrayList实现
共同特性:
-
-
写时复制策略, 通过 ReentrantLock控制并发
-
适用于读多写少的场景(读操作并没有加锁)
-
只能保证数据最终一致性,并不能保证数据实时一致性,当一个线程在写数据时,另一线程读只能读到当时已写的那部分数据,还未写完的数据无法读到。
-
Lock(接口):
唯一实现类
ReentrantLock,基于volatile和CAS实现
与synchronized区别:
- synchronized是JAVA关键字,会自动释放锁,Lock是一个接口,需手动释放锁
-
都是可重入的独占锁
-
synchronized适用于少量代码同步的情况,lock适用于大量代码同步的情况
-
synchronized是非公平锁, ReentrantLock可设置非公平锁或公平锁,默认非公平锁
-
Condition:
用来控制线程间通信的类
-
-
-
必须配合Lock使用,可用来实现等待/通知模式
-
await()/signal()/signalAll()可替代Object的wait()/notify()/notifyAll(),只能在lock.lock()和lock.unlock()之间使用
-
signal() 可唤醒指定线程,而notify()只能唤醒随机线程
-
-
ReadWriteLock(接口):
实现类
ReentrantReadWriteLock
读写锁,适用于写写/读写需要互斥而读读不需要互斥的场景,避免在任何情况下都使用独占锁,提高性能。ReadLock/WriteLock实现了Lock,用法同
ReentrantLock。
线程池:
线程复用、减少创建和销毁线程带来的开销、可用来控制并发数量,也能保证创建的最大线程数不超过系统承受能力。
线程数量的设置:
-
CPU密集型:线程数=CPU数( Runtime.getRuntime().availableProcessors())
-
IO密集型: 线程数=任务数
线程池创建:
ExecutorService service =
new
ThreadPoolExecutor
(
int
corePoolSize,
int
maximumPoolSize,
long
keepAliveTime,
TimeUnitunit,
BlockingQueue
<Runnable>
workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
corePoolSize : 线程池核心池的大小。
maximumPoolSize : 线程池的最大线程数。
keepAliveTime : 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit : keepAliveTime 的时间单位。
workQueue : 用来储存等待执行任务的队列。
threadFactory : 线程工厂。
handler 拒绝策略。
阻塞队列:
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列(不指定队列大小时为无界队列)。
SynchronousQueue: 一个不存储元素的阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
拒绝策略:
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。 (默认)
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务。(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
PS: 线程池用完需要手动关闭 shutdown()
ForkJoinPool:(JDK1.7之后提供)
https://blog.csdn.net/f641385712/article/details/83749798
ThreadLocal:
设计:每个线程Thread都维护一个ThreadLocalMap(ThreadLocal有一个静态内部类ThreadLocalMap,这个map维护一个Entry数组,Entry的key为ThreadLocal对象,value为变量值)。所以变量的设置和获取都是依赖当前线程的ThreadLocalMap来操作的。
内存泄漏分析:在用
线程池管理线程的情况下,由于ThreadLocalMap是依赖于线程的,当线程存活时,即使key(ThraedLocal)的外部引用已经没了
(强引用链①会断开),若Entry没有被手动删除则value依旧是被Entry所引用的,所以不会被GC,同时它也不可被访问(ThreadLocal用完被GC导致Entry的key为null),因此造成内存泄漏
内存泄漏
根本原因:ThreadLocal的生命周期与当前线程Thread的生命周期一样长,若没有手动删除Entry就会发生内存泄漏
造成内存泄露的两个前提:1、线程使用完未销毁(不好控制,尤其是在用线程池的情况下) 2、未手动删除Entry(相对容易控制,因此在使用ThreadLocal时记得手动remove())
ThreadLocal如何解决的:在ThreadLocal的set()和get()方法中对脏Entry自动进行清理
为什么Entry的key采用弱引用:将ThreadLocal对象的生命周期与线程生命周期解绑,即ThreadLocal不在被使用时是可以被回收的,那么在下一次再对ThreadLocal操作时Entry会因为key为null被清理,从而避免内存泄漏。也算是未手动清理使用完的Entry的一道保障
![](https://i-blog.csdnimg.cn/blog_migrate/d0cc62453c4720c6f55a4636e5c35073.png)
常用场景:用来解决数据库连接、Session管理
特点:
-
-
在特定场景下可替代 synchronized通过无锁方式解决共享变量线程安全问题,提高性能,提高并发量
-
可解决参数多层传递的高耦合问题
-
于
synchronized区别:
-
-
-
-
synchronized 只能串行处理数据,时间换空间,保证多线程下共享变量的同步
-
ThreadLocal 可并行处理数据,空间换时间,为每个线程提供一个共享变量的副本,保证多线程下的数据隔离
-
-
-
PS:ThreadLocalMap是如何解决哈希冲突的?与HashMap解决哈希冲突方式有何区别?