JUC
–总结
1、进程、线程概念
进程:它是操作系统动态执行的基本单元
线程:cpu将线程作为独立运行和独立调度的基本单位。
白话:
进程:就是操作系统中运行的一个程序,QQ.exe, music.exe, word.exe ,这就是多个进程
线程:每个进程中都存在一个或者多个线程,比如用word写文章时,就会有一个线程默默帮你定时自动保存。
并发 / 并行是什么?
并发和并行, 都可以表示两个或多个任务一起执行
并发,是逻辑上的同时发生, 多个任务交替执行,cpu
来调度
并行, 物理上的同时发生,并行的多个任务是真实的同时执行
wait / sleep 的区别
1、来自不同的类
- 这两个方法来自不同的类
2、有没有释放锁(释放资源)
- sleep方法没有释放锁,而wait方法释放了锁, 得其他线程可以使用同步控制块或者方法。
- sleep有时间限制,而wait是无限期的除非用户主动notify
** 、使用范围不同**
sleep
可以在任何地方使用,wait,notify、notifyAll
只能在同步控制方法或者同步控制块里面使用
2、Lock锁
使用 juc.locks
包下的类操作 Lock 锁 + Lambda 表达式
// 启动
new Thread(()-> {
for (int i = 1; i < 40; i++) saleTicket.saleTicket();
}, "A").start()
class Ticket{
private Lock lock = new ReentrantLock();
public void saleTicket(){
lock.lock(); // 加锁
try {
// 业务代码
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();// 解锁
}
}
}
Lambda 表达式,让Ticket 这个类更加纯粹!不像传统的 synchronized 需要这个去实现Runnable接口,这样就限制了这个类,耦合性变强了
synchronized 和 lock 区别
-
synchronized是关键字,Lock是类
-
synchronized无法判断锁的状态,Lock可以判断是否获取到锁
-
synchronized会自动释放锁,Lock需在
finally
中手工释放锁 -
synchronized的锁可重入、不可中断、非公平;而Lock锁可重入、可判断、可公平
-
// 设置 true or false 来实现(true)公平和(false)非公平锁 private Lock lock = new ReentrantLock(true);
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题
private Lock lock = new ReentrantLock();
lock.lockInterruptibly()
void lockInterruptibly() throws InterruptedException
InterruptedException
is thrown
- 在进入该方法时设置中断状态;
- 或在获取锁时被中断,并且支持获取锁的中断
抛出InterruptedException
,并清除当前线程的中断状态
lockInterruptibly() 和 lock()的区别:
- lockInterruptibly():中断锁,线程等待锁的过程中如果被中断,则会立刻进入该线程,响应中断异常(异常抛出的话就进入上层处理异常)
- lock():线程在等待锁的过程中,不会响应中断(会一直等待获取锁),但是在中断点(sleep)会响应之前的中断
3、生产者和消费者
生产者和消费者 synchroinzed 版
题目:
- 现在两个线程,可以操作初始值为0的一个变量
- 实现一个线程对该变量 + 1,一个线程对该变量 -1
- 实现交替10次
4个线程,两个加,两个减,生产者和消费者 synchroinzed
版 中如果使用if判断 ,会出现虚假唤醒!
public synchronized void increment() throws InterruptedException {
// 判断该不该这个线程做
if (number==1){ // 如果使用if判断 ,会出现虚假唤醒
this.wait();
}
// 干活
number++;
System.out.println(
Thread.currentThread().getName()+"\t"+number
);
// 通知
this.notifyAll();
}
我的理解:锁是锁住的对该对象数据修改的那么部分代码,只允许一个线程去操作,其它的线程暂停执行!因此这里的理解是,当number==0
, 两个加线程都进入increment
方法,然后经过了 if(number == 1)
的判断,都决定要去number++
,但是由于synchronized
的存在,只能由其中某一个线程先去执行!此时number == 1
了,然后,这个线程操作结束,假设cpu此时恰好调度到了另一个加线程,那么if(number == 1)
因为已经在之前判断过了,那么此时线程唤醒的位置就直接从number++
开始执行,因此number == 2
了,这是不对的!这样就引起了线程不安全问题,虚假唤醒,因为这个后来执行的加线程,么有再次去判断number
的值,因此要解决这个问题,我们需要用while来代替if,这样哪怕唤醒,while
这个循环是不停判断的,也会再次判断number
的值,number == 1
,那么就执行 this.wait()
,这个加线程不能再往下执行
太多话了,其实就是if只判断一次,那么这个线程假如正好是再判断完之后被暂停的,那么下次唤醒,就会继续往下执行,用while的话,那么会循环判断,保证什么时候唤醒,都进行重新判断
新版生产者和消费者写法
其实就是将sychronized
改成 lock.lock()
,
之前线程通信的 this.wait()
改成
private Condition condition = lock.newCondition();
condition.await();
之前线程通信this.notifyAll()
改成 condition.signalAll();
那么为什么有了 老版的 synchronized , this.wait 和 this.notify 这些方法还需要
使用新的lock 锁 以及 condition 类的 await 和 signalAll 这些方法呢?
自然是因为第一 lock锁性能更好!第二condition类还可以完成精确通知顺序访问这个功能。
什么是精确通知顺序访问?
就是实现多线程之间按顺序调用
题目:多线程之间按顺序调用,实现 A->B->C
// 资源类 属性,方法
class Data3{
private int num = 1; // A1 B2 C3
// 定义锁
Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition(); //3个判断,交替执行 A--B--C--A
private Condition condition2 = lock.newCondition(); //3个判断,交替执行 A--B--C--A
private Condition condition3 = lock.newCondition(); //3个判断,交替执行 A--B--C--A
// 3个方法、作业,合3为1
// +1
public void print5(){
// 加锁
lock.lock();
try {
//判断
while (num!=1){
condition1.await(); //等待
}
// 干活
for (int i = 1; i <=5 ; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
// 第一个线程通知第二个线程,第二个线程通知第三个.... 计数器
num=2;
// 通知第二个线程干活,指定谁干活
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
public void print10() {
// 加锁
lock.lock();
try {
//判断
while (num!=2){
condition2.await(); //等待
}
// 干活
for (int i = 1; i <=10 ; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
// 第一个线程通知第二个线程,第二个线程通知第三个.... 计数器
num=3;
// 通知第二个线程干活,指定谁干活
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
public void print15() {
// 加锁
lock.lock();
try {
//判断
while (num!=3){
condition3.await(); //等待
}
// 干活 = 业务代码
for (int i = 1; i <=15 ; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
num=1;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 解锁
lock.unlock();
}
}
}
不同的condition实例,放在不同的线程当中,标记着不同的线程的通知信号!
利用标志位num,来决定哪些condition实例对应的线程来等待,
condition1.await(); //等待
condition2.await(); //等待
condition3.await(); //等待
通过通知不同的condition实例condition1.singal()|condition2.singal()|condition3.singal()
来唤醒对应等待的condition1|condition2|condition3
所在的线程继续工作!
由此,我们来定义通知不同的condition实例的顺序,也就达到了多线程之间按顺序调用的自定义的方式!这就是精确顺序通知!
4、8锁的现象
其实我觉得,你需要能分的清楚,锁的对象是谁,就可以算是没太大问题了!
- 被synchronized修饰的方法,锁的对象是方法的调用者。因为两个方法的调用者是同一个,所以两个方法用的是同一个锁,先调用方法的先执行。
- 普通方法没有被synchronized修饰,不是同步方法,不受锁的影响,所以不需要等待
- 被synchronized修饰的方法,锁的对象是方法的调用者。因为用了两个对象调用各自的方法,所以两个方法的调用者不是同一个,所以两个方法用的不是同一个锁,后调用的方法不需要等待先调用的方法。
- 被synchronized和static修饰的方法,锁的对象是类的class对象。因为两个同步方法都被static修饰了,所以两个方法用的是同一个锁,后调用的方法需要等待先调用的方法。
总结: 看这里就好了!
- 对于普通同步方法,锁的是当前实例对象
- 对于静态同步方法,锁的是当前的Class对象。
- 因此你只要确定了,每个线程锁的是不是同一个对象,就能知道,线程间执行的是否会产生阻塞了!就能理解锁的问题
5、集合类不安全
集合不安全,因为多线程同时去修改集合数据,自然是不安全的
list解决方案:
/**
* 善于总结:
* 1、 故障现象: ConcurrentModificationException
* 2、 导致原因: 多线程操作集合类不安全
* 3、 解决方案:
* List<String> list = new Vector<>(); // Vector 是一个线程安全的类,效率低下 50
* List<String> list = Collections.synchronizedList(new ArrayList<>()); // 60
* List<String> list = new CopyOnWriteArrayList<>(); // JUC 100 推荐使用
*/
写入时复制(CopyOnWrite)思想
读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array
也就是读的时候不上锁,写的时候上锁
这也是为什么CopyOnWriteArrayList
为什么并发安全且性能比前两种解决方案好的原因!
同理 set解决方案:
/**
* 善于总结:
* 1、 故障现象: ConcurrentModificationException 并发修改异常!
* 2、 导致原因: 并发下 HashSet 存在安全的问题
* 3、 解决方案:
* Set<String> set = Collections.synchronizedSet(new HashSet<>()); 60
* Set<String> set =new CopyOnWriteArraySet<>(); // 100
*
*/
同理 map解决方案:但是名字有变化ConcurrentHashMap
// 解决方案:Map<String, String> map = new ConcurrentHashMap<>();
6、Callable
多线程中,第3种获得多线程的方式,Callable。它与Runnable有什么区别呢?
- Callable 有返回值
- Callable 抛出异常
如果你不使用线程池的方式去使用实现了Callable接口的类创建新线程,而是想要new Thread(Runnable)
的方式去创建新线程,我们就要用到 FutureTask
这个类,
因为new Thread(Runnable)
只接收实现了Runnable接口的类的实例,因此我们需要通过 new FutureTask(Callable接口实现的类的实例)
适配一下,你可以这么理解,等于就是变成了Runnable模式了。
public class Test1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Thread(Runnable)
// Thread(RunnableFuture)
// Thread(FutureTask)
MyThread myThread = new MyThread();
FutureTask task = new FutureTask(myThread); // 适配类
// 会打印几次 end
new Thread(task,"A").start(); // 执行线程
new Thread(task,"B").start(); // 执行线程。细节1:结果缓存!效率提高N倍
System.out.println(task.get());// 获取返回值, get()
// 细节2:task.get() 获取值的方法一般放到最后,保证程序平稳运行的效率,因为他会阻塞等待结果产生!
// 线程是一个耗时的线程,不重要!
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("end");
TimeUnit.SECONDS.sleep(3);
return 1024;
}
}
创建FutureTask
,在运行时执行给定的Callable。
FutureTask
实现了 Runnable接口,又可以接收 Callabe,因此可以在这里用来当作适配类
7、常用辅助类
CountDownLatch
CyclicBarrier
Semaphore
CountDownLatch
你可以把它称作减法计数器
通过new CountDownLatch(6);
规定计数器要完成任务的数量
通过 new CountDownLatch(6).countDown()
需要完成任务数量-1
通过new CountDownLatch(6).await()
来阻塞其它线程的执行,任务数量为0,则唤醒刚阻塞的线程继续执行
package com.coding.demo03;
import java.util.concurrent.CountDownLatch;
// 程序如果不加以生活的理解再加上代码的测试,你就算不会
public class CountDownLatchDemo {
// 有些任务是不得不阻塞的 减法计数器
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6); // 初始值
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"Start");
// 出去一个人计数器就 -1
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await(); // 阻塞等待计数器归零
// 阻塞的操作 : 计数器 num++
System.out.println(Thread.currentThread().getName()+"===END");
}
}
CyclicBarrier
也可称作加法计数器
package com.coding.demo03;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
// CyclicBarrier 栅栏 加法计数器
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 集齐7个龙珠召唤神龙 ++ 1
// public CyclicBarrier(int parties, Runnable barrierAction)
// 等待cyclicBarrier计数器满,就执行后面的Runnable,不满就阻塞
CyclicBarrier cyclicBarrier = new CyclicBarrier(8, new Runnable() {
@Override
public void run() {
System.out.println("神龙召唤成功!");
}
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集了第"+temp+"颗龙珠");
try {
cyclicBarrier.await(); // 等待 阻塞
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
new CyclicBarrier(8, ()->{})
更加类似于一种回调的方式,第一个参数放置需要完成的任务数量。然后在多个线程中放置 cyclicBarrier.await()
,当然要放在业务代码的下方,因为这个方法是等待阻塞的。,然后等待完成任务的数量到达8,就执行第二参数里面的内容!
Semaphore 信号量
可以用抢车位例子来描述它的作用
Semaphore semaphore = new Semaphore(3);// 模拟资源类,有3个空车位
semaphore.acquire(); // acquire 得到
多个线程中通过这个来抢车位,没抢到的,则会阻塞下面代码的执行,直到有空车位可以抢,并且抢到,才继续执行下去
semaphore.release(); // 释放这个位置
抢到车位的人,可以通过这个方法释放车位的位置,好让其它没有抢到车位的人有机会入住
// 抢车位
public class SemaphoreDemo {
public static void main(String[] args) {
// 模拟6个车,只有3个车位
Semaphore semaphore = new Semaphore(3); // 3个位置
for (int i = 1; i <= 6; i++) {
new Thread(()->{
// 得到车位
try {
semaphore.acquire(); // 得到
System.out.println(Thread.currentThread().getName()+"抢到了车位");
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放位置
}
},String.valueOf(i)).start();
}
}
}
信号量主要用于两个目的:一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
8、读写锁
Interface ReadWriteLock
实现了这个接口的类 ReentrantReadWriteLock
独占锁(写锁):指该锁一次只能被一个线程锁持有。对于ReentranrLock
和 Synchronized
而言都是独占锁。
共享锁(读锁):该锁可被多个线程所持有。
为什么要这个锁的出现呢?自然是为了提高性能, ReentrantReadWriteLock
其读锁时共享锁,写锁是独占锁,读锁的共享锁可保证并发读是非常高效的
往往我们实际上修改共享资源,担心的就是修改写入的时候,多线程引发的不安全。但是读取这个资源,其实没有关系,ReentrantLock
和 sychronized
就是不管三七二十一,读写全给你用独占锁锁上,性能自然不如ReentrantReadWriteLock
public class ReadWriteDemo {
public static void main(String[] args) {
MyCache2 myCache = new MyCache2();
// 多个线程同时进行读写
// 五个线程在写 线程是CPU调度的
for (int i = 1; i < 5; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
// 五个线程在读
for (int i = 1; i < 5; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
// 线程操作资源类,存在问题的
class MyCache2{
private volatile Map<String,Object> map = new HashMap<>();
// ReadWriteLock --> ReentrantReadWriteLock lock不能区分读和写
// ReentrantReadWriteLock 可以区分读和写,实现更加精确的控制
// 读写锁
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 写。独占
public void put(String key,String value){
// lock.lock 加锁
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
// 存在别的线程插队
System.out.println(Thread.currentThread().getName()+"写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock(); // lck.unlock();
}
}
// 多线程下尽量加锁!
// 读
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object result = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取结果:"+result);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
当然这里有个问题,关于读锁不加和加上,有什么区别吗?
不加的话,读的线程和写的线程,就会交替执行,加上,则会表现为互斥,所有读的线程先执行,然后执行所有写的线程!这样做的好处,有待大佬告知!
9、阻塞队列
当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,从队列中添加元素的操作将会被阻塞
阻塞队列的好处:
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue
都给你一手包办了。
加粗部分阻塞队列优先学习
ArrayBlockingQueue
:由数组结构组成的有界阻塞队列。
LinkedBlockingQueue
:由链表结构组成的有界(默认值为:integer.MAX_VALUE)阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
PriorityBlockingQueue
:支持优先级排序的无界阻塞队列
DelayQueue
:使用优先级队列实现的延迟无界阻塞队列。
LinkedTransferQueue
:由链表组成的无界阻塞队列。
LinkedBlockingDeque
:由链表组成的双向阻塞队列。
阻塞队列API
的使用
尽量按组匹配使用
ArrayBlockingQueue
同步队列
// 队列大小 ,有容量的阻塞队列
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
SynchronousQueue
同步队列
SynchronousQueue 没有容量。
与其他的 BlockingQueue
不同,SynchronousQueue
是一个不存储元素的 BlockingQueue
。每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
/ 同步队列
// 每一个 put 操作。必须等待一个take。否则无法继续添加元素!
public class Test5 {
public static void main(String[] args) {
// 不用写参数!
SynchronousQueue<String> queue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"put 1");
queue.put("1");
System.out.println(Thread.currentThread().getName()+"put 2");
queue.put("2");
System.out.println(Thread.currentThread().getName()+"put 3");
queue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
}
}
10、线程池
池化技术的思想,通过预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接获取,避免多次重复创建、销毁带来的开销。
线程池的优势:
它的主要特点为:线程复用,控制最大并发数,管理线程。
- 第一:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低统的稳定性,使用线程池可以进行统一分配,调优和监控。
Java中的线程池是通过 Executor 框架实现的, 该框架中用到了 Executor
,Executors
,
ExecutorService
,ThreadPoolExecutor
这几个类
三大方法
1、Executors.newFixedThreadPool(int)
执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程
// 池子大小为5
ExecutorService threadPool = Executors.newFixedThreadPool(5);
try {
for (int i = 1; i <= 10; i++) threadPool.execute(Runnable); // 这个来执行线程的任务
} catch(Exception) {
e.printStackTrace();
} finally {
// 用完一定要记得关闭
threadPool.shutdown
}
2、Executors.newSingleThreadExecutor()
只有一个线程
// 不管有多少任务,始终只有一个线程去办理
ExecutorService threadPool = Executors.newSingleThreadExecutor();
try {
for (int i = 1; i <= 10; i++) threadPool.execute(Runnable); // 这个来执行线程的任务
} catch(Exception) {
e.printStackTrace();
} finally {
// 用完一定要记得关闭
threadPool.shutdown
}
3、Executors.newCachedThreadPool();
执行很多短期异步任务,线程池根据需要创建新线程,但在先构建的线程可用时将重用他们。
可扩容,遇强则强
// 一池N线程,可扩容伸缩
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 1; i <= 10; i++) threadPool.execute(Runnable); // 这个来执行线程的任务
} catch(Exception) {
e.printStackTrace();
} finally {
// 用完一定要记得关闭
threadPool.shutdown
}
ThreadPoolExecutor
七大参数
操作:查看三大方法的底层源码,发现本质都是调用了 new ThreadPoolExecutor ( 7 大参数 )
我们平常使用的时候,也不要去使用executor的三大方法!直接使用这个来创建线程池!
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
{
// 内部代码
}
corePollSize
::核心线程数maximumPoolSize
:最大线程数keepAliveTime
:空闲的线程保留的时间。TimeUnit
:空闲线程的保留时间单位。BlockingQueue< Runnable>
:阻塞队列,存储等待执行的任务ThreadFactory
:线程工厂,用来创建线程,一般默认即可RejectedExecutionHandler
::队列已满,而且任务量大于最大线程的异常处理策略(4大策略)ThreadPoolExecutor.AbortPolicy
:丢弃任务并抛出RejectedExecutionException异常。ThreadPoolExecutor.DiscardPolicy
:也是丢弃任务,但是不抛出异常。ThreadPoolExecutor.DiscardOldestPolicy
:丢弃阻塞队列最前面的任务,然后重新尝试执行任务ThreadPoolExecutor.CallerRunsPolicy
:由调用线程处理该任务
用银行办理业务模型来理解这些参数:
- 1,2窗口是原本就打开的窗口 对应的就是
corePollSize参数
:核心线程数 - 候客区的大小 对应
BlockingQueue< Runnable>参数
:阻塞队列 - 3,4,5对应的就是如果候客区满了,能够打开的最大窗口数 也就是
maximumPoolSize参数
:最大线程数 - 全部都满了,也就是窗口和候客区都满了,再进来人,就对应
RejectedExecutionHandler
:拒绝策略
思考题:线程是否越多越好?
一个计算为主的程序(专业一点称为CPU密集型程序)。多线程跑的时候,可以充分利用起所有的cpu核心,比如说4个核心的cpu,开4个线程的时候,可以同时跑4个线程的运算任务,此时是最大效率。因此对于cpu密集型的任务来说,线程数等于cpu数是最好的了
如果是一个磁盘或网络为主的程序(IO密集型),此时 线程数等于IO任务数是最佳的。甚至是两倍于任务数
11、四大函数式接口
java.util.function , Java 内置核心四大函数式接口,可以使用lambda表达式
-
函数型接口,有一个输入,有一个输出
-
public class Demo01 { public static void main(String[] args) { // new Runnable(); ()-> {} // // Function<String,Integer> function = new Function<String,Integer>() { @Override // 传入一个参数,返回一个结果 public Integer apply(String o) { System.out.println("into"); return 1024; } // }; // 链式编程、流式计算、lambda表达式 Function<String,Integer> function = s->{return s.length();}; System.out.println(function.apply("abc")); } }
-
-
断定型接口,有一个输入参数,返回只有布尔值
-
public class Demo02 { public static void main(String[] args) { // Predicate<String> predicate = new Predicate<String>(){ // @Override // public boolean test(String o) { // if (o.equals("abc")){ // return true; // } // return false; // } // }; Predicate<String> predicate = s->{return s.isEmpty();}; System.out.println(predicate.test("abced")); } }
-
-
消费型接口,有一个输入参数,没有返回值
-
public class Demo03 { public static void main(String[] args) { // 没有返回值,只能传递参数 消费者 // Consumer<String> consumer = new Consumer<String> () { // @Override // public void accept(String o) { // System.out.println(o); // } // }; Consumer<String> consumer =s->{System.out.println(s);}; consumer.accept("123"); // 供给型接口 只有返回值,没有参数 生产者 } }
-
-
供给型接口,没有输入参数,只有返回参数
-
public class Demo04 { public static void main(String[] args) { // Supplier<String> supplier = new Supplier<String>() { // @Override // public String get() { // return "aaa"; // } // }; Supplier<String> supplier = ()->{return "aaa";}; System.out.println(supplier.get()); } }
-
函数接口最大的好处是啥?就是可以使用lamda表达式,代替这些接口的写法,从而大大降低代码量!
12、Stream流式计算
流(Stream)到底是什么呢?
是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
就是操作集合的数据的方法
特点:
- Stream 自己不会存储元素。
- Stream 不会改变源对象,相反,他们会返回一个持有结果的新Stream。
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
list.stream()
.filter(u->{return u.getId()%2==0;})
.filter(u->{return u.getAge()>24;})
.map(u->{return u.getUsername().toUpperCase();})
.sorted((o1,o2)->{return o2.compareTo(o1);})
.limit(1)
.forEach(System.out::println);
13、分支合并
思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。
主要有两步:第一、任务切分;第二、结果合并
线程池中的每个线程都有自己的工作队列(PS:这一点和ThreadPoolExecutor
不同,ThreadPoolExecutor
是所有线程公用一个工作队列,所有线程都从这个工作队列中取任务),当自己队列中的任务都完成以后,会从其它线程的工作队列中偷一个任务执行,这样可以充分利用资源。
工作窃取
工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。工作窃取的运行流程图如下:
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。
- 核心类**
ForkJoinPool
** - ->
ForkJoinPool
拥有一个内部类workQueue
(拥有工作窃取功能) - ->
workQueue
有一个ForkJoinWorkerThreadowner
线程,加一个这个线程需要处理的任务ForkJoinTask<?>[] array
的任务队列,那么新任务,就是加入到这个队列当中! - ->
ForkJoinTask
代表运行在ForkJoinPool
中的任务。主要有三个方法,两个子类- 三个方法
fork()
在当前线程运行的线程池中安排一个异步执行。简单的理解就是再创建一个子任务join()
当任务完成的时候返回计算结果。invoke()
开始执行任务,如果必要,等待计算完成。
- 两个子类
RecursiveAction
一个递归无结果的ForkJoinTask
(没有返回值)RecursiveTask
一个递归有结果的ForkJoinTask
(有返回值)
- 三个方法
ublic class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
private static final Long temp = 10000L; // 临界值
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
// 计算
@Override
protected Long compute() {
// 如果这个数 超过中间值,就分任务计算!
if (end-start<=temp){ // 正常计算
Long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
}else {
// 获取中间值
long middle = (end + start) / 2;
ForkJoinDemo right = new ForkJoinDemo(start, middle);// 第一个任务
right.fork();
ForkJoinDemo left = new ForkJoinDemo(middle+1, end);// 第一个任务
left.fork();
// 合并结果
return right.join() + left.join();
}
}
}
public class ForkJoinTest {
public static void main(String[] args) {
// test1(); // 10582 ms 60
// test2(); // 9965 ms 90
// test3(); // 158 ms 101
}
// 正常测试
public static void test1(){
long start = System.currentTimeMillis();
Long sum = 0L;
for (Long i = 0L; i <= 10_0000__0000 ; i++) {
sum +=i;
}
long end = System.currentTimeMillis();
System.out.println("time:"+(end-start)+" sum:"+sum);
}
// ForkJoin测试
public static void test2(){
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinDemo forkJoinDemo = new ForkJoinDemo(0L,10_0000__0000L);
Long sum = forkJoinPool.invoke(forkJoinDemo);
long end = System.currentTimeMillis();
System.out.println("time:"+(end-start)+" sum:"+sum);
}
// Stream并行流测试
public static void test3(){
long start = System.currentTimeMillis();
long sum = LongStream.rangeClosed(0, 10_0000__0000).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("time:"+(end-start)+" sum:"+sum);
}
}
大数据处理下, 并行流自然是最快的,第二是ForkJoin,第三是普通循环
但是小数据下,普通循环才是最快的!
14、异步回调
这块不太理解,先放例子把!
Future
它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在Future
中出发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不再需要等待耗时的操作完成。
CompletableFuture
的runAsync
只是简单的异步执行一个线程,但是它将返回一个CompletableFuture
,有了这个CompletableFuture
,可以重新组装和调配,这是和一个普通Runnable
不同之处。
package com.coding.stream;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
// CompletableFuture 异步回调, 对将来的结果进行结果,ajax就是一种异步回调!
public class CompletableFutureDemo {
public static void main(String[] args) throws Exception {
// 多线程也可以异步回调
//
// // 没有返回结果,任务执行完了就完毕了! 新增~
// CompletableFuture<Void> voidCompletableFuture = CompletableFuture.runAsync(() -> {
// // 插入数据,修改数据
// System.out.println(Thread.currentThread().getName() + " 没有返回值!");
// });
//
// System.out.println(voidCompletableFuture.get());
// 有返回结果 ajax。 成功或者失败!
CompletableFuture<Integer> uCompletableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + " 有返回值!");
// int i = 10/0;
return 1024;
});
// 有一些任务不紧急,但是可以给时间做!占用主线程!假设这个任务需要返回结果!
System.out.println(uCompletableFuture.whenComplete((t, u) -> { // 正常编译完成!
System.out.println("=t==" + t); // 正常结果
System.out.println("=u==" + u); // 信息错误!
}).exceptionally(e -> { // 异常!
System.out.println("getMessage=>" + e.getMessage());
return 555; // 异常返回结果
}).get());
}
}
public class Future {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Void> vcompletableFuture = CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "没有返回, update mysql ok");
});
vcompletableFuture.get();
System.out.println("111111");
CompletableFuture firstFuture = CompletableFuture.supplyAsync(()->{
System.out.println("start to execute supplyAsync");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end to execute supplyAsync");
return "firstFuture";
}).thenAccept(otxGroupData -> {
System.out.println("start to execute thenAccept=====>"+otxGroupData);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end to execute thenAccept");
}).exceptionally((e) -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e1) {
e.printStackTrace();
}
return null;
});
try {
firstFuture.get();
System.out.println("main end");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
15、JMM
JMM
即为JAVA 内存模型(java memory model),JMM规定了内存主要划分为主内存和工作内存两种。
JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所
以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写
入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个
线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。因为JMM制定了
一套标准来保证开发者在编写多线程程序的时候,能够控制什么时候内存会被同步给其他线程。
JMM 关于同步的规定:
1、线程解锁前,必须把共享变量的值刷新回主内存
2、线程加锁前,必须读取主内存的最新值到自己的工作内存
3、加锁解锁是同一把锁
内存交互操作
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类
型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
线程A感知不到线程B操作了值的变化!如何能够保证线程间可以同步感知这个问题呢?只需要使用
Volatile关键字即可!volatile 保证线程间变量的可见性,简单地说就是当线程A对变量X进行了修改后,
在线程A后面执行的其他线程能看到变量X的变动,更详细地说是要符合以下两个规则 :
- 线程对变量进行修改之后,要立刻回写到主内存。
- 线程对变量读取的时候,要从主内存中读,而不是缓存。
16、volatile
volitile 是 Java 虚拟机提供的轻量级的同步机制,三大特性:
1、保证可见性
2、不保证原子性
3、禁止指令重排
代码验证可见性:
public class Test1 {
// volatile 不加volatile没有可见性
// 不加 volatile 就会死循环,这里给大家将主要是为了面试,可以避免指令重排
// volatile 读取的时候去主内存中读取在最新值!
private volatile static int num = 0;
public static void main(String[] args) throws InterruptedException { // Main线程
new Thread(()->{ // 线程A 一秒后会停止! 0
while (num==0){
}
}).start();
TimeUnit.SECONDS.sleep(1);
num = 1;
System.out.println(num);
}
}
验证 volatile 不保证原子性
原子性理解:不可分割,完整性,也就是某个线程正在做某个具体的业务的时候,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败。
public class JMMVolatileDemo02 {
private volatile static int num = 0;
public static void add(){
// 因为这一步操作,实际上底层是有很多步操作的,由于这一步的非原子性操作(上图所示),因此下面
// 多线程执行的时候,会出现线程不安全情况!
num++;
}
// 结果应该是 num 为 2万,测试看结果
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
add();
}
},String.valueOf(i)).start();
}
// 需要等待上面20个线程都全部计算完毕,看最终结果
while (Thread.activeCount()>2){
// 默认一个 main线程 一个 gc 线程
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
num++ 在多线程下是非线程安全的,如何不加 synchronized解决?
// 遇到问题不要着急,要思考如何去做!
public class Test2 {
// 为解决上面num++的非原子性操作,我们采用AtomicInteger来进行操作
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
num.getAndIncrement(); // 等价于 num++
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
add(); // 20 * 1000 = 20000
}
},String.valueOf(i)).start();
}
// main线程等待上面执行完成,判断线程存活数 2
while (Thread.activeCount()>2){ // main gc
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
指令重排
volatile 实现了禁止指令重排优化,从而避免 多线程环境下程序出现乱序执行的现象。
17、深入单例模式
1、饿汉式
饿汉式是最简单的单例模式的写法,保证了线程的安全。
但是在Hungry类中,我定义了四个byte数组,当代码一运行,这四个数组就被初始化,并且放入内存了,如
果长时间没有用到getInstance
方法,不需要Hungry类的对象,这不是一种浪费吗?
public class Hungry {
// 这种单例模式的缺点就是,浪费内存空间,相较于那种用到才去创建实例的模式
private byte[] data1 = new byte[10240];
private byte[] data2 = new byte[10240];
private byte[] data3 = new byte[10240];
private byte[] data4 = new byte[10240];
// 单例模式核心思想,构造器私有!
private Hungry(){
}
// 就是不管三七二十一直接在内部创建实例
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
2、懒汉式
加一个判断,多线程操作会有问题!
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName()+"Start");
}
private static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}// 测试并发环境,发现单例失效
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
进行加锁操作,可避免多线程操作问题!
public class LazyMan {
private LazyMan() { }
private static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) { // 这一层if 决定时候要不要加锁,如果实例都有了,还加锁干嘛
// 多加一层锁,避免多线程操作问题!
synchronized (LazyMan.class) {
if (lazyMan == null) { // 这一层if 才是去判断是否存在LazyMan实例
// 但是这个new LazyMan()的操作又是非原子性的,又会有问题出现
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
lazyMan = new LazyMan();
不是原子性操作,至少会经过三个步骤:
- 分配对象内存空间
- 执行构造方法初始化对象
- 设置instance指向刚分配的内存地址,此时instance !=null;
由于指令重排,导致A线程执行 lazyMan = new LazyMan();的时候,可能先执行了第三步(还没执行第
二步),此时线程B又进来了,发现lazyMan已经不为空了,直接返回了lazyMan,并且后面使用了返回
的lazyMan,由于线程A还没有执行第二步,导致此时lazyMan还不完整,可能会有一些意想不到的错
误,所以就有了下面一种单例模式
// 这种单例模式只是在上面DCL单例模式增加一个volatile关键字来避免指令重排
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan(); // 可能存在指令重排!
/*
A: 1 3 2
B: lazyMan = null ;
1. 分配对象的内存空间
2. 执行构造方法初始化对象
3. 设置实例对象指向刚分配的内存的地址, instance = 0xfffff;
*/
}
}
}
return lazyMan;
}
3、静态内部类
还有这种方式是第一种饿汉式的改进版本,同样也是在类中定义static变量的对象,并且直接初始化,不过是移到了静态内部类中,十分巧妙。既保证了线程的安全性,同时又满足了懒加载。
public class Holder {
private Holder() { }
public static Holder getInstance() {
return InnerClass.holder;
}
private static class InnerClass {
private static final Holder holder = new Holder();
}
}
4、万恶的反射
前面三种模式,都可以被反射破坏,因此还要针对反射特殊处理,
public class LazyMan {
// 增加标志位
private static boolean flag = false;
private LazyMan(){
// 防止反射破坏
synchronized (LazyMan.class){
if (flag==false){
flag = true;
}else {
throw new RuntimeException("不要试图使用反射破坏单例模式");
}
}
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan(); // 可能存在指令重排!
}
}
}
return lazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
//LazyMan instance1 = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructors = LazyMan.class.getDeclaredConstructor(null);
declaredConstructors.setAccessible(true); // 无视 private 关键字
Field flag = LazyMan.class.getDeclaredField("flag");
flag.setAccessible(true);
LazyMan instance1 = declaredConstructors.newInstance();
flag.set(instance1,false);
LazyMan instance2 = declaredConstructors.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
增加的标志位 flag来防止反射,但是如果flag名字被人知道,直接反射修改flag,还是不安全,因此来到了枚举的方案
5、枚举
枚举天然带了防止反射破坏单例模式的功能,源码就不看了!
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
枚举是目前最推荐的单例模式的写法,因为足够简单,不需要开发自己保证线程的安全,同时又可以有
效的防止反射破坏我们的单例模式
18、深入理解CAS
CAS : 比较并交换
一句话:真实值和期望值相同,就修改成功,真实值和期望值不同,就修改失败!
/*
* public final boolean compareAndSet(int expect, int update) {
* return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
* }
* 底层其实是调用了unsafe类的方法,unsafe的方法都是native方法,用来直接操作内存的,是原子指令
* 不会造成数据不一致的问题
*/
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
// compareAndSet 简称 CAS 比较并交换!
// compareAndSet(int expect, int update) 我期望原来的值是什么,如果是,就更新
// a
System.out.println(atomicInteger.compareAndSet(5, 2020)+"=>"+atomicInteger.get());
// c 偷偷的改动
System.out.println(atomicInteger.compareAndSet(2020, 2021)+"=>"+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 5)+"=>"+atomicInteger.get());
// b
System.out.println(atomicInteger.compareAndSet(5, 1024)+"=>"+atomicInteger.get());
}
}
CAS 的缺点
1、循环时间长开销很大。
可以看到源码中存在 一个 do…while 操作,如果CAS失败就会一直进行尝试。
2、只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作。但是:
对多个共享变量操作时,循环CAS就无法保证操作的原子性,这时候就可以用锁来保证原子性。
3、引出来 ABA 问题???
19、原子引用
ABA问题怎么产生的?
CAS算法实现一个重要前提:需要取出内存中某时刻的数据并在当下时刻比较并交换,那么在这个时间
差内会导致数据的变化
比如说一个线程one从内存位置V中取出A,这个时候另一个线程two也从内存中取出A,并且线程two进
行了一些操作将值变成了B,然后线程two又将 V位置的数据变成A,这时候线程one进行CAS操作发现内
存中仍然是A,然后线程one操作成功。
要解决ABA问题,我们就需要加一个版本号
利用AtomicReference
(也就是原子引用)类加版本号
package com.coding.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
import java.util.concurrent.locks.ReentrantLock;
/**
* AtomicReference 原子引用
* AtomicStampedReference 加了时间戳 类似于乐观锁! 通过版本号
*/
public class CASDemo2 {
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
new Thread(()->{
//1 、 获得版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("T1 stamp 01=>"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,
atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println("T1 stamp 02=>"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,
atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println("T1 stamp 03=>"+atomicStampedReference.getStamp());
},"T1").start();
new Thread(()->{
// GIT 看到数据被动过了!
//1 、 获得版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("T1 stamp 01=>"+stamp);
// 保证上面的线程先执行完毕!
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = atomicStampedReference.compareAndSet(100, 2019,
stamp, stamp + 1);
System.out.println("T2 是否修改成功:"+b);
System.out.println("T2 最新的stamp:"+stamp);
System.out.println("T2 当前的最新值:"+atomicStampedReference.getReference());
},"T2").start();
}
}
20、Java锁
1、公平锁非公平锁
- 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
- 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比现申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。
并发包中的 ReentrantLock
的创建可以指定构造函数 的 boolean类型来得到公平锁或者非公平锁,默认是非公平锁!对于Synchronized
而言,也是一种非公平锁
2、可重入锁
可重入锁最大的作用就是避免死锁
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
也就是说,线程可以进入任何一个它已经拥有的锁,所同步着的代码块。 好比家里进入大门之后,就可以进入里面的房间了
public class RTLock {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendSMS();
},"T1").start();
new Thread(()->{
phone.sendMail();
},"T2").start();
}
}
class Phone {
public synchronized void sendSMS(){ // 外面的锁
System.out.println(Thread.currentThread().getName()+" sendSMS");
sendMail(); // 这个方法本来也是被锁的,但是由于获得了外面的锁,所以这个锁也获得了!
}
public synchronized void sendMail(){
System.out.println(Thread.currentThread().getName()+" sendMail");
}
}
3、自旋锁
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下
文切换的消耗,缺点是循环会消耗CPU
compareAndSwapInt
就是采用了自旋锁的方式
自定义一个自旋锁:
// coding自己定义的自旋锁!
public class MyLock {
// 原子引用 CAS
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"==mylock");
// 期望是空的没有加锁, thread // 自旋,(循环!)
while (atomicReference.compareAndSet(null,thread)){// cas
}
}
// 解锁
public void myUnlock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"==myUnlock");
}
}
这里进行测试自己自定义的自旋锁!
package com.coding.lock;
import java.util.concurrent.TimeUnit;
public class SpinLockDemo {
public static void main(String[] args) {
MyLock myLock = new MyLock();
new Thread(()->{
myLock.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
myLock.myUnlock();
},"T1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
myLock.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
myLock.myUnlock();
},"T2").start();
}
}
4、死锁
产生死锁主要原因:
1、系统资源不足
2、进程运行推进的顺序不合适
3、资源分配不当
产生死锁的例子:
package com.coding.lock;
import java.util.concurrent.TimeUnit;
public class DeadLock {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldLockThread(lockA,lockB),"T1").start();
new Thread(new HoldLockThread(lockB,lockA),"T2").start();
}
}
class HoldLockThread implements Runnable{
private String lockA;
private String lockB;
public HoldLockThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
// A 想要拿B
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+"lock:"+lockA+"=>get" + lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// B想要拿到A
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"lock:"+lockB+"=>get" + lockA);
}
}
}
}
平常开发中如何分析出哪里产生了死锁
- 1、查看JDK目录的bin目录
- 2、使用 jps -l 命令定位进程号
- 3、使用 jstack 进程号 找到死锁查看, 分析堆栈信息