• JAVA面经整理(4)


    一)Volitaile关键字的作用:

    1)保证多线程环境下共享变量的可见性,对于一个线程对于一个共享表变量的修改,其他线程可以立即看到修改之后的共享变量的值

    2)可以增加内存屏障来放置多个指令之间的重排序

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

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

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

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

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

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

    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,可能就是说把地址空间赋给引用了,然后再进行初始化;

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

    1. private Singleton(){};
    2. private static Singleton singleton=null;
    3. public static Singleton GetInstance(){
    4. if(singleton==null){
    5. synchronized(Object.class){
    6. if(singleton==null){
    7. singleton=new Singleton();
    8. }
    9. }
    10. }
    11. return singleton;
    12. }
    13. }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    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中所有线程尝试获取到该锁;

     

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

    当数组为空的时候使用volatile初始化的目的是防止有其他线程已经初始化过这个数组(内存可见性),然后就是这个使用CAS设置该节点就是为了防止有别的线程已经设置了该节点,如果发现有人设置了该节点CAS设置失败,直接synchronized给这个哈希桶加锁

    总的来说,就是线程切换或者加锁解锁都是因为需要用户态和内核态的切换,从而导致的开销大

    三)说一说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()休眠当前线程,park和unpark本身就是静态方法,LockSupport.unpark(线程对象),LockSupport可以不搭配synchronized和lock来结合使用,这里面得park方法那个线程调用LockSupport.park()方法,拿一个线程就会阻塞

    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中的唤醒机制,是将加了锁的线程随机唤醒,此时就会发生严重的问题,此时可能唤醒的是生产者和消费者,因为生产者和消费者加的是同一把锁,如果是唤醒的是生产者,此时会浪费资源,可能会导致消费者永远也不会消费消息队列中的元素

    1. public class DemoWorld {
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread t1=new Thread(()->{
    4. System.out.println("线程1开始阻塞");
    5. LockSupport.park();
    6. System.out.println("线程1继续执行");
    7. });
    8. Thread t2=new Thread(()->{
    9. System.out.println("线程2开始阻塞");
    10. LockSupport.park();
    11. System.out.println("线程2继续执行");
    12. });
    13. Thread t3=new Thread(()->{
    14. LockSupport.unpark(t1);
    15. });
    16. t1.start();
    17. t2.start();
    18. Thread.sleep(3000);
    19. t3.start();
    20. }
    21. }
    六)读写锁:创建读写锁,提高程序的执行性能,适用于读多写少

    读写锁是将一把锁分成两部分,读锁和写锁,读锁是允许多个线程同时获得的,因为读操本身就是线程安全的,而写锁是互斥锁,是不允许多个线程同时获得些写锁的,况且写操作和读操作也是互斥的,读读不互斥,写写互斥,读写互斥

    1)提高了程序执行的性能,多个读锁可以同时进行,相对于普通锁来说在任何情况下都要排队执行来说,读写锁提高了并发程序的执行性能

    2)避免读到临时数据,读锁和写锁是互斥排队执行的,这样就保证了读取操作不会读到写一半的临时数据

    多个线程获取到读锁,称之为读读不互斥,一个线程不能同时获取到读锁和写锁,写锁和写锁之间进行互斥

    1)读读不互斥

    1. public static void main(String[] args) throws InterruptedException {
    2. final ReentrantReadWriteLock commonLock=new ReentrantReadWriteLock();
    3. final ReentrantReadWriteLock.ReadLock readLock=commonLock.readLock();//获取到读写锁中的读锁
    4. final ReentrantReadWriteLock.WriteLock writeLock= commonLock.writeLock();//获取到读写锁中的写锁
    5. Thread t1=new Thread(()->{
    6. try {
    7. readLock.lock();
    8. System.out.println("线程1获取到了读锁");
    9. }finally {
    10. readLock.unlock();
    11. System.out.println("线程1释放了读锁");
    12. }
    13. });
    14. Thread t2=new Thread(()->{
    15. try {
    16. readLock.lock();
    17. System.out.println("线程2获取到了读锁");
    18. }finally {
    19. readLock.unlock();
    20. System.out.println("线程2释放了读锁");
    21. }
    22. });
    23. t1.start();
    24. t2.start();
    25. }

    2)读写互斥,可以看到一个线程不能同时获取到读写锁中的读锁和写锁

    1. public static void main(String[] args) throws InterruptedException {
    2. final ReentrantReadWriteLock commonLock=new ReentrantReadWriteLock();
    3. final ReentrantReadWriteLock.ReadLock readLock=commonLock.readLock();//获取到读写锁中的读锁
    4. final ReentrantReadWriteLock.WriteLock writeLock= commonLock.writeLock();//获取到读写锁中的写锁
    5. Thread t1=new Thread(()->{
    6. try {
    7. writeLock.lock();
    8. System.out.println("线程1获取到了写锁");
    9. }finally {
    10. writeLock.unlock();
    11. System.out.println("线程1释放了写锁");
    12. }
    13. });
    14. Thread t2=new Thread(()->{
    15. try {
    16. readLock.lock();
    17. System.out.println("线程2获取到了读锁");
    18. }finally {
    19. readLock.unlock();
    20. System.out.println("线程2释放了读锁");
    21. }
    22. });
    23. t1.start();
    24. t2.start();
    25. }

    1. public class DemoWorld {
    2. public static void main(String[] args) throws InterruptedException {
    3. final ReentrantReadWriteLock commonLock=new ReentrantReadWriteLock();
    4. final ReentrantReadWriteLock.ReadLock readLock=commonLock.readLock();//获取到读写锁中的读锁
    5. final ReentrantReadWriteLock.WriteLock writeLock= commonLock.writeLock();//获取到读写锁中的写锁
    6. Thread t1=new Thread(()->{
    7. try {
    8. writeLock.lock();
    9. System.out.println("线程1获取到了写锁");
    10. }finally {
    11. writeLock.unlock();
    12. System.out.println("线程1释放了写锁");
    13. }
    14. });
    15. Thread t2=new Thread(()->{
    16. try {
    17. writeLock.lock();
    18. System.out.println("线程2获取到了写锁");
    19. }finally {
    20. writeLock.unlock();
    21. System.out.println("线程2释放了写锁");
    22. }
    23. });
    24. t1.start();
    25. t2.start();
    26. }
    27. }

    七)公平锁和非公平锁有什么区别? 

    公平锁:每一个线程获取到锁的的顺序总是按照线程访问锁的先后顺序来进行获取的,最前面访问锁的那个线程总是能最先获取到锁

    非公平锁:每一个线程获取锁的顺序是随机的,并不会遵循先来后到的规则,所有线程会竞争并获取锁

    公平锁的运行原理:

    3.1)获取到锁的时候,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会先唤醒等待队列的队首的线程去获取到锁,锁的使用顺序就是队列中的先后顺序,在整个过程中,线程会从运行状态切换成休眠状态,再从休眠状态变成运行状态

    3.2)在整个过程中,线程每一次休眠和恢复都需要进行用户态和内核态的切换,这个状态的转换是比较慢的,更注重的是资源的平均分配,但是按序唤醒线程的开销比较大,所以公平锁执行效率比较慢

    非公平锁的运行原理:

    4.1)当线程尝试获取到锁的时候,会先通过CAS来进行尝试获取到锁,如果获取到锁就直接拥有锁,如果锁获取失败就进入到阻塞队列,等待下一次获取到锁,获取到锁不用遵循先来后到的规则,避免线程恢复和休眠的操作,加速了程序的执行效率,不用遵循先来先到的规则

    4.2)非公平锁的吞吐率(单位时间内获取到锁的速率)要比公平锁的概率更高,但是可能会出现线程饿死的情况,资源分配随机性比较强,非公平锁性能更高,非公平锁可能出现线程饥饿的情况

    八)JUC包下面的Exchange交换器:实现两个线程之间的数据交换的

    1)exchange(V x):等待另一个线程到达此交换点,然后将对象传输给另一个线程,并从另一个线程中得到交换的对象,如果另一个线程未到达交换点,那么调用exchange得线程会一直进行休眠除非遇到了线程中断;

    2)exchange(V x,long timeout,Timeunit unit):等待另一个线程到达交换点,然后将这个对象传输给另一个线程了,并从另一个线程中得到要交换的对象,如果说另一个线程未达到次交换点,那么此线程会一直进行休眠,直到遇到了线程中断,或者等待的时间超过了设定的时间,那么会直接抛出异常;

    3)也就是说exchange方法到达了一个交换点之后,线程会在这个交换点进行休眠等待,直到另一个线程也调用了exchange方法,他们会进行相互交换数据,然后会执行后续的代码

    4)Exchange是用来实现两个线程之间的数据交换的,它可以进行传输任意类型的数据,只需要在进行创建的时候定义泛型类型就可以了,它的核心方法是exchange方法

    当线程执行到这个方法之后,当前线程会执行休眠操作,会进行等待另一个线程进行这个交换点,如果说另一个线程进入到了交换点,那么两者会进行交换数据,并执行接下来的流程

    1. class Person{
    2. public String username;
    3. public String desc;
    4. }
    5. public class DemoWorld {
    6. public static void main(String[] args) throws InterruptedException {
    7. Exchanger exchanger=new Exchanger<>();
    8. Thread t1=new Thread(()->{
    9. Person person1=new Person();
    10. person1.username="线程1";
    11. person1.desc="我是在t1线程创建的,现在过得很好";
    12. try {
    13. Thread.sleep(1000);
    14. Person person=exchanger.exchange(person1);//此时交换完成之后获取到了线程2的person2
    15. Thread.sleep(1000);
    16. System.out.println("当前打印的线程是"+Thread.currentThread().getName()+person.username+person.desc);
    17. } catch (InterruptedException e) {
    18. e.printStackTrace();
    19. }
    20. },"线程1");
    21. Thread t2=new Thread(()->{
    22. Person person2=new Person();
    23. person2.username="线程1";
    24. person2.desc="我是在t1线程创建的,现在过得很好";
    25. try {
    26. Thread.sleep(1000);
    27. Person person=exchanger.exchange(person2);//此时交换之后获取到了线程1中的person1
    28. Thread.sleep(1000);
    29. System.out.println("当前打印的线程是"+Thread.currentThread().getName()+person.username+person.desc);
    30. } catch (InterruptedException e) {
    31. e.printStackTrace();
    32. }
    33. },"线程2");
    34. t1.start();
    35. t2.start();
    36. }
    37. }

    九)进程和线程有什么区别?

    为什么进程之间是相互独立的?不能访问相互的资源和文件?

    为什么屏蔽进程之间内存的获取和共享,有一些敏感的进程,不能让其他非当前进程来进行访问,比如说我打开一个进程,一个工商银行,不能让其他进程能访问我的私密信息,这就是为什么进程之间不可以相互访问,保护隐私,进程就好比一家公司,线程就是公司里面的一个一个的员工

    上下文,状态,优先级,记账信息不共享,操作系统的调度器会非常频繁的进行线程切换,哪怕某个进程做某个工作做了一半,也有可能被打断;

    单个CPU已经达到极限了,多核CPU代替单核CPU

    在代码执行任务的时候,先把任务进行拆分,又有多个CPU来并发式的执行

    什么情况下会造成线程从用户态到内核态的切换呢?

    1)首先,如果在程序运行过程中发生中断或者异常,系统将自动切换到内核态来运行中断或异常处理机制

    2)此外,程序进行系统调用也会从用户态切换到内核态

    1)进程包含线程,如果将进程比作工厂,那么线程就是工厂中的若干流水线

    2)创建线程比创建进程更轻量,销毁线程比销毁进程更轻量,调度线程比调度进程更轻量

    3)切换速度不同:线程切换上下文速度是很快的,但是进程的上下文切换速度比较慢

    4)操作系统创建进程,要给进程分配资源,进程是操作系统进行资源分配的最小单位,操作系统创建的线程,是要在CPU上面进行调度执行,线程是操作系统进行调度执行的最小单位

    5)进程具有独立性,进程与进程之间资源不共享,每一个进程都有自己的虚拟地址空间,同一个进程的多个线程之间,共用这一块虚拟地址空间,一个进程挂了,不会影响到其他进程,但是同一个进程的多个线程,是在用同一个虚拟内存空间,一个线程挂了,是可能影响到其他线程的,甚至可能会导致整个进程崩溃

    十)start和run方法有什么区别? 

    run只是一个普通的方法,描述了任务的内容,start是一个特殊的方法,会在系统中创建线程

    1)方法性质不同:调用start方法可以直接启动线程,并使线程进入就绪,当run方法执行完了,线程,也就结束了,但是如果直接执行run方法,会当作普通方法来调用,还是在main方法进行的,不会创建一个新线程;

    2)执行速度不同:run方法也叫作线程体,它里面包含了具体要执行的业务代码,当进行调用run方法的时候,会立即执行run方法的代码,但是当我们调用start方法的时候,本质上是启动了一个线程并将这个线程的状态设置为就绪状态,也就是说调用start()方法,程序不会立即执行

    3)调用次数不同:run方法是普通方法,普通方法是可以被调用多次,但是start方法是创建新线程执行任务,而start方法只能调用一次,否则就会出现IllegalThreadStateException非法线程状态

    Start()方法会改变线程的状态,从NEW状态编程running状态,futureTask属于同步阻塞

    为什么start方法只能调用一次呢?

    原因是当start代码实现的第一行,会先进行判断当前的状态是不是0,也就是说是否是新建状态,如果不是新建状态NEW,那么就会抛出IllegalThreadStateException非法线程状态异常

    当线程调用了第一个start方法之后,线程的状态就会由新建状态NEW变成RUNNABLE状态,此时再次调用start方法,JVM就会判断当前线程已经不等于新建状态了,从而会抛出IllegalThreadStateException异常,所以线程状态是不可逆的;

    1. public static void main(String[] args) throws IOException, InterruptedException {
    2. Thread thread=new Thread(){
    3. public void run(){
    4. System.out.println(Thread.currentThread().getName()+"正在执行");
    5. }
    6. };
    7. thread.start();//thread.run()
    8. thread.sleep(1000);
    9. System.out.println("main线程正在执行");
    10. }
    十二)synchronized的三种用法

    1)修饰普通方法:加在访问修饰限定符,方法返回值之间

    public synchronized void method(){};修饰普通方法,作用的对象是调用这个方法的对象

    2)修饰静态方法:public static synchronized void staticMethod{};,当synchronized修饰静态方法的时候,锁的是类对象,这个锁对于所有调用这个锁的对象都是互斥的:注意,当修饰静态方法的时候,所有调用这个静态方法的对象都是互斥的,但是普通方法是指对对象级别的,不同的对象有着不同的锁

    3)修饰代码块:在我们的日常开发中,最常用的是给代码块加锁,而不是给方法进行加锁,因为给方法进行加锁,相当于是给整个方法全部进行加锁,这样的话锁的粒度就太大了,程序的执行性能就会受到影响,加锁的对象常用this或者xxx.class这样的形式来进行表示

    十三)线程的中断:

    线程的中断,核心就是让线程的入口函数也就是run方法执行完毕,它指的是内存中的线程结束了,而不一定是Thread对象销毁;

    在JAVA中停止线程有三种方法:

    1)使用标志位,在程序的执行代码中使用一个标志位来控制程序的执行,当标志位是true的时候,线程可以继续执行,当标志位是false的时候,线程退出循环或者执行完任务之后停止,所以可以通过设置标志位来停止线程的执行,缺点就是线程中断的不够及时,因为在线程执行过程中无法调用while(flag)来及时判断线程是否处于终止状态,只能在下一轮中进行判断是否要终止当前线程,所以中断线程不及时

    2)调用interrupt方法来中断线程的执行,当线程被中断的时候,线程本身会受到一个中断信号,可以在代码中检查线程的中断状态并进行处理,线程在接收到中断指令之后,立即中断了线程,相比于上一种自定义中断标识符的方法来说,它能更及时的响应中断线程指令

    3)stop方法,但是现在已经是一种被弃用的方法了,是一个非安全的方法,因为他可能会导致线程的资源不会被正确的释放,可能会导致资源泄露等问题

    十四)wait和sleep有什么区别? 

    wait方法和sleep方法都是用来将线程进入到休眠状态的,并且咱们的sleep方法和wait方法都是可以响应interrupt中断,也就是说在线程进行休眠的过程中,如果收到interrupt的中断信号,都可以进行响应并进行中断,并且都可以抛出InterruptedException异常

    1)wait 方法属于 Object 类的方法,而 sleep 属于 Thread 类的方法

    2)语法使用不同,wait必须和synchronized一起进行搭配使用,否则就会抛出IIIegalMonitorStateException异常,而sleep无需和synchronized一起使用;

    1. Object object=new Object();
    2. System.out.println("wait前");
    3. object.wait();
    4. System.out.println("wait执行完成之后");

    Exception in thread "main" java.lang.IllegalMonitorStateException

    3)wait会自动进行释放锁,调用wait的线程会主动进入到waitset队列里面,但是sleep不会主动释放锁,sleep在休眠状态并不会释放锁;

    4)调用sleep方法会自动进入到time-waitting状态,但是调用wait方法会进入到waitting状态

    5)等待机制:sleep是指定一个固定的时间去进行阻塞等待,wait既可以指定时间,又可以无限进行等待

    6)唤醒机制:wait唤醒是可以通过notify机制或者interrupt或者时间到来进行唤醒,sleep通过时间到或者interrupt来唤醒;

    7)方法设计初衷:wait的作用主要是为了协调线程之间的先后顺序,这样的场景并不适合sleep,sleep只是为了让线程休眠,并不会涉及到多个线程之间的配合

    1. public static void main(String[] args) {
    2. Object locker=new Object();
    3. Thread t1=new Thread(()->{
    4. synchronized (locker){
    5. try {
    6. System.out.println("线程1获取到了锁,开始调用wait方法等待");
    7. locker.wait();
    8. System.out.println("线程1结束等待又重新获取到了锁");
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. }
    12. }
    13. });
    14. Thread t2=new Thread(()->{
    15. System.out.println("请输入一个整数");
    16. Scanner scanner=new Scanner(System.in);
    17. int num=scanner.nextInt();
    18. synchronized (locker){
    19. locker.notify();
    20. }
    21. });
    22. t1.start();
    23. t2.start();
    24. }
    1. public static void main(String[] args) {
    2. Object object=new Object();
    3. Thread t1=new Thread(()->{
    4. synchronized (object){
    5. try {
    6. object.wait();
    7. } catch (InterruptedException e) {
    8. e.printStackTrace();
    9. }
    10. }
    11. });
    12. }
    13. 1)在上面的这个代码中,wait那个对象,就需要针对哪一个对象来进行加锁
    14. 2)我们synchronized锁住的对象和调用wait的对象是一样子的
    十五)wait操作为什么是原子的?wait和notify为什么一定要搭配synchronized进行使用?

    synchronized加锁的对象和调用wait和notify的对象必须是一致

    wait的操作一共有三步:

    1)wait主动释放当前线程持有的锁

    2)等待当前线程被唤醒

    3)wait不断去尝试重新获取到锁

     

     上面的这个过程没有任何问题,但是下面这个过程就会导致线程1一直阻塞

    1)是为了防止多线程并发过程中,程序的执行混乱问题

    2)咱们现在来进行实现一个阻塞队列,假设wait方法和notify方法不需要进行加锁操作,当我们进行读取数据的时候,如果有数据就会进行返回数据,没有数据就会阻塞等待数据,实现代码如下:

    1. class MyBlockingQueue{
    2. Queue queue=new LinkedList<>();
    3. public void put(String data){
    4. //像咱们的阻塞队列里面加入数据
    5. queue.add(data);
    6. //唤醒线程继续向下执行,此时的唤醒是指唤醒take方法的线程
    7. notify();
    8. }
    9. public String take() throws InterruptedException{
    10. while(queue.isEmpty()){
    11. wait();
    12. }
    13. return queue.remove();
    14. }
    15. }

    上面的程序执行过程分成三步:

    1)线程1执行take方法首先判断当前队列中是否存在数据

    2)如果说当前队列中没有数据,那么执行wait休眠操作(在while循环里面进行执行wait操作,等待进行唤醒)

    3)线程2给队列中添加元素,并唤醒线程1继续执行

    1)上述执行流程是有问题的,假设线程1执行完take方法的时候,进行判断队列为空,刚要执行wait操作进行休眠,但是此时线程2执行put操作突然进行添加数据

    2)然而之前线程1已经执行完判断了,所以就会直接进入到休眠状态,此时线程1一直进行wait操作,此时况且线程2进行插入的数据永远不能被线程1读取,那么就会造成程序并发执行导致执行结果混乱的问题,会导致线程一直进行休眠的问题

    十六)为什么wait被定义在Object类中,而sleep定义在Thread类中 

    1)因为JAVA中每一个对象上都有一把监视器锁,因为每一个对象都可以上锁,这就要求在对象头上要求有一个用来保存锁信息的位置,因为这个锁是对象级别的,而不是线程级别的,wait,notify都是针对于锁级别的操作,他们的锁属于对象,所以定义在Object类中最合适,因为Object是所有对象的父类,因为如果把wait/notify/notifyAll方法定义在Thread类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时wait方法定义到Thread类中,既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程
    2)让某个线程暂停运行一段时间,其控制范围是由当前线程决定,也就是说,在线程里面决定

    两者都可以让线程暂停一段时间,但是本质的区别是一个线程的运行状态控制,一个是线程之间的通讯的问题

    十七)sleep和yield有什么区别?

    sleep和yield都是Thread类中的静态方法,yield是暂停当前执行的线程对象,就是放弃当前拥有的CPU资源,并执行其他线程,就是让当前运行的线程回到就绪状态,也就是可执行状态,就是以保证相同优先级的状态具有执行机会

    1)sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给优先级低的线程以运行的机会,而yield()方法只会给相同优先级或者更高优先级的线程以运行机会,yield(),虽然不会经常用到,让线程主动让出CPU,但是不会改变线程的状态
    2)线程执行sleep()方法后会转入阻塞状态,所以,执行sleep()方法的线程在指定的时间内肯定不会被执行,而yield()方法只是使当前线程重新回到可执行状态,所以执行yield()方法的线程有可能在进入到可执行状态后马上又被执行。
    3)sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;

    线程休眠:sleep

    操作系统是如何管理进程的?是通过一个类来进行描述的,通过一个双向链表来进行组织的,这个说法可以是说针对只有一个线程的进程,是如此的,但是如果说一个进程里面有多个线程,每一个线程里面都有一个PCB,一个进程对应的就是一组PCB;

    1)咱们的上面的这个双向链表属于就绪队列,但是在我们的操作系统内核里面,这样的队列却不是有多个,如果某一个线程调用了sleep方法,这个PCB就会进入到阻塞队列 

    2)咱们的操作系统进行调度线程的时候,就是从就绪队列里面挑选合适的PCB到CPU上面运行,那么我们阻塞队列里面的PCB就只能干等着,啥时候这个PCB可以回到就绪队列里面呢?那么只有说睡眠时间到了,咱们的系统才会把刚才的这个PCB从阻塞队列挪回到就绪队列里面

    3)只有就绪队列里面的PCB才有被调度上CPU执行的权力,阻塞队列里面的PCB没有资格进入到CPU上面执行

    4)一个进程对应的就是一组PCB,每一个PCB上面就有一个字段叫做tgroupID,这个ID其实就是进程的tgroupID,同一个进程中的若干个线程的tgroupID其实是相同,linux的系统内核是不区分进程和线程的,linux内核只认PCB,进程和线程其实是咱们程序员写有关于应用程序的代码才搞出来的词,实际上linux内核只认PCB,在linux系统内核里面我们把线程称之为轻量级进程,只不过是有些PCB共用同一个内存,有些PCB共用同一块虚拟地址空间,有些PCB有不同的虚拟地址空间,前面所说的进程线程概念是站在一个更加抽象的角度,站在用户写代码的角度来进行看待的,但在操作系统内核实现的角度,是一视同仁,使用同样的方式来进行表述的;

    5)咱们的上面的这个链表就是就绪队列的双向链表,如果我们的某个线程调用了sleep方法,这个PCB就会进入到阻塞队列,实际上,咱们的操作系统在进行调度线程的时候,就是从我们的就绪队列中查找合适的PCB到我们的CPU上面执行,当我们的睡眠时间到了,系统就会把刚才这个PCB从阻塞队列挪回到就绪队列,join也会导致线程进入到阻塞队列里面

    使用JAVA来进行打印线程的所有状态: 

    1. public class Teacher {
    2. public static void main(String[] args) {
    3. for(Thread.State value:Thread.State.values()){
    4. System.out.println(value);
    5. }
    6. }
    7. }

    也就是说这些状态是JAVA自己搞出来的,就和操作系统中的PCB的状态没有啥关系

    1. public static void main(String[] args)throws InterruptedException{
    2. Thread t1=new Thread(){
    3. public void run() {
    4. for(int i=0;i<10;i++)
    5. {
    6. // System.out.println(Thread.currentThread().getName());
    7. }
    8. try{
    9. sleep(100);
    10. }catch(InterruptedException e)
    11. {
    12. e.printStackTrace();;
    13. }
    14. System.out.println("线程结束");
    15. }};
    16. System.out.println(t1.getName());//获取线程名字
    17. System.out.println(t1.getPriority());//获线程优先级
    18. System.out.println(t1.isDaemon());//该线程是否为守护线程
    19. System.out.println(t1.getId());
    20. System.out.println(t1.isAlive());
    21. System.out.println(t1.isInterrupted());
    22. System.out.println(t1.getState());//获取到指定线程的状态
    23. t1.start();
    24. while(t1.isAlive()){
    25. System.out.println(t1.getState());
    26. System.out.println(t1.isInterrupted());
    27. }
    28. System.out.println(t1.getState());
    29. }}
    十八)线程的状态有哪些?

  • 相关阅读:
    nuxt3正确使用keepalive页面缓存组件缓存
    Python常见问题
    《网络安全笔记》第二章:Windows基础命令
    企业架构LNMP学习笔记48
    shell脚本下用plot给iostat和CPU的采样画图的写法
    【gock】 无侵入的 Http Mock 工具
    《代码整洁之道》精华速览,助你提升代码质量
    Python系列(20)—— 排序算法
    时序分析基础(2)——input_delay
    基于ssm的宠物医院管理系统的设计与实现
  • 原文地址:https://blog.csdn.net/weixin_61518137/article/details/133484358