源码+官方文档
JUC是 在Java 5.0添加的 java.util.concurrent包的简称,目的就是为了更好的支持高并发任务, 让开发者利用这个包进行的多线程编程时可以有效的减少竞争条件和死锁线程。
Runnable没有返回值,效率比Callable相对低。
JUC是继承Callable接口的。
进程是一个程序,一个进程往往可以包含多个线程(至少一个线程)。
Java默认有2个线程:main线程,GC线程。
线程是进程中的一个方法,比如进程是一个音乐软件,其中后台下载音乐就是一个线程。
对于Java而言:Thread、Runnable、Callable
Java 真的可以开启线程吗? 开不了
并发编程:并发、并行
并发(多线程操作同一个资源):CPU 一核 ,模拟出来多条线程,天下武功,唯快不破,快速交替
并行(多个人一起行走):CPU 多核 ,多个线程可以同时执行; 线程池
并发编程的本质:充分利用CPU的资源
所有的公司都很看重!
企业,挣钱=> 提高效率,裁员,找一个厉害的人顶替三个不怎么样的人;
人员(减) 、技术成本(高)
关于锁的释放:wait会释放锁,sleep是抱着锁睡觉的,不会释放。
会出现多个线程争夺资源,而导致顺序不对。
传统的Synchronized
其实就是让多个线程进行排队进行操作。
公平锁:十分公平:可以先来后到
非公平锁:十分不公平:可以插队 (默认)
Synchronized 和 Lock 区别
Synchronized 内置的Java关键字, Lock 是一个Java类
Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下 去;
Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以 判断锁,非公平(可以 自己设置);
Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)。
思考:锁是什么,如何判断锁是谁的?
面试的:单例模式、排序算法、生产者和消费者,死锁
生产者和消费者问题Synchronized版本
问题存在,A、B、C、D4个线程!虚假唤醒
这种问题会造成死锁问题。将if判断替换成while判断。
Condition 精准的通知和唤醒线程
通过Condition进行顺序执行任务。实际上是通过Condition精确的监视每一个方法,需要手动设置唤醒的指定线程,以及使当前线程等待。
可以这样理解:班主任通知班长让同学们打卡,班长然后一个一个提醒同学,学生打完卡,汇报给班长,然后班长汇报给班主任。这是顺序的执行,但是中间班长通知所有同学,可以多线程进行。
两个同步方法,一个对象调用,标准情况下,两个线程先打印短信还是电话?发短信
sendSms延迟4秒,两个线程先打印发短信还是 打电话?发短信
synchronized 锁的是方法的调用者,也就是对象锁。两个方法持有的是同一把锁,因此谁先拿到锁谁先执行 。
一个同步方法,一个普通方法,一个对象调用,增加一个普通方法后,先执行发短信还是Hello?普通方法,因为这里没有锁,不是同步方法,不受锁的影响
普通方法没有锁,不需要竞争锁。
synchronized 锁的是方法的调用者,也就是对象锁。两个对象分别调用两个方法持有的是两把把锁,打电话不需要等待。如果不沉睡,锁的是对象,因为是不同的两个对象,所以并不受锁的影响。
两个静态同步方法,一个对象调用,增加两个静态的同步方法,只有一个对象,先打电话还是发短信?打电话?
static方法类一加载就会执行,synchronized 锁的是Class对象,所以两个方法持有一把锁,谁先得到谁先执行
两个对象!增加两个静态的同步方法, 先打印 发短信?打电话
static方法类一加载就执行,synchronized 锁的是Class对象即类所,两个方法持有两把把锁,而打电话不沉睡4秒
1个静态的同步方法,1个普通的同步方法 ,一个对象,先打印 发短信?打电话? 打电话
原因:静态同步方法和普通同步方法分别是类锁和对象锁,相当于两把锁,普通同步方法不要等待。
1个静态的同步方法,1个普通的同步方法 ,两个对象,先打印 发短信?打电话?打电话
原因:静态同步方法和普通同步方法分别是类锁和对象锁,相当于两把锁,普通同步方法不要等待。
小结:
普通带锁方法:锁对象,同一对象下的才按顺序执行,如果是同一个类下的不同对象则不受影响。
静态带锁方法:锁类,同一个类下的所有对象的所有带锁方法都得按顺序执行。
CopyOnWriteArrayList比Vector高效,因为Vector使用synchronized会速度慢,而CopyOnWriteArrayList使用了lock锁。
CopyOnWriteArraySet是线程安全版本的Set实现,它的内部通过一个CopyOnWriteArrayList来代理读写等操作,使得CopyOnWriteArraySet表现出了和CopyOnWriteArrayList一致的并发行为,他们的区别在于数据结构模型的不同,set不允许多个相同的元素插入容器中。
HashSet的底层实现是HashMap。
countDownLatch.countDown()是数量减1
countDownLatch.await()用于等待计数器归零,然后向下执行
每次有线程调用countDown()数量减一,假设计数器变为0,countDownLatch.await()被唤醒,才可以继续往下执行。
类似于加法计数器。
semaphore:信号量
实例:抢车位,6辆车只有3个停车位
semaphere.acquire()获得,假设如果已经满了,就会等待到被释放为止。
semaphere.release()释放,会将当前信号量进行释放,然后唤醒等待线程。
作用:多个共享资源的互斥的使用!并发限流,控制到最大的线程数。
通过ReadWriteLock readWriteLock = new ReentrantReadWriteLock()更细粒度的去控制读写操作。
阻塞队列:
BlockingQueue BlockingQueue 不是新的东西
学会使用队列
添加、移除
四组API
方式
抛出异常
有返回值,不抛出异常
阻塞 等待
超时等待
SynchronousQueue同步队列
没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素!
程序的运行会占用系统的资源!
池化技术是优化资源的使用!(线程池、连接池、内存池、对象池)
池化技术:事先准备好一些资源,要有人来使用,就来我这里来拿,用完之后还回来。
线程池的好处:1. 降低资源的消耗;2. 提高响应的速度;3. 方便管理
核心作用:线程复用、可以控制最大并发数、管理线程。
线程池面试:三大方法、7大参数、4种拒绝策略
创建单个线程:newSingleThreadExecutor
创建固定线程池大小:newFixedThreadPool
创建可缓存线程池:newCachedThreadPool
都是通过ThreadPoolExecutor实现的。
源码分析
本质上是调用了ThreadPoolExecutor,而里面的参数就是所谓的7大参数。
最大线程到底如何定义?
CPU密集型,几核就是几,可以保持CPU的效率最高
IO 密集型:设置大于判断程序中十分耗IO的线程数量,比如程序中有15个大型任务,io十分占用资源!
必须掌握:lambda表达式、链式编程、函数式接口、Stream流式计算
函数式接口:只有一个方法的接口
Java 8为函数式接口引入了一个新注解@FunctionalInterface,主要用于编译级错误检查,加上该注解,当你写的接口不符合函数式接口定义的时候,编译器会报错。
四大函数式接口:Consumer、Function、Predicate、Supplier
Predicate:断定型接口,有一个输入参数,返回值只能是布尔值
大数据分为存储+计算。
集合、MySQL的本质是用来存储东西的。
计算都应该交给流来实现。
ForkJoin在JDK1.7出现的,并行执行任务!提高效率,适合大数据量。
ForkJoin是将一个大任务拆分为多个子任务进行操作。就是分而治之思想。
ForkJoin特点:工作窃取
这个里面维护的都是双端队列。
Future设计的初衷:对某个未来的事件结果进行建模,类似于ajax
请你谈谈volatile的理解
volatile是java虚拟机提供轻量级的同步机制
JMM是java内存模型,不存在的东西,就是一个概念/约定。
关于JMM的一些同步约定:
线程 工作内存、主内存
8种操作:
8种原子操作如下:
lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:
(1)不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
(2)不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
(3)不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
(4)一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
(5)一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
(6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
(7)如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
(8)对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。
问题: 程序不知道主内存的值已经被修改过了
原子性:不可分割
线程A在执行任务的时候,不能被打扰的,也不被分割,要么同时成功,要么同时失败。
如果不加lock和synchronized,怎么保证原子性。
使用原子类,解决原子性问题
这些类的操作直接底层操作系统挂钩。在内存中修改至,Unsafe是一个很特殊的类。
指令重排:写的程序,计算机并不是按照写的程序去执行的。
源代码-> 编译器优化的重排->指令并行也可能会重排->内存系统也会重排->执行
所期望的是1234步骤进行顺序执行,但是计算机会进行指令重排(比如2134,1324也可以执行)。
volatile可以避免指令重排。
内存屏障,CPU指令。作用:
1、保证特定的操作的执行顺序!
2、可以保证某些变量的内存可见性 (利用这些特性volatile实现了可见性)
volatile是可以保持可见性,不能保证原子性,由于内存屏障,可以保证避免指令重排的现象发生。
饿汉式、DCL懒汉式(用到了volatile)
单例不安全,可以通过反射进行破坏
CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么就执行操作!如果不是就一直循环,因为其底层是自旋锁。
缺点:
ABA问题:A被一个线程修改成B然后又被另一个修改成A,实际对象已经发生改变,可以通过增加版本号来改变。

解决ABA 问题,引入原子引用! 对应的思想:乐观锁!
带版本号的原子操作!
Integer 使用了对象缓存机制,默认范围是 -128 ~ 127 ,推荐使用静态工厂方法 valueOf 获取对象实例,而不是 new,因为 valueOf 使用缓存,而 new 一定会创建新的对象分配新的内存空间;
AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题。 正常在业务操作,这里面比较的都是一个个对象。
可重入锁(递归锁)
spinlock
自定义锁测试
排查问题:
Synchronized锁升级
偏向锁:没有线程竞争的情况,锁会偏向于线程A,不需要再次获取,而是直接进入。如果存在多个线程来抢占锁,线程B来抢占所,锁会升级为轻量级锁。(默认4秒后开启,-XX:BiasedLockingStartupDelay=0)
轻量级锁:为了避免线程阻塞,通过自旋锁来实现(也就是重试)。 如果重试后,还是抢不到锁,升级为重量级锁。
高性能:响应(低延时)(缓存、JVM优化)、吞吐(高吞吐量、高并发)(集群、负载均衡)
不是
$N_{threads}= N_{CPU}U_{CPU}(1+W/C)$
$N_{CPU}$是处理器的核的数目,可以通过Runtime.getRuntime.availableProcessors()得到
$U_{CPU}$是期望的CPU的利用率(该值应该介于0~1之间)
synchronized悲观锁与JUC乐观锁
悲观锁:总感觉有人要自己竞争,因此一直加锁。
乐观锁:总感觉没人和自己竞争,一直不设防。
synchronized本质上是一个悲观锁。
JUC的乐观锁是自旋锁,自旋锁的实现方式是通过CAS实现的。
就是A被修改为B,然后又被修改为A。
(版本号/boolean)
CAS操作cpu本身有指令支持-不保障原子性
什么时候使用CAS,什么使用悲观锁
能使用synchronized的时候,就优先先使用syn。
JDK1.5之后,synchronized内部有锁升级过程,偏向锁-> 自旋锁(轻量级)->重量级锁(悲观排队锁)