Java多线程知识面试题(收录全网总结最全面的面试题)

55 阅读8分钟

1.线程和进程的区别

进程是资源分配的最小单位,线程是资源调度的最小单位

一个进程有多个线程。各个线程都拥有自己的堆栈、计数器、局部变量并且共享进程的资源,提高了cpu的利用率。

2.创建线程的四种方式

1.继承于Thread类

/**
 * 多线程的创建,方式一:继承于Thread类
 * 1.创建一个继承于Thread类的子类
 * 2.重写Thread类的run()方法
 * 3.创建Thread类的子类对象
 * 4.通过此对象调用start()
 */
//1.创建一个继承于Thread类的子类
 class MyThread extends  Thread{
    //2.重写Thread类的run()方法
    public void run(){
        //遍历100以内的所有的偶数
         for (int i=0;i<100;i++){
             System.out.println(Thread.currentThread().getName() + "运行,i = " + i) ;
         }
    }
}
 
public class ThreadTest  {
    public static void main(String[] args) {
        //3.创建Thread类的子类对象
        MyThread mt1 = new MyThread() ;    // 实例化对象
        MyThread mt2 = new MyThread() ;    // 实例化对象
        mt1.setName("线程A");
        mt2.setName("线程B");
        //4.通过此对象调用start()
        mt1.start();
        mt2.start();
    }
}

2.实现Runnable接口

        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                for (int i=0;i<10;i++){
                    System.out.println("子线程输出,i = " + i) ;
                }
            }
    };
        Thread t1=new Thread(runnable);
        t1.start();
        for (int i=0;i<10;i++){
            System.out.println("主线程输出,i = " + i) ;
        }
 
 
       //也可以简化成
        new Thread(() -> {
                for (int i=0;i<10;i++) {
                    System.out.println("子线程输出,i = " + i);
                }
        }).start();
        for (int i=0;i<10;i++){
            System.out.println("主线程输出,i = " + i) ;
        }

3.通过Callable和FutureTask创建线程,Callable用于产生结果,FutureTask用于获取结果

public class MyCallable implements Callable<String> {
 
    @Override
    public String call() throws Exception {
        int sum=0;
        for (int i = 0; i < 100; i++) {
            sum+=i;
        }
        return "子线程的返回结果是:"+sum;
    }
 
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask  task=new FutureTask(new MyCallable());
        Thread t1=new Thread(task);
        t1.start();
 
         //get()方法等待任务的程序执行完毕后才会执行
        System.out.println(task.get());
    }
 
}

4.通过线程池创建

@Configuration
@EnableAsync
public class TaskExecutePool {
 
        @Bean
        public Executor myTaskAsyncPool() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(5); //
            executor.setMaxPoolSize(20);  //最大线程数(要大于等于核心线程数)
            executor.setQueueCapacity(1000); //队列大小
            executor.setKeepAliveSeconds(300); //线程最大空闲时间
            executor.setThreadNamePrefix("async-Executor-");//线程前缀名称
            executor.setRejectedExecutionHandler(new   ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
            executor.initialize();
            return executor;
        }
}

java线程池.png

3.线程的start()和run()方法的区别

线程的start()方法用于启动线程,run()方法用于执行线程代码,run()可以重复调用,start()只能调用一次。

调用start()方法无需等待run()方法执行完毕,可以直接继续执行其他代码,此时线程处于就绪状态并没有运行,通过此Thread类调用run方法来完成其运行状态,run方法结束后线程终止,执行其他线程。

通过start()方法来调用run()方法而不是直接调用run()方法是因为通过start()方法启动线程,使线程处于就绪状态,直接调用run()是此Thread普通方法的调用。

4.线程的状态

java多线程的状态.webp

新建:创建一个线程对象

就绪:通过start()方法使该线程处于就绪状态,等待线程的调用

运行:拿到了cpu的使用权,线程要进入到运行状态必须是就绪状态

阻塞:处于运行状态中的线程因为某种原因暂时放弃cpu的使用权,停止执行,进行阻塞状态,直到进入就绪状态才会被再次调用进入运行状态

阻塞分成三种等待阻塞、同步阻塞、其他阻塞

等待阻塞:运行中的线程执行wait()方法,jvm将线程放入等待队列(waitting queue)中,使线程进入等待阻塞状态

同步阻塞:线程在获取synchronized 同步锁失败,jvm将线程放入锁池(lock pool)中,线程会进入同步阻塞状态

其他阻塞:调用线程的sleep()或者join()方法或发出了I/O请求时线程进入到阻塞状态,当 sleep()状态超时、join()等待线程终止或者超 时、或者 I/O 处理完毕时,线程重新转入就绪状态。

结束:线程run()、main()方法执行结束,或者因异常退出了run()方法

5.线程同步

1.同步代码块

作用:把出现线程安全问题的核心代码给上锁。

原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

                    synchronized (同步锁对象){
                            操作共享资源的代码(核心代码 )
                    }

锁对象要求:对于当前同时执行的线程来说是同一个对象即可。对于实例方法建议使用this作为锁对象,对于静态方法建议使用字节码(类名.class)对象作为锁对象。

2.同步方法

作用:把出现线程安全问题的核心方法给上锁。

原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

修饰符 synchronized 返回值类型 方法名称(形参列表) {
    操作共享资源的代码
}

同步方法底层是有隐式锁对象,锁的范围是整个方法代码。如果方法是实例方法,同步方法默认用this作为锁的对象。如果方法是静态方法,同步方法默认用类名.class作为锁的对象。

3.Lock锁

//final修饰后锁对象唯一
private  final Lock  lock=new ReentrantLock();
//上锁
lock.lock();
//解锁
lock.unlock();

synchronized与Lock都可以解决线程安全问题,synchronized机制在执行完相应的同步代码以后,自动的释放同步监听器。Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())

优先使用顺序:Lock→同步代码块(已经进入了方法体,分配了相应的资源)→ 同步方法(在方法体之外)

6.死锁的定义以及发生条件

死锁:两个或两个以上的线程抢占资源造成的互相等待

产生死锁的四个条件:

1.互斥条件:某段时间内资源已被一个进程占用,如果还有其他进程请求该资源,只能等待占用该资源的进程用完释放

2.请求和保持条件:进程在请求新资源的时候保持对已有资源的占有

3.不剥夺条件:进程已获得的资源在未使用完前不会被剥夺,使用完后释放

4.环路等待条件:发生死锁时必然存在一个进程

7. Java 中用到的线程调度算法是什么?

有两种调度模型:分时调度模型和抢占式(java默认使用)调度模型。

分时调度模型: 平均分配每个线程占用的 CPU 的时间片

抢占式调度模型: 让优先级高的线程占用CPU,如果线程优先级相同,那么就随机选择一个线程

8.notify()和 notifyAll()有什么区别

当一个线程进入 wait 之后,就必须等其他线程 notify/notifyall,使用 notifyall,可以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。

9.wait 和 sleep 方法的区别

sleep()没有释放锁,让线程休眠指定的时间

wait方法释放了锁,等待其他线程调用notify/notifyAll唤醒

10.进程之间的通信方式

1.管道:内核中维护的一块内存缓冲区

2.命名管道:可以在不存在亲缘关系的进程中通信

3.信号:一种通知机制

4.消息队列:是一个消息链表,既可读消息,也可以写消息

5.共享内存:多个线程共享一片内存区域

6.内存映射:将磁盘文件映射到内存,修改内存就可以修改磁盘文件

7.信号量:设计了信号量以保证多进程并发访问共享变量的安全。信号量实则是一个计数器,拥有原子操作P和V。当信号量值小于等于0之后再进行P操作会把对应线程或进程阻塞。

8.Socket:一般用于不同主机之间的进程通信

11.乐观锁和悲观锁

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的

12.Synchronize与ReentrantLock区别

synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;