锁一定要确认明白锁的对象,否则和没加锁一样
JUC 简介
- 在 Java 5.0 提供了
java.util.concurrent
(简称JUC)包,在此包中增加了在并发编程中很常用的工具类,
用于定义类似于线程的自定义子系统,包括线程池,异步 IO 和轻量级任务框架;还提供了设计用于多线程上下文中
的 Collection 实现等;
可重入锁(递归锁):什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
可中断锁:可中断锁就是可以响应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。在前面演示tryLock(long time, TimeUnit unit)和lockInterruptibly()的用法时已经体现了Lock的可中断性。
公平锁:公平锁即 尽量 以请求锁的顺序来获取锁。比如,同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。而非公平锁则无法保证锁的获取是按照请求锁的顺序进行的,这样就可能导致某个或者一些线程永远获取不到锁。
自旋锁:自旋锁是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。
CAS: Compare And Swap比较并交换
如何防止比较时值被替换 汇编代码:Unsafe : lock Cmpxchg(指令),lock保证这cpu在这Cmpxchg条汇编运行时部可被修改
ABA:在本线程读取修改时其他线程修改数次最后结果还是一样原结果一样,可以通过添加版本号来辨别是否中途修改过.
死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁
死锁排查:
jps -l 查找进程号
jstack 进程号 排除死锁问题
synchronzed锁(可重入锁,不可中断锁,非公平锁):排队获取锁进行数据操作
synchronzed对象方法锁:锁的是调用对象,同一个对象调用,先调用的先拿到锁。不同对象的锁互不干预
synchronzed静态(static)方法锁:锁的是全局唯一的Class类模板,先调用先拿锁,与对象无关,因为static是Class
Lock:ReentrantLock(JUC)锁(可重入锁,可判断锁,不可中断锁,非公平锁(可自己控制))
读写锁(读(共享锁),写(排他锁)):ReadWriteLock 写是原则性操作(不可同时写),加锁,读不用考虑。
Conditions(JUC)锁的工具类:JUC中提供锁操作方法的类:
里面有一些方法:awatie()等待;signalAll()唤醒;
Conditions的优势:
lock和synchronzed的区别:
1.synchronzed 内置java关键字
2.synchronzed 无法获取锁状态,Lock可以判断是否获取锁
3.synchronzed会自动释放锁,lock必须手动释放,如果不释放,就会死锁
生产者,消费者问题
八锁现象问题:
什么是虚假唤醒?
举个例子,我们现在有一个生产者-消费者队列和三个线程。
1) 1号线程从队列中获取了一个元素,此时队列变为空。
2) 2号线程也想从队列中获取一个元素,但此时队列为空,2号线程便只能进入阻塞(cond.wait()),等待队列非空。
3) 这时,3号线程将一个元素入队,并调用cond.notify()唤醒条件变量。
4) 处于等待状态的2号线程接收到3号线程的唤醒信号,便准备解除阻塞状态,执行接下来的任务(获取队列中的元素)。
5) 然而可能出现这样的情况:当2号线程准备获得队列的锁,去获取队列中的元素时,此时1号线程刚好执行完之前的元素操作,返回再去请求队列中的元素,1号线程便获得队列的锁,检查到队列非空,就获取到了3号线程刚刚入队的元素,然后释放队列锁。
6) 等到2号线程获得队列锁,判断发现队列仍为空,1号线程“偷走了”这个元素,所以对于2号线程而言,这次唤醒就是“虚假”的,它需要再次等待队列非空。
集合线程安全(Collections集合工具类中有线程安全集合的方法)
Set set=Collections.Collections.synchronizedSet(new HashSet());
List list=Collections.Collections.synchronizedList(new ArrayList());
JUC中的线程安全集合(为什么它是线程安全的,因为它加了Lock锁,比synchronzed锁效率更高,是通过写入复制来写入值的):
List list=new CopyOnwriteArrayList();
Set set=new CopyOnwriteArraySet();
HashMap hashMap= new ConcurrentHashMap();
Callable(没搞懂回去补一下):
Callable和Runnable相差不多,都是线程执行的方法。
Callable无法直接通过Thread直接调用开始线程
JUC常用辅助类:
1.CountDownLatch 倒计数门闸锁(当计数器为0时,才会继续执行)
CountDownLatch.countDown(); 数量减一
CountDownLatch.await(); 等待计数器归0,然后往下执行
使用场景,多个线程操作,每个线程操作完成就减1当线程全部执行完毕计数器为0时关闭线程池等操作
2.CyclicBarrier 可重用栅栏
从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。
它的作用就是会让所有线程都等待完成后才会继续下一步行动。
CyclicBarrier.await; 等待
使用场景:一个线程组的线程需要等待所有线程完成任务后再继续执行下一次任务
3.Semaphore 信号量
在信号量上我们定义两种操作: acquire(获取) 和 release(释放)。当一个线程调用acquire操作时,它要么通过 成 功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。release(释放)实际上会将 信号量 的值加1,然后唤醒等待的线程。
acquire(获取)
release(释放)
使用场景:信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
阻塞队列(Concurrent集合中的BlockingQueue,父类Collection):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6gHnJsvc-1602486458616)(E:\chenpeng\技术\学习知识总结\Typora图片位置\18.png)]
队列特性:
先进先出(FIFO):先插入的队列s的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一 种公平性。
后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。
使用场景:多线程,线程池中使用
方法:1.放入数据
(1)offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法的线程);
(2)offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
(3)put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
(4)add()插入数据如果队列满了,就返回false并且抛出异常
2. 获取数据
(1)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
(2)poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则等待到时间超时还没有数据可取,返回失败。
(3)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
(4)drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
(5) remove()取出最前的数据,如果无数据取出失败,就返回异常
(6) take()取出最前的数据,如果无数据取出失败,就一直等待数据,知道有数据取出(阻塞线程)
图:
方法 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入 | add(e) | offer | put | offer(e,time,unit) |
移除 | remove | poll | take | poll(time,unit) |
检查 | element | peek | 不可用 | 不可用 |
同步队列(SynchronusQueue只有一个容量,数据操作,(加,取,加,取)这样执行,不能加入第二个元素,无元素就进行等待)
线程池(3大方法,7大参数,4大拒绝策略):
(阿里巴巴开发手册)【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
池化技术:
池化技术能够减少资源对象的创建次数,提高程序的性能,特别是在高并发下这种提高更加明显。使用池化技 术缓存的资源对象有如下共同特点:1,对象创建时间长;2,对象创建需要大量资源;3,对象创建后可被重复 使用。
线程池的好处:
01:降低资源的消耗
02:提高响应的速度
03:方便管理
(重点)线程复用、可以控制最大并发数、管理线程
三大方法:
1:Executors.newSingleThreadExecutor() //单个线程
2:Executors.newFixedThreadPool(int nThreads) //创建一个固定的线程池的大小
3:Executors.newCachedThreadPool() //缓存池,可伸缩的, 遇强则强,遇弱则弱
七大参数:
int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂,创建线程的,一般不用动
RejectedExecutionHandler handler //拒绝策略
四种拒绝策略:
- new ThreadPoolExecutor.AbortPolicy() //银行满了,还有人进来,不处理这个人的,抛出异常
- new ThreadPoolExecutor.CallerRunsPolicy() //哪来的去哪里
- new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
- new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试和最早的竞争,也不会抛出异常
问题:线程池这么设置最大参数:(调优问题)
cpu密集型:CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。
在多重程序系统中,大部份时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部份时间用在三角函数和开根号的计算,便是属于CPU bound的程序。
CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。
io密集型:IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。
I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。
四大函数式接口(一个接口中,实现类必须实现抽象方法,有且只有一个这就是函数式接口):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xLLwXIgk-1602486458620)(E:\chenpeng\技术\学习知识总结\Typora图片位置\19.png)]
(重点必须会)Stream流式计算(提供一种对 Java 集合运算和表达的高阶抽象 ,效率飞快,数据处理工具):
ForkJoin(并行执行任务,使用场景,在大数据情况下使用,数据量不大不要使用):
从JDK1.7开始,Java提供ForkJoin框架用于并行执行任务,它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。
1、ForkJoinPool
既然任务是被逐渐的细化的,那就需要把这些任务存在一个池子里面,这个池子就是ForkJoinPool,它与其它的ExecutorService区别主要在于它使用“工作窃取“,那什么是工作窃取呢?:一个大任务会被划分成无数个小任务,这些任务被分配到不同的队列,这些队列有些干活干的块,有些干得慢。于是干得快的,一看自己没任务需要执行了,就去隔壁的队列里面拿去任务执行。
ForkJoinTask就是ForkJoinPool里面的每一个任务。他主要有两个子类:RecursiveAction和RecursiveTask。然后通过fork()方法去分配任务执行任务,通过join()方法汇总任务结果,这就是整个过程的运用。他有两个子类,使用这两个子类都可以实现我们的任务分配和计算。
(1)RecursiveAction 一个递归无结果的ForkJoinTask(没有返回值)
(2)RecursiveTask 一个递归有结果的ForkJoinTask(有返回值)
ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责存放程序提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组负责执行这些任务。
异步回调:
Future 对未来的方法返回结果类型:
实现类:CompletableFuture.runAsync() 无返回值方法
CompletableFuture.supplyAsync() 有返回值的方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PyVoCTv4-1602486458623)(E:\chenpeng\技术\学习知识总结\Typora图片位置\20.png)]
JMM:java内存模型
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1lxaoPcI-1602486458628)(E:\chenpeng\技术\学习知识总结\Typora图片位置\21)]
Java 内存模型-同步八种操作
(1)lock(锁定):作用于 主内存的变量,把一个变量标记为一条线程独占状态
(2)unlock(解锁):作用于 主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
(3)read(读取):作用于 主内存的变量,把一个变量值从主内存传输到线程的 工作内存中,以便随后的load动作使用
(4)load(载入):作用于 工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
(5)use(使用):作用于 工作内存的变量,把工作内存中的一个变量值传递给执行引擎
(6)assign(赋值):作用于 工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
(7)store(存储):作用于 工作内存的变量,把工作内存中的一个变量的值传送到 主内存中,以便随后的write的操作
(8)write(写入):作用于 工作内存的变量,它把store操作从工作内存中的一个变量的值传送到 主内存的变量中
原子类:
在Java并发比编程中,要想保证一些操作不被其他线程干扰,就需要保证原子性,JDK中提供了13个原子操作类来帮助我们进行开发,
原子更新基本类型
使用原子的方式更新基本类型,Atomic包提供了以下3个类。
(1)AtomicBoolean: 原子更新布尔类型。
(2)AtomicInteger: 原子更新整型。
(3)AtomicLong: 原子更新长整型。
以上3个类提供的方法几乎一模一样,以AtomicInteger为例进行详解,AtomicIngeter的常用方法如下:
(1)int addAndGet(int delta): 以原子的方式将输入的数值与实例中的值相加,并返回结果。
(2)boolean compareAndSet(int expect, int update): 如果输入的值等于预期值,则以原子方式将该值设置为输入的值。
(3)int getAndIncrement(): 以原子的方式将当前值加1,注意,这里返回的是自增前的值。
(4)void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
(5)int getAndSet(int newValue): 以原子的方式设置为newValue,并返回旧值。
Atomic包里的类基本都是使用Unsafe下的CAS方法实现
原子更新数组
通过原子的方式更新数组里的某个元素,Atomic包提供了以下的4个类:
(1)AtomicIntegerArray: 原子更新整型数组里的元素。
(2)AtomicLongArray: 原子更新长整型数组里的元素。
(3)AtomicReferenceArray: 原子更新引用类型数组里的元素。
原子更新字段类
Atomic包提供了四个类进行原子字段更新:
(1)AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
(2)AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
(3)AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
(4)AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。
原子更新引用类型
Atomic包提供了以下三个类:
(1)AtomicReference: 原子更新引用类型。
(2)AtomicReferenceFieldUpdater: 原子更新引用类型的字段。
(3)AtomicMarkableReferce: 原子更新带有标记位的引用类型。
单例模式
饿汉式(线程安全,调用效率高,但是不能延时加载):
一上来就把单例对象创建出来了,要用的时候直接返回即可,这种可以说是单例模式中最简单的一种实现方式。但是问题也比较明显。单例在还没有使用到的时候,初始化就已经完成了。也就是说,如果程序从头到位都没用使用这个单例的话,单例的对象还是会创建。这就造成了不必要的资源浪费。所以不推荐这种实现方式
懒汉式(线程安全,调用效率不高,但是能延时加载):
需要使用时在创建对象实例
Double CheckLock实现单例:DCL也就是双重锁判断机制(由于JVM底层模型原因,偶尔会出问题,不建议使用)
静态内部类实现模式(线程安全,调用效率高,可以延时加载)
枚举类(线程安全,调用效率高,不能延时加载,可以天然的防止反射和反序列化调用)
锁
自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
- 自旋锁:自旋,jvm默认是10次吧,有jvm自己控制。for去争取锁
- 自旋锁的其他种类
- 阻塞锁:被阻塞的线程,不会争夺锁。
- 可重入锁: 多次进入改锁的域
- 读写锁
- 互斥锁:锁本身就是互斥的
- 悲观锁:不相信,这里会是安全的,必须全部上锁
- 乐观锁:相信,这里是安全的。
- 公平锁:有优先级的锁
- 非公平锁:无优先级的锁
- 偏向锁:无竞争不锁,有竞争挂起,转为轻量锁
- 对象锁:锁住对象
- 线程锁
- 锁粗化:多锁变成一个,自己处理
- 轻量级锁:CAS 实现
- 锁消除:偏向锁就是锁消除的一种
- 锁膨胀:jvm实现,锁粗化
- 信号量:使用阻塞锁 实现的一种策略