【面试问题】多线程---2021面试题汇总 2万字含泪大总结

前言:这篇文化章中引用了蛮多github上javaGuide的内容,当然也有很多我自己总结的面试问题,在自己总结的同时也希望帮助到别的小伙伴!

javaGuide链接:github地址

操作系统---2021面试题汇总传送门:一篇文章解决操作系统面试题

目录

第一部分:多线程基础

1.线程和进程的区别?

2.线程都有哪些状态 ?

3.为什么使用多线程?

4.等待态和阻塞态的区别是什么?

5.实现多线程的方式有哪些?

6.并发和并行的区别?

7.多线程带来的问题?

8.什么是上下文切换?

9.如何减少上下文切换?

10.多线程三要素?

11.ConcurrentHashMap

第二部分:对象及变量的并发访问

1.解释Synchronized关键字?

2.Synchronized关键字的作用范围?

3.、jmm内存模型和Volatile关键字

4.Synchronized和Volatile的区别?

5.Synchronized和Lock的区别?

6.Synchronized的底层原理?

第三部分:线程间的通信

第四部分:锁

1.锁的分类?

2.死锁

3.如何解开死锁?

4.CAS锁以及ABA问题?

5.jdk1.6后对Synchronized做了怎样的优化?

6.锁的升级?

7.锁能否降级?

第五部分:线程池

1.使用线程池的原因?

2.线程池的七大参数是哪些?

3.线程池的工作流程?

4.执行 execute()方法和 submit()方法的区别是什么呢?

5.关闭线程池

6.创建线程池

第六部分:Atomic原子类

1.什么是原子类?

2.原子类有哪几种?

3.简单介绍一下 AtomicInteger 类的原理


第一部分:多线程基础

1.线程和进程的区别?

1.进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。

2.一个进程可以拥有很多个线程,而一个线程只能属于一个进程。

3.每个进程有自己独立的内存空间,程序之间的切换带来很大的开销。而线程共享进程的内存资源,每个线程有自己的Jvm栈和PC,线程之间的切换开销较小。

2.线程都有哪些状态 ?

新建(NEW):new一个线程对象。

可运行(RUNNABLE):线程对象创建后,调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。

运行(RUNNING):可运行状态的线程获得了cpu 时间片 ,执行run()方法中的代码。

阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu时间片,暂时停止运行。直到线程进入可运行状态,才有机会再次获得cpu时间片进入运行状态。

死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。

3.为什么使用多线程?

1.充分利用cpu多核的特性,提高程序性能。

2..线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

4.等待态和阻塞态的区别是什么?

1.waiting态:指线程(获取到了对象锁)已经进入到了run()方法中,但因为一些原因让放弃了cpu的占有权。

2.blocking态:指线程(未获取到对象锁)还未进入run()方法,正在请求获取cpu时间片。

5.实现多线程的方式有哪些?

1.继承Thread类

//定义一个继承Thread类的Runner类
class Runner extends Thread {
    //定义线程所拥有的方法
    public void run (){
        System.out.println("线程创建完毕");
    }
}
​
Runner r = new Runner();
r.start();
//如果使用r.run()仅相当于方法的调用
​

2.实现Runnable接口

//定义一个实现Runnable接口的类
class MyThread implements Runnable {
    public void run (){
        System.out.println("创建成功");
    }
}        
//创建MyThread的对象,并用这个对象作为Thread类构造器的参数
MyThread r = new MyThread();
Thread t = new Thread(r);
t.start();

3.实现Callable接口(可以返回值)

步骤:1.重写Call方法 2.start前新建一个Callable的实例 3.新建FutureTask实例,以Callable实例作为参数 4.以FutureTask实例作为参数新建Thread 5.调用start方法启动。

public static void main(String[] args) throws Exception {
        //创建Callable接口的实现类的实例化对象
        CallableImpl callable = new CallableImpl();
​
        // 第一步:创建一个“未来任务类”对象。
        // 参数非常重要,需要给一个Callable接口实现类对象。
        FutureTask task = new FutureTask(callable);
​
        // 创建线程对象
        Thread t = new Thread(task);
​
        // 启动线程
        t.start();
​
        // 这里是main方法,这是在主线程中。
        // 在主线程中,怎么获取t线程的返回结果?
        // get()方法的执行会导致“当前线程阻塞”
        Object obj = task.get();
        System.out.println("线程执行结果:" + obj);
​
        // main方法这里的程序要想执行必须等待get()方法的结束
        // 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
        // 另一个线程执行是需要时间的。
        System.out.println("hello world!");
    }
}
//实现Callable接口
class CallableImpl implements Callable{
    @Override
    public Object call() throws Exception { // call()方法就相当于run方法。只不过这个有返回值
        // 线程执行一个任务,执行之后可能会有一个执行结果
        // 模拟执行
        System.out.println("call method begin");
        Thread.sleep(1000 * 10);//当前线程睡眠10秒
        System.out.println("call method end!");
        int a = 100;
        int b = 200;
        return a + b; //自动装箱(300结果变成Integer)
    }
4.Callable和Runnable接口实现线程的区别:runnable不可以抛出异常和返回值,callable可以配合futuretask的get()获取到返回值,也可以抛出异常。

6.并发和并行的区别?

  • 并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);

  • 并行: 单位时间内,多个任务同时执行。

7.多线程带来的问题?

1.线程安全性

2.内存泄漏

3.死锁

8.什么是上下文切换?

由于现在的cup都是多核的,所以说必然是有多个线程的,所谓上下文切换,就是指线程在运行期间,让出cpu给其它线程使用,保存当前线程的上下文并且加载占用cpu的线程的上下文。

9.如何减少上下文切换?

上下文切换每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。

所以:1.避免创建不需要的线程 2.协程 3.CAS算法 等

10.多线程三要素?

1.原子性:一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么全部不执行!

2.可见性:一个线程对某变量的操作,对于其他的线程来说都是可见的。

3.有序性:程序执行的顺序按照代码的先后顺序执行。

11.ConcurrentHashMap

我们知道 HashMap 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 Collections.synchronizedMap() 方法来包装我们的 HashMap。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。

所以就有了 HashMap 的线程安全版本—— ConcurrentHashMap 的诞生。

ConcurrentHashMap 中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。

第二部分:对象及变量的并发访问

1.解释Synchronized关键字?

1.synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

2.在 Java 早期版本中,synchronized 属于 重量级锁,效率低下,这是因为监视器锁(monitor)是依赖于底层的操作系统来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

3.Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

2.Synchronized关键字的作用范围?

1.可以作用在方法上:作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

synchronized void method() {
    //业务代码
}

2.可以作用在类,或者是静态方法上(也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁):

public void a() {
    synchronized(b.class) { //b是类
        //...
    }
}
synchronized static void method() {
    //业务代码
}

3.可以作用在代码块上(指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁):

public void a() {
    synchronized(this) {
        //...
    }
}

3.、jmm内存模型和Volatile关键字

在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。要解决这个问题,就需要把变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

所以,volatile 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。

4.Synchronized和Volatile的区别?

1.synchronized可以保证原子性以及可见性,volatile只可以保证可见性

2.synchronized可以修饰方法,代码块,类,而volatile只能修饰变量

3.synchronized解决多线程之间的同步性问题,而volatile解决多线程之间的可见性问题。

5.Synchronized和Lock的区别?

1、Synchronized 内置的Java关键字,Lock是一个Java类

2、Synchronized 无法判断获取锁的状态,Lock可以判断

3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁

4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。

5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;

6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;

6.Synchronized的底层原理?

1.修饰代码块的情况:

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class

从上面我们可以看出:

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。

另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

2.修饰方法的情况:

public class SynchronizedDemo2 {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}

synchronized关键字原理

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

3.总结

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

7.为什么加锁和释放锁会导致上下文切换

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。

第三部分:线程间的通信

1.可以讲一下join()方法吗?

join()方法是Thread类中的一个方法,该方法的定义是等待该线程终止。比如现在有两个线程t1和t2,如何保证t2线程在t1执行完毕后再执行呢?只需在t2的代码块中加入t1.join()即可。

 例:让线程按照t1->t2->t3的顺序执行

public class 测试join {
    public static void main(String[] args) {
        Thread t1 =new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("----t1 is running");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("----t1 is dead");
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    t1.join();
                    System.out.println("----t2 is running");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("----t2 is dead");
                }

            }
        });

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    t2.join();
                    System.out.println("----t3 is running");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("----t3 is dead");
                }

            }
        });


        t1.start();
        t2.start();
        t3.start();

    }
}

 运行结果:

----t1 is running
----t1 is dead
----t2 is running
----t2 is dead
----t3 is running
----t3 is dead

第四部分:锁

1.锁的分类?

1.乐观锁和悲观锁

乐观锁:总是认为不会出现多线程安全问题,则不加锁。CAS是乐观锁的实现方式。

悲观锁:总是以为会出现多线程安全问题,所以加锁。Synchronized就是一种悲观锁,

2.共享锁和独占锁

独享锁:一个线程可用 共享锁:可用多个锁同时使用

3.公平锁和非公平锁

公平锁:线程获取锁的方式采用先到先得

非公平锁:允许插队,进行抢占。

4.可重入锁(Synchronized和Lock)

获取到对象锁时,再次申请该对象锁,依然可以申请到。

public class Demo01 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sms();
        },"A").start();
        new Thread(()->{
            phone.sms();
        },"B").start();
    }
​
}
​
class Phone{
    public synchronized void sms(){
        System.out.println(Thread.currentThread().getName()+"=> sms");
        call();//这里也有一把锁
    }
    public synchronized void call(){
        System.out.println(Thread.currentThread().getName()+"=> call");
    }
}

5.自旋锁和CAS锁

CAS锁:Atmotic原子类的实现方式,后面会详细讲到,

自旋锁:通过CAS进行实现

public class SpinlockDemo {
​
    // 默认
    // int 0
    //thread null
    AtomicReference<Thread> atomicReference=new AtomicReference<>();
​
    //加锁
    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"===> mylock");
​
        //自旋锁
        while (!atomicReference.compareAndSet(null,thread)){
            System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");
        }
    }
​
​
    //解锁
    public void myUnlock(){
        Thread thread=Thread.currentThread();
        System.out.println(thread.getName()+"===> myUnlock");
        atomicReference.compareAndSet(thread,null);
    }
​
}
​
public class TestSpinLock {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        reentrantLock.unlock();
​
​
        //使用CAS实现自旋锁
        SpinlockDemo spinlockDemo=new SpinlockDemo();
        new Thread(()->{
            spinlockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinlockDemo.myUnlock();
            }
        },"t1").start();
​
        TimeUnit.SECONDS.sleep(1);
​
​
        new Thread(()->{
            spinlockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinlockDemo.myUnlock();
            }
        },"t2").start();
    }
}

6.偏向锁

偏向锁操作流程偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。但是从我们跑的代码输出却看不到偏向锁这个东东。为啥对象实例化出来之后,对象头里是不支持偏向的呢?其实是JVM搞的鬼,JVM虽然默认启用偏向锁,但启动后4秒内并不支持。可以通过-XX:BiasedLockingStartupDelay=0参数将JVM启动后支持偏向锁的延迟时间设置为0,这样就可以看到偏向锁的输出了

2.死锁

img

​
​

import java.util.concurrent.TimeUnit;
​
public class DeadLock {
    public static void main(String[] args) {
        String lockA= "lockA";
        String lockB= "lockB";
​
        new Thread(new MyThread(lockA,lockB),"t1").start();
        new Thread(new MyThread(lockB,lockA),"t2").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()+" lock"+lockA+"===>get"+lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);
            }
        }
    }
}

3.如何解开死锁?

1、使用jps定位进程号,jdk的bin目录下: 有一个jps

命令:jps -l

image-20200812214833647

2、使用jstack 进程进程号 找到死锁信息

image-20200812214920583

一般情况信息在最后:

image-20200812214957930

4.CAS锁以及ABA问题?

public class casDemo {
    //CAS : compareAndSet 比较并交换
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
​
        //boolean compareAndSet(int expect, int update)
        //期望值、更新值
        //如果实际值 和 我的期望值相同,那么就更新
        //如果实际值 和 我的期望值不同,那么就不更新
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
​
        //因为期望值是2020  实际值却变成了2021  所以会修改失败
        //CAS 是CPU的并发原语
        atomicInteger.getAndIncrement(); //++操作
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
    }
}
 

CAS:CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类(原子类)的实现就是基于 CAS 的。

如何实现线程安全:线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。(比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。)

缺点:1.ABA问题,2.只能保证一个共享变量的原子性,3.自旋时间过久会导致cpu资源的损耗,相当于死循环。

ABA问题:

线程1:期望值是1,要变成2;

线程2:两个操作:

  • 1、期望值是1,变成3

  • 2、期望是3,变成1

所以对于线程1来说,A的值还是1,所以就出现了问题,骗过了线程1;

public class casDemo {
    //CAS : compareAndSet 比较并交换
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
​
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
​
        //boolean compareAndSet(int expect, int update)
        //期望值、更新值
        //如果实际值 和 我的期望值相同,那么就更新
        //如果实际值 和 我的期望值不同,那么就不更新
        System.out.println(atomicInteger.compareAndSet(2021, 2020));
        System.out.println(atomicInteger.get());
​
        //因为期望值是2020  实际值却变成了2021  所以会修改失败
        //CAS 是CPU的并发原语
//        atomicInteger.getAndIncrement(); //++操作
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
    }
}

如何解决ABA问题:使用引用类型原子类(AtomicStampedReference),通过版本号是否变化来判断是否发生了ABA问题。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
​
/**
 * Description:
 *
 * @author jiaoqianjin
 * Date: 2020/8/12 22:07
 **/
​
public class CASDemo {
    /**AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
     * 正常在业务操作,这里面比较的都是一个个对象
     */
    static AtomicStampedReference<Integer> atomicStampedReference = new
            AtomicStampedReference<>(1, 1);
​
    // CAS compareAndSet : 比较并交换!
    public static void main(String[] args) {
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("a1=>" + stamp);
            
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改操作时,版本号更新 + 1
            atomicStampedReference.compareAndSet(1, 2,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1);
            
            System.out.println("a2=>" + atomicStampedReference.getStamp());
            // 重新把值改回去, 版本号更新 + 1
            System.out.println(atomicStampedReference.compareAndSet(2, 1,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1));
            System.out.println("a3=>" + atomicStampedReference.getStamp());
        }, "a").start();
        
        // 乐观锁的原理相同!
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("b1=>" + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicStampedReference.compareAndSet(1, 3,
                    stamp, stamp + 1));
            System.out.println("b2=>" + atomicStampedReference.getStamp());
        }, "b").start();
    }
}
​

5.jdk1.6后对Synchronized做了怎样的优化?

Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。

6.锁的升级?

1.某线程进入到代码块后,判断进入的线程和当前持有锁的线程是否为同一个?如果是,则拿到锁。(偏向锁)

2.接上,如果否,升级到CAS轻量级锁,若CAS获取到锁,则结束,否则,接下。(CAS轻量级锁)

3.升级为自旋锁,短暂自旋后,若未获取到锁,则升级为重量级锁,

7.锁能否降级?

不能

第五部分:线程池

1.使用线程池的原因?

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2.线程池的七大参数是哪些?

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 核心线程数线程数定义了可以同时运行的最少线程数量。

  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。

  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

ThreadPoolExecutor其他常见参数:

  1. keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;

  2. unit : keepAliveTime 参数的时间单位。

  3. threadFactory :创建线程的工厂。

  4. handler :饱和策略、也叫拒绝策略,当任务队列和线程池(达到最大线程数)都满了,即到达饱和状态。

  • AbortPolicy(默认策略):抛出 RejectedExecutionException来拒绝新任务的处理。

  • ThreadPoolExecutor.CallerRunsPolicy 调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。

  • DiscardPolicy:不处理新任务,直接丢弃掉。

  • DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。

  • CallerRunsPolicy:调用者所在线程来运行任务。

3.线程池的工作流程?

1.判断核心线程池中的线程是否都在执行任务。

否:创建一个新的工作线程来执行任务 是:下一步;

2.判断任务队列是否已满。

否:将新任务保存在任务队列中 是:下一步;

3.判断线程池里的线程是否都在工作状态。

否:创建一个新的工作线程来执行任务 是:下一步;

4.按照设置的拒绝策略来处理无法执行的任务。

4.执行 execute()方法和 submit()方法的区别是什么呢?

  1. execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

  2. submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

5.关闭线程池

通过shutdown()或者shutdowm()来关闭线程池。他们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt来中断线程。

6.创建线程池

方式一:通过构造方法实现

方式二:通过 Executor 框架的工具类 Executors 来实现

我们可以创建三种类型的 ThreadPoolExecutor:

  • FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

  • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。

  • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

第六部分:Atomic原子类

1.什么是原子类?

原子类有CAS实现,所谓原子类,同一时间只能被一个线程操作的类,并且不可中断。

2.原子类有哪几种?

基本类型

使用原子的方式更新基本类型

  • AtomicInteger:整形原子类

  • AtomicLong:长整型原子类

  • AtomicBoolean:布尔型原子类

数组类型

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类

  • AtomicLongArray:长整形数组原子类

  • AtomicReferenceArray:引用类型数组原子类

引用类型

  • AtomicReference:引用类型原子类

  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

  • AtomicMarkableReference :原子更新带有标记位的引用类型

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器

  • AtomicLongFieldUpdater:原子更新长整形字段的更新器

  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

3.简单介绍一下 AtomicInteger 类的原理

// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
​
static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}
​
private volatile int value;

其中:Unsafe类中都是一些native方法(java无法实现对内存的操作,要通过本地native方法实现 ) ,valueOffset是变量value的内存偏移,通过unsafe对象调用objectFieldOffset方法获取,value使用volatile定义,实现对所有线程的可见性。AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小树ぅ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值