十五、异步回调
1.什么是异步回调?
同步回调和异步回调, 主要体现在其是否需要等待.
同步调用,:如果C处理一个问题需要花很长时间, 我们需要等待这个问题处理完,再继续执行其他任务。
异步调用:如果C处理这个需要等待的问题时不需要等待得到结果, 而是扔给S去处理,C然后接着,去做其他事情。
2.CompletableFuture-异步回调
CompletableFuture在Java里面被用于异步编程,异步通常意味着非阻塞,可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。CompletableFuture实现了Future, CompletionStage接口.
没有返回值的异步回调
public class Demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//runAsync();异步执行
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
//异步任务发起时不占用程序的时间
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":runAsync-->Void");
});
//因为有异步调用,所以主线程会继续执行以下代码,等阻塞时间过后,才会得到以上结果
System.out.println("主线程正在执行!!!!");
completableFuture.get();//获取执行结果
}
}
有返回值的异步回调
/**
* 有返回值的异步回调
*/
public class Demo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//runAsync();异步执行
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName() + ":runAsync-->Integer");
return 2021;
});
System.out.println(completableFuture.whenComplete((t, u) -> {
System.out.println("t-->" + t);//t为正常的返回结果,u为null
System.out.println("u-->" + u);//错误的返回结果,t为null,u为错误信息
}).exceptionally((e) -> {
//失败返回值
System.out.println(e.getMessage());//可以获取到错误的返回结果
return 99999;
}).get());
}
}
十六、JMM
1.什么是JMM?
JMM:Java内存模型,不存在的东西,概念!约定!
JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有实例变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存由缓存和堆栈两部分组成,缓存中保存的是主存中变量的拷贝,缓存可能并不总和主存同步,也就是缓存中变量的修改可能没有立刻写到主存中;堆栈中保存的是线程的局部变量,线程之间无法相互直接访问堆栈中的变量。
2.关于JMM的一些同步的约定
1、线程解锁前,必须把共享变量立刻刷回主存。(先将主存数据复制到该线程的私有工作内存,再修改后给到主存)
2、线程加锁前,必须读取主存中的最新值到工作内存中!
3、加锁和解锁是同一把锁
3.内存交互8个操作
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
4.JMM对这八种指令的使用,制定的规则
①必须成对出现,如read和load,use和assign等
②不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
③不允许一个线程将没有assign的数据从工作内存同步回主内存
④一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
⑤一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
⑥如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
⑦如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
⑧对一个变量进行unlock操作之前,必须把此变量同步回主内存
5.程序不知道主内存的值已经发生了变化
不加Volatile,次线程不知道主线程已经修改了num的值,所以程序会死循环
public class JMMTest {
//不加Volatile程序会死循环
private static int num = 0;
public static void main(String[] args){
new Thread(()->{
while (num==0){
}
},"次线程").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println("main线程"+num);
}
}
十七、Volatile
1.请你谈谈你对Volatile的理解
Volatile是Java虛拟机提供轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排(有序)
2.保证可见性
加Volatile可以保证可见性,即次线程知道主线程修改了num的值
//加Volatile可以保证可见性,即次线程知道主线程修改了num的值
private volatile static int num = 0;
3.不保证原子性
原子性:不可分割.
线程A在执行任务的时候,不能被打扰的。也不能被分割。要么同时成功。要么同时失败(回滚)。
测试:
public class VolatileTest2 {
private volatile static int num =0;
public static void add(){
num++;
}
public static void main(String[] args){
//20个线程
for (int i = 1; i <=20 ; i++) {
new Thread(()->{
//每个线程执行1000次
for (int j = 0; j <1000 ; j++) {
add();
}
},"线程"+i).start();
}
while (Thread.activeCount()>2){
//还有除过主线程和gc线程的其他线程
//yield()方法的作用是放弃当前线程获取CPU的执行权,将让其它的线程去获取
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
由结果可以看出volatile,不能保证原子性,但是方法加了synchronized或者lock就可以保证原子性
public synchronized static void add(){
num++;
}
但是不加lock和synchronized,如何保证原子性
使用原子类的包装类
public class AtomicTest3 {
private volatile static AtomicInteger num =new AtomicInteger();
public static void add(){
//num++;
num.getAndIncrement();//AtomicInteger+1 CAS
}
public static void main(String[] args){
//20个线程
for (int i = 1; i <=20 ; i++) {
new Thread(()->{
//每个线程执行1000次
for (int j = 0; j <1000 ; j++) {
add();
}
},"线程"+i).start();
}
while (Thread.activeCount()>2){
//还有除过主线程和gc线程的其他线程
//yield()方法的作用是放弃当前线程获取CPU的执行权,将让其它的线程去获取
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
4.禁止指令重排
什么是指令重排:你写的程序,计算机并不是按照你写的那样去执行的。
源代码–>编译器优化的重排–>指令并行也可能会重排–>内存系统也会重排–>执行
处理器在进行指令重排的时候.考虑:数据之间的依赖性!
intx=1;//1
inty=2;//2
x=x+5;//3
y=x*x;//4
我们所期望的执行顺序: 1234 但是 可能执行的时候回变成2134 1324
上述的过程就可以当做是指令的重排,即内部执行顺序,和我们的代码顺序不一样
但是指令重排也是有限制的,即不会出现下面的顺序
4 3 2 1
因为处理器在进行重排时候,必须考虑到指令之间的数据依赖性
因为步骤 4:需要依赖于 y的申明,以及x的申明,故因为存在数据依赖,无法首先执行
例子:int a,b,x,y=0
上面的代码,不存在数据的依赖性,因此编译器可能对数据进行重排
重排后和预期结果不同.
volatile可以避免指令重排
内存屏障。CPU指令。
作用:
1、保证特定的操作的执行顺序!
2、可以保证某些变量的内存可见性( 利用这些特性volatile实现了可见性)
Volatile是可以保持可见性。不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!
十八、单例模式
1.单例设计模式特点:
①一个类只允许产生一个实例化对象。
②类的构造器的访问权限设置为private
③类内部创建该类的对象,并且对象也必须声明为static
④提供公共的static’方法,返回类的对象
单例模式又分为:饿汉式和懒汉式
2.饿汉式单例
饿汉式:在声明的同时初始化该对象,在类创建的同时就已经创建好一个静态的对象,以后不在改变。线程是安全的
public class SingletonDemo1 {
public static void main(String[] args){
Book book1 = Book.getInstance();
Book book2 = Book.getInstance();
System.out.println("创建的是否为同一个book对象:"+(book1==book2));//true
}
}
/**
* 饿汉式单例:
*/
class Book{
//1.构造器私有
private Book() {
}
//2.内部创建对象
//3.此对象必须为静态,否则获得对象实例的getInstance方法无法获取
//饿汉式,声明的同时初始化该对象
private static Book book = new Book();
//4.提供公共的static方法,返回类的对象
public static Book getInstance(){
return book;
}
}
3.懒汉式单例
懒汉式:延时加载,它是在需要的时候才创建对象,在创建实例对象时不加上synchronized则会导致对对象的访问不是线程安全的。
public class SingletonDemo2 {
public static void main(String[] args){
Dog dog1 = Dog.getInstance();
Dog dog2 = Dog.getInstance();
System.out.println("创建的是否为同一个book对象:"+(dog1==dog2));//true
}
}
/**
* 饿汉式单例:
*/
class Dog{
//1.构造器私有
private Dog() {
}
//2.内部创建对象
//3.对象声明为static
//懒汉式:对象声明但是没有初始化
private static Dog dog = null;
//4.提供公共的static方法,返回类的对象,延迟加载
public static Dog getInstance(){
if (dog == null){
dog = new Dog();
}
return dog;
}
饿汉式和懒汉式比较
饿汉式:
坏处:对象加载时间过长。
好处:饿汉式是线程安全的
懶汉式:
好处:延迟对象的创建。
写法坏处:线程不安全。
4.DCL单例(双重检测模式)
不加锁,线程不安全
public class Animals{
private Animals() {
System.out.println(Thread.currentThread().getName()+"已启动");
}
private static Animals animal = null;
public static Animals getInstance(){
if (animal == null){
animal = new Animals();
}
return animal;
}
//多线程并发
public static void main(String[] args){
for (int i = 1; i <= 10; i++) {
new Thread(()->{
Animals.getInstance();
}).start();
}
}
}
双重检测锁加原子性判断,从而避免指令重排
/**
* 饿汉式单例:加锁synchronized
*/
public class Animals{
private Animals() {
System.out.println(Thread.currentThread().getName()+"已启动");
}
private volatile static Animals animal = null;
public static Animals getInstance(){
if (animal == null){
synchronized (Animals.class){
if (animal == null){
animal = new Animals();
}
}
}
return animal;
}
//多线程并发
public static void main(String[] args){
for (int i = 1; i <= 10; i++) {
new Thread(()->{
Animals.getInstance();
}).start();
}
}
}
为什么需要双重检测的原因?
第一个if语句,用来确认调用getInstance()时animal是否为空,如果不为空即已经创建,则直接返回,如果为空,那么就需要创建实例,于是进入synchronized同步块。
synchronized加类锁,确保同时只有一个线程能进入,进入以后进行第二次判断,是因为,对于首个拿锁者,它的时段animal肯定为null,那么进入new Animals()对象创建,而在首个拿锁者的创建对象期间,可能有其他线程同步调用getInstance(),那么它们也会通过if进入到同步块试图拿锁,因为有第一个线程已经拿到锁,所以该线程会等待第一个线程释放锁,从而阻塞。
这样的话,当首个拿锁者完成了对象创建,之后的线程都不会通过第一个if了,而这期间阻塞的线程开始唤醒,它们则需要靠第二个if语句来避免再次创建对象。
为什么对象要加volatile修饰
因为animal=new Animals();不是一个原子性操作
步骤
- 1、分配内存空间
- 2、执行构造方法,初始化对象
- 3、把这个对象指向这个空间
正常期望顺序是123,而如果指令重排按照132 执行时,A线程还没有完成构造,但B线程会发现已经有对象指向一片内存空间,即发现有对象,所以会返回。所以为了避免指针重排对象的声明还需要加volatile
5. 静态内部类单例
public class Holder {
private Holder(){
}
//内部类
private static class InnerClass(){
private static final Holder HOLDER = new Holder();
}
private static Holder getInstance(){
return InnerClass.HOLDER;
}
}
第一次加载Holder类时,并不会实例化HOLDER ,只有第一次调用getInstance方法时,Java虚拟机才会去加载Holder类,继而延时实例化HOLDER
十九、CAS
1.什么是CAS
CAS–>compareAndSet:比较并交换
CAS :比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环!
缺点:
1、 循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题
public class CASDemo {
public static void main(String[] args){
//AtomicInteger原子性Integer 设置初始值
AtomicInteger atomicInteger = new AtomicInteger(2020);
//public final boolean compareAndSet(int expect, int update) 期望,更新
//是否达到期望,达到更新,要么不更新,CAS是CPU的并发原语
System.out.println(atomicInteger.compareAndSet(2020, 2021));//2021
System.out.println(atomicInteger.get());
//此时期望修改为2021,我们的初始值是2020,没有达到期望,所以不更新期望还是2021
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
2.unsafe类
3.ABA问题
public class CASDemo {
public static void main(String[] args){
//AtomicInteger原子性Integer 设置初始值
AtomicInteger atomicInteger = new AtomicInteger(1);
//public final boolean compareAndSet(int expect, int update) 期望,更新
// ABA 线程1不知道线程2修改过资源
//========================线程2================================
System.out.println(atomicInteger.compareAndSet(1, 3));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(3, 1));
System.out.println(atomicInteger.get());
//========================线程1================================
System.out.println(atomicInteger.compareAndSet(1, 2));
System.out.println(atomicInteger.get());
}
}
二十、原子引用
解决ABA问题,引入原子引用—>增加版本号
带版本号的原子操作!
AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
public class CASDemo2 {
public static void main(String[] args){
//AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
//正常在业务操作,这里面比较的都是一个个对象
AtomicStampedReference<Integer> ar = new AtomicStampedReference<>(1,1);
new Thread(()->{
int stamp = ar.getStamp();//获得当前版本号 1
System.out.println("A1---->"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//1改为3
System.out.println("A是否修改数据:"+ar.compareAndSet(1, 3,
ar.getStamp(), ar.getStamp() + 1));
System.out.println("A2---->"+ar.getStamp());//修改后的版本号 2
//3修改回去1
System.out.println("A是否修改数据:"+ar.compareAndSet(3, 1,
ar.getStamp(), ar.getStamp() + 1));
System.out.println("A3---->"+ar.getStamp());//修改后的版本号 1
},"A").start();
//乐观锁的原理相同
new Thread(()->{
int stamp = ar.getStamp();//获得版本号
System.out.println("B1---->"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B是否修改数据:"+ar.compareAndSet(1, 6,
ar.getStamp(), ar.getStamp() + 1));
System.out.println("B2---->"+ar.getStamp());
},"B").start();
}
}
A1修改了初始值,又重新改回去,但B不知道(ABA)
注意:Integer使用了对象缓存机制,默认范围是-128 ~ 127 ,推荐使用静态工厂方法valueOf获取对象实例,而不是new,因为valueOf使用缓存,而new 一定会创建新的对象分配新的内存空间
二十一、各种锁
1.公平锁、非公平锁
公平锁:非常公平,不能够插队,必须先来后到!
Lock lock = new ReentrantLock(true);
非公平锁:非常不公平,可以插队( 默认都是非公平)
Lock lock = new ReentrantLock();
2.可重入锁
也叫递归锁,拿到了外面的锁之后,就可以拿到里面的锁,自动获得
synchronized 可重入锁:获得外层的锁,自动获取到里面的锁
/**
*synchronized 可重入锁
**/
public class Demo {
public static void main(String[] args){
Phone phone = new Phone();
new Thread(()->{
phone.sendMs();
},"线程A").start();
new Thread(()->{
phone.sendMs();
},"线程B").start();
}
}
class Phone{
public synchronized static void sendMs() {
System.out.println(Thread.currentThread().getName()+"发消息");
call();
}
public synchronized static void call() {
System.out.println(Thread.currentThread().getName()+"打电话");
}
}
lock锁:需要拿两把锁
public class Demo2 {
public static void main(String[] args){
Phone2 phone = new Phone2();
new Thread(()->{
phone.sendMs();
},"线程A").start();
new Thread(()->{
phone.sendMs();
},"线程B").start();
}
}
class Phone2{
Lock lock = new ReentrantLock();
public void sendMs() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"发消息");
call();
} finally {
lock.unlock();
}
}
public void call() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"打电话");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
3.自旋锁(spinLock)
3.1什么是自旋锁
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
3.2自定义实现自旋锁
利用原子引用类(AtomicReference)自定义实现的自旋锁 new 一个原子引用类,对象类型为Thread
public class SpinLockDemo3 {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void Lock(){
//获取当前线程
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"准备上锁");
//利用CAS自旋锁
//compareAndSet(null,current),如果需要读写的值不是null(null即无锁状态)
// 那么返回false;thread(当前线程)获取锁。
while(!atomicReference.compareAndSet(null,thread)){
//Do nothing
}
}
//解锁
public void unLock() {
//获取当前线程
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "已解锁");
//自旋锁
atomicReference.compareAndSet(thread, null);
}
}
测试
public class SpinLockTest {
public static void main(String[] args) throws InterruptedException {
//创建自旋锁(CAS)
SpinLockDemo3 lock = new SpinLockDemo3();
new Thread(() -> {
lock.Lock();
try {
//阻塞5s
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unLock();
}
}, "线程1").start();
//阻塞是为了先让线程1先拿到锁
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
lock.Lock();
try {
//阻塞2s
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unLock();
}
}, "线程2").start();
}
}
lock()方法利用的CAS,当第一个线程1获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程1没有释放锁,另一个线程2又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到1线程调用unlock方法释放了该锁。
4.死锁
4.1 线程死锁是指:多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此此程序不可能正常终止。
如下图所示,有两个线程A,B,两个资源A锁,B锁,线程A持有的资源A锁,线程B持有B锁,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
测试代码
public class DeadLockDemo {
public static void main(String[] args){
String lockA = "lockA";
String lockB = "lockB";
new Thread(new MyThread(lockA,lockB),"线程A").start();
new Thread(new MyThread(lockB,lockA),"线程B").start();
}
}
class MyThread implements Runnable{
//创建两个资源
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+"拥有->"+lockA+" want to get "+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"拥有->"+lockB+" want to get "+lockA);
}
}
}
}
4.2产生死锁的四个条件:
(1).互斥条件:该资源任何一个时刻只由一个线程占有。
(2).请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3).不剥夺条件:线程已获得的资源在未使用之前不能被其他线程强行剥夺,只能自己使用完后才释放资源。
(4).循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
自私,不让,不准抢,必须轮流
4.3如何避免死锁?
上面说了死锁的四个必要条件,为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了。现在我们来挨个分析一下:
(1).破坏互斥条件:这个条件无法破坏,因为我们用锁本来就是让他们单独拥有
(2).破坏请求与保持条件:一次性把所有进程需要的资源全部拿走。这样就不会在运行的途中进行再去申请资源了。
(3).破坏不剥夺条件:以退为进。当某个线程申请不到资源的时候,把自己拥有的资源都释放
(4).破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。