Java并发知识笔记
1.关于各种锁总结:
互斥锁:即同一时间只允许一个线程修改共享资源
缺点:
当线程A获取到互斥锁时,线程B要获取锁时,线程B将获取锁失败,并释放掉cpu供其他线程使用(即内核将线程从运行态切换到睡眠态),当锁被释放掉以后,内核将线程B从睡眠态又会切换为就绪态
这样就造成了两次线程的上下文切换,开销成本较高,
关于对上下文的理解:
当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
现假如:
线程B里需要执行的代码本就时间很短,可能比切换时间短很多,这样就造成了严重的浪费
自旋锁:在同一时间也是只允许一个线程修改共享资源
较互斥锁的优势:
当线程A获取锁,线程B获取锁失败,此时并不会向互斥一样去切换上下文而是通过CPU提供的CAS,在用户态完成加锁和解锁操作,较互斥锁开销较小,线程B将发生忙等待,直到拿到锁为止。
缺点:
1.需要注意,在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。
2.当获取锁的线程代码执行时间很长时,自旋的线程会长时间占用CPU资源,所以自旋的时间和被锁住的代码执行的时间是成正比关系的
读写锁:
读锁(共享锁):同一时间允许读的线程并发读取
写锁(独占锁):同一时间只允许一个写线程获取锁
读优先锁:
当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取读锁。
注意:如果一直有读锁在获取,会造成写线程的饿死
写优先锁:
当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取读锁
注意:这里读线程也可能会被饿死
公平读写锁:
公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。
乐观锁:
乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。(先斩后奏么,哈哈哈)
注意:
乐观锁全程没有加锁,所以它也叫无锁编程
悲观锁(互斥锁,自旋锁,读写锁)
悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。
2.发布对象
使一个对象能够被当前范围之外的代码所使用
对象逸出
一种错误的发布。当一个对象还没有构造完成时,就使它被其它线程所见
安全发布对象
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存到volatile类型域或者AtomicReference对象中
- 将对象的引用保存你到某个正确构造对象的final类型域中
- 将对象的引用保存到由锁保护的域中
不可变对象
对象创建以后其状态就不能修改
对象所有域都是final类型
对象是正确创建的(在对象创建期间,this引用没有逸出)
final关键字
修饰类:不能被继承
修饰方法:
1.锁定方法不被继承类修改
2.效率
3.在新版本中一个类的private方法会被隐式指定为final方法
修饰变量:基本数据类型变量,引用类型变量
3.线程封闭
把对象封装到一个线程里,只有这一个线程能看到这个对象
如何实现:
堆栈封闭:局部变量,无并发问题,多个线程访问一个方法里的局部变量都会将局部变量拷贝到自身线程的内存里
threadlocal线程封闭:特别好的封闭方法
线程不安全类与写法
StringBuilder->StringBuffer
SimpleDateFormate ->JodaTime
ArrayList,HashSet,HashMap等Collections
先检查再执行:if(condition(a)){handle(a);}
线程安全-同步容器
ArrayList->Vector,Stack
HashMap->HashTable(key,value不能为null)
Collections.synchronizedXXX(List,Set,Map)
线程安全-并发容器 J.U.C
ArrayList -> CopyOnWriteArrayList
HashSet,TreeSet -> CopyOnWriteArraySet ConcurrentSkipListSet
HashMap,TreeMap -> ConcurrentHashMap ConcurrentSkipListMap
4.安全共享对象策略-总结
- 线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改
- 共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它
- 线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其它线程无需额外的同步就可以通过公共接口随意访问它
- 被守护对象:被守护对象只能通过获取特定的锁来访问
5.AbstractQueuedSynchronizer–AQS
- CountDownLatch
- Semaphore
- CyclicBarrier
6.ReentrantLock与锁
ReentrantLock(可重入锁)和synchronized区别
- 可重入性
- 性能的区别
- 功能区别
- 锁实现
ReentrantLock独有的功能
- 可指定是公平锁还是非公平锁
- 提供了一个condition类,可以分组唤醒需要唤醒的线程
- 提供了能够中断等待锁的线程的机制,lock.lockInterruptibly()
Runnable:线程执行,无返回值
Callable:线程执行后,可以获取返回值,并抛出异常
Future接口:取消,查询是否被取消,查询是否完成,获取结果,监控别的线程
FutureTask:集合了Future和Callable
在执行复杂逻辑的时候,用另外一个线程去计算返回值,当前线程在使用返回值之前,可以做其它的操作,需要返回值时再通过future得到
BlockingQueue接口
当队列中为空时,取出线程将阻塞,当队列满时,加入队列的线程将阻塞
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wba0hgHx-1637848699496)(C:\Users\张烈文\AppData\Roaming\Typora\typora-user-images\1607941235009.png)]
7.线程池:
new Thread弊端:
- 每次new Thread 新建对象,性能差
- 线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多的系统资源导致死机或OOM
- 缺少更多功能,如更多执行,定期执行,线程中断
线程池的好处:
- 重用存在的线程,减少对象创建,消亡对象创建,消亡的开销,性能佳
- 可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞
- 提供定时执行,定期执行,单线程,并发数控制等功能
ThreadPoolExecutor
参数:
corePoolSize,maximumPoolSize,workQueue
三个参数的关系:(当前线程数cnt)
1.当前运行的线程数<corePoolSize,直接创建新的线程处理任务,即使线程池中的其他线程空闲
2.当corePoolSize<=cnt<maximumPoolSize,只有当workQueue满的时候才创建新的线程去处理任务
3.当corePoolSize=maximumPoolSize时,创建的线程池大小是固定的,如果有新任务提交,当workQueue没满的时候,将请求放入workQueue中,等待空闲线程去取出任务进行处理,如果满了,还有新任务提交,则通过具体的策略参数来指定策略去处理任务
workQueue:
workQueue是线程池中保存等待执行的任务的阻塞队列,当提交新任务到线程池以后,线程池会根据当前线程池中正在运行着的线程的数量,决定该任务的处理方式,总共有三种:
-
直接切换
常用的队列:SynchronousQueue
-
使用无界队列
一般使用基于链表的阻塞队列LinkedBlockingQueue。 这种方式:线程池中能够创建的最大线程数就是corePoolSize,而maximumPoolSize就不会起作用了。当线程池中所有的核心线程都是RUNNING状态时,这时一个新任务提交就会放入等待队列中
-
使用有界队列
一般使用ArrayBlockingQueue。使用该方式可以将线程池的最大线程数量限制为maximumPoolSize。这样能够降低资源的消耗,但同时也使得线程池对线程的调度变得更困难,因为线程池和队列都是有限的值,所以要想使线程池处理任务的吞吐率达到一个相对合理的范围,又想使线程调度相对简单,并且还要尽可能的降低线程池对资源的消耗,就需要合理的设置这两个的数量 -----如果想要降低系统资源的消耗(包括CPU的使用率,操作系统资源的消耗,上下文环境切换的开销等) 解决办法: 可以设置较大的队列容量和较小的线程池容量,但这样也会降低线程处理任务的吞吐量
相关方法:
execute():提交任务,交给线程池执行
submit():提交任务,能够返回执行结果 execute+Future
shutdown():关闭线程池,等待任务都执行完
shutdownNow():关闭线程池,不等待任务执行完
getTaskCount():线程池已执行和未执行的任务总数
getCompletedTaskCount():已完成的任务数量
getPoolSize():线程池当前的线程数量
getActiiveCount():当前线程池中正在执行任务的线程数量
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-StLQ5i4B-1637848699498)(C:\Users\张烈文\AppData\Roaming\Typora\typora-user-images\1607955673368.png)]
相关类的测试:
@Slf4j
public class ThreadPoolExample2 {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(0);
// executorService.schedule(()->{
// log.warn("schedule run");
// },3, TimeUnit.SECONDS);
// executorService.shutdown();
//该方法会一直执行,即调用该方法一秒后,每个3秒执行一次该方法
// executorService.scheduleAtFixedRate(() -> {
// log.warn("schedule run");
//
// }, 1, 3, TimeUnit.SECONDS);
// 一个定时器,每隔五秒就会执行和上面的方法有点类似
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
log.warn("Timer run");
}
},new Date(),5*1000);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ySVeB8h-1637848699499)(C:\Users\张烈文\AppData\Roaming\Typora\typora-user-images\1607956882640.png)]
多线程并发最佳实践
1. 使用本地变量
2. 使用不可变类
3. 最小化锁的作用域范围:S=1/(1-a+a/n)
4. 使用线程池的Executor,而不是直接new Thread执行
5. 宁可使用同步(countdownlatch,semophore,cyclicbarrier),也不要使用线程的wait和notify
6. 使用BlockingQueue实现生产-消费模式
7. 使用并发集合而不是加了锁的同步集合而不是加了锁的同步集合
8. 使用Semaphore创建有界的访问
}
}
[外链图片转存中...(img-9ySVeB8h-1637848699499)]
#### 多线程并发最佳实践
- 使用本地变量
- 使用不可变类
- 最小化锁的作用域范围:S=1/(1-a+a/n)
- 使用线程池的Executor,而不是直接new Thread执行
- 宁可使用同步(countdownlatch,semophore,cyclicbarrier),也不要使用线程的wait和notify
- 使用BlockingQueue实现生产-消费模式
- 使用并发集合而不是加了锁的同步集合而不是加了锁的同步集合
- 使用Semaphore创建有界的访问