Java——10个关于Java中多线程并发的面试题

Posted 张起灵-小哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java——10个关于Java中多线程并发的面试题相关的知识,希望对你有一定的参考价值。

1.多线程的创建方式有几种?

答案:应该是3种。

1.1 继承Thread类,重写run方法

/**
 * 实现线程的第一种方式: 直接继承java.lang.Thread,重写run方法
 * 怎么创建线程?  new就可以了
 * 怎么启动线程?  start就可以了
 */
class MyThread extends Thread {

    @Override
    public void run() {
        //这段程序运行在分支线程中(分支栈)
        for (int i=0;i<10;i++) {
            System.out.println("分支线程---> " + i);
        }
    }
}

public class ThreadTest02 {

    public static void main(String[] args) {
        //新建一个分支线程对象
        MyThread myThread=new MyThread();
        //启动线程
        //start方法的作用是:启动一个分支线程,在JVW中开辟一个新的栈空间
        //只要栈空间开辟出来,start方法就结束了,线程就启动成功了,启动成功的线程会自动调用run方法
        //run方法在分支线程的栈底部,main方法在主线程的栈底部,run和main是平级的
        myThread.start();
        //下面的代码运行在主线程中
        for (int i=0; i<10; i++) {
            System.out.println("主线程---> " + i);
        }
    }
}

1.2 实现Runnable接口,重写run方法

/**
 * 实现线程的第二种方式: 实现java.lang.Runnable接口,实现run方法
 */
class MyRunnable implements Runnable {

    @Override
    public void run() {
        //分支线程
        for (int i=0;i<10;i++) {
            System.out.println("分支线程---> " + i);
        }
    }
}

public class ThreadTest03 {

    public static void main(String[] args) {
        //将一个可运行对象封装成一个线程对象
        Thread thread=new Thread(new MyRunnable());
        //启动线程
        thread.start();
        //主线程
        for (int i=0; i<10; i++) {
            System.out.println("主线程---> " + i);
        }
    }
}

1.3 实现Callable接口,重写call方法,其中采用线程池

package com.szh.thread;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;

/**
 * 实现线程的第三种方式:实现Callable接口
 */
class MyCallable implements Callable<Object> {

    private String taskNum;

    public MyCallable(String taskNum) {
        this.taskNum=taskNum;
    }

    @Override
    public Object call() throws Exception {
        System.out.println(">>> " + taskNum + "任务启动");
        Date dateTest1=new Date();
        Thread.sleep(1000);
        Date dateTest2=new Date();
        long time=dateTest2.getTime() - dateTest1.getTime();
        System.out.println(">>> " + taskNum + "任务终止");
        return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";
    }
}

public class ThreadTest16 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("-----程序开始运行-----");

        Date date1=new Date();

        int taskSize=5;
        //创建一个线程池
        ExecutorService pool= Executors.newFixedThreadPool(taskSize);
        //创建具有多个返回值的任务
        List<Future> list=new ArrayList<>();
        for (int i=0;i<taskSize;i++) {
            Callable callable=new MyCallable(i + " ");
            //执行任务并获取Future对象
            Future future=pool.submit(callable);
            //添加到list集合中
            list.add(future);
        }
        //关闭线程池
        pool.shutdown();

        //获取所有并发任务的运行结果
        for (Future f : list) {
            //从Future对象上获取到任务的返回值,打印输出到控制台
            System.out.println(">>> " + f.get().toString());
        }

        Date date2=new Date();
        System.out.println("-----程序结束运行-----,程序运行时间【" + (date2.getTime()-date1.getTime()) + "毫秒】");
    }
}

2.说说Runnable和Callable的区别?

  • Callable能够抛出checked exception(受检异常),而Runnable不可以。 
  • Callable可以返回一个泛型V,而Runnable不可以。
  • Callable可以获取线程的执行结果,Runnabl不可以。

3.说说wait和sleep方法的不同?

最大的不同是在等待时wait会释放锁,而sleep一直持有锁。wait通常被用于线程间交互,sleep通常被用于暂停执行。 

4.synchronized和volatile关键字的作用、区别?

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

● 保证了不同线程对这个变量进行操作时的可见性,不保证原子性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

● 禁止进行指令重排序。

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法级别的。
  • volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

5.什么是线程池,如何使用?

线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。

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

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

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

在JDK的java.util.concurrent.Executors中提供了生成多种线程池的静态方法。 

5.1 创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行

ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

5.2 创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。

ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(10);

5.3 创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

5.4 创建一个固定大小的线程池,支持定时及周期性任务执行。

ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(10);

6.线程池的启动策略是什么?

1、线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

2、当调用execute()方法添加一个任务时,线程池会做如下判断:

(1)如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

(2)如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

(3)如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建线程运行这个任务;

(4)如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。

(5)当一个线程完成任务时,它会从队列中取下一个任务来执行。

(6)当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。

所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

7.说说synchronized和Lock的区别?

  • synchronized是关键字,Lock是类。
  • synchronized无法获取锁的状态,Lock可以。
  • synchronized会自动释放锁,Lock需要手动。
  • synchronized没有Lock锁灵活(Lock锁可以自己定制)。

8.请写一段简单的死锁代码

class MyThread1 extends Thread {
    Object o1;
    Object o2;

    public MyThread1(Object o1,Object o2) {
        this.o1=o1;
        this.o2=o2;
    }

    @Override
    public void run() {
        synchronized (o1) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2) {

            }
        }
    }
}

class MyThread2 extends Thread {
    Object o1;
    Object o2;

    public MyThread2(Object o1,Object o2) {
        this.o1=o1;
        this.o2=o2;
    }

    @Override
    public void run() {
        synchronized (o2) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1) {

            }
        }
    }
}

public class DeadLock {

    public static void main(String[] args) {
        Object o1=new Object();
        Object o2=new Object();

        //这两个线程共享o1、o2对象
        Thread thread1=new MyThread1(o1,o2);
        Thread thread2=new MyThread2(o1,o2);

        //启动线程
        thread1.start();
        thread2.start();
    }
}

9.请说出同步线程及线程调度相关的方法?

wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;

sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;

notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;

notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

注意:java 5通过Lock接口提供了显示的锁机制,Lock接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调。

10.非阻塞算法CAS

首先我们需要了解悲观锁和乐观锁

悲观锁:假定并发环境是悲观的,如果发生并发冲突,就会破坏一致性,所以要通过独占锁彻底禁止冲突发生。有一个经典比喻,“如果你不锁门,那么捣蛋鬼就回闯入并搞得一团糟”,所以“你只能一次打开门放进一个人,才能时刻盯紧他”。

乐观锁:假定并发环境是乐观的,即虽然会有并发冲突,但冲突可发现且不会造成损害,所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。可类比的比喻为,“如果你不锁门,那么虽然捣蛋鬼会闯入,但他们一旦打算破坏你就能知道”,所以“你大可以放进所有人,等发现他们想破坏的时候再做决定”。通常认为乐观锁的性能比悲观所更高,特别是在某些复杂的场景。这主要由于悲观锁在加锁的同时,也会把某些不会造成破坏的操作保护起来;而乐观锁的竞争则只发生在最小的并发冲突处,如果用悲观锁来理解,就是“锁的粒度最小”。但乐观锁的设计往往比较复杂,因此,复杂场景下还是多用悲观锁。首先保证正确性,有必要的话,再去追求性能。

乐观锁的实现往往需要硬件的支持,多数处理器都都实现了一个CAS指令,实现“Compare And Swap”的语义(这里的swap是“换入”,也就是set),构成了基本的乐观锁。CAS包含3个操作数:

需要读写的内存位置V

进行比较的值A

拟写入的新值B

当且仅当位置V的值等于A时,CAS才会通过原子方式用新值B来更新位置V的值;否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。一个有意思的事实是,“使用CAS控制并发”与“使用乐观锁”并不等价。CAS只是一种手段,既可以实现乐观锁,也可以实现悲观锁。乐观、悲观只是一种并发控制的策略。

以上是关于Java——10个关于Java中多线程并发的面试题的主要内容,如果未能解决你的问题,请参考以下文章

Java——15个关于Java中多线程并发的面试题

面试15个顶级Java多线程面试题及回答线索

关注15个顶级Java多线程面试题及回答

华为18级大佬总结的15个顶级多线程面试题及答案

最常见的15个Java多线程,并发面试问题

java中多线程地并发运行是啥意思?有啥作用.好处?