CopyOnWrite集合
CopyOnWrite就是写时复制,向集合中添加元素是,先将当前集合进行copy,复制出一个新的集合,然后向新集合中添加元素,添加完成后再将原集合的引用指向新集合。好处在于可以对CopyOnWrite集合进行并发的读,而不需要加锁。这是一种读写分离的思想。
具体实现类CopyOnWriteArrayList和CopyOnWriteArraySet,其实CopyOnWriteArraySet的底层实现还是CopyOnWriteArrayList。
CopyOnWriteArrayList
CopyOnWrite适用于读多写少的并发场景下。例如白名单、黑名单、商品的类目等很少进行修改,但是需要经常访问。
优点:
减少扩容开销
一般建议可以使用批量添加,减少添加次数,底层采用System.arrayCopy实现
缺点:
内存占用问题,会有频繁的GC操作
数据的一致性问题。CopyOnWrite只能保证最终一致性,不能保证实时一致性。如果需 要写入的数据立即就能读取到,则不能使用CopyOnWrite集合。
Monitor对象
monitor可以翻译成监视器或者管程,是由JVM提供的,当线程执行到对象方法的临界区时,首先查看对象头中是否已经有关联的Monitor对象,如果已经关联加入了monitor对象,则将当前线程添加到。
monitor对象的entrylist中;如果没有关联monitor对象,则改变对象的对象头的mark word或者指向一个monitor对象。如果已经指向了一个monitor对象,则表示monitor对象的拥有者变成了当前线程,当前线程执行完成后monitor再释放线程,重新到entrylist中查找一个新的拥有者,一般非公平竞争。
一个对象关联一个monitor对象,当一个对象持有monitor对象地址后,它就处理锁定状态,当线程执行到monitorenter指令时,则会尝试获取对象所对应的monitor。
JVM给每个对象和class字节码都会设置一个监听器monitor,用于检测并发代码的重入。
不加synchronized的对象不会关联monitor。
死锁
使用多进程和多线程改善了系统资源的利用率并提高了系统的处理能力(吞吐量)。并发执行也带来了死锁问题。死锁就是指多个线程因为竞争资源而造成的互相等待的一种僵局状态,如果没有外力作用,这些程序都无法向前推进。
活锁和死锁的区别
处于活锁状态的进程或者线程的状态是不断改变的,活锁可以理解为一种特殊的饥饿。死锁和活锁最大的区别是前者的状态不可能改变,但是后者的状态可以改变,但是都是不能继续执行。
如何判断一个线程是否拥有锁?
在Thread类中有一个方法holdsLock(obj)可以判断当前线程是否拥有obj对象的锁
死锁的原因
1、系统中不可剥夺资源的竞争。
2、不可剥夺条件
3、进程或者线程的推进顺序非法
死锁的4大必要条件
死锁的产生需要4个必要条件,也就是说满足4个条件不一定死锁,但是死锁一定满足4个条件
1、互斥条件。竞争的资源具有排他性
2、不可剥夺性条件。只能由获取资源的进程或线程主动释放,不能剥夺
3、请求和保持条件。可以请求新资源,同时永远不释放已经获取到的资源
4、循环等待条件。出现了循环等待链
如何避免死锁
强制定义加锁顺序。线程按照一定的顺序加锁,避免出现嵌套封锁,如果一定出现嵌套问题,则定义一个加锁顺序
加锁时限。线程尝试获取锁时加上一个时间限制,超过时间限制则放弃对该锁的申请,同时释放资源占用的资源
让程序每次至多只能获取一个锁,这种方式在多线程环境下不现实。一般在设计时要求必须考虑清楚锁的顺序,尽量减少嵌套加锁。
既然死锁是两个线程无限制等待对方持有的锁,那么理论上来说只要给等待时间添加上限即可,synchronized不具备这个功能,所以在开发中可以使用Lock接口中的tryLock方法尝试获取锁,这个方法可以指定超时上限,在等待一段时间后会自动返回失败信息。
死锁检测
当出现死循环、死锁、阻塞等问题时,获取线程dump文件是最好的解决问题的途径。所谓线程的dump文件就是线程堆栈。
获取dump文件的步骤:
1、可以使用命令jps获取线程的pid,在linux中还可以使用ps -ef | grep java
2、可以使用jstack pid 命令打印线程堆栈
Thread类中提供了一个getStackTrace()方法获取线程的堆栈
线程相关模型
volatile
轻量级同步解决方案,保证了不同线程对变量进行操作的可见性,禁止进行指令重排,实现了对这个变量操作的有序性,volatile只能保证对单次读写的原子性,但是针对i++之类的操作不能保证原子性。
volatile变量的内存可见性是基于内存屏障实现的。
只有在状态真正独立于程序内其它内容时才能使用volatile。
AQS模型
AQS抽象队列同步器,AQS时JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相同的同步器的一个同步框架。AQS中维护了一个volatile int state(共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时进入此队列)。
AQS思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态;如果被请求的共享资源已经被占用,则需要通过线程阻塞等待已经唤醒时锁分配的机制,这个机制是由AQS通过CLH队列锁实现的,就是将暂时获取不到锁的线程加入到队列中。
AQS的实现类以ReentrantLock为例,state初始值为0,表示未锁定状态,A线程调用lock()方法时,会调用tryAcquire独占该锁,并将state+1,此后其它线程再调用tryAcquire尝试加锁操作时就会失败,直到A线程调用unlock方法释放锁,并将state=0为止,其它线程才由机会获取锁。锁是可以重入的,重复获取则state累加。
信号量Semaphone
semaphone实际上就是一个功能完备的计数器,主要用于控制对有限资源的访问数量,能监控由多少个线程等待获取资源,并且通过信号量可以得知可用资源的数目。底层实现AQS,支持公平和非公平。
应用场景:对有限资源的使用限制
1.Semaphore(int permits)其中参数表示初始可用的资源数,并不一定是资源的最大数
2.Semaphore(int permits, boolean fair) 布尔类型的参数表示是否采用公平策略获取锁,true 表示先来先得,就是公平锁。
3.acquire()申请资源,当申请的资源>现有可用资源时,申请资源的线程被阻塞,直到有可用资源或者申请线程被打断。如果线程被打断则抛出异常。
4.release()释放一个资源
障碍器CyclicBarrier
CyclicBarrier作用就是会让所有线程等待完成后才会执行下一个动作。一般用于大型任务划分为多个小子任务去执行,当子任务参收后才执行主任务。
CyclicBarrier(int parties)参数就是参与的子线程的个数
CyclicBarrier(int parties, Runnable barrierAction) Runnable参数就是最有一个到达线程要作的任务
await() 线程调用await()方法表示自己已经达到了障碍器,子线程阻塞等待主任务执行后继续执行。