文章目录:

1.多线程的创建方式,你知道几种?

1.1 继承Thread类(重写run()方法)

1.2 实现Runnable接口(重写run()方法)

1.3 实现Callable接口(重写call()方法)

2.说说实现Runnable和Callable这两个接口的区别?

3.说说synchronized和Lock的区别?

4.synchronized和volatile的区别?

5.说说wait和sleep这两个方法的不同?

6.JVM对锁的优化了解吗?

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

8.线程池的启动策略?

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

10.什么是CAS?

11.简单聊聊乐观锁与悲观锁

12.启动线程时,调用的是start方法还是run方法?

13.写一段简单的死锁代码

14.你是如何合理配置线程池大小的?

15.线程池的具体配置参数(七大参数 + 四种拒绝策略)


1.多线程的创建方式,你知道几种?

这个问题了话,其实已经说烂了,天天学,天天说,不就是那三种吗?继承Thread类(重写run()方法)、实现Runnable接口(重写run()方法)、实现Callable接口(重写call()方法)。

1.1 继承Thread类(重写run()方法)

package com.szh.begin;

/**
 * 实现多线程的第一种方式:继承Thread类,重写run()方法
 */
public class Test01 {

    static class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }

    public static void main(String[] args) {
        MyThread t1=new MyThread();
        MyThread t2=new MyThread();

        t1.setName("t1");
        t2.setName("t2");

        //start方法的作用是:启动一个分支线程,在JVW中开辟一个新的栈空间
        //只要栈空间开辟出来,start方法就结束了,线程就启动成功了,启动成功的线程会自动调用run方法
        //run方法在分支线程的栈底部,main方法在主线程的栈底部,run和main是平级的
        t1.start();
        t2.start();

    }
}

如果你觉得上面main方法中代码写的比较多,你也可以修改为下面这种方式:👇👇👇

package com.szh.begin;

/**
 * 实现多线程的第一种方式:继承Thread类,重写run()方法
 */
public class Test01 {

    static class MyThread extends Thread {

        public MyThread(String name) {
            super();
        }
        
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }

    public static void main(String[] args) {
        new MyThread("t1").start();
        new MyThread("t2").start();
    }
}

1.2 实现Runnable接口(重写run()方法)

传统写法

package com.szh.begin;

/**
 * 实现多线程的第二种方式:实现Runnable接口,重写run()方法
 * 传统写法
 */
public class Test02 {

    static class MyRunnable implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new MyRunnable(),"t1").start();
        new Thread(new MyRunnable(),"t2").start();
    }
}

匿名内部类写法

package com.szh.begin;

/**
 * 实现多线程的第二种方式:实现Runnable接口,重写run()方法
 * 匿名内部类写法
 */
public class Test03 {

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " ---> " + i);
                }
            }
        },"t1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + " ---> " + i);
                }
            }
        },"t2").start();

    }
}

1.3 实现Callable接口(重写call()方法)

package com.szh.begin;

import java.util.concurrent.*;

/**
 * 实现多线程的第三种方式:实现Callable接口,重写call()方法
 * 使用Future接收线程的执行结果
 */
public class Test04 {

    static class MyCallable implements Callable<Object> {

        @Override
        public Object call() throws Exception {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " ---> " + i);
            }
            return "当前执行线程的id为:" + Thread.currentThread().getId();
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个固定大小的线程池
        ExecutorService service= Executors.newFixedThreadPool(2);
        //提交执行
        Future<Object> future1=service.submit(new MyCallable());
        Future<Object> future2=service.submit(new MyCallable());
        //获取call()方法的返回值(即线程执行结果的返回值)
        Object obj1=future1.get();
        Object obj2=future2.get();
        //打印
        System.out.println(obj1);
        System.out.println(obj2);
        //关闭线程池
        service.shutdown();
    }
}

2.说说实现Runnable和Callable这两个接口的区别?

  • 实现Runnable接口时,重写的是run()方法,该方法不能抛出异常;而实现Callable接口时,重写的是call()方法,该方法可以抛出checked exception编译时异常(受检异常)。
  • 实现Runnable接口无法获取线程的执行结果;而实现Callable接口可以通过get()方法获取线程的执行结果。
  • 实现Callable接口可以返回一个泛型<V>;而实现Runnable接口不可以。

3.说说synchronized和Lock的区别?

  • synchronized是一个关键字(一般称 内部锁);Lock是一个接口(一般称 显示锁)。
  • synchronized不能获得锁的状态;Lock可以。
  • synchronized会自动释放锁,不需要手动释放锁;Lock必须通过unlock()方法手动释放锁。
  • synchronized在发生异常时,会自动释放占有的锁对象,因此不会造成死锁现象;Lock在发生异常时,如果没有主动调用unlock()方法释放占有的锁对象了话,则可能造成死锁现象,因此使用Lock时一般在finally代码块中释放锁对象。

最后简单聊一聊synchronized的原理:

synchronized是Java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁,使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。

执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。

执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。

synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。


4.synchronized和volatile的区别?

  • 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:①保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。   ②禁止进行指令重排序(就是在不影响单线程程序执行结果的情况下进行最优执行排序)。
  • volatile本质上是在告诉JVM,当前共享变量的值在该线程的工作内存中是不确定的,需要从主内存中读取该共享变量的值;synchronized则是锁定当前共享变量,此时只能有一个线程访问它,其他线程会被阻塞。
  • volatile仅能用来修饰变量;synchronized可以用来修饰变量、方法。
  • volatile仅能保证对变量的修改可见性,不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

5.说说wait和sleep这两个方法的不同?

  • wait方法是Object类中的,是每一个对象都具有的;sleep方法是Thread这个线程类的。
  • 执行wait方法之后,当前线程会立刻释放锁对象;而执行sleep方法之后,当前线程仅仅暂停执行,并不会释放锁对象。
  • 调用这两个方法时,程序都会产生编译时异常(InterruptedException)。
  • sleep方法不一定非要定义在同步代码中;wait方法必须定义在同步代码中。

6.JVM对锁的优化了解吗?

  • 锁偏向:锁偏向是针对加锁操作的一种优化。如果一个线程获得了锁,那么这个锁就进入了偏向模式(偏向这个线程)。当这个线程再次请求锁时,无需进行任何同步操作,这样可以节省有关锁申请的时间,提升了程序性能。
  • 轻量级锁:对于锁竞争比较激烈的场景,每次都是不同的线程来请求锁,那么上面说的偏向锁就失效了。如果锁偏向失败,JVM不会立即挂起线程,而是使用另一种优化手段:轻量级锁。  当第一个线程尝试获取锁时,这个锁会进入偏向模式(偏向这个线程),这个线程在修改锁对象头成为偏向锁时使用CAS操作,将锁对象头中的threadId改成自己的id,之后再访问锁对象时,只需要对比id即可。一旦有第二个线程来获取锁对象,因为偏向锁不会主动释放,所以此时就存在竞争了,JVM会检查原来持有锁对象的线程是否存活,如果不存活,则锁重新偏向新的线程;如果存活,则执行原来线程的栈,检查该锁对象的使用情况,如果仍然需要偏向锁,则此时偏向锁升级为轻量级锁。
  • 重量级锁:轻量级锁认为竞争存在,但竞争的程度较轻,一般两个线程对同一个锁的操作会错开,或者稍微等待一下(自旋),另一个线程就会释放锁。但是当自旋超过一定次数,A线程持有锁、B线程在自旋、又来了一个C线程,那么此时轻量级锁就会膨胀为重量级锁。重量级锁除了持有锁的线程外,其余线程全部阻塞。

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

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

在JDK的java.util.concurrent.Executors 这个工具类中提供了创建线程池的多种静态方法。 

//创建一个可缓存的线程池,此线程池不会对大小做限制,其大小完全依赖于操作系统(或者说JVM)
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

//创建一个固定大小的线程池,每次提交一个任务就创建一个线程,直到达到线程池的最大大小
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);

//创建一个固定大小的线程池,该线程池支持定时周期的执行某些任务
ExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);

//创建一个单线程的线程池,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

8.线程池的启动策略?

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

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

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

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

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

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

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

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

(6)当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。


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

  • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
  • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
  • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
  • 注意:java 5通过Lock接口提供了显示的锁机制,Lock接口中定义了加锁(lock()方法)和解锁(unLock()方法),增强了多线程编程的灵活性及对线程的协调。

10.什么是CAS?

CAS(Compare And Swap)比较与交换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false 。当然CAS一定要与volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

但是在CAS中会存在一个ABA问题,就是说假如我们期望的count变量值为100,之后有一个A线程访问它将其修改为了200,后来又有一个B线程访问它将其修改回了100,那么是否就认为count变量的值没有被其他线程更新呢?显然不是啊,它明显的被A、B两个线程更新过。这个共享变量count就经历了 A → B → A 过程。

如果想要规避ABA问题,可以使用原子变量类中的AtomicStampedReference,它可以为共享变量引入一个修订号(或者叫时间戳),每次修改共享变量时,相应的修订号就会增加1,ABA问题的过程就转变为:[A,0] → [B,1] → [A,2] ,每次修改共享变量都会导致修订号的增加,通过修订号就可以准确的判断共享变量是否被其他线程修改过。


11.简单聊聊乐观锁与悲观锁

  • 乐观锁:乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
  • 悲观锁:就很悲观了,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

12.启动线程时,调用的是start方法还是run方法?

  • start方法: 作用是启动一个新的分支线程,在JVM中开辟一个新的栈空间,只有栈空间开辟出来,start方法就结束了,同时该分支线程也就启动成功了。启动成功的线程会执行它的run方法。
  • run方法:就是普通的方法调用,不会启动新的线程。

13.写一段简单的死锁代码

package com.szh.test;

/**
 *
 */
public class DeadLock {

    private static final Object OBJ1=new Object();
    private static final Object OBJ2=new Object();

    static class MyThread implements Runnable {

        @Override
        public void run() {
            if ("a".equals(Thread.currentThread().getName())) {
                synchronized (OBJ1) {
                    System.out.println(Thread.currentThread().getName() + " 获得了OBJ1锁,还需要获得OBJ2锁...");
                    synchronized (OBJ2) {
                        System.out.println(Thread.currentThread().getName() + " 已经获得了OBJ1、OBJ2这两把锁");
                    }
                }
            }
            if ("b".equals(Thread.currentThread().getName())) {
                synchronized (OBJ2) {
                    System.out.println(Thread.currentThread().getName() + " 获得了OBJ2锁,还需要获得OBJ1锁...");
                    synchronized (OBJ1) {
                        System.out.println(Thread.currentThread().getName() + " 已经获得了OBJ1、OBJ2这两把锁");
                    }
                }
            }
        }
    }

    public static void main(String[] args) {

        new Thread(new MyThread(),"a").start();
        new Thread(new MyThread(),"b").start();

    }
}

14.你是如何合理配置线程池大小的?

首先,需要考虑到线程池所进行的工作的性质:IO密集型?CPU密集型?

简单的分析来看,如果是CPU密集型的任务,我们应该设置数目较小的线程数,比如CPU数目加1。如果是IO密集型的任务,则应该设置可能多的线程数,由于IO操作不占用CPU,所以,不能让CPU闲下来。当然,如果线程数目太多,那么线程切换所带来的开销又会对系统的响应时间带来影响。 


15.线程池的具体配置参数(七大参数 + 四种拒绝策略)

  • int corePoolSize 核心线程池大小
  • int maximumPoolSize 最大核心线程池大小
  • long keepAliveTime 超时存活时间
  • TimeUnit unit 超时单位
  • BlockingQueue workQueue 阻塞队列
  • ThreadFactory threadFactory 线程工厂,用于创建线程
  • RejectedExecutionHandler handler 拒绝策略
  • AbortPolicy 策略,会抛出异常
  • CallerRunsPolicy 策略,只要线程池没关闭,会在调用者线程中运行当前被丢弃的任务。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • DiscardOldestPolicy 策略,将任务队列中最老的任务丢弃,尝试再次提交新任务
  • DiscardPolicy 策略,直接丢弃这个无法处理的任务