JAVA面经整理(4)

一)Volitaile关键字的作用:

volatile的使用:常常用于一写多读的情况下,解决内存可见性和指令重排序

JAVA内存的JMM模型:主要是用来屏蔽不同硬件和操作系统的内存访问差异的,在不同的硬件和不同的操作系统内存的访问是有差异的,这种差异会导致相同的代码在不同的硬件和操作系统会有不同的行为,JMM内存模型就是为了解决这个差异,统一相同代码在不同硬件和不同操作系统的差异的

JAVA内存模型规定:所有的变量(包括普通成员变量和静态成员变量),都是必须存储在主内存里面,每一个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行,线程是不可以直接读写主内存的变量

JAVA面经整理(4)_第1张图片

但是Java的内存模型会带来一个新的问题,那就是说当某一线程修改了主内存共享变量的值之后,那么其他线程可能就不会感知到此值被修改了,它会一直使用工作内存的旧值,这样程序的执行就不会符合我们的预期了

内存可见性:指的是多个线程同时进行操作同一个变量,其中某一个线程修改了变量的值之后,其他线程无法进行感知变量的修改,这就是内存可见性问题

关键字volitaile和synchronized就可以强制保证接下来的操作是在操作内存,在生成的java字节码中强制插入一些内存屏障的指令,这些指令的效果,就是强制刷新内存,同步更新主内存和工作内存中的内容,在牺牲效率的时候,保证了准确性

JAVA面经整理(4)_第2张图片

synchronized,双重if,volatile

指令重排序是指编译器或者CPU优化程序的一种手段,调整指令执行的先后顺序,提高程序的执行性能,但是在多线程情况下会出现问题

1)之前咱们在说volatile的时候是说,此处的volatile是为了保证让其他线程修改了这里面的instance之后,保证后面的线程可以及时感知到修改,因为其他线程不也是加上synchronized来进行修改的吗?

2)当我们去执行instance=new instance()的时候,我们本质上干了三件事情

2.1)创建内存

2.2)针对内存空间进行初始化

2.3)把内存的地址赋值给引用

3)上面的这三个步骤可能会触发指令重排序,也就是说乱序执行,这里的执行顺序,可能是1,2,3,也可能是1,3,2,可能就是说把地址空间赋给引用了,然后再进行初始化;

JAVA面经整理(4)_第3张图片

咱们加上了volatile就可以保证这里面的指令就是按照1,2,3的顺序来进行执行的,保证其他线程拿到的实例也是一个完整的实例

  private Singleton(){};
    private static Singleton singleton=null;
    public static Singleton GetInstance(){
        if(singleton==null){
            synchronized(Object.class){
                if(singleton==null){
                    singleton=new Singleton();
                }
            }
        }
        return singleton;
    }
}

JAVA面经整理(4)_第4张图片

单例模式适用于经常被访问的对象

或者是创建和销毁需要需要进行调用大量资源和时间的对象

1)创建一个私有的构造方法:防止外部直接new破坏单例模式

2)创建一个私有变量static保存该单例对象

3)提供公开的static方法返回单例对象

饿汉模式:在类加载的时候直接创建并进行初始化对象,在程序启动的时候只进行加载一次

实现简单,不存在线程安全问题,但是因为类加载的时候就创建了该对象

创建之后如果没有进行使用,那么就造成了资源浪费,依赖的是classLoader机制

懒汉模式:延迟加载,只有被使用的时候,才会被初始化

枚举:在第一次被使用的时候,才可以被JAVA虚拟机进行加载并初始化,所以他也是线程安全,并且是懒加载

enum TestEnum{//不要加class
    RED,Blue;//加上分号
    public static TestEnum GetInstance(){//返回类型是你自定义的类名,不是enum
        return RED;
    }
}
二)synchronized的底层实现原理: 

synchronized底层是通过JVM内置的监视器锁来实现的,而监视器锁有是依靠于操作系统的底层mutex互斥量来实现的,进入到synchronized修饰的代码,相当于加了moniterenter,结束synchronized修饰的代码,相当于是moniterexit

JAVA面经整理(4)_第5张图片

监视器:监视器是一种机制,用来进行保障任何时候,都只有一个线程来进行执行指定区域的代码

1)一个监视器就类似于一个建筑,建筑里面有一个特殊的房间,这个房间同一时刻只能被一个线程所占有,一个线程从进入到该房间到离开该房间,可以全程占有该房间的所有数据;

2)进入该建筑叫做进入监视器,进入该房间叫做获得监视器,独自占有该房间叫做拥有监视器,离开该房间叫做释放监视器,离开该建筑叫做退出监视器

JAVA面经整理(4)_第6张图片

synchronized修饰的代码块,进入到代码块被moniterenter,然后退出代码块moniterexit

监视器锁就是类似于一个房间,同一时刻只会允许一个人进来,在任何时候都是只能有一个人进来,是依靠ObjectMoniter实现的

1)_recursions是某一个线程某一次重复获取到锁的次数,可重入锁代表某一个线程可以重复的获取锁,因为synchronized是可重入锁,线程是可以重复的获取到这把锁,那么某一个线程每一次获取到锁的时候,计数器就会记录该线程和获取到锁的次数,每获取到一次锁,进入到这个房间,_recursions++,每当离开这个房间一次,那么这个计数器就--,当_recursions=0的时候,说明此时这个监视器是没有人的,就放开房间让其他线程进入

2)count记录每一个线程获取到锁的次数,就是前前后后这个这个线程一共获取这把锁多少次

3)_owner:The Owner的拥有者,是持有该ObjectMonitor监视器对象的线程;

4)_EntryList:EntryList监控集合,存放的是处于阻塞状态的线程队列,在多线程情况下,竞争失败的线程会进入到EntryList阻塞队列;

5)WaitSet:存放的是处于wait状态的线程队列,当线程拥有监视器锁得时候调用到了wait()方法之后,会自动释放监视器锁,this.owner=null,释放监视器锁的线程会进入到waitSet队列,

监视器的执行流程如下:

1)线程通过CAS(对比并进行替换)尝试获取该锁,如果获取成功,那么将owner字段设置成当前线程,表明该线程已经持有这把锁,并将_recursions冲入次数的属性+1,如果获取失败就先通过自旋CAS来进行获取该锁,如果还是失败那么就把当前线程放入到EntryList监测队列,进入到阻塞状态;

2)当拥有锁的线程执行了wait方法之后,调用wait的线程释放锁,将owner变量设置成null状态,同时把该线程放入到waitSet带授权队列中等待被唤醒;

3)当调用某一个拥有监视器锁的线程调用notify方法时,随机唤醒WaitSet队列中的某一个线程来尝试获取锁,等待拥有监视器锁的调用notify的线程释放锁后,当调用notifyAll时随机唤醒所有WaitSet的队列的线程尝试获取该锁;

4)当拥有监视器的线程执行完了释放锁之后,会唤醒EntryList中所有线程尝试获取到该锁;

JAVA面经整理(4)_第7张图片 JAVA面经整理(4)_第8张图片

wait方法也是可以指定休眠时间的,比如说现在有两个线程,线程1进入到了synchronized修饰的方法之后,调用wait方法的那一刻,线程1会放弃synchronzied的那把锁,线程1从进入到waitting状态,线程2获取到了同一把锁,然后执行对象的notifyAll方法,执行完线程2的synchronized方法之后线程2释放锁,然后去尝试唤醒所有wait的线程,然后所有的wait的线程都去尝试争夺这同一把锁,但是如果是线程2调用的是notify方法,然后其他wait的线程只会被唤醒一个,然后尝试获取到锁执行;

三)说一说synchronized锁升级的流程:

偏向锁,指的是偏向某一个线程,指的是所有的线程来了之后会进行判断,对象头中的头部保存当前拥有的锁的线程ID,判断当前线程ID是否等于_owner的线程ID,等于说明你拥有这个线程,就可以进入执行

1)无锁:刚一开始的时候,没有线程访问synchronized修饰的代码,说明此时是处于无锁状态

2)偏向锁:当某一个线程第一次访问同步代码块并获取到这把锁的时候,锁的对象头里面将线程的ID记录下来,下一次再有线程过来的时候,程序会直接判断对象头中的线程ID(第一次访问锁的线程ID)和实际访问程序的线程ID是否相同,如果是同一个,那么程序会继续向下访问,如果不相同,说明有两个线程以上进行争夺锁,于是尝试通过CAS获取到这把锁,如果获取不到,就升级成轻量级锁

3)轻量级锁:这个还没有放弃挣扎,还会通过自旋的方式尝试得到锁,如果通过一定的次数得不到锁,因为synchronized是自适应自旋锁,synchronized是根据上一次自旋的结果来去决定这一次自旋的次数的,如果这个线程是通过上一次自旋来获取到锁的话那么会有极大的大概率这一次也是可能通过自旋的方式来获取到锁的,如果上一次获取次数也比较少,那么这一次自旋的次数也会变少,如果一定的自旋次数获取不到锁,直接阻塞到EntryList

4)重量级锁:升级成重量级锁

四)synchronized是固定自旋次数吗?

synchronized本身是一个自适应自旋锁,自适应自旋锁指的是线程尝试获取到锁的次数不是一个固定值而是一个动态变化的值,这个值会根据前一次线程自旋的次数获取到锁的状态来决定此次自选的次数,比如说上一次通过自选成功的获取到了锁,那么synchronized会自动判断通过这一次自旋获取到锁的概率也会大一些,那么这一次自旋的次数就会多一些,如果通过上一次自旋没有成功获取到锁,那么这一次成功获取到锁的概率也会变得非常低,所以为了避免资源的浪费,就会少循环或者是不循环,简单来说就是如果这一次自旋成功了,下一次自旋的次数会多一些,否则下一次自选的次数会少一些

五)线程通讯的方法都有哪些?

线程通讯指的是多个线程之间通过某一种机制进行协调和交互,例如线程等待和通知机制就是线程通讯的主要手段之一,就是一个线程休眠了,另外一个线程进行唤醒,每一个等待唤醒的手段都是有着不同的应用场景,下一个唤醒手段就是上一个唤醒手段的补充

1)wait和notify使用必须和synchronized搭配一起使用,况且wait会主动释放锁;

2)可以唤醒加了同一把锁下面的两个不同的线程组,Condition可以有更多的分支,能唤醒的更加精准,每一组线程都可以使用一个Condition来进行等待和唤醒,生产者不要唤醒生产者消费者不要唤醒消费者,在生产者里面可以调用消费者的Condition2进行唤醒

3)可以指定某一个线程来唤醒,LockSupport.park()休眠当前线程LockSupport.unpark(线程对象),LockSupport可以不搭配synchronized和lock来结合使用

JAVA面经整理(4)_第9张图片

JAVA面经整理(4)_第10张图片

2)一个lock可以创建多个Conidtion此时就可以调用Condition的await()方法和signal()方法

一个Lock可以创建多个Condition对象,搞一个Condition叫做生产者,再Condition搞一个叫做消费者,可以有更多的分支,唤醒就变的更加的精准,每一组线程可以使用一个Condition来进行等待和唤醒的操作,分两组绑定Condition;

2.1)一堆生产者可以使用一个Condtion对象1来进行唤醒,可以使用Condition对象1调用await()方法进行休眠生产者,如果想要唤醒生产者,就可以调用Condition对象1的signal来唤醒生产者

2.2)一堆消费者可以使用一个Condtion对象2来进行唤醒,可以使用Condition对象2调用await()方法进行休眠消费者,如果想要唤醒消费者,就可以调用Condition对象1的signal来唤醒消费者

2.3)但是生产者和消费者加的都是同一把锁,这样使用Condition类就可以唤醒加了同一把锁的两组线程进行唤醒了,可以指定的某一组线程中的某一个线程进行唤醒

但是两堆生产者和消费者都是加的同一把锁,所以就可以根据哪一个Condition对象来唤醒的是生产者还是消费者,也是随机唤醒,但是也是可以指定唤醒那一组,是生产者还是消费者,但是wait和notify一个锁,一个对象只能有一组,同时生产者也是可以调用消费者的一个Condition进行唤醒了

1)现在有一个生产者消费者模型,生产者会产生一些任务存放到任务队列中,消费者是从任务队列中取出任务进行消费执行,生产者和消费者都是一组线程;

2)没有任务,生产者休眠,为了保证资源不被浪费,消息队列没有任务,消费者也会休眠,假设生产者线程组的某一个生产者有任务开始就开始被唤醒将任务放到消息队列里面此时被唤醒的生产者将任务推动到消息队列里面,第二步就是休眠唤醒消费者去消费任务,如果此时使用的是Object中的唤醒机制,是将加了锁的线程随机唤醒,此时就会发生严重的问题,此时可能唤醒的是生产者和消费者,因为生产者和消费者加的是同一把锁,如果是唤醒的是生产者,此时会浪费资源,可能会导致消费者永远也不会消费消息队列中的元素

你可能感兴趣的:(java,开发语言)