目录
synchronized
class TestSynchronized{
|
类别 | Synchronized | Lock |
存在形式 | JAVA关键字,JVM | 是一个类 |
锁的释放 |
| 在finally中必须释放锁,不然会造成死锁 |
获取锁 | 如果A线程获取了锁,那么B线程等待。如果A线程阻塞,那么B线程会一直等待 | Lock有多种锁的获取方式,都可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可判断 |
锁类型 | 悲观锁(可重入 不可中断 非公平) | 乐观锁(可重入 可判断 可公平(两者皆可)) |
性能 | 少量同步 | 大量同步 |
| 偏向锁-》轻量级锁-》重量级锁(1.6后) | 互斥锁ReentrantLock |
Volatile
volatile:用来修饰被不同线程访问和修改的变量
volatile关键字与JVM内存模型相关
Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引入了同步块和volatile关键字机制
volatile关键字机制:
synchronized修饰方法和代码块,以实现同步
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的值。volatile经常被误用进行原子性的操作。但是这些操作并不是真正的原子性。在读取加载之后,如果变量发生了变化,线程工作内存中的值由于已加载,不会产生对应的变法。对于volatile修改的变量,JVM只是保证从内存加载到线程工作内存的值是最新的。
交互图:
public class VolatileTest { |
信号量Semaphore
在多线程中,线程间传递信号的一种方式。
与互斥量的区别
- 互斥量用于线程的互斥,信号量用于线程的同步
- 互斥量值只能为0/1,信号量值可以为非负整数。信号量可以实现多个同类资源的多线程互斥和同步。
- 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
class TestSemaphore{ |
实现所有线程在等待某个事件的发生才去执行
1.闭锁CountDownLatch
闭锁是典型的等待事件发生的同步工具类,将闭锁的初始值设置1,所有线程调用await方法等待,当事件发生时调用countDown将闭锁值减为0,则所有await等待闭锁的线程得以继续执行。
class TestCountDownLatch{ |
2.阻塞队列BlockingQueue
所有等待事件的线程尝试从空的阻塞队列获取元素,将阻塞,当事件发生时,向阻塞队列中同时放入N个元素(N的值与等待的线程数相同),则所有等待的线程从阻塞队列中取出元素后得以继续执行。
3.信号量Semaphore
设置信号量的初始值为等待的线程数N,一开始将信号量申请完,让剩余的信号量为0,待事件发生时,同时释放N个占用的信号量,则等待信号量的所有线程将获取信号量得以继续执行。
class TestSemaphore{ |
4.栅栏CyclicBarrier
设置栅栏的初始值为1,当事件发生时,调用barrier.wait()冲破设置的栅栏,将调用指定的Runable线程执行,在该线程中启动N个新的子线程执行。这个方法并不是让执行中的线程全部等待在某个点,待某一事件发生后继续执行。
class TestCyclicBarrier{ |
CAS缺陷以及如何解决
参考地址:https://blog.youkuaiyun.com/jeffleo/article/details/75269618
CAS:Compare And Swap,即比较并交换。(AtomicInteger)
实现了Java多线程的并发操作。整个AQS同步组件、Atomic原子类操作等都是以CAS实现的,甚至ConcurrentHashMap在1.8的版本中也调整为了CAS+Synchronized。可以说CAS是整个JUC的基石。
缺陷:
主要表现在三个方法:
循环时间太长
只能保证一个共享变量原子操作
ABA问题:解决方案则是版本号
AQS
参考资料:https://www.cnblogs.com/daydaynobug/p/6752837.html
AQS:AbstractQueuedSynchronized, 抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch
AQS里的state只有两种状态:0表示未锁定,1表示锁定
死锁、活锁
死锁:指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。4个产生条件:
- 互斥条件
- 请求和保持条件
- 不剥夺条件
- 环路等待条件
活锁:指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开。
线程池
资源地址:https://blog.youkuaiyun.com/wxq544483342/article/details/53118674
线程池的种类
1、创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE
ExecutorService pool = Executors.newCachedThreadPool();
2、创建容量为1的缓冲池
ExecutorService pool = Executors.newSingleThreadExecutor();
3、创建固定容量大小的缓冲池
ExecutorService pool = Executors.newFixedThreadPool(int);
4、创建一个定长线程池,支持定时及周期性任务执行。
ExecutorService pool = Executors.newScheduledThreadPool(1);
5、创建一个拥有多个任务队列(以便减少连接数)的线程池。
ExecutorService pool = Executors.newWorkStealingPool();
ExecutorService类的方法:
void shutdown(); 关闭线程池 不可以再 submit 新的 task,已经 submit 的将继续执行 List<Runnable> shutdownNow(); 关闭线程池 试图停止当前正在执行的 task,并返回尚未执行的 task 的 list boolean isShutdown(); 是否已经关闭了线程池,当调用shutdown()或shutdownNow()方法后返回为true。 boolean isTerminated(); 当调用shutdown()方法后,并且所有提交的任务完成后返回为true; 当调用shutdownNow()方法后,成功停止后返回为true; boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; <T> Future<T> submit(Callable<T> task); <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task); <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException; <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException; <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException; <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; |
配置线程池大小
- CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
- IO密集型任务,参考值可以设置为2*NCPU
工作机制及原理
线程池的两个核心队列:
- 线程等待池,即线程队列BlockingQueue
- 任务处理池(PoolWorker),即正在工作的Thread列表(HashSet)
线程池的核心参数:
- 核心池大小(corePoolSize),即固定大小,设定好之后,线程池的稳定峰值,达到这个值之后池的线程数大小不会释放。
- 最大处理线程池数(maximumPoolSize),当线程池里面的线程数超过corePoolSize,小于maximumPoolSize时会动态创建与回收线池里面的线程池资源。
ThreadPoolExecutor类
构造方法:
/** * * @param corePoolSize 核心池大小 默认情况下,在创建好线程池之后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务, * 当线程池中的线程数量达到corePoolSize后,就会把这些任务放到缓存队列中 * @param maximumPoolSize 线程池最大线程数量,表示在线程池中最多能创建线程的数量;在corePoolSize和maximumPoolSize的线程数会被自动释放,而小于corePoolSize的则不会。 * @param keepAliveTime 表示线程没有执行任务时最多保持多久时间会终止。 * 默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会生效,直到线程池数量不大于corePoolSize, * 即只有当线程池数量大于corePoolSize数量,超出这个数量的线程一旦到达keepAliveTime就会终止。 * 但是如果调用了allowCoreThreadTimeout(boolean)方法,即使线程池的线程数量不大于corePoolSize,线程也会在keepAliveTime之后就终止,知道线程池的数量为0为止。 * @param unit 参数keepAliveTime的时间单位,一个时间单位枚举类。 Nanos/Micros/Millis/Seconds/Minutes/Hours/Days * @param workQueue 一个阻塞队列,用来存储等待执行任务的队列,这个参数选择也很重要,会对线程池的运行过程产生重大影响。 * 一般来说,这里的阻塞队列就是(ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue)。 * @param threadFactory 线程工厂,主要用来创建线程;可以是一个自定义的线程工厂,默认就是Executors.defaultThreadFactory()。用来在线程池中创建线程。 * @param handler 表示当拒绝处理任务时的策略,也是可以自定义的,默认是我们前面的4种取值: * ThreadPoolExecutor.AbortPolicy(默认的,一言不合即抛异常的) * ThreadPoolExecutor.DiscardPolicy(一言不合就丢弃任务) * ThreadPoolExecutor.DiscardOldestPolicy(一言不合就把最近的任务给抛弃,然后执行当前任务) * ThreadPoolExecutor.CallerRunsPolicy(由调用者所在线程来执行任务) */ public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue, public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; } |
ThreadLoad原理
class TestThreadLocal{
|
LockSupport工具
LockSupport提供了park和unpark方法实现阻塞和解除阻塞,都是基于“许可(permit)”作为关联。permit相当于一个信号量(0,1),默认是0。
使用偏移量来获取对象。是因为线程已经被阻塞了,如果不通过内存的方式,直接调用线程内的方法,线程是不会响应的。
park()、unpark()和wait()、notify()的区别
- 面向的主体不同。LockSupport是针对Thread进行阻塞,可以指定阻塞的对象,每次可以唤醒指定具体的线程。Wait()是以对象,阻塞当前的线程和唤醒单个线程或者所有线程。
- 实现的机制不同。LockSupport可以指定monitor的object对象,但和object.wait()两者的阻塞队列并不交叉。
static void testLockSupport(){ |
Condition原理
在经典的生产者和消费者模式中,可以使用Object.wait()和Object. Notify()阻塞和唤醒线程,但这样处理只有一个等待队列。在可重入锁ReentrantLock中,使用AQS的condition可以实现设置多个等待队列,可以使用lock.newCondition生成一个等待队列。
static void testCondition() throws InterruptedException { |
Fork/Join框架的理解
并行流:把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。(fork/join框架)
串行流:则相反
Fork/Join框架:
在必要的情况下,将一个大的任务进行拆分(fork)成若干个子任务(拆到临界值),再将一个个任务的结果进行join汇总。
Fork/Join与线程池的区别:
Fork/Join采用“工作窃取模式”,当执行新的任务时可以将其拆分成更小的任务执行,并将小任务加到线程队列中。然后再随机从一个线程中偷一个任务并把他加入到自己的队列中。
例如:CPU上有两个线程A/B,A已经执行完了,B还有任务未执行。这时A将B队尾的任务偷过来,加入到自己的队列中。相对于传统的线程池,Fork/Join更有效的使用了CPU资源。
// ForkJoin这个框架的实现需要继承RecursiveTask 或者 RecursiveAction, |
分段锁(JUC)的原理、锁力度的减小思考
容器中有多把锁,每一把锁用于锁容器中的一部分数据。当多线程时访问容器中的不同数据段的数据时,线程间就不会发生锁竞争了。因而有效的提高了高并发的访问效率。例如ConcurrentHashMap使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也可被其他线程访问。ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所在的散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。合理的使用散列算法来使关键字均匀分布,那么可使对锁的请求减少到原来的1/16。这样可使ConcurrentHashMap的并发达到16个线程。
独占锁:只有一把锁,每次只能一个线程能访问。例如HashTable。采用串行方式,会降低其伸缩性。一般有三种方式降低锁的竞争:
- 减少锁的持有时间
- 降低锁的请求频率
- 使用带有协调机制的独占锁,这些机制允许更高的并发性。
八种阻塞队列以及各个阻塞队列的特性
public class TestQueue { |