整理了一些知识点(不适合什么都不明白的新人)
目录
ReentrantLock锁和Synchronized锁 区别
同步 异步 阻塞 非阻塞
同步 异步 与 阻塞 非阻塞 没有直接关系
同步请求:A调用B,B的处理是同步的,在处理完之前他不会通知A,只有处理完之后才会明确的通知A。
异步请求:A调用B,B的处理是异步的,B在接到请求后先告诉A我已接到请求,然后异步去处理,最后通过回调等方式再通知A。
阻塞请求:A调用B,A一直等着B的返回,别的事情什么也不干。
非阻塞请求:A调用B,A不用一直等着B的返回,先去忙别的事情了。
同步和异步的区别就是被调用方的执行方式和返回时机。同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方。
阻塞和非阻塞的区别就是调用者 阻塞一直等着,非阻塞去做其他的事情。
同步、异步说的是被调用者 阻塞、非阻塞说的是调用者
参考网上 老王的烧水壶的例子
1、同步阻塞:
老张在厨房用普通水壶烧水,一直在厨房等着(阻塞),盯到水烧开(同步);
2、异步阻塞:
老张在厨房用响水壶烧水,一直在厨房中等着(阻塞),直到水壶发出响声(异步),老张知道水烧开了;
3、同步非阻塞(这种状态 基本上没有...):
老张在厨房用普通水壶烧水,在烧水过程中,就到客厅去看电视(非阻塞),然后时不时去厨房看看水烧开了没(轮询);
4、异步非阻塞:
老张在厨房用响水壶烧水,在烧水过程中,就到客厅去看电视(非阻塞),当水壶发出响声(异步),老张就知道水烧开了;
Java中的 IO操作 分为:BIO(同步阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞) 效率由低→高
线程的生命周期
1、创建 2、就绪 3、执行 4、阻塞 5、销毁
创建线程的3种方式
- 继承自 Thread 类 (不推荐)
- 实现 Runnable 接口
- 实现 Callable 接口
Callable 与 Runnable 有两点不同
- call()获得 返回值。而 Callable 和 Future 则很好地解决了 这个问题;
- call()可以抛出异常。而 Runnable 只有通过 setDefaultUncaughtExceptionHandler()的方式才能在主线程中捕捉到子线程异常。
创建线程的例子:
public class CallableDemo {
public static void main(String[] args) {
//推荐使用线程池
// FutureTask futureTask = new FutureTask(new StudyCall());
// new Thread(futureTask).start(); // 启动线程
// Object o = null;
// try {
// o = futureTask.get();
// } catch (InterruptedException e) {
// e.printStackTrace();
// } catch (ExecutionException e) {
// e.printStackTrace();
// }
// System.out.println("-结果- "+o.toString());
//推荐使用线程池
StudyCall studyCall = new StudyCall();
ThreadPoolExecutor pool = new ThreadPoolExecutor(2,3,2, TimeUnit.MICROSECONDS,new LinkedBlockingDeque<>());
Future future = null;
future = pool.submit(()->{
try {
System.out.println(studyCall.call());
} catch (Exception e) {
e.printStackTrace();
}
});
pool.shutdown();
}
}
class StudyCall implements Callable{
@Override
public Object call() throws Exception {
for (int i = 0; i < 3; i++) {
System.out.println("i:"+i);
}
return "qwe";
}
}
锁
Java 中常用锁实现的方式有两种
- 并发包中的锁类
- 利用同步代码块
并发包中的锁类
并发包的类族中, Lock 是 JUC 包的顶层接口,它的实现逻辑并未用到 synchroniz时,而是利用了 volatile 的可见性。
图为 Lock 的继承类图,ReentrantLock 对于 Lock 接口的实现主要依赖了 Sync,而 Sync 继承了AbstractQueuedSynchronizer ( AQS )
它是 JUC 包实现同步的 基础工具。在 AQS 中 , 定义了一个 volatile int state 变量作为共享资源,如果线程获 取资源失败, 贝lj进入同步 FIFO 队列中等待;如果成功获取资源就执行临界区代码。
执行完释放资源时, 会通知同步队列中的等待线程来获取资源后出队并执行。
AQS 是抽象类,内置自旋锁实现的同步队列,封装入队和出 队的操作,提供独占、共享、中断等特性的方法。AQS 的子类可以定义不同的资源实现不同性质的方法。
比如可重入锁ReentrantLock,定义 state为 0 时可以获取资源并置为 l。若已获得资源,state不断加 l ,在释放资源时 state 减 l 直至为 0 ;
CountDownLatch 初始时 定义了资源总量 state=count, countDown() 不断将 state 减 l 当 state=O 时才能获得锁 ,释放后 state 就一直为 0。
所有结程调用 await() 都不会等待,所以 CountDownLatch 是 次性的,用完后如果再想用就只能重新创建一个,如果希望循环使用 ,推荐使用基于 Reentran tLock 实现的 CyclicBarrier。
Semaphore 与 CountDownLatch 略有不同 , 同样也是定义了资源总量 state=permits,
当 state>O 时就能获得锁, 并将 state 减 l ,当 state=O 时只能等待其他线程释放锁,当释放锁时 state 加 l , 其他等待线程又能获得 这个锁。
当 Semphore 的 permits 定义为 l 时,就是互斥锁,当 permits>I 就是共享锁。
JDK8 提出了一个新的锁 StampedLock, 改进了读写锁 ReentrantReadWriteLock。 这些新增的锁相关类不断丰富了 几JC 包的内容, 降低了并发编程的难度,提高了锁 的性能和安全性。
利用同步代码块
同步代码块一般使用 Java 的 synchronized 关键字来实现,有两种方式对方法进行 加锁操作第 ,在方法签名处加 synchronized 关键字;第二,使用 synchronized(对 象或类)进行同步。这里的原则是锁的范围尽可能小,锁的时间尽可能短,即能锁对象, 就不要锁类,能锁代码块,就不要锁方法。
1、synchronized 修饰方法 ,
2、synchronized 代码块 ---------- 属于对象 1,2 功能一样 只不过 代码块修饰的 比较方便
2、static synchronized 修饰方法 --------属于类
同步锁:当使用synchroinzed锁住一段的代码片段,同步监视器是java中任意的一个对象,只要保证多个线程看到的该对象是"同一个",即可保证同步块中的代码是并发安全的。
互斥锁:当使用synchroinzed锁住多段不同的代码片段,但是这些同步块使用的同步监视器对象是同一个时,那么这些代码片段之间就是互斥的。多个线程不能同时执行他们。
死锁:当多个线程都持有自己的锁,但是都等对方先释放锁时就会出现"僵持"的情况,使得所有线程进入阻塞状态。
ReentrantLock锁和Synchronized锁 区别
比较方面 | SynChronized | ReentrantLock(实现了 Lock接口) |
原始构成 | 它是java语言的关键字,是原生语法层面的互斥,需要jvm实现 | 它是JDK 1.5之后提供的API层面的互斥锁类 |
代码编写 | 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用,更安全, | 而ReentrantLock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要lock()和unlock()方法配合try/finally语句块来完成, |
灵活性 | 锁的范围是整个方法或synchronized块部分 | Lock因为是方法调用,可以跨方法,灵活性更大 |
等待可中断 | 不可中断,除非抛出异常 释放锁方式: 1.代码执行完,正常释放锁; 2.抛出异常,由JVM退出等待 | 持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待 方法: 1.设置超时方法 tryLock(long timeout, TimeUnit unit),时间过了就放弃等待; 2.lockInterruptibly()放代码块中,调用interrupt()方法可中断,而synchronized不行 |
是否公平锁 | 非公平锁 | 两者都可以,默认公平锁,构造器可以传入boolean值,true为公平锁,false为非公平锁, |
条件Condition | 通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能. | |
高级功能 | 提供很多方法用来监听当前锁的信息,如: getHoldCount() getQueueLength() isFair() isHeldByCurrentThread() isLocked() |
线程同步
volatile
volatile是一个变量修饰符,只能用来修饰变量。无法修饰方法及代码块等。
volatile具有可见性、有序性,不具备原子性
volatile作为java中的关键词之一,用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排。
因为所有的操作都需要同步给内存变量, 所以 volatile 一定会使线程的执行速度变慢, 故要审慎定义和使用 volatile 属性。
public class Singleton {
//其中使用volatile关键字修饰可能被多个线程同时访问到的singleton
private volatile static Singleton singleton;
private Singleton (){}
// 使用双重锁校验的形式实现单例
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
线程池
底层实现
ThreadPoolExecutor pool = new ThreadPoolExecutor(7个参数); 推荐用此方式 创建线程池
int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler
- 参数1,corePoolSize:表示常驻核心线程数。如果等于0,则任务执行完成以后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。
- 参数2,maximumPoolSize:表示线程池能够容纳同时执行的最大线程数。从上方示例代码中的第一处来看,必须大于或等于1.如果待执行的线程数大于此值,需要借助第五个参数的帮助,缓存在队列中。如果maximumPoolSize与corePoolSize相等,即是固定大小线程池。
- 参数3,keepAliveTime:表示线程池中线程空闲时间,当空闲时间达到keepAliveTime值时,线程会被销毁,直到只剩下corePoolSize个线程为止,避免浪费内存和句柄资源。在默认情况下,当线程池的线程数大于corePoolSize时,keepAliveTime才会起作用。但是当ThreadPoolExecutor的allowCoreThreadTimeOut变量设置为true时,核心线程超时后也会被回收。
- 参数4,TimeUnit:表示时间单位。KeepAliveTime的时间单位通常是TimeUnit.SECONDS。
- 参数5,workQueue:表示缓存队列。当请求的线程数大于corePoolSize时,线程进入BlockingQueue阻塞队列(请注意,是当corePoolSize不够用时,将任务加入缓存队列,当缓存队列也容纳不下任务时,再开辟新的线程来处理任务,直到线程数到达maximumPoolSize)。后续示例代码中使用的LinkedBlockingQueue是单向链表,使用锁来控制入队和出队的原子性,两个锁分别控制元素的添加和获取,是一个生产消费模型队列。
- 参数6,threadFactory表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给这个factory增加组名前缀来实现的。在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的。
- 参数7,handler表示执行拒绝策略的对象。当第五个参数workQueue的任务缓存区到达上限后,并且活动线程数等于maximumPoolSize的时候,线程池通过该策略处理请求,这是一种简单的限流保护
五种线程池
Executors(Java提供用来创建线程池的类,在JUC包下)的核心方法有五个(这五个方法每个都有多种重载方法,我只选取了一种):
1、Executor.newWorkStealingPool:JDK8引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争,此构造方法中把CPU数量设置为默认的并行度。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
2、Executors.newCachedThreadPool:maximumPoolSize最大可以至Integer.MAX_VALUE,是高度可伸缩的线程池,如果达到这个上限,相信没有任何服务器能够继续工作,肯定会抛出OOM异常。keepAliveTime默认60秒,工作线程处于空闲状态,则回收工作线程。如果任务数增加,再次创建出新线程处理任务。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
3、Executors.newScheduledThreadPool:线程数最大值Integer.MAX_VALUE,与上述相同,存在OOM风险。它是ScheduledExecutorService接口家族的实现类,支持定时及周期性任务执行。相比Timer,ScheduledExecutorService更安全,功能更加强大,与newCachedThreadPool的区别是不回收工作线程。
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
4、Executors.newSingleThreadExecutor:创建一个单线程的线程池,相当于单线程串行执行所有任务,保证按任务的提交顺序依次执行。
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
5、Executors.newFixedThreadPool:输入的参数即是固定线程数,即是核心线程数也是最大线程数,不存在空闲线程,所以keepAliveTime等于0:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//这里输入的队列没有指明长度,看一下LinkedBlockingQueue的构造方法
public LinkedBlockingQueue(){
this(Integer.MAX_VALUE);
}
//使用这样的无界队列,如果瞬间请求量很大的话,会有OOM风险
线程池的四种工作队列
1、ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
3、SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4、PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
线程池的拒绝策略
当请求任务不断的过来,而系统此时又处理不过来的时候,我们需要采取的策略是拒绝服务。RejectedExecutionHandler接口提供了拒绝任务处理的自定义方法的机会。在ThreadPoolExecutor中已经包含四种处理策略。
- AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。
- CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
- DiscardOleddestPolicy策略: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
- DiscardPolicy策略:该策略默默的丢弃无法处理的任务,不予任何处理。
除了JDK默认提供的四种拒绝策略,我们可以根据自己的业务需求去自定义拒绝策略,自定义的方式很简单,直接实现RejectedExecutionHandler接口即可。
执行流程:当线程数小于corePoolSize时,每添加一个任务,则立即开启线程执行;当corePoolSize满的时候,后面添加的任务将放入缓冲队列workQueue等待;当workQueue满的时候,看是否超过maximumPoolSize线程数,如果超过,则拒绝执行,如果没有超过,则创建线程理解执行;
ABC三个线程如何保证顺序执行
- 用Thread.join() 方法,或者线程池newSingleThreadExecutor(原理是会将所有线程放入一个队列,而队列则保证了FIFO)
- 也可以通过ReentrantLock,state整数用阿里判断轮到谁来执行