并发
线程的5种状态
java.lang.Object的常用方法
- getClass() 获取类结构信息
- toString() 把对象转变成字符串
- hashCode() 获取哈希码
- equals(Object) 默认比较对象的地址值是否相等,子类可以重写比较规则
- notify() 多线程中唤醒功能
- notifyAll() 多线程中唤醒所有等待线程的功能
- wait() 让持有对象锁的线程进入等待
- wait(long timeout) 让持有对象锁的线程进入等待,设置超时毫秒数时间
- wait(long timeout,int nanos) 让持有对象锁的线程进入等待,设置超时纳秒数时间
为什么Java把wait与notify放在Object中?
- 功能角度
- wait与notify的原始目的,是多线程场景下,某条件触发另一逻辑,该条件对应的直接关系为某种对象,进而对应为Object,其对应为内存资源。
- Thread对应为CPU,与具体条件不是直接关系,Thread是对象的执行依附者。
- 内存角度
- 线程的同步需要Monitor的管理,其与实际操作系统的重型资源(锁)相关。
- 只有涉及多线程的场景,才需要线程同步,如果wait与notify放在Thread,则每个Thread都需要分配Monitor,浪费资源。
- 如果放在Object,单线程场景不分配Monitor,只在多线程分配。分配Monitor的方法为检测threadId的不同。
sleep、yield、wait、join的区别
- sleep:Thread类的方法,必须带一个时间参数。会让当前线程休眠进入阻塞状态并释放CPU,提供其他线程运行的机会且不考虑优先级,但如果有同步锁,则sleep不会释放锁即其他线程无法获得同步锁,可通过调用interrupt()方法来唤醒休眠线程。针对一个线程
- wait:Object类的方法,只能在同步环境中被调用,必须放在循环体和同步代码块中,执行该方法的线程会释放锁,进入线程等待池中等待被再次唤醒(notify随机唤醒,notifyAll全部唤醒,线程结束自动唤醒),唤醒后放入锁池中竞争同步锁
- yield:让出CPU调度,Thread类的方法,类似sleep只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会。 yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。调用yield方法只是一个建议,告诉线程调度器我的工作已经做的差不多了,可以让别的相同优先级的线程使用CPU了,没有任何机制保证采纳。
- join:一种特殊的wait,当前运行线程调用另一个线程的join方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。注意该方法也需要捕捉异常。等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。
为何不推荐使用stop()和suspend()方法?
- stop()方法不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。
- suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用 wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。
8种线程同步方法
线程安全:同一时刻只能有一个线程访问共享变量。
- synchronized同步方法
- synchronized同步代码块
- Lock
- Volatile
- wait与notify
- Threadlocal
- 阻塞队列
- 使用原子变量
Synchronized在静态方法和非静态方法的区别
synchronized 的用法可以从两个维度上面分类:
- 根据修饰对象分类
- 修饰代码块
- synchronized(this|object) {}
- synchronized(类.class) {}
- 修饰方法
- 修饰非静态方法(对象锁)
- 修饰静态方法(类锁)
- 根据获取的锁分类
- 获取对象锁:线程同时开始,同时结束
- synchronized(this|object) {}
- 修饰非静态方法
- 获取类锁:采用类锁一次只能通过一个。
- synchronized(类.class) {}
- 修饰静态方法,非静态方法
- 使用
- 对于静态方法,由于此时对象还未生成,所以只能采用类锁。
- 只要采用类锁,就会拦截所有线程,只能让一个线程访问。
- 对于对象锁(this),如果是同一个实例,就会按顺序访问,但是如果是不同实例,就可以同时访问。
- 如果对象锁跟访问的对象没有关系,那么就会都同时访问。
线程的创建方式
启动线程有如下三种方式:
- 继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
package com.thread; public class FirstThreadTest extends Thread{ int i = 0; //重写run方法,run方法的方法体就是现场执行体 public void run(){ for(;i<100;i++){ System.out.println(getName()+" "+i); } } public static void main(String[] args) { for(int i = 0;i< 100;i++){ System.out.println(Thread.currentThread().getName()+" : "+i); if(i==20) { new FirstThreadTest().start(); } } } } |
上述代码中Thread.currentThread()方法返回当前正在执行的线程对象。
- 通过Runnable接口创建线程类:使用实现Runnable接口的方式创建的线程可以处理同一资源,从而实现资源的共享.
- 适合多个相同程序代码的线程去处理同一个资源(多线程内的数据共享)
- 增加程序健壮性,数据被共享时,仍然可以保持代码和数据的分离和独立
- 避免java特性中的单继承限制
- 更能体现java面向对象的设计特点
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
package com.thread; public class RunnableThreadTest implements Runnable{ private int i; public void run(){ for(i = 0;i <100;i++){ System.out.println(Thread.currentThread().getName()+" "+i); } } public static void main(String[] args) { for(int i = 0;i < 100;i++){ System.out.println(Thread.currentThread().getName()+" "+i); if(i==20){ RunnableThreadTest rtt = new RunnableThreadTest(); new Thread(rtt,"新线程1").start(); new Thread(rtt,"新线程2").start(); } } } } |
- 通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
package com.thread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask;
public class CallableThreadTest implements Callable<Integer>{ public static void main(String[] args) { CallableThreadTest ctt = new CallableThreadTest(); FutureTask<Integer> ft = new FutureTask<>(ctt); for(int i = 0;i < 100;i++){ System.out.println(Thread.currentThread().getName()+" 的变量i的值"+i); if(i==20) { new Thread(ft,"有返回值的线程").start(); } } try{ System.out.println("子线程的返回值:"+ft.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }
@Override public Integer call() throws Exception{ int i = 0; for(;i<100;i++){ System.out.println(Thread.currentThread().getName()+" "+i); } return i; } } |
Java中提供的线程池
Executors类提供了4种不同的线程池:newCachedThreadPool, newFixedThreadPool, newSingleThreadExecutor, newScheduledThreadPool
- newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
- newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
- newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。容易OOM。
- newScheduledThreadPool:适用于执行延时或者周期性任务。
自定义线程池(ThreadPoolExector)
new ThreadPoolExecutor( 2, 9, 1L, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy()); |
- 七大参数:
- corePoolSize(常驻核心线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
- maximumPoolSize(线程池最大线程数):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
- keepAliveTime(线程存活保持时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
- unit(时间单位)
- workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。
- threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
- handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。
- 四种拒绝策略:
- ThreadPoolExecutor.AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
- 配置线程池
- CPU密集型任务:尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
- IO密集型任务:可以使用稍大的线程池,一般为2*CPU核心数。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。CPU核心线程数/1-阻塞系数(0.8~0.9)
- 混合型任务:可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
线程池的状态
- RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。
- SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。
- STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
- TIDYING(整理状态):所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。
- TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
线程池中shutdown()和shutdownNow()方法的区别
shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断。而shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回。
线程池中 submit() 和 execute() 方法的区别
- execute():只能执行 Runnable 类型的任务。
- submit():可以执行 Runnable 和 Callable 类型的任务。
- submit()能获取返回值(异步)以及处理Exception。
java线程池与tomcat线程池策略算法上的异同
- java线程池
- 如果当前运行的线程,少于corePoolSize,则创建一个新的线程来执行任务。
- 如果运行的线程等于或多于 corePoolSize,将任务加入 BlockingQueue。
- 如果 BlockingQueue 内的任务超过上限,则创建新的线程来处理任务。
- 如果创建的线程超出 maximumPoolSize,任务将被拒绝策略拒绝。
- tomcat线程池
- 如果当前运行的线程,少于corePoolSize,则创建一个新的线程来执行任务。
- 如果线程数大于 corePoolSize了,Tomcat 的线程不会直接把线程加入到无界的阻塞队列中,而是去判断submitedCount(已经提交线程数)是否等于 maximumPoolSize。
- 如果等于,表示线程池已经满负荷运行,不能再创建线程了,直接把线程提交到队列,
- 如果不等于,则需要判断,是否有空闲线程可以消费。
- 如果有空闲线程则加入到阻塞队列中,等待空闲线程消费。
- 如果没有空闲线程,尝试创建新的线程。(这一步保证了使用无界队列,仍然可以利用线程的 maximumPoolSize)。
- 如果总线程数达到 maximumPoolSize,则继续尝试把线程加入 BlockingQueue 中。
- 如果 BlockingQueue 达到上限(假如设置了上限),被默认线程池启动拒绝策略,tomcat 线程池会 catch 住拒绝策略抛出的异常,再次把尝试任务加入中 BlockingQueue 中。
- 再次加入失败,启动拒绝策略。
- 源码分析
Tomcat源码中,为扩展线程池,主要修改了:
- 自定义ThreadPoolExecutor,直接继承JDK的ThreadPoolExecutor,重写部分逻辑,线程池核心方法execute(),Tomcat简单做了修改,还是将工作任务交给父类,也就是Java原生线程池处理,但增加了一个重试策略。如果原生线程池执行拒绝策略的情况,抛出 RejectedExecutionException 异常。这里将会捕获,然后重新再次尝试将任务加入到 TaskQueue ,尽最大可能执行任务。
public void execute(Runnable command, long timeout, TimeUnit unit) { // 它是一个 AtomicInteger 变量,将会实时统计已经提交到线程池中,但还没有执行结束的任务。也就是说 submittedCount 等于线程池队列中的任务数加上线程池工作线程正在执行的任务。 this.submittedCount.incrementAndGet();
try { super.execute(command); } catch (RejectedExecutionException var9) { if (!(super.getQueue() instanceof TaskQueue)) { this.submittedCount.decrementAndGet(); throw var9; }
TaskQueue queue = (TaskQueue)super.getQueue();
try { //拒绝后,再尝试入队,还不行则抛出异常 if (!queue.force(command, timeout, unit)) { this.submittedCount.decrementAndGet(); throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull")); } } catch (InterruptedException var8) { this.submittedCount.decrementAndGet(); throw new RejectedExecutionException(var8); } } } |
- 实现TaskQueue,直接继承LinkedBlockingQueue ,重写 offer 方法。
public class TaskQueue extends LinkedBlockingQueue<Runnable> { ...... //tomcat-util-10.0.0-M6.jar public boolean offer(Runnable o) { if (this.parent == null) { //1.若没有给出tomcat线程池对象,则调用父类方法 return super.offer(o); } else if (this.parent.getPoolSize() == this.parent.getMaximumPoolSize()) { //2.若当前线程数已达到最大线程数,则放入阻塞队列 return super.offer(o); } else if (this.parent.getSubmittedCount() <= this.parent.getPoolSize()) { //3.若当前已提交任务数量小于等于最大线程数,说明此时有空闲线程。此时将任务放入队列中,立刻会有空闲线程来处理该任务 return super.offer(o); } else { //4.若当前线程数小于最大线程数,返回false,此时线程池将会创建新线程!!! return this.parent.getPoolSize() < this.parent.getMaximumPoolSize() ? false : super.offer(o); } } } |
ThreadLocal
https://segmentfault.com/a/1190000037728236?utm_source=tag-newest
ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
- ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是:
- Synchronized是通过线程等待,牺牲时间来解决访问冲突
- ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
- 应用场景
- 数据库连接池的实现:获取connection
- 提升性能和安全,如SimpleDateFormat
- 底层实现
- ThreadLocal仅仅是个变量访问的入口;
- 每一个Thread对象都有一个ThreadLocalMap对象,这个ThreadLocalMap持有对象的引用;
- ThreadLocalMap以当前的threadLocal对象为key,以真正的存储对象为value。get()方法时通过threadLocal实例就可以找到绑定在当前线程上的副本对象。
- 每个线程都有一个属于自己的ThreadLocalMap类,用于关联多个以ThreadLocal对象为key,以要存储的数据为value的Entry对象。线程内部无法进行GC,其内部存储实体结构Entry<ThreadLocal, T>继承自java.lan.ref.WeakReference,且该Entry对象的key是一个弱引用对象,当jvm发现内存不足时,会自动回收弱引用指向的实例内存,只回收ThreadLocal对象,而非整个Entry,所以线程变量中的T对象还是在内存中存在的,所以内存泄漏的问题还没有完全解决。在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
- 复用线程Threadlocal变量的解决方法:
- 保证每次都用新的值覆盖线程变量;
- 保证在每个请求结束后清空线程变量。
- 使用ThreadLocal时遵守以下两个原则:
①ThreadLocal申明为private static final。
Private与final 尽可能不让他人修改变更引用,Static 表示为类属性,只有在程序结束才会被回收。
②ThreadLocal使用后务必调用remove方法。
最简单有效的方法是使用后将其移除。
Synchornized底层实现
在理解锁实现原理之前,先了解一下Java的对象头和Monitor,在JVM中,对象是分成三部分存在的:对象头、实例数据、对齐填充。
- 实例数据存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐;
- 对其填充不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍,对齐填充仅仅是为了使字节对齐。
- 对象头是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word 和 Class Metadata Address组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address是类型指针指向对象的类的元数据,JVM通过该指针确定该对象是哪个类的实例。
锁的类型和状态(四种)在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。每一个锁都对应一个monitor对象,每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。
监视器(Monitor)内部如何线程同步?
监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。
synchronized可重入的实现
每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
锁的优化
- 锁升级:锁的4种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高),锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。目的是为了提高获得锁和释放锁的效率。
- 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
- 锁消除:Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间
Synchronized和Lock
- 实现层面不一样。synchronized 是Java关键字,JVM层面实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁
- 是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要再 finally{}代码块显式地中释放锁
- 是否一直等待。synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时
- 获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功
- 功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可中断、可公平和不公平、细分读写锁提高效率
//创建一个公平锁,构造传参true Lock lock = new ReentrantLock(true); |
- 锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。ReenTrantLock提供了一个Condition类,用来实现分组唤醒需要唤醒的线程,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
Volatile
- 使用volatile必须具备以下2个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
- 实现方式
- 当被volatile关键字修饰的资源有变化的时候,计算机会把CPU中的缓存资源重新刷新一遍,达到变量可见性一致的效果。
- 当前计算机基本为多核多线程,在CPU中有一个缓存一致性的协议,由于这个协议使得CPU缓存资源刷新,最终达到变量可见性一致的效果。
- JVM对其禁止指令重排序在硬件层面的实现就是通过在volatile修饰的变量前后插入内存屏障。
- lock:解锁时,jvm会强制刷新cpu缓存,导致当前线程更改,对其他线程可见。
- volatile:标记volatile的字段,在写操作时,会强制刷新cpu缓存,标记volatile的字段,每次读取都是直接读内存。
- final:即时编译器在final写操作后,会插入内存屏障,来禁止重排序,保证可见性
- 使用场景
- 状态标记量
- Double check
Synchronized和Volatile
- synchronized 可以作用于变量、方法、对象,volatile 只能作用于变量。
- synchronized 可以保证线程间的有序性(无法保证线程内的有序性,线程内的代码可能被 CPU 指令重排序)、原子性和可见性,volatile 只保证可见性和有序性,无法保证原子性。
- synchronized 线程阻塞,volatile 线程不阻塞。
- volatile 本质是告诉jvm当前变量在寄存器中的值是不安全的需要从内存中读取;sychronized 则是锁定当前变量,只有当前线程可以访问到,请求该变量的其他线程被阻塞。
- volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
copyonwrite
- 实现就是写时复制,在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,然后用现在的数组去替换成员变量的数组(就是get等读取操作读取的数组)。这个机制和读写锁是一样的,但是比读写锁有改进的地方,那就是读取的时候可以写入的 ,这样省去了读写之间的竞争。同时写入的时候怎么办呢,当然果断还是加锁。
- 适用场景:copyonwrite的机制虽然是线程安全的,但是在add操作的时候不停地拷贝是一件很费时的操作,所以使用到这个集合的时候尽量不要出现频繁的添加操作,而且在迭代的时候数据更新也是不及时的,数据太多的时候,实时性可能就差距很大。在多读取,少添加的时候,效果还是不错的。
Java的锁
- 公平锁/非公平锁
公平锁:线程申请锁的顺序来获取锁;非公平锁:允许加塞,有可能会造成优先级反转或者饥饿现象。
- 优先级反转:是指一个低优先级的任务持有一个被高优先级任务所需要的共享资源。高优先任务由于因资源缺乏而处于受阻状态,一直等到低优先级任务释放资源为止。而低优先级获得的CPU时间少,如果此时有优先级处于两者之间的任务,并且不需要那个共享资源,则该中优先级的任务反而超过这两个任务而获得CPU时间。如果高优先级等待资源时不是阻塞等待,而是忙循环,则可能永远无法获得资源,因为此时低优先级进程无法与高优先级进程争夺CPU时间,从而无法执行,进而无法释放资源,造成的后果就是高优先级任务无法获得资源而继续推进。
- 解决方案:
(1)设置优先级上限,给临界区一个高优先级,进入临界区的进程都将获得这个高优先级,如果其他试图进入临界区的进程的优先级都低于这个高优先级,那么优先级反转就不会发生。
(2)优先级继承,当一个高优先级进程等待一个低优先级进程持有的资源时,低优先级进程将暂时获得高优先级进程的优先级别,在释放共享资源后,低优先级进程回到原来的优先级别。嵌入式系统VxWorks就是采用这种策略。
(3)使用中断禁止,通过禁止中断来保护临界区,采用此种策略的系统只有两种优先级:可抢占优先级和中断禁止优先级。前者为一般进程运行时的优先级,后者为运行于临界区的优先级。
- 线程饥饿:是指如果事务T1封锁了数据R,事务T2又请求封锁R,于是T2等待。T3也请求封锁R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求......T2可能永远等待,这就是饥饿。
- 独占锁/共享锁
独占锁:写锁,ReentrantLock;共享锁:读锁,ReadWriteLock
- 互斥锁/读写锁
独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
- 可重入锁(递归锁)
是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。ReentrantLock、Synchronized
- 自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。如CAS
- 乐观锁/悲观锁
- 分段锁
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
- 偏向锁/轻量级锁/重量级锁
非公平锁和公平锁在reetrantlock里的实现
对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁还需要判断当前节点是否有前驱节点,如果有,则表示有线程比当前线程更早请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
死锁
- 定义:两个线程或两个以上线程因争夺资源而出现线程互相等待的现象。
- 原因:
- 循环等待条件:若干资源形成一种头尾相接的循环等待资源关系。
- 互斥条件:一个资源一次只能被一个进程访问。
- 请求保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。
- 不剥夺条件:进程已经获得的资源,在未使用完之前不能强行剥夺,而只能由该资源的占有者进程自行释放。
- 解决方法:
- 银行家算法,操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程本次申请的资源数是否超过了该资源所剩余的总量。若超过则拒绝分配资源,若能满足则按当前的申请量分配资源,否则也要推迟分配。
- 指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。
- 问题:CPU达到100%
- 排查:(Linux)
- top -c查看CPU资源使用情况,定位进程
- ps –mp 进程号 –o THREAD,tid,time定位线程
- printf "%x\n" 12785将线程ID转为16进制格式(英文小写)
- jstack 进程号 | grep 线程ID –A60定位具体出错代码行
ReentrantLock(可重入锁)和Synchornized区别
- ReentrantLock主要利用CAS+AQS来实现。ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁,如果此时已经有线程占据了锁,那就加入CLH同步队列并且被挂起。当锁被释放之后,排在CLH同步队列队首的线程会被唤醒,然后CAS再次尝试获取锁。
- 相同点:都可以做到同一线程,同一把锁,可重入代码块。
- 不同点
- Synchornized为非公平锁。ReentrantLock可以实现公平锁和非公平锁,默认非公平锁。可重入锁又称递归锁,线程可以进入任何一个已经拥有的锁同步着的代码块,可以避免死锁。
- synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
- Synchornized竞争锁时会一直等待;reentrantLock可以尝试获取锁,并得到获取结果或者超时
- synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放
- synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
JUC框架图
CAS(compare and swap)
- 定义:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。
- 缺点:
- 循环时间长,循环消耗CPU
- 只能保证一个共享变量的原子操作
- ABA问题:(狸猫换太子)A->B->A,解决方法:时间戳原子引用(版本控制)AutomicStampedReference类
- Java8优化:
由于采用这种CAS机制是没有对方法进行加锁的,所以,所有的线程都可以进入increment()这个方法,假如进入这个方法的线程太多,就会出现一个问题:每次有线程要执行第三个步骤的时候,i的值老是被修改了,所以线程又到回到第一步继续重头再来。而这就会导致一个问题:由于线程太密集了,太多人想要修改i的值了,进而大部分人都会修改不成功,白白着在那里循环消耗资源。
为了解决这个问题,Java8引入了一个cell[]数组,它的工作机制是这样的:假如有5个线程要对i进行自增操作,由于5个线程不是很多,起冲突的几率较小,那就让他们按照以往正常的那样,采用CAS自增。但是,如果有100个线程要对i进行自增操作,冲突就会大大增加,系统就会把这些线程分配到不同的cell数组元素去,假如cell[10]有10个元素,且元素的初始化值为0,那么系统就会把100个线程分成10组,每一组对cell数组其中的一个元素做自增操作,这样到最后,cell数组10个元素的值都为10,系统再把这10个元素的值进行汇总,进而得到100,就等价于100个线程对i进行了100次自增操作。
Lock实现类
所有的实现类:AQS(AbstractQueuedSynchronizer)、ReentrantLock、ReadWriteLock、CountDownLatch、Semphore。
- Semaphore类是一个计数信号量,必须由获取它的线程释放,通常用于限制可以访问某些资源(物理或逻辑的)线程数目。也是采用的AQS的state技术,通过state来控制线程数量,state表示线程操作的数量。
- ReadWriteLock类
AQS(AbstractQueuedSynchronizer)
- AQS的本质上是一个同步器/阻塞锁的基础框架,其作用主要是提供加锁、释放锁,并在内部维护一个FIFO等待队列,用于存储由于锁竞争而阻塞的线程。
- 定义:是一个用于构建锁和同步容器的框架。它能降低构建锁和同步器的工作量,还可以避免处理多个位置上发生的竞争问题。在基于AQS构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量。AQS支持独占锁(exclusive)和共享锁(share)两种模式。无论是独占锁还是共享锁,本质上都是对AQS内部的一个变量state的获取。state是一个原子的int变量,用来表示锁状态、资源数等。
- 独占锁:只能被一个线程获取到(Reentrantlock)
- 共享锁:可以被多个线程同时获取(CountDownLatch,ReadWriteLock)
- AQS内部的数据结构与原理
AQS内部实现了两个队列,一个同步队列,一个条件队列。
同步队列的作用是:当线程获取资源失败之后,就进入同步队列的尾部保持自旋等待,不断判断自己是否是链表的头节点,如果是头节点,就不断尝试获取资源,获取成功后则退出同步队列。
条件队列是为Lock实现的一个基础同步器,并且一个线程可能会有多个条件队列,只有在使用了Condition(控制哪些线程会被唤醒)才会存在条件队列。是需要与Lock配合使用的,提供多个等待集合,更精确的控制(底层是park/unpark机制)
- AQS同步队列中的节点状态
- 自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源,只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
private transient volatile Node head; //等待队列的头 private transient volatile Node tail; //等待队列的尾 private volatile int state; //原子性的锁状态位,ReentrantLock对该字段的调用是通过原子操作compareAndSetState进行的 protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); } |
cyclicbarrier和countdownlatch的区别
- CountDownLatch和CyclicBarrier都能够实现线程之间的等待
- CountDownLatch一般用于某个线程A或多个线程,等待若干个其他线程执行完任务之后,它才执行;CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量;调用await()方法的线程会被阻塞,直到计数器减到 0 的时候,才能继续往下执行;调用了await()进行阻塞等待的线程,它们阻塞在Latch门闩/栅栏上;只有当条件满足的时候(countDown() N次,将计数减为0),它们才能同时通过这个栅栏;以此能够实现,让所有的线程站在一个起跑线上。
- CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
- CountDownLatch是减计数,计数减为0后不能重用;而CyclicBarrier是加计数,可置0后复用。
五种IO模型
网络模型可以分为阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO。网络IO操作(read/write系统调用)其实分成了两个步骤:第一:发起IO请求 ;第二:实际的IO读写(内核态与用户态的数据拷贝)。
阻塞与非阻塞IO的区别在于第一步,发起IO请求的进程是否会被阻塞,如果阻塞直到IO操作完成才返回那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO;
同步IO和异步IO的区别在于第二步,实际的IO读写(内核态与用户态的数据拷贝)是否需要进程参与,如果需要进程参与则是同步IO,如果不需要进程参与就是异步IO;如果实际的IO读写需要请求进程参与,那么就是同步IO。因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO
- BIO:同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
- NIO:NIO是一种同步非阻塞的I/O模型,NIO中的所有IO都是从 Channel(通道)开始的。
- 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
- 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。
- 复用IO:基本思路就是通过slect或poll、epoll 来监控多fd(文件描述符),来达到不必为每个fd创建一个对应的监控线程,从而减少线程资源创建的目的。
- 信号驱动IO:信号驱动IO不是用循环请求询问的方式去监控数据就绪状态,而是在调用sigaction时候建立一个SIGIO的信号联系,当内核数据准备好之后再通过SIGIO信号通知线程数据准备好后的可读状态,当线程收到可读状态的信号后,此时再向内核发起recvfrom读取数据的请求,因为信号驱动IO的模型下应用线程在发出信号监控后即可返回,不会阻塞,所以这样的方式下,一个应用线程也可以同时监控多个fd。
- AIO:异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统(内核)会通知相应的线程进行后续的操作。
- 总结:对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
https://blog.csdn.net/weixin_43809223/article/details/100529810
系统如何提高并发性?
- 提高CPU并发计算能力
- 多进程&多线程
- 减少进程切换,使用线程,考虑进程绑定CPU
- 减少使用不必要的锁,考虑无锁编程
- 考虑进程优先级
- 关注系统负载
- 改进I/O模型
- DMA技术
- 异步I/O
- 改进多路I/O就绪通知策略,epoll
- Sendfile
- 内存映射
- 直接I/O