1、 volatile 关键字
volatile是一个类型修饰符的指令关键字。
程序运行时,各个线程会将变量从主存拷贝到工作内存中,这就导致了工作内存中修改的变量值并不能实时和 主存中的值保持一致,当多线程同时操作时就会出现脏读现象。volatile避免了这一现象。在对volatile修饰的变量进行写操作是,会添加lock指令,当前运行内存修改后会实时写回系统内存。synchronize和final 也可以保证内存可见性的
volatile另一个功能就是禁止编译优化对代码的指令重排。volatile修饰的变量,在编译后会使用插入内存屏障的方法禁止编译器指令重排
volatile能保证内存可见性和有序性,但是不能保存原子性
内存屏障保证禁止指令重排
字节码层面会加一个ACC_VOLATILE
到JVM层面就是内存屏障 (JVM添加) : loadLoad (两个load之间),StoreStore, LoadStore,StoreLoad
2、synchronize
synchronize 是JVM层面的同步。通过进入、退出对象监视器(monitor)来实现的,在编译之后在同步方法调用前加入monitor.enter指令,在退出或异常时插入class字节码层面 使用 的是 monitor.exit 指令来保持同步。
synchronize 修饰的地方不同锁住的对的对象也不同,
同步方法锁住的是当前对象、同步静态方法锁住的是当前Class对象,
synchronize(this) 和 synchronize(xxx.class)分别锁住的是实例对象和类
synchronize 底层实现,对于方法块中使用的是monitor.enter对象监视器。
节码层面 在方法中使用过的是ACC_SYNCHRONIZE 标识。
在对象中锁状态存储在对象头 mark work里,
1.6对synchronize进行了优化
锁获取和升级过程
线程A进来以后,锁对象是无锁状态,当前线程通过CAS操作获取偏量锁,如果没有其他线程竞争锁,A会一直持有这个偏向锁。如果这个时候有其他线程B进行竞争,如果A没有在使用锁,那么B获取该对象的偏量锁。所以如果一所总是被多个线程访问,偏量锁就是多余的。当前线程A还在执行,就会将偏向锁升级为轻量级锁,这个时候刚进来的线程B会进行CAS自旋获取轻量级锁,如果这是线程A还在使用这个锁,那么改锁膨胀变为重量级锁。
https://blog.csdn.net/baidu_38083619/article/details/82527461 锁升级过程图片展示
3、AQS&ReentrantReadWriteLock&ReentrantLock
AQS是JUC包实现的核心,一些并发类都是基于AQS实现的。
AQS底层实现:
volatile int 类型 state + 队列实现
其中两个方法acquire() 和 release() 方法实现获取和释放锁资源。
两个方法中都掉用了tryAcquire() ,tryRelease() 方法判断能否获取或者释放。这两个方法在AQS里是直接抛出异常,需要在子类里实现自己对state的值的判断逻辑来判断是否能获取或者释放锁资源。获取不到锁时会将当前线程使用CAS+自旋的方式放入到队列的尾部等待。
AQS可实现两种锁:
共享锁 (ReentrantReadWriteLock)
独占锁(ReentrantLock)
https://www.jianshu.com/p/da9d051dcc3d
ReentrantLock是基于AQS实现的一种可重入独占锁。可以通过参数控制公平和非公平(公平锁代表的基于排队模式获取锁,将线程加入到队列中然后循序唤醒执行;非公平锁指的是当线程进来时可以尝试获取锁,没有获取到之后才会加入到队列中)
ReentrantReadWriteLock 是基于AQS实现的一种读写分离的共享锁。读读不互斥,读写、写写互斥。可通过参数实现公平性。锁降级不可升级
4、线程状态
1、New(新建状态)
2、Runable(运行状态,包括真正的运行和就绪状态等待被CPU调用)
3、Waiting(等待状态,无限期等待只有被其他线程唤醒)
4、Timed waiting(有期限的等待,可以自己唤醒)
5、Blocked(阻塞状态)
6、Terminated(结束状态,线程终止)
new状态通过start方法让线程进入runable状态,系统调度线程进入running状态,这时可通过thread.yeild()方法让线程放弃CPU执行但是并不会释放锁资源进入Runable状态。或者当Running时竞争互斥锁时失败时进入Blocking状态,Bolcking状态获取到锁资源时刻恢复到Runable状态。Running状态可通过Thread.sleep(Long)、Object.wait(Long),Thread.join(Long),LockSupport.parkNanos()、LockSupport.parkUntil() 进入 Timed Waiting 状态 或者是 不带参数的wait()、join()、LockSupport.park()方法进入waiting状态。进入waiting状态之后(期限或者无期限)可通过Object.notify()、notifyAll()、LockSupport/unpark()方法唤醒到Runable。
5、线程池
newThread 会有很多弊端,多线程运行时,多次new Thread效率会比较差。我们要维持一个长时间维持的Thread避免开销,并且限定最大线程数。
线程池创建使用ThreadPoolExecutor
corePoolSize, 核心线程数
maximumPoolSize ,最大线程数, 当核心线程满且阻塞队列也满时才会判断当前线程数是否小于最大线程数,并决定是否创建新线程。
keepAliveTime, 核心线程数以上的线程存活时间
unit, 存活时间单位
workQueue, 阻塞队列,无界队列(LinkedBlockingQueue)、有界队列(遵循FIFO原则的队列如ArrayBlockingQueue,另一类是优先级队列如PriorityBlockingQueue) 、同步移交队列(SynchronousQueue)
threadFactory, 线程创建工程
handler,当阻塞队列和最大线程满了之后的拒绝策略。1、抛异常 2、丢弃最老的任务 3、返回给调用线程 4、直接丢弃
无界队列
队列大小无限制,常用的为无界的LinkedBlockingQueue,使用该队列做为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM。阅读代码发现,Executors.newFixedThreadPool 采用就是 LinkedBlockingQueue,当QPS很高,发送数据很大,大量的任务被添加到这个无界PriorityBlockingQueue 中的优先级由任务的Comparator决定。 中,导致cpu和内存飙升服务器挂掉。
有界队列
常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue,另一类是优先级队列如LinkedBlockingQueue。中的优先级由任务的Comparator决定。
使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。
虽然会有部分任务被丢失,但是我们线上是排序日志搜集任务,所以对部分对丢失是可以容忍的。
同步移交队列
如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列
CountDownLatch和CyclicBarrier区别:
1.countDownLatch是一个计数器,线程完成一个记录一个,计数器递减,只能只用一次
2.CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供reset功能,可以多次使用