Java并发编程面试题


进程和线程的区别是什么?

  • 进程是运行中的程序,线程是进程的内部的一个执行序列(一个进程内部有多个线程,多个线程共享进程的资源)
  • 进程是资源分配的单元,线程是执行行单元
  • 进程间切换代价大,线程间切换代价小
  • 进程拥有资源多,线程拥有资源少

    对于这种题目,专业术语不好理解,采用形象一点的比喻更好,比如(取自知乎):
    开个 QQ,开了一个进程;开了迅雷,开了一个进程。
    在 QQ 的这个进程里,传输文字开一个线程、传输语音开了一个线程、弹出对话框又开了一个线程。
    所以运行某个软件,相当于开了一个进程。在这个软件运行的过程里(在这个进程里),多个工作支撑的完成 QQ 的运行,那么这“多个工作”分别有一个线程。

创建线程的几种方式?

1、继承 Thread 类

方式 1:

public MainTest{
	public static void main(String[] args){
    	Thread t = new MyThread();
        t.start();
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 100; i++) {
            System.out.println(super.getName()+":"+i);
        }
    }
}

方式 2:

Thread t1 = new Thread("t1") {
    @Override
    public void run() {
        System.out.println("hello thread");
    }
};

t1.start();
System.out.println(t1.getName());
System.out.println("当前主线程是:"+ Thread.currentThread().getName());

方式 3:Lambda 接口

Thread t1 = new Thread(() -> {
    log.debug("开始");
    sleep(1);
    log.debug("结束");
    R = 10;
},"t1");
t1.start();

t1.join();
log.debug(Thread.currentThread().getName());

2、使用 Runnable 配合 Thread

实现 Runnable 接口,重写 run 方法
run 方法里写的是要执行的任务,该方法将【线程】和【任务】分开了

class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 创建线程对象
Thread t = new Thread(runnable,"t1");
// 启动线程
t.start();*/

3、FutureTask 配合 Thread

继承 Thread 和实现 Runnable 接口这两种创建线程的方法都没有返回值,FutureTask 方法可以有返回值
https://www.cnblogs.com/cjsblog/p/10905263.html
https://blog.csdn.net/qq_39654841/article/details/90631795

FutureTask 实现了 Future 接口,Future 接口有 5 个方法:
1、boolean cancel(boolean mayInterruptIfRunning)
尝试取消当前任务的执行。如果任务已经取消、已经完成或者其他原因不能取消,尝试将失败。如果任务还没有启动就调用了 cancel(true),任务将永远不会被执行。如果任务已经启动,参数 mayInterruptIfRunning 将决定任务是否应该中断执行该任务的线程,以尝试中断该任务。
如果任务不能被取消,通常是因为它已经正常完成,此时返回 false,否则返回 true
2、boolean isCancelled()
如果任务在正常结束之前被被取消返回 true
3、boolean isDone()
正常结束、异常或者被取消导致任务完成,将返回 true
4、V get()
等待任务结束,然后获取结果,如果任务在等待过程中被终端将抛出 InterruptedException,如果任务被取消将抛出 CancellationException,如果任务中执行过程中发生异常将抛出 ExecutionException。
5、V get(long timeout, TimeUnit unit)
任务最多在给定时间内完成并返回结果,如果没有在给定时间内完成任务将抛出 TimeoutException。

FutureTask futureTask = new FutureTask<Integer>(() ->{
    System.out.println("执行线程任务");
    return 100;
});
try {
    Thread t3 = new Thread(futureTask, "t3");
    t3.start();

    Object o = futureTask.get();
    System.out.println(o);
} catch (ExecutionException e) {
    e.printStackTrace();
}

打印结果:
    执行线程任务
    100

4、线程池

  • 使用线程池
public static void test01(){
    ExecutorService es = Executors.newFixedThreadPool(100);
    es.submit(()->{
        //start
        System.out.println("开始执行线程--->"+Thread.currentThread().getName());
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

线程的生命周期和线程的几种状态?


线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态。
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start 方法。该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。
3、运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了 run 方法,该线程结束生命周期。

阻塞的情况又分为三种:
(1)等待阻塞:运行的线程执行 wait 方法,该线程会释放占用的所有资源,JVM 会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用 notify 或 notifyAll 方法才能被唤醒,wait 是 object 类的方法,所有对象都可以调用;

(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入“锁池”中;

(3)其他阻塞:运行的线程执行 sleep 或 join 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep 状态超时、join 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。sleep 是 Thread 类的静态方法;

同步方法和同步代码块的区别是什么 ?

在 Java 语言中,每一个对象有一把锁。线程可以使用 synchronized 关键字来获取对象上的锁。synchronized 关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁)。
数据安全问题出现的条件:

  • 多线程环境
  • 共享数据
  • 有多条语句操作共享数据

在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?

链接:https://www.nowcoder.com/questionTerminal/26fc16a2a85e49a5bd5fc2b5759dbbc2
来源:牛客网
在 java 虚拟机中, 每个对象( Object 和 class )通过某种逻辑关联监视器,每个监视器和一个对象引用相关联, 为了实现监视器的互斥功能, 每个对象都关联着一把锁。一旦方法或者代码块被 synchronized 修饰, 那么这个部分就放入了监视器的监视区域, 确保一次只能有一个线程执行该部分的代码, 线程在获取锁之前不允许执行该部分的代码 另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案

什么是死锁(deadlock)?

链接:https://www.nowcoder.com/questionTerminal/09b51b00891543d6b08ace80c0704b01
来源:牛客网
死锁 :是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去
1、因为系统资源不足。
2、进程运行推进顺序不合适。
3、资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则 就会因争夺有限的资源而陷入死锁。其次,进程
运行推进顺序与速度不同,也可能产生死锁。

如何确保 N 个线程可以访问 N 个资源同时又不导致死锁 ?

使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线
程都是以同样的顺序加锁和释放锁,就不会出现死锁了。

锁池和等待池?

1、锁池
所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线
程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到
后会进入就绪队列进行等待 cpu 资源分配。
2、等待池
当我们调用 wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了
notify()或 notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放
到锁池,而 notifyAll()是将等待池的所有线程放到锁池当中

sleep()、wait()、join()、yield()的区别

1、sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。
2、sleep 方法不会释放 lock,但是 wait 会释放锁,而且会加入到等待队列中。

sleep 就是把 cpu 的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回 cpu 资源,参与 cpu
的调度,获取到 cpu 资源后就可以继续运行了。而如果 sleep 时该线程有锁,那么 sleep 不会释放这个锁,而
是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁。也就是说无法执行程
序。如果在睡眠期间其他线程调用了这个线程的 interrupt 方法,那么这个线程也会抛出
interruptexception 异常返回,这点和 wait 是一样的。

而 wait 会释放锁 交出 CPU 的执行权,进入到等待池中,直到调用 notify 或 notifyAll 后才会唤醒,进入就绪状态等待 CPU 调用;

3、sleep 方法不依赖于同步器 synchronized,但是 wait 需要依赖 synchronized 关键字。
4、sleep 不需要被唤醒(休眠之后推出阻塞),但是 wait 需要(不指定时间需要被别人中断)。
5、sleep 一般用于当前线程休眠,或者轮循暂停操作,wait 则多用于多线程之间的通信。
6、sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞
争到锁继续执行的。
7、yield()执行后线程直接从 Running(运行状态)进入 Runnable(就绪状态),会打断 synchronize 锁 让出 CPU 的执行权;
8、join()执行后线程进入阻塞状态,例如在线程 B 中调用线程 A 的 join(),那线程 B 会进入到阻塞队
列,直到线程 A 结束或中断线程;
举例:

public static void main(String[] args) throws InterruptedException {
  Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
      try {
        Thread.sleep(3000);
     } catch (InterruptedException e) {
          e.printStackTrace();
     }
      System.out.println("22222222");
   }
 });
  t1.start();
  t1.join();
  // 这行代码必须要等t1全部执行完毕,才会执行
  System.out.println("1111");
}

输出:
22222222
1111

什么叫 线程上下文切换?

(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完;
  • 垃圾回收;
  • 有更高优先级的线程需要运行;
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法;

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的,
当前线程的状态包括:程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等;

线程上下文 频繁切换 当然也会影响性能;

如何减少 上下文切换?

减少上下文切换的方法有无锁并发编程、CAS 算法、使用最少线程和使用协程

  • 无锁并发编程

    多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的 ID 按照 Hash 算法取模分段,不同的线程处理不同段的数据。

  • CAS 算法

    Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。

  • 使用最少线程

    避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。

  • 协程

    在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

JUC 常见方法

主要是 Thread.java 类

方法名 static 功能说明 注意
start() 启动一个新线程,在新线程中运行 run 方法中的代码 start 方法只是让线程进入就绪状态,里面代码不一定立刻运行,只有当 CPU 将时间片分给线程时,才能进入运行状态,执行代码。每个线程的 start 方法只能调用一次,调用多次就会出现 IllegalThreadStateException
run() 新线程启动会调用的方法 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
join() 等待线程运行结束
join(long n) 等待线程运行结束,最多等待 n 毫秒
getId() 获取线程长整型的 id id 唯一
getName() 获取线程名
setName(String) 修改线程名
getPriority() 获取线程优先级
setPriority(int) 修改线程优先级 java 中规定线程优先级是 1~10 的整数(默认是 5),较大的优先级能提高该线程被 CPU 调度的机率
getState() 获取线程状态 Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted() 判断是否被打断 不会清除 打断标记
isAlive() 线程是否存活(还没有运行完毕)
interrupt() 打断线程 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记,park 的线程被打断,也会设置 打断标记
interrupted() static 判断当前线程是否被打断 会清除打断标记(如果原来是 true,调用该方法后先返回 true 然后将打断标记改变一下变为 false)
currentThread() static 获取当前正在执行的线程
sleep(long n) static 让当前执行的线程休眠 n 毫秒,休眠时让出 cpu 的时间片给其它线程 会抛出异常
yield() static 提示线程调度器让出当前线程对 CPU 的使用 主要是为了测试和调试
State public enum State { 枚举 列出线程 6 种状态

构造方法:
重构了

public Thread( );
public Thread(Runnable target);
public Thread(String name);
public Thread(Runnable target, String name);
public Thread(ThreadGroup group, Runnable target);
public Thread(ThreadGroup group, String name);
public Thread(ThreadGroup group, Runnable target, String name);
public Thread(ThreadGroup group, Runnable target, String name, long stackSize);

Runnable target

实现了Runnable接口的类的实例。要注意的是Thread类也实现了Runnable接口,因此,从Thread类继承的类的实例也可以作为target传入这个构造方法。

String name

线程的名子。这个名子可以在建立Thread实例后通过Thread类的setName方法设置。如果不设置线程的名子,线程就使用默认的线程名:Thread-N,N是线程建立的顺序,是一个不重复的正整数。

ThreadGroup group

当前建立的线程所属的线程组。如果不指定线程组,所有的线程都被加到一个默认的线程组中(默认情况下,所有的线程都属于主线程组)

long stackSize

线程栈的大小,这个值一般是CPU页面的整数倍。如x86的页面大小是4KB.在x86平台下,默认的线程栈大小是12KB.

一个普通的java类只要从Thread类继承,就可以成为一个线程类。并可通过Thread类的start方法来执行线程代码。虽然Thread类的子类可以直接实例化,但在子类中必须要覆盖Thread类的run方法才能真正运行线程的代码。

原文链接:https://blog.csdn.net/liushijiao258/article/details/7941901

创建一个线程组,创建其他线程的时候,把其他线程的组指定为我们自己新建线程组
Thread(ThreadGroup group, Runnable target, String name)

// ThreadGroup(String name)
ThreadGroup tg = new ThreadGroup("这是一个新的组");

MyRunnable my = new MyRunnable();
// Thread(ThreadGroup group, Runnable target, String name)
Thread t1 = new Thread(tg, my, "刘织忋");
Thread t2 = new Thread(tg, my, "马化腾");

System.out.println(t1.getThreadGroup().getName());
System.out.println(t2.getThreadGroup().getName());

//通过组名称设置后台线程,表示该组的线程都是后台线程
tg.setDaemon(true);
//可通过ThreadGroup方法统一操作整个线程组线程

线程池

线程池概述
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
在 JDK5 之前,我们必须手动实现自己的线程池,从 JDK5 开始,Java 内置支持线程池。

Executors 工厂类

避免死锁的几个常见方法

举例出现死锁的情况:
锁是个非常有用的工具,运用场景非常多,因为它使用起来非常简单,而且易于理解。但同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用。让我们先来看一段代码,这段代码会引起死锁,使线程 t1 和线程 t2 互相等待对方释放锁。

public class DeadLockDemo {
    privat static String A = "A";
    private static String B = "B";
    public static void main(String[] args) {
        new DeadLockDemo().deadLock();
    }
    private void deadLock() {
        Thread t1 = new Thread(new Runnable() {
            @Override
            publicvoid run() {
                synchronized (A) {
                    try {
                        Thread.currentThread().sleep(2000);
                        } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (B) {
                        System.out.println("1");
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            publicvoid run() {
                synchronized (B) {
                    synchronized (A) {
                        System.out.println("2");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

这段代码只是演示死锁的场景,在现实中你可能不会写出这样的代码。但是,在一些更为复杂的场景中,你可能会遇到这样的问题,比如 t1 拿到锁之后,因为一些异常情况没有释放锁(死循环)。又或者是 t1 拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉。一旦出现死锁,业务是可感知的,因为不能继续提供服务了,那么只能通过 dump 线程查看到底是哪个线程出现了问题,以下线程信息告诉我们是 DeadLockDemo 类的第 42 行和第 31 行引起的死锁。

  • 避免一个线程同时获取多个锁。
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用 lock.tryLock(timeout)来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

对线程安全的理解

不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获
得正确的结果,我们就说这个对象是线程安全的

**堆 **是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分
配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了
要还给操作系统,要不然就是内存泄漏。
在 Java 中,堆是 Java 虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚
拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及
数组都在这里分配内存。

**栈 **是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈
互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语
言里面显式的分配和释放。
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己
的内存空间,而不能访问别的进程的,这是由操作系统保障的。

在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以
访问到该区域,这就是造成线程安全问题的潜在原因。

Thread、Runable 的区别

无论使用 Runnable 还是 Thread,都会 new
Thread,然后执行 run 方法。
用法上,如果有复杂的线程操作需求,那就选择继承 Thread,如果只是简
单的执行一个任务,那就实现 runnable
实现 Runnable 接口比继承 Thread 类所具有的优势:
1):把【线程】和【任务】(要执行的代码)分开
,Thread 代表线程
,Runnable 可运行的任务(线程要执行的代码)

2):可以避免 java 中的单继承的限制(继承 Thread 类实现 run 方法,实现 Runnable 接口实现重写 run 方法)

3):线程池只能放入实现 Runable 或 callable 类线程,不能直接放入继承 Thread 的类

用法上,如果有复杂的线程操作需求,那就选择继承 Thread,如果只是简
单的执行一个任务,那就实现 runnable。

对守护线程 Deamon 的理解

默认情况下,java 进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完 java 进程也会停止。普通线程 t1 可以调用 t1.setDeamon(true); 方法变成守护线程

注意 垃圾回收器线程就是一种守护线程,
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求,

**守护线程的作用是什么?
**
举例, GC 垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的 Thread,程序就
不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线
程会自动结束。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

守护线程的应用:
(1)来为其它线程提供服务支持的情况;
(2)或者在任何情况下,程序结束时,这个线
程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要
正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都
是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。
(3)thread.setDaemon(true) 必须在 thread.start()之前设置,否则会抛出一个
IllegalThreadStateException 异常。
(4)不能把正在运行的常规线程设置为守护线程。
(5)在 Daemon 线程中产生的新线程也是 Daemon 的。
(6)守护线程不能用于去访问固有资源,比如读写操作或者计算逻辑。因为它会在任何时候甚至在一个操作
的中间发生中断。
(7)Java 自带的多线程框架,比如 ExecutorService,会将守护线程转换为用户线程,所以如果要使用后台线
程就不能用 Java 的线程池。

注意: 由于守护线程的终止是自身无法控制的,因此千万不要把 IO、File 等重要操作逻辑分配给它;

ThreadLocal 的原理和使用场景

ThreadLocal 内存泄露原因,如何避免

并发、并行、串行的区别

并发的三大特性

volatile

为什么用线程池?解释下线程池参数?

简述线程池处理流程

线程池中阻塞队列的作用?为什么是先添加列队而不是先

创建最大线程?

线程池中线程复用原理


文章作者: CoderXiong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 CoderXiong !
  目录