Java中的多线程(面试常问)

  1. interrupt
void interrupt():发送一个中断请求给线程,这个线程的中断状态将被设置为True。如果这个线程当前被一个sleep调用阻塞,那么将抛出一个InterruptedException异常。
static boolean interrupted():检查当前线程是否被中断。静态方法,会将当前线程中断状态设置为false.
boolean inInterrupted():检查一个线程是否已经被中止了。与static interrupted不同,这个调用不会改变线程的中断状态。
  1. 线程状态
新生线程:new Thread(r)线程并没有开始运行。
可运行线程(Runnable):一旦调用start方法,该线程就是可运行的。可运行并不代表已经运行,有可能正正待cpu时间片。
被阻塞线程:原因
        线程调用sleep方法进入睡眠状态
        该线程正在等待一个I/O操作
        该线程正在试图得到一个锁,而该锁正被其他线程所持有
        线程正等待某个触发事件
        有人调用了suspend()方法
从阻塞到运行操作方法:和原因对应
        睡眠状态的线程已经过了指定的毫秒数
        等待的I/O操作已经完成
        线程等待的锁已经被释放
        另一个线程发出信号表明条件已经发生变化
死亡线程:原因
        run方法正常退出而自然死亡
        未捕获的异常中止了run方法使线程猝死
  1. 条件对象/条件变量
  是什么?有什么用?
通常,一个线程进入临界区,却发现必须等待满足某个条件后才能运行。
此时,就需要一个条件对象来管理已经获得了锁却不能开始执行有用工作的线程。
如何解决:每一个锁都有一个或多个相关联的条件对象,只有拿到锁对应的关联对象,线程才能执行。可以通过 锁.newCondition()方法获得一个条件对象。
  1. Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?
sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复。
wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),
只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。
  1. 请说出与线程同步以及线程调度相关的方法。
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态
  1. 安全失败(safe -fail)和快速失败(fast-fail)的区别
在 java.util 包的集合类就都是快速失败的
在使用迭代器对集合进行迭代的过程中,如果 A 线程正在对集合进行遍历,此时 B 线程对集合进行修改(增加、删除、修改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常。
而 java.util.concurrent 包下的类都是安全失败
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常
  1. synchronized关键字的优化过程
锁是加在对象的markword中。JVM根据最后三位的状态判断需要加什么类型的锁
    markword中的具体信息(锁升级的过程)
        无锁态:对象刚new出来的时候是没有锁状态的
        偏向锁:偏向与第一个拿到资源的线程。通过54位的JavaThread指针来锁定对象
        自旋锁/轻量级锁:当资源竞争激烈时,撤销偏向锁。通过每个线程栈中的Lock Record指针锁定对象(通过CAS)。
        重量级锁:需要向内核态申请。一旦对象加了重量级锁,所有线程请求都将进入一个阻塞队列wait,直到资源空闲。

    锁释放的时机
        1.当前线程的同步方法、代码块执行结束的时候释放。
        2.当前线程在同步方法、同步代码块中遇到breakreturn 终于该代码块或者方法的时候释放。
        3.出现未处理的error或者exception导致异常结束的时候释放。
        4.程序执行了 同步对象 wait 方法 ,当前线程暂停,释放锁。

在这里插入图片描述
在这里插入图片描述

  1. synchronized关键字以及和volatile关键字的区别
实现多个线程访问资源的同步性,可以保证被其修饰的方法或者代码块在任意时刻只有一个线程执行。

底层原理
    1. Java代码层面:加上Synchronized
    2. 字节码层面:monitorenter  monitorexit
    3. JVM层面:锁的升级过程(从偏向锁-》轻量级锁-》重量级锁)
    4. CPU层面:lock cmpchxg

Synchronized不用于String、Integer、Long类型的数据原因
    原因是Java的自动封箱和解箱操作在作怪。这里的i++实际上是i = new Integer(i+1),所以执行完i++后,i已经不是原来的对象了,同步块自然就无效了。Long 类型同理
    synchronized 锁的是对象,这个时候string值相同,但是不同对象,所以就无法锁住。
    所以使用String 的时候需要使用intern方法。intern是一个 native 的方法,如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后,再返回,因此intern每次返回同一个对象。这个时候使用synchronized 就可以锁住了。

Synchronized和lock的区别(Synchronized具备的功能Lock基本上都有,Lock的实现更加灵活)
    Synchronized获取锁的线程释放锁有两种方式
        获取锁的线程执行完了改代码块,然后线程释放对锁的占有
        线程执行发生异常,JVM会让线程自动释放锁

    Synchronized中,如果获取锁的线程(已经占有锁)此时等待某个I/O获得其他原因被阻塞了,但是又没有释放锁,其他线程只有眼巴巴的等待,此时执行效率就会降低。因此需要一种机制可以让线程不用无限制等待下去,通过Lock就可以办到。
    Lock
        lock()tryLock()tryLock(long time, TimeUnit unit)lockInterruptibly()是用来获取锁的。
        每个方法的区别
            lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
            tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
            lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。


    与lock的区别:
        Synchronized是java内置的关键字,Lock是一个类,通过这个类可以实现同步访问Java.util.concurrent.locks.lock
        采用Synchronized不用手动释放锁,执行完成后或者异常退出自动释放。而采用Lock必须手动释放锁,如果没有释放锁,则会发生死锁现象。释放锁必须在finally块中执行。
        Lock可以让等待锁的线程响应中断(通过lockInterruptibly()方法-》对interrupt()做出响应),而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;(interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程)
        通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
        Lock可以提高多个线程进行读操作的效率。

    可重入锁
        上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

    与ReentrantLock的区别(实现了lock的接口:java.util.concurrent.locks.Lock(接口).ReentrantLock)
        底层实现
            ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。

        公平非公平
             ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。

        手动释放
             ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()unlock()方法配合try/finally语句块来完成,使用释放更加灵活。

        是否可中断
            ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。

        锁是否可绑定Condition
            synchronized不能绑定; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。

        锁的对象
            synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。


    公平锁和非公平锁
        在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
        ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

9.Synchronized不用于String、Integer、Long类型的数据原因

原因是Java的自动封箱和解箱操作在作怪。这里的i++实际上是i = new Integer(i+1),所以执行完i++后,i已经不是原来的对象了,同步块自然就无效了。Long 类型同理
synchronized 锁的是对象,这个时候string值相同,但是不同对象,所以就无法锁住。
所以使用String 的时候需要使用intern方法。intern是一个 native 的方法,如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后,再返回,因此intern每次返回同一个对象。这个时候使用synchronized 就可以锁住了。

在这里插入图片描述
10. Synchronized和lock的区别(Synchronized具备的功能Lock基本上都有,Lock的实现更加灵活)

Synchronized获取锁的线程释放锁有两种方式
        获取锁的线程执行完了改代码块,然后线程释放对锁的占有
        线程执行发生异常,JVM会让线程自动释放锁

    Synchronized中,如果获取锁的线程(已经占有锁)此时等待某个I/O获得其他原因被阻塞了,但是又没有释放锁,其他线程只有眼巴巴的等待,此时执行效率就会降低。因此需要一种机制可以让线程不用无限制等待下去,通过Lock就可以办到。
    Lock
        lock()tryLock()tryLock(long time, TimeUnit unit)lockInterruptibly()是用来获取锁的。
        每个方法的区别
            lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
            tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
            lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
与lock的区别:
        Synchronized是java内置的关键字,Lock是一个类,通过这个类可以实现同步访问Java.util.concurrent.locks.lock
        采用Synchronized不用手动释放锁,执行完成后或者异常退出自动释放。而采用Lock必须手动释放锁,如果没有释放锁,则会发生死锁现象。释放锁必须在finally块中执行。
        Lock可以让等待锁的线程响应中断(通过lockInterruptibly()方法-》对interrupt()做出响应),而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;(interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程)
        通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
        Lock可以提高多个线程进行读操作的效率。
可重入锁
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。
与ReentrantLock的区别(实现了lock的接口:java.util.concurrent.locks.Lock(接口).ReentrantLock)
底层实现
ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
公平非公平
 new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。
手动释放
ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()unlock()方法配合try/finally语句块来完成,使用释放更加灵活。
是否可中断
 ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
锁是否可绑定Condition
synchronized不能绑定; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。
锁的对象
 synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
公平锁和非公平锁
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
 ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

在这里插入图片描述
在这里插入图片描述
11.线程和进程

进程
    进程是程序执行的一次过程,系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即一个进程从创建、运行到消亡的过程。

线程
    线程是比进程更小的执行单位。一个进程执行过程中可以产生多个线程。

不同
    与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程都有自己的程序计数器、虚拟机栈、本地方法栈。所以系统产生一个线程,或在各个线程之间切换工作时,负担要比进程小的多。正因为如此,线程称为轻量级进程。
堆和方法区(又叫非堆)

为什么程序计数器、虚拟机栈、本地方法栈是线程私有的呢?
    程序计数器:线程切换后能恢复到正确的执行位置。
    虚拟机栈:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表(存储一些基本数据类型、引用类型、returnAddress类型)、操作数栈、动态链接、方法出口等等信息。从方法调用直至完成的过程,就对应一个帧栈在java虚拟机栈中入栈和出栈的过程。对这个区域存在两种异常:如果线程请求栈的深度超过了jvm分配的栈深度,则会抛出StackOverflowError异常。如果虚拟机栈可以动态扩展,如果扩展时无法申请足够的内存,就会抛出OutOfMemoryError异常。
    本地方法栈:和虚拟机栈发挥的作用类似。区别是:虚拟机栈为虚拟机执行java方法(字节码)服务,本地方法则为虚拟机使用到的native方法服务。故为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈都是线程私有的。

  1. 为什么调用了start()方法时会执行run()方法,为什么不直接调用run()方法呢?
    在这里插入图片描述

Volatile修改的变量表示共享且不稳定的,每次使用都要到主存中读取。

volatile写的内存语义如下:
        当写一个volatile变量时,JMM(Java memory model)会把该线程对应的本地内存中的共享变量值刷新到主内存。

    volatile读的内存语义如下: 
        当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

    防止JVM指令重排
        如何防止?为了实现volatile的内存语义,编译器在生成字节码时(JVM层面),会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
        store可以理解为写   load可以理解为读

    保证变量的可见性
        如何实现:
        CPU从内存读取数据先到Cache,然后调入CPU内核。当CPU读取到volatile变量时,做了两件事情。首先将当前内核高速缓存行的数据立刻写回内存。然后使其他内核里缓存了该内存地址的数据无效(如何实现这一步呢)。
        MESI协议。主要思路是:当CPU写数据时,如果发现操作的变量是共享变量,即其他CPU中也存在该变量的副本,那么他会发出信号通知其他CPU将该变量的缓存行设置为无效状态。当其他CPU使用这个变量时,首先会去嗅探是否有对该变量更改的信号,当发现这个变量的缓存行已经无效时,会从新从内存中读取这个变量。

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210508211402110.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDc4OTg3Ng==,size_16,color_FFFFFF,t_70)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
13. AQS:AbstractQueuedSynchronizer在java.util.concurrent.locks包下

  AQS 是⼀个⽤来构建锁和同步器的框架,使⽤ AQS 能简单且⾼效地构造出应⽤⼴泛的⼤量的同步器 。
    AQS的原理
        如果被请求的资源空闲,则将当前请求资源的线程设置为工作线程,并将共享资源设置为锁定状态。如果被请求的共享资源被占用,则AQS将获取不到锁的线程(此时资源被占用,即资源的锁也被占用)加入到CLH阻塞队列中。CLH(是一个虚拟的双向队列,AQS将每条请求资源的线程封装成一个CLH锁队列的一个节点实现锁的分配)

    AQS定义两种资源共享方式
        Exclusive独占:只有一个线程能执行,如ReentrantLock(一次只允许一个线程访问。)。又分为公平锁和非公平锁。
            公平锁:对于所得竞争按照先来后到原则。
            非公平锁:对于锁的竞争是抢占式。

        Share共享:多个线程可以同时执行,如CountDownLatch、Semaphore、CyclicBarrier、ReadWriteLock。

    AQS的组件
        Semaphore(信号量,允许多个线程同时访问某个资源。)
        CountDownLatch(是一个同步工具类,协调多个线程之间的同步。通常用来控制线程等待,可以让某一个线程等待直到倒计时结束在开始运行。)
        CyclicBarrier(循环栅栏。和CountDownLatch功能类似,但又比其强大和复杂。字面意思是可循环使用Cyclic的栅栏Barrier。他要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造⽅法是 CyclicBarrier(int parties) ,其参数表示屏障拦截的线程数量,每个线程调⽤ await() ⽅法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
        ReadWriteLock(读的时候锁定资源,其他所有读的线程都可以读,其他所有写的线程必须等待读完之后才能进行写操作)

在这里插入图片描述
14. sleep、wait、notify、notifyAll、LockSupport

sleep
        正在执行的线程主动让出CPU,是的CPU执行其他线程。在sleep指定的时间过后,CPU才会回到这个线程上继续执行下去。属于Thread类,是类的方法。而锁是对象级别的,所以调用sleep方法的时候无关锁的状态。(sleep方法的目的是让当前线程暂停运行一段时间,而与对象锁相关的信息无影响,如果执行sleep方法时是处于持有对象锁的状态,那么睡眠时依然持有对象锁,如果执行sleep方法时不是处于持有对象锁的状态,睡眠期间也不会持有对象锁。)

    wait
        调用wait方法可以使当前线程进入等待唤醒状态,该线程会处于等待唤醒状态直到另一个线程调用了notify/notifyAll方法。Object类方法,在Synchronized中运行,否则会抛出IllegalMonitorStateException异常。(在调用wait方法时,线程会释放锁并进入等待状态。在被唤醒后,该线程会一直处于等待获取锁的状态直到它重新获取到锁,然后才可以重新恢复运行状态。)

    等待池waitset
        假设线程a调用了某个对象的wait方法,线程a就会释放该对象的锁,同是线程a就进入到了该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

    锁池entrylist
        假设线程a已经拥有了某个对象(不是类)的锁,而其它线程b、c想要调用这个对象的某个synchronized方法(或者块),由于 b、c线程在进入对象的synchronized方法(或者块)之前必须先获得该对象锁的拥有权,而恰巧对象的锁目前正被线程a所占用,此时b、c线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。

    notify
        只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。注意:调用notify唤醒的线程,处于等待获取锁状态,且这个被唤醒的线程,相对于其他在等待获取锁的线程,没有任何特权,也没有任何劣势,即公平竞争。

    notifyAll
        让所有处于等待池的线程全部进入锁池去竞争获取锁的机会

    LockSupport
        是JUC包中的一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞。
        LockSupport类的核心方法其实就两个:park()unpark(),其中park()方法用来阻塞当前调用线程,unpark()方法用于唤醒指定线程。
        这其实和Object类的wait()signal()方法有些类似,但是LockSupport的这两种方法从语意上讲比Object类的方法更清晰,而且可以针对指定线程进行阻塞和唤醒。

  1. stop和suspend方法弃用的原因
stop:强制解除线程获得的所有锁定。不安全。
    suspend:容易造成死锁。假如有A,B两个线程,A线程在获得某个锁之后被suspend阻塞,这时A不能继续执行,线程B在或者相同的锁之后才能调用resume方法将A唤醒,但是此时的锁被A占有,B不能继续执行,也就不能及时的唤醒A,此时A,B两个线程都不能继续向下执行而形成了死锁。这就是suspend被弃用的原因
  1. ThreadLocal(java.lang包下的ThreadLocal泛型类)
进程中的所有线程共享进程全局变量,如果每个线程都想有自己的变量,则可以使用ThreadLocal创建线程的本地变量。
    get()/set()方法用户获取或者改变对应的值。set()方法底层是map,其中的key是当前线程,value是传入的值。
    ThreadLocal应用在什么地方
        Spring关于Transaction的处理,mybatis关于分页的处理

    ThreadLocal内存泄露的原因
        没有及时remove导致map中的value泄露

在这里插入图片描述
17. CAS(Compare and swap:保证无锁状态下多个线程对同一数据的更新)

 ABA问题:A->B->A。解决方法:版本号机制
    自旋锁/无锁:一个含义,在原地转圈。

在这里插入图片描述

18.双重检查锁

双重检查锁(DCL:Double Check Lock)单例模式下多个线程访问同一个实例化对象
    情况一:uniqueSingleton被实例化了两次并且被不同对象持有。完全违背了单例的初衷。
    情况二:加锁,虽然解决了问题,但是因为用到了synchronized,会导致很大的性能开销,并且加锁其实只需要在第一次初始化的时候用到,之后的调用都没必要再进行加锁。
    情况三:双重检查锁(double checked locking)是对上述问题的一种优化。先判断对象是否已经被初始化,再决定要不要加锁。隐患:指令重排
    情况四:双重检查:如果多个线程同时了通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象(第一次检查的作用:如果不为空,则之间返回创建的对象,避免synchronized区域的操作带来的时间开销)。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
19. Java中的四种引用:强软弱虚

   强引用:平时new的对象都是强引用。此类对象只有当栈中没有指针指向该对象时,GC自动收回该对象所占的内存。
    软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。(可以用来实现内存敏感的高速缓存)软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
    弱引用:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。(防止内存泄露)
    虚引用:(管理堆外内存,也就是直接内存)虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

在这里插入图片描述
20. 线程池

线程池ThreadPoolExecutor
    线程池好处
        降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
        提高响应速度:任务到达时,无需等待线程创建即可立刻执行。
        提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以统一分配、调优和监控。
        提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

    实现原理
        预先启动一些线程,线程无限循环从任务队列中获取一个任务进行执行,直到线程池被关闭。如果某个线程因为执行某个任务发生异常而终止,那么重新创建一个新的线程而已,如此反复。

    线程池的状态
        线程池和线程一样拥有自己的状态,在ThreadPoolExecutor类中定义了一个volatile变量runState来表示线程池的状态,线程池有四种状态,分别为RUNNING、SHURDOWN、STOP、TERMINATED。
             线程池创建后处于RUNNING状态。
             调用shutdown后处于SHUTDOWN状态,线程池不能接受新的任务,会等待缓冲队列的任务完成。
             调用shutdownNow后处于STOP状态,线程池不能接受新的任务,并尝试终止正在执行的任务。
             当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。


    处理流程
        核心线程池 -》工作队列-》 线程池
        首先判断核心线程池是否都在执行任务,如果不是,(则表示核心线程池空闲或者还有核心线程没有被创建。)则创建一个新的线程来执行任务。如果所有核心线程都在执行任务,则进入下个流程。
        线程池判断工作队列是否以慢,如果没有,则将新提交的任务存储在工作队列中。如果满了,则进入下个流程。
        线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
        饱和策略

在这里插入图片描述

线程池大小配置
        一般需要根据任务的类型来配置线程池大小:
        如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
        如果是IO密集型任务,参考值可以设置为2NCPU
        当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。
    创建线程池
        使用Executors
          弊端:
        new ThreadPoolExecutor()

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值