-
多线程
-
解决多线程安全
-
乐观锁悲观锁
-
乐观锁:每次拿数据时候都认为别人不会修改,期间不上锁,但是在更新数据的时候判断一下在此期间有没有人修改过数据。一般通过版本号和cas算法实现。乐观锁适用于多读的使用,提高吞吐量。
-
悲观锁:每次拿数据都会认为别人会修改,期间上锁,其他访问如果想拿数据则需要阻塞等待。synchronized和lock锁都是悲观锁。多写使用悲观锁。
-
-
CAS
-
轻量级同步机制
-
主要用于实现多线程环境下的无锁算法和数据结构,保证并发安全性。可以在不使用锁的情况下,对共享数据进行线程安全的操作。
-
主要参数:要更新的内存位置,期望的值和新值。
-
执行过程
-
获取要更新的内存位置var
-
将期望值expected与var比较,两者相等,内存位置的值var更新为新值new
-
两者不相等,则说明有其他线程修改了内存位置的值var。cas失败,重新尝试
-
-
java中的实现类:Unsafe
-
ABA问题
-
ABA问题指在CAS操作过程中,如果变量的值被改为了A,B,再改回A,而CAS操作是能够成功的,这时候可能导致程序出现意外结果,在高并发场景下,使用CAS操作可能存在ABA问题,也就是在一个值被修改之前,先被其他线程修改为另外的值再被修改回原值,瓷实CAS操作会认为这个值没有被修改过,导致数据不一致。
-
解决:java提供了AtomicStampedReference类,类中使用版本号来控制解决ABA问题,每个共享变量都会关联一个版本号
-
-
CPU空转
-
CAS自旋时间过长,如果某个线程一直自旋等待,会浪费线程资源
-
解决:自适应自旋锁的方式,前几次重试等待,后面使用阻塞等待。
-
当一个线程请求获取锁时,如果当前线程已经持有锁,则将计数器加1,否则使用CAS操作来获取锁。这样可以避免了使用synchronized关键字或者ReentrantLock等锁的实现机制。
-
当线程获取锁失败时,使用自旋等待的方式,这样可以避免线程进入阻塞状态,避免了线程上下文切换的开销。当重试次数小于10时,使用自旋等待的方式,当重试次数大于10时,则使用阻塞等待的方式。这样可以在多线程环境下保证线程的公平性和效率。
-
在释放锁时,如果计数器大于0,则将计数器减1,否则将锁的拥有者设为null,唤醒其他线程。这样可以确保在有多个线程持有锁的情况下,正确释放锁资源,并唤醒其他等待线程,保证线程的正确性和公平性。
-
-
-
cas应用场景
-
通常用于乐观锁和无锁算法
-
线程安全计数器
-
队列
-
数据库并发控制
-
自旋锁
-
线程池
-
-
-
-
synchronized锁:
-
使用方式:修饰实例方法,修饰静态方法,修饰代码块
-
讲解:synchronized锁是用来解决线程安全问题的,被该锁修饰的代码块或方法任意时刻都只能有一个线程去执行。jdk1.6版本之前是重量级锁,之后锁升级,加了偏向锁,自旋锁,适应性自旋锁,轻量级锁,锁粗化等。
-
该锁是存在锁升级的:当线程的量达到临界区,如果锁存在竞争,则会升级为轻量级锁,自旋锁和自适应自旋锁。自旋锁的自旋次数达到设置的次数还没有获取到锁时,则会升级为重量级锁。
-
重量级锁耗费资源,因为线程获取重量级锁时,其他线程会被阻塞,再次唤醒的话需要借助操作系统去完成,要从用户态转换为内核态,转换状态非常耗费资源。
-
-
lock锁
-
实现ReentrantLock
-
实现ReentrantReadWriteLock读写锁
-
读写锁特性
-
非公平模式读写顺序不确定
-
公平模式读写顺序确定
-
可重入线程获取锁后还可以获取该锁
-
锁降级
-
锁升级,读锁变写锁
-
锁降级,写锁变读锁
-
-
ReentrantReadWriteLock不支持锁升级,会产生死锁。
-
读锁和写锁互斥
-
-
-
lock锁效率高于Synchronized锁
-
synchroizes和lock锁区别
-
synchroizes是java中的一个关键字,属于内置语言,用来解决多线程安全问题,隐式锁,遇到异常自动释放。synchrionized使用Object对象本身的wait,notify,notifyAll调度机制,而lock可以使用condition进行线程之间的调度,synchronized存在明显性能问题就是读与读之间的互斥。
-
lock是一个接口
-
lock等待锁过程中可以中断等待,而synchronized只能等待锁的释放,不能响应中断。
-
-
-
-
线程池
-
为什么使用线程池:
-
new Thread新建对象性能差
-
new Thread线程缺乏统一管理可能导致资源占用
-
new Thread功能单一
-
-
通过Executors工具类新建线程池方式
-
newCachedThreadPool:可缓存线程池,线程池长度超过需求,可灵活回收空闲线程,无可回收则新建线程。
-
newFixedThreadPool:定长线程池,可控制线程最大并发数,超出线程可在队列等待
-
newScheduledThreadPool:定长线程池,支持定时以及周期性任务。
-
newSingleThreadExecutor:创建一个单线程线程池,它只会用唯一的工作线程来执行任务。
-
-
Executors工具类新建线程池弊端
-
newFixedThreadPool和newSingleThreadExector:主要问题是堆积的请求处理队列可能会耗费非常大内存,甚至oom。
-
newCachedThreadPool和newScheddulledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
-
-
所以使用推荐的ThreadPoollExecutor
-
corePoolSize:核心线程数,不能小于0
-
maxiumumPoolSize:最大线程数,要大于核心线程数
-
keepAliveTIme:线程池闲置时间 不小于0
-
unit:线程时间单位
-
workQueue:线程池中认为队列 不为空
-
threadFactory:线程工厂 不为空
-
拒绝策略:不为空
-
丢弃任务不抛出异常
-
丢弃等待最久任务
-
丢弃任务抛出异常
-
主线程直接执行,不抛弃任务,也不抛出异常。
-
-
-
线程池创建流程
-
线程池内部其实是构建了一个生产者,消费者模型,将线程和任务解耦,提高线程复用率。
-
线程池运行主要分为两个部分,任务管理和线程管理。任务管理是生产者,任务提交后,线程池会判断该任务的后续流转
-
直接申请线程执行该任务
-
缓存到队列中去等待线程执行
-
拒绝该任务
-
线程管理部分是消费者,他们被统一维护在线程池内,根据任务请求进行线程分配,线程执行完之后会继续执行新的任务,线程获取不到任务时,就会被回收。
-
-
设置线程池参数
-
cpu密集型任务:该任务需要大量运算,没有阻塞,cpu全速运行,该任务只有在多核cpu上才能实现加速。
-
io密集型任务:该任务需要大量io,大量的阻塞。比如,数据库插入数据时,就是将存在磁盘的数据插入到数据库中,此时数据库就是空闲时间。
-
针对上诉两类任务,分别设置线程池大小
-
cpu密集型的任务,尽可能设置少线程,大小参照cpu核数。
-
io密集型,应该多点线程,因为过多的线程使cpu在io阻塞时还会调用其他线程。这时线程池大小应该设置为当前机器cpu核数的两倍基础上多些。
-
-
-
实战
-
-
-
-
-
多线程面试级理解
最新推荐文章于 2024-11-11 00:03:55 发布