Java多线程

1、并行和并发有什么区别?

并行:多个任务在计算机中同时执行,是同一时间动手做多件事情的能力

多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。

并发:多个任务在计算机中交替执行,是同一时间应对多件事情的能力

举例:吃馒头


2、线程和进程的区别?

1、进程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的,当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。

进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

2、线程

一个进程之内可以分为一到多个线程。

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行。线程是cpu调度和分派的基本单位,在实际开发过程中,一般是考虑多线程并发。


3、守护线程是什么?

守护线程(daemon thread),是个服务线程,用来监视和服务其它线程。


4、线程的创建方式?(高频)

继承Thread类

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("MyThread...run...");
    }

    
    public static void main(String[] args) {

        // 创建MyThread对象
        MyThread t1 = new MyThread() ;
        MyThread t2 = new MyThread() ;

        // 调用start方法启动线程
        t1.start();
        t2.start();

    }
    
}

实现runnable接口配合 Thread

public class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("MyRunnable...run...");
    }

    public static void main(String[] args) {

        // 创建MyRunnable对象
        MyRunnable mr = new MyRunnable() ;

        // 创建Thread对象
        Thread t1 = new Thread(mr) ;
        Thread t2 = new Thread(mr) ;

        // 调用start方法启动线程
        t1.start();
        t2.start();

    }

}

实现Callable接口,使用FutureTask 配合 Thread

public class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("MyCallable...call...");
        return "OK";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 创建MyCallable对象
        MyCallable mc = new MyCallable() ;

        // 创建F
        FutureTask<String> ft = new FutureTask<String>(mc) ;

        // 创建Thread对象
        Thread t1 = new Thread(ft) ;
        Thread t2 = new Thread(ft) ;

        // 调用start方法启动线程
        t1.start();

        // 调用ft的get方法获取执行结果
        String result = ft.get();

        // 输出
        System.out.println(result);

    }

}

线程池创建线程

public class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("MyRunnable...run...");
    }

    public static void main(String[] args) {

        // 创建线程池对象
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        threadPool.submit(new MyRunnable()) ;

        // 关闭线程池
        threadPool.shutdown();

    }

}


5、说一下 Runnable 和 Callable 有什么区别?(高频)

Runnable接口中的run()方法的返回值是void,在其中可以定义线程的工作任务,但无法返回值。

Callable接口中的call()方法是有返回值的,是一个泛型,一般会和Future、FutureTask配合,能异步地得到线程的执行结果。


6、sleep() 和 wait() 有什么区别?(高频)

相同点:

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

不同点:

1、方法归属不同

  • sleep(long) 是 线程类(Thread)的静态方法

  • wait(),wait(long) 都是 Object类 的成员方法,每个对象都有

2、语法使用不同

  • sleep 可以单独使用,无需配合 synchronized 一起使用

  • wait 方法必须配合 synchronized 一起使用,不然在运行时就会抛出IllegalMonitorStateException 的异常

2、唤醒方式不同

  • sleep 方法必须要传递一个超时时间的参数,休眠时开始阻塞,线程的监控状态依然保持着,当指定的休眠时间到了就会自动恢复运行状态。

  • wait(long) 和 wait() 还可以被 notify 唤醒, wait() 方法如果没有指定时间的话,那么该线程如果不被唤醒就会一直等下去。

3、锁特性不同

  • wait()方法会在线程休眠的同时释放锁,其余线程也可以去抢锁。只有当有notify()方法唤醒它时,它才会再次从等待池中出来和其他线程共同抢锁,抢到后继续执行中断之前的业务。

  • sleep方法如果在 synchronized 代码块中执行,线程虽然处于休眠状态,但是不会释放锁,其他线程依旧无法访问该对象


7、notify()和 notifyAll()有什么区别?(高频)

notifyAll:唤醒所有wait的线程

notify:只随机唤醒一个 wait 线程


8、线程的 run()和 start()有什么区别?(高频)

start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。

run():封装了要被线程执行的代码,可以被调用多次。


9、如何停止一个正在运行的线程?(高频)

使用退出标志,使线程正常退出

public class MyThread extends Thread {

    volatile boolean flag = false ;     // 线程执行的退出标记

    @Override
    public void run() {
        while(!flag) {
            System.out.println("MyThread...run...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        // 创建MyThread对象
        MyThread t1 = new MyThread() ;
        t1.start();

        // 主线程休眠6秒
        Thread.sleep(6000);

        // 更改标记为true
        t1.flag = true ;

    }
}

使用stop方法强行终止

public class MyThread extends Thread {

    volatile boolean flag = false ;     // 线程执行的退出标记

    @Override
    public void run() {
        while(!flag) {
            System.out.println("MyThread...run...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        // 创建MyThread对象
        MyThread t1 = new MyThread() ;
        t1.start();

        // 主线程休眠2秒
        Thread.sleep(6000);

        // 调用stop方法
        t1.stop();

    }
}

使用interrupt方法中断线程

public class MyThread extends Thread {

    volatile boolean flag = false ;     // 线程执行的退出标记

    @Override
    public void run() {
        while(!flag) {
            System.out.println("MyThread...run...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        // 创建MyThread对象
        MyThread t1 = new MyThread() ;
        t1.start();

        // 主线程休眠2秒
        Thread.sleep(6000);

        // 调用interrupt方法
        t1.interrupt();

    }
}


10、有三个线程 T1 , T2 , T3 ,如何保证顺序执行?(高频)

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。

public class JoinTest {

    public static void main(String[] args) {

        // 创建线程对象
        Thread t1 = new Thread(() -> {
            System.out.println("t1");
        }) ;

        Thread t2 = new Thread(() -> {
            try {
                t1.join();                          // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2");
        }) ;


        Thread t3 = new Thread(() -> {
            try {
                t2.join();                              // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t3");
        }) ;

        // 启动线程
        t1.start();
        t2.start();
        t3.start();

    }
}


11、线程有哪些状态?

Java中的线程状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:

public class Thread {
    
    public enum State {
    
        /* 新建 */
        NEW , 

        /* 可运行状态 */
        RUNNABLE , 

        /* 阻塞状态 */
        BLOCKED , 

        /* 无限等待状态 */
        WAITING , 

        /* 计时等待 */
        TIMED_WAITING , 

        /* 终止 */
        TERMINATED;
    
    }
    
    // 获取当前线程的状态
    public State getState() {
        return jdk.internal.misc.VM.toThreadState(threadStatus);
    }
    
}

站在Java API的角度,通过源码我们可以看到Java中的线程存在6种状态,每种线程状态的含义如下:

线程状态

具体含义

新建(NEW)

一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。

可运行状态(RUNABLE)

当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。

阻塞状态(BLOCKED)

没有获取锁的线程。当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。

等待状态(WAITING)

获取锁的线程,调用锁对象.wait方法。一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。

有时限的等待(TIMED_WAITING)

一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:调用线程Thread的sleep(long)方法;获取锁的线程,调用锁对象Object.wait(long)方法;join(long)。

终结状态(TERMINATED)

run方法中代码执行完毕。一个完全运行完成的线程的状态,也称之为终止状态、结束状态。

各个状态的转换,如下图所示:


12、讲一下synchronized关键字的底层原理?(高频)

① synchronized同步代码块的情况

public class SynchronizedDemo {

    public void method() {

        synchronized (this) {
            System.out.println("synchronized 代码块");
        }

    }
    
}

通过javap查看字节码文件信息,如下所示:

从上面我们可以看出:

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

② synchronized修饰方法的的情况

public class SynchronizedDemo2 {

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

}

通过javap(javap -v xxx.class)查看字节码文件信息,如下所示:

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


13、lock锁和synchronized锁区别

1、语法层面

  • synchronized 是关键字

底层是通过c++代码实现锁(monitor)

synchronized同步代码块执行完毕会自动释放锁

  • lock是一个接口

用 java 语言实现

需要手动调用 unlock 方法释放锁

2、功能层面

  • 具备基本的互斥、同步、锁重入功能

1)互斥:多个线程获取同一把锁,只有一个线程可以成功,其他线程失败;进入阻塞队列等待;等待获取锁的线程释放锁;然后再去抢占

2)同步:获取锁的线程,如果条件不满足,调用锁对象wait进入等待队列等待;等条件满足了,并且其他线程唤醒了;

3)锁重入:获取锁的线程,再次获取同一把锁

  • Lock 提供了许多 synchronized 不具备的功能:

1)获取阻塞状态的线程

2)公平锁:

①.已经处在阻塞队列中的线程始终都是公平的,先进先出

②.公平锁是指未处于阻塞队列中的线程来争抢锁,如果队列不为空,则到队尾等待

③.非公平锁是指未处于阻塞队列中的线程来争抢锁,与队列头唤醒的线程去竞争,谁抢到算谁的

3)可超时tryLock

4)多条件变量:功能类似于普通 synchronized 的 wait,notify,用在当线程获得锁后,发现条件不满足时,临时等待的链表结构

①.Condition condition = lock.newCondition();

②.可以利用条件变量进行await();将线程加入等待队列

③.可以利用条件变量进行condition.signal();唤醒等待队列某个线程

④.可以利用条件变量进行condition.signalAll();唤醒等待队列某个线程

5)Lock 有适合不同场景的实现


14、Java中Synchronized 和 ReentrantLock有什么不同?(高频)

  • 相似点:

这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的。

  • 区别:

这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要JVM实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try...finally语句块来完成。

  • 相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下2项:

① 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。

② Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

(公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁。)


15、悲观锁和乐观锁的区别?(高频)

  • 悲观锁:悲观锁的核心代表是synchronized、Lock

思想:

1.只有获取锁的线程才能操作共享变量,每次只有一个线程能获取锁成功,获取锁失败的线程都要进入阻塞队列等待;

2.线程从可运行到阻塞,再从阻塞到可运行,涉及到上下文切换,如果频繁发生会影响效率

3.synchronized和Lock在获取锁的时候,如果锁已经被其他线程占用,还会重试几次,如果重试几次还不能获取锁,那就进入阻塞状态,进入阻塞状态就会引发上下文切换。

  • 乐观锁:乐观锁的代表是AtomicInteger,使用CAS保证原子性

思想:

1.每次只有一个线程能修改成功共享变量,其他修改失败的线程不需要阻塞,不断重试直到成功

2.由于所有的线程一直运行,不需要阻塞,不牵扯频繁上下文切换

3.需要多核cup的支持,cup>线程数量

修改失败的线程不能停止,既然不能停止,那就需要其他cpu来执行线程;否则也会出现上下文切换;


16、简述一下你对线程池的理解?为什么要使用线程池?(高频)

线程池是一种多线程处理方式,创建若干个可执行的线程放入一个容器(队列)中,从容器(队列)中获取线程不用自行创建,使用完毕不需要销毁线程而是放回容器(队列)中,从而减少创建和销毁线程对象的开销。

合理利用线程池能够带来三个好处

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。每个工作线程都可以被重复利用,可执行多个任务。

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

第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。通过调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。


17、线程池的构造方法里几个参数的作用分别都是什么?

ThreadPoolExecutor构造方法中的7个参数

corePoolSize: 核心线程数目 - 池中会保留的最多线程数

maximumPoolSize: 最大线程数目 - 核心线程+救急线程的最大数目

keepAliveTime: 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放

unit: 时间单位 - 救急线程的生存时间单位,如秒、毫秒等

workQueue: 工作队列,当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

threadFactory: 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等

handler: 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略


18、线程池流程

  1. 当我们向线程池中添加任务

注意:线程池中不能添加线程,只能添加任务,线程池中的线程是由线程池自己管理;

2.有核心线程执行任务

1)如果核心线程都在执行任务

2)将任务加入到任务队列

等待核心线程执行完任务后,从队列中获取任务执行

如果队列已经满了,创建救急线程,由救急线程执行任务,救急线程执行完任务后,如果在keepAliveTime时间内没有任务执行,则销毁;如果救急线程也无法创建,则触发拒绝策略。


19、线程池拒绝策略有哪些?

  • 抛异常 java.util.concurrent.ThreadPoolExecutor.AbortPolicy

  • 由调用者执行任务 java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy

  • 丢弃任务 java.util.concurrent.ThreadPoolExecutor.DiscardPolicy

  • 丢弃最早排队任务,把新加任务添加到工作队列java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy


20、Java中活锁和死锁有什么区别?

活锁:一个线程在执行的时候影响到了另外一个线程的执行,而另外一个线程的执行同时影响到了该线程的执行那么就有可能发生活锁。同死锁一样,发生活锁的线程无法继续执行。然而线程并没有阻塞——它们在忙于响应对方无法恢复工作。这就相当于两个在走廊相遇的人:甲向他自己的左边靠想让乙过去,而乙向他的右边靠想让甲过去。可见他们阻塞了对方。甲向他的右边靠,而乙向他的左边靠,他们还是阻塞了对方。

死锁:线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。

死锁演示:

Thread的子类:

public class DeadThread extends Thread {

    // 定义成员变量,来切换线程去执行不同步代码块的执行
    private boolean flag ;
    public DeadThread(boolean flag) {
        this.flag = flag ;
    }

    @Override
    public void run() {

        if(flag) {

            synchronized (MyLock.R1) {   
                System.out.println(Thread.currentThread().getName() + "---获取到了R1锁,申请R2锁....");
                synchronized (MyLock.R2) {
                    System.out.println(Thread.currentThread().getName() + "---获取到了R1锁,获取到了R2锁....");
                }
            }

        }else {

            synchronized (MyLock.R2) {    
                System.out.println(Thread.currentThread().getName() + "---获取到了R2锁,申请R1锁....");
                synchronized (MyLock.R1) {
                    System.out.println(Thread.currentThread().getName() + "---获取到了R2锁,获取到了R1锁....");
                }
            }
            
        }

    }

}

锁接口:

public interface MyLock {

    // 定义锁对象
    public static  final Object R1 = new Object() ;
    public static  final Object R2 = new Object() ;

}

测试类:

public class DeadThreadDemo1 {

    public static void main(String[] args) {

        // 创建线程对象
        DeadThread deadThread1 = new DeadThread(true) ;
        DeadThread deadThread2 = new DeadThread(false) ;

        // 启动两个线程
        deadThread1.start();
        deadThread2.start();

    }

}

控制台输出结果:

Thread-0---获取到了R1锁,申请R2锁....
Thread-1---获取到了R2锁,申请R1锁....

此时程序并没有结束,这种现象就是死锁现象...线程Thread-0持有R1的锁等待获取R2锁,线程Thread-1持有R2的锁等待获取R1的锁。


21、如何进行死锁诊断?

当程序出现了死锁现象,我们应该如何进行诊断呢?使用jdk自带的工具: jstack

对下面的程序使用jstack进行死锁诊断:

C:\Users\Administrator>jps
7408
8144 DeadThreadDemo1
3620
9108 Launcher
9180 Jps

C:\Users\Administrator>jstack -l 8144
2019-10-19 14:52:01
Full thread dump Java HotSpot(TM) 64-Bit Server VM (11+28 mixed mode):
...
"Thread-0":
        at com.itheima.javase.security.demo10.DeadThread.run(DeadThread.java:19)
        - waiting to lock <0x000000008c591218> (a java.lang.Object)   // 等待锁0x000000008c591218
        - locked <0x000000008c591208> (a java.lang.Object)              // 已经拥有的锁0x000000008c591208
"Thread-1":
        at com.itheima.javase.security.demo10.DeadThread.run(DeadThread.java:29)
        - waiting to lock <0x000000008c591208> (a java.lang.Object)   // 等待锁0x000000008c591208
        - locked <0x000000008c591218> (a java.lang.Object)              // 已经拥有的锁0x000000008c591218

Found 1 deadlock.        // 发现了一个1个死锁


C:\Users\Administrator>


22、Linux环境下如何查找哪个线程使用CPU最长?

步骤如下:

1、通过ps -ef | grep java查询指定的进程id

2、通过top -H -p pid查看指定进程中的线程信息

3、将线程的id的十进制数据转换成十六进制:printf "%x" tid

4、通过jstack -l pid查询进行中的线程nid(native thread id)


23、ThreadLocal 是什么?有哪些使用场景?(高频)

ThreadLocal,线程本地变量,可以实现对象的线程隔离,为共享变量在每个线程中创建一个副本,每个线程都可以访问自己 内部的副本变量。通过 ThreadLocal 保证线程的安全性 。

可以在一个线程内共享数据。在我们的项目中使用ThreadLocal存储的是解析token以后的用户数据。即是在一个线程的多个方法中实现共享资源对象。

其实在 ThreadLocal 类中有一个静态内部类 ThreadLocalMap(其类似于 Map), 用键值对的形式存储每一个线程的变量副本, Map的键是当前线程对象,值就是要共享的数据。

使用完 ThreadLocal 之后,记得调用 remove 方法。如果没有手动删除对应 key 就会导致这块内存即不会回收也无法访问,也就是内存泄 漏。 但在不使用线程池的前提下, 即使不调用 remove 方法,线程的"变量副本"也会被 gc 回收,即不会造成内存泄漏的情况。


24、ThreadLocal的原理

  • 每一个线程都有一个成员变量,类型是ThreadLocalMap,用来存储资源对象

  • 当调用ThreadLocal的set方法的时候,就是将ThreadLocal作为Key,将资源对象作为value存储到当前线程的ThreadLocal Map属性中

  • 当调用ThreadLocal的get方法的时候,就是将ThreadLocal作为Key,从当前线程的ThreadLocalMap属性中获取资源对象

  • 当调用ThreadLocal的remove方法的时候,就是将ThreadLocal作为Key,从当前线程的ThreadLocalMap属性中删除资源对象

  • 对资源进行线程隔离的,实际上是每个线程中的ThreadLocalMap成员变量

代码:

public class ThreadLocalTest {
    private static ThreadLocal<Integer> a = new ThreadLocal<>();
    private static ThreadLocal<Integer> b = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            a.set(1);
        },"t1").start();

        new Thread(() -> {
            a.set(1);
        },"t2").start();

        new Thread(() -> {
            a.set(1);
        },"t3").start();
    }
}


25、ThreadLocalMap扩容机制?索引冲突?

  • 扩容机制

1.如果频繁在某个线程中使用不同ThreadLocal添加元素

2.当元素个数超过容器2/3进行扩容

3.ThreadLocalMap的默认容量16

  • 索引冲突:开放地址法


26、线程池中分配线程的依据?(高频)

① 高并发、任务执行时间短的业务,线程池程数可以设置为CPU核数+1,减少线程上下文的切换

② 并发不高、任务执行时间长的业务要区分开看

  • 假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务

  • 假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和①一样把线程池中的线程数设置得少一些,减少线程上下文的切换

③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考②。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。


27、线程池核心线程数和最大线程数如何进行设置?

1、CPU密集型

CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。

CPU 使用率较高(例如:计算一些复杂的运算,逻辑处理等情况)非常多的情况下,线程数一般只需要设置为CPU核心数的线程个数就可以了。 这一类型多出现在开发中的一些业务复杂计算和逻辑处理过程中。

2、IO密集型

IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。

CPU 使用率较低,程序中会存在大量的 I/O 操作占用时间,导致线程空余时间很多,所以通常就需要开CPU核心数两倍的线程。当线程进行 I/O 操作 CPU 空闲时,启用其他线程继续使用 CPU,以提高 CPU 的使用率。例如:数据库交互,文件上传下载,网络传输等。

线程等待时间所占比例越高,需要越多线程,启用其他线程继续使用CPU,以此提高CPU的利用率;线程 CPU 时间所占比例越高,需要越少的线程,这一类型在开发中主要出现在一些计算业务频繁的逻辑中。

总结

CPU密集型 可以理解为 就是处理繁杂算法的操作,对硬盘等操作不是很频繁,比如一个算法非常之复杂,可能要处理半天,而最终插入到数据库的时间很快。

IO密集型可以理解为简单的业务逻辑处理,比如计算1+1=2,但是要处理的数据很多,每一条都要去插入数据库,对数据库频繁操作。

核心线程

CPU密集型:核心线程数=CPU核心数(或 核心线程数=CPU核心数+1)。

I/O密集型:核心线程数=2*CPU核心数(或 核心线程数=CPU核心数/(1-阻塞系数))。

最大线程

CPU密集型应用,最大线程设置为 N+1。

IO密集型经验应用,最大线程设置为 2N+1 (N为CPU数量,下同)。


28、如果你提交任务时,核心线程池已经满了,这时会发生什么?(高频)

① 无界队列

如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务

② 有界队列

如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy


29、创建线程池有哪几种方式?(高频)

① 通过Executor框架的工具类Executors来实现,我们可以创建三种类型的:

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

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

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

② 《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor 的方式去创建

ThreadPoolExecutor最完整的构造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

参数说明

corePoolSize:   核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime:  空闲线程最大存活时间,不能小于0
unit:           时间单位
workQueue:      任务队列,不能为null
threadFactory:  创建线程工厂,不能为null      
handler:        任务的拒绝策略,不能为null   

任务的拒绝策略种类:

ThreadPoolExecutor.AbortPolicy:             丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy:            丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy:    抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy:        调用任务的run()方法绕过线程池直接执行。


30、如何控制某个方法允许并发访问线程的数量?

Semaphore两个重要的方法分别是:

1. semaphore.acquire() 请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)

2. semaphore.release()释放一个信号量,此时信号量个数+1

线程任务类:

public class CarThreadRunnable implements Runnable {

    // 创建一个Semaphore对象,限制只允许2个线程获取到许可证
    private Semaphore semaphore = new Semaphore(2) ;

    @Override
    public void run() {                         // 这个run只允许2个线程同时执行

        try {

            // 获取许可证
            semaphore.acquire();
            
            System.out.println(Thread.currentThread().getName() + "----->>正在经过十字路口");

            // 模拟车辆经过十字路口所需要的时间
            Random random = new Random();
            int nextInt = random.nextInt(7);
            TimeUnit.SECONDS.sleep(nextInt);

            System.out.println(Thread.currentThread().getName() + "----->>驶出十字路口");

            // 释放许可证
            semaphore.release();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

测试类:

public class SemaphoreDemo01 {

    public static void main(String[] args) {

        // 创建线程任务类对象
        CarThreadRunnable carThreadRunnable = new CarThreadRunnable() ;

        // 创建5个线程对象,并启动。
        for(int x = 0 ; x < 5 ; x++) {
            new Thread(carThreadRunnable).start();
        }

    }

}


31、线程池中 submit()和 execute()方法有什么区别?(高频)

1、接收的参数不一样

2、submit有返回值,而execute没有


33、什么是Java内存模型?

JMM(Java Memory Model),即Java内存模型,是Java虚拟机规范中所定义的一种内存模型。

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

特点:

1.所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

2.每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。

3.线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。

|


33、volatile 是什么?可以保证有序性吗?

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

① 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。

② 禁止进行指令重排序,可以保证有序性。

指令重排:计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排。处理器在进行重排序时,必须要考虑指令之间的数据依赖性

如下代码:

public void mySort() {
    int x = 11;
    int y = 12;
    x = x + 5;
    y = x * x;
}

按照正常的顺序进行执行,那么执行顺序应该是:1 2 3 4 。但是如果发生了指令重排,那么此时的执行顺序可能是:① 1 3 2 4 或 ② 2 1 3 4 ,但是肯定不会出现:4 3 2 1这种顺序,因为处理器在进行重排时候,必须考虑到指令之间的数据依赖性。多线程环境里编译器和CPU指令优化根本无法识别多个线程之间存在的数据依赖性,比如说下面的程序代码如果两个方法在两个不同的线程里面调用就可能出现问题。

private static int value;
private static volatile boolean flag;

public static void init(){
    value=8;     //语句1 
    flag=true;   //语句2  
}

public static void getValue(){
    if(flag){
        System.out.println(value);
    }
}

根据上面代码,如果程序代码运行都是按顺序的,那么getValue() 中打印的value值必定是等于8的,不过如果init()方法经过了指令重排序,那么结果就不一定了。进行重排序后代码执行顺序可能如下:

flag=true;  //语句2  
value=8;    //语句1

如果init()方法经过了指令重排序后,这个时候两个线程分别调用 init()和getValue()方法,那么就有可能出现下图的情况,导致最终打印出来的value数据等于0。

解决方案:使用volatile修饰flag,禁止指令重排。

原理说明:添加了一个内存屏障,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化


34、volatile能否保证线程安全

不能。

volatile的特性:

  • 可见性:一个线程对共享变量进行了修改,另外一个线程能否看到修改后的结果

  • 有序性:一个线程内代码是否按照编写顺序执行

  • 原子性:一个线程内多行代码以一个整体运行,期间不能有其他线程的代码插队

public class Test01 {
    private volitle static int a = 0;
    public static void main(String[] args) {
     
        for (int i = 1; i <= 10000; i++) {
            new Thread(() -> {
                a++;
            }).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println(a);
    }
}

volatile能否保证线程的可见性、有序性(单例设计模式)


35、什么是CAS?

CAS的全称是: Compare And Switch(比较并交换); 是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。CAS的作用是可以保证对共享变量的操作是原子性。

原理:操作共享变量之前,先查看该共享变量是否进行了修改,如果进行了修改,重试;如果没有进行修改;修改共享变量

CAS有3个操作数:内存值V,旧的预期值A,要修改的新值B。当且仅当旧预期值A和内存值V相同时,将内存值V修改为B并返回true,否则什么都不做,并返回false。

public class Test01 {

    private static Unsafe unsafe;

    static {
        //Unsafe:cas的核心类
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(Unsafe.class);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }


    public static void main(String[] args) throws Exception {
        //通过cas原子性操作int类型的一个方法
        Account account = new Account();
        Field balance = Account.class.getDeclaredField("balance");
        long offset = unsafe.objectFieldOffset(balance);
        while (true) {
            int current = account.balance;
            int change = current + 5;
            boolean b = unsafe.compareAndSwapInt(account, offset, current, change);
            if(b) {
                break;
            }
        }


        System.out.println(account.balance);
    }


}

class Account {
    public volatile int balance = 10;
}


36、CAS有什么缺陷,如何解决?

① ABA问题

并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。可以通过AtomicStampedReference「解决ABA问题」,它是一个带有标记(数据版本号)的原子引用类,通过控制变量值的版本来保证CAS的正确性。

② 循环时间长开销

自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~

③ 只能保证一个变量的原子操作

CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。

可以通过这两个方式解决这个问题:

  • 使用互斥锁来保证原子性

  • 将多个变量封装成对象,通过AtomicReference来保证原子性


37、@Async注解

  • 含义

1.在方法上使用该@Async注解,申明该方法是一个异步任务;

2.在类上面使用该@Async注解,申明该类中的所有方法都是异步任务;

3.使用此注解的方法的类对象,必须是spring管理下的bean对象;

4.要想使用异步任务,需要在主类上开启异步配置,即,配置上@EnableAsync注解;

  • 使用:在Spring中启用@Async:

1.@Async注解在使用时,如果不指定线程池的名称,则使用Spring默认的线程池,Spring默认的线程池为SimpleAsyncTaskExecutor。

2.方法上一旦标记了这个@Async注解,当其它线程调用这个方法时,就会开启一个新的子线程去异步处理该业务逻辑。

  • 原理概括:

@Async 的原理是通过 Spring AOP 动态代理 的方式来实现的。

Spring容器启动初始化bean时,判断类中是否使用了@Async注解,如果使用了则为其创建切入点和切入点处理器,根据切入点创建代理,在线程调用@Async注解标注的方法时,会调用代理,执行切入点处理器invoke方法,将方法的执行提交给线程池中的另外一个线程来处理,从而实现了异步执行。

所以,需要注意的一个错误用法是,如果a方法调用它同类中的标注@Async的b方法,是不会异步执行的,因为从a方法进入调用的都是该类对象本身,不会进入代理类。

因此,相同类中的方法调用带@Async的方法是无法异步的,这种情况仍然是同步。

  • 注意点

1.注解@Async的方法不是public方法

2.注解@Async的返回值只能为void或Future

3.注解@Async方法使用static修饰也会失效

4.spring无法扫描到异步类,没加注解@Async或@EnableAsync注解

5.调用方与被调用方不能在同一个类

6.类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象

7.在Async方法上标注@Transactional是没用的.但在Async方法调用的方法上标注@Transcational是有效的


38、什么是AQS?

1、AQS:翻译过来应该是抽象队列同步器 ,本质上是一个框架,通过这个框架可以实现阻塞锁的操作

2、juc包下的工具类,在实现相关功能的时候,都是以AQS为基础进行实现

3、ReentrantLock的互斥功能使用的就是AQS中独占模式进行实现的,Semaphore在实现允许多个线程同时访问某一个共享的资源的时候使用的就是AQS中的共享模式。

AQS全名:AbstractQueuedSynchronizer翻译过来应该是抽象队列同步器,是并发容器J.U.C(java.util.concurrent)下locks包内的一个类。如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等都用到了它。

AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表。如下所示:

AQS中维护了一个volatile int state(共享资源)和一个CLH队列。我们可以通过修改state字段表示的同步状态来实现多线程的独占模式共享模式(加锁过程)。

具体过程如下所示:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值