0
点赞
收藏
分享

微信扫一扫

面试题分享之Java并发篇

金刚豆 2024-05-08 阅读 13

系列文章目录

前言

        今天给小伙伴们分享我整理的关于Java并发的一些常见面试题,这期涉及到线程的一些知识,所以要求小伙伴有一些操作系统的知识,不清楚也不要紧,也不是什么很难的知识点。🌈


一、什么是线程?什么是进程?

  • 线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
  • 进程:进程是操作系统分配资源的基本单位,它是程序在计算机上的一次执行活动。当系统为一个程序分配资源后,该程序就成为一个独立的进程。

二、说一下线程的生命周期,它有几种状态

线程的生命周期包含五个阶段,即五种状态,分别是:

  1. 新建状态(New):新创建了一个线程对象,但还没有调用start()方法。在这个阶段,线程只是被分配了必要的资源,并初始化其状态。

  2. 就绪状态(Runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权。换句话说,线程已经做好了执行的就绪准备,表示可以运行了,但还不是正在运行的线程。

  3. 运行状态(Running):当就绪的线程被调度并获得CPU资源时,便进入运行状态,开始执行run()方法的线程执行体。在这个阶段,线程正在执行其任务。

  4. 阻塞状态(Blocked):在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态。阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。阻塞的情况可能包括:

    • 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。
    • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
    • 阻塞于锁:线程试图获取某个锁,但该锁当前被其他线程持有。

    直到线程进入就绪状态,才有机会转到运行状态。

  5. 死亡状态(Dead):当线程退出run()方法时,线程就会自然死亡,处于终止或死亡状态,也就结束了生命周期。

这五个状态构成了线程从创建到消亡的完整生命周期。

三、说一下你对守护线程的了解?

守护线程Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出,因此,在守护线程中执行涉及I/O操作的任务可能会导致数据丢失或其他不可预测的问题。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。

使用方法

  1. 创建线程:首先,你需要创建一个继承自Thread类的新线程或者实现Runnable接口的对象。

  2. 设置守护线程:在调用start()方法之前,通过调用线程的setDaemon(true)方法将其设置为守护线程。

  3. 启动线程:调用线程的start()方法启动线程。

示例

public class DaemonThreadExample extends Thread{
    public DaemonThreadExample() {
        // 默认构造函数
    }
    @Override
    public void run() {
        while (true) {
            // 守护线程执行的代码
            System.out.println("守护线程正在运行....");
            try {
                Thread.sleep(1000); // 暂停一秒
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 如果守护线程被中断,则退出循环
                break;
            }
        }
    }

    public static void main(String[] args) {
        // 创建守护线程对象
        DaemonThreadExample daemonThread = new DaemonThreadExample();

        // 设置为守护线程
        daemonThread.setDaemon(true);

        // 启动守护线程
        daemonThread.start();

        // 主线程执行其他任务,例如休眠一段时间
        try {
            Thread.sleep(5000); // 主线程休眠5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 当主线程结束时,守护线程也会立即停止
        System.out.println("当主线程结束时,守护线程停止.");
    }
}

注意事项

守护线程在Java编程中有多种应用场景,这些场景通常涉及需要在后台运行的任务,以支持其他线程或执行特定的服务,比如:日志记录、定时任务、数据统计、垃圾回收等。

给大家写一个日志记录的场景:

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class LogRecorder {

    private final ScheduledExecutorService scheduler;
    private final BufferedWriter logWriter;

    public LogRecorder(String logFilePath) throws IOException {
        // 创建一个单线程的守护线程池
        scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread thread = new Thread(r);
            // 设置为守护线程
            thread.setDaemon(true);
            return thread;
        });

        // 初始化日志文件的写入器
        logWriter = new BufferedWriter(new FileWriter(logFilePath, true));
    }

    // 启动日志记录任务
    public void startLogging() {
        // 每隔一段时间记录一条日志(这里假设为每5秒)
        scheduler.scheduleAtFixedRate(() -> {
            try {
                // 模拟生成一条日志
                String logMessage = "日志信息: " + System.currentTimeMillis();
                logWriter.write(logMessage);
                logWriter.newLine();
                logWriter.flush();
                System.out.println(logMessage);
            } catch (IOException e) {
                e.printStackTrace();
                // 可以在这里处理异常,例如重新打开文件或记录错误日志
            }
        }, 0, 5, TimeUnit.SECONDS);
    }

    // 停止日志记录任务并关闭文件写入器
    public void stopLogging() throws IOException {
        scheduler.shutdown(); // 停止任务调度
        logWriter.close(); // 关闭文件写入器
    }

    public static void main(String[] args) throws IOException {
        // 假设日志文件路径为"logs/application.log"
        String logFilePath = "文件地址/xxx.log";
        LogRecorder logRecorder = new LogRecorder(logFilePath);

        // 启动日志记录任务
        logRecorder.startLogging();

        // 模拟主线程执行一些任务
        try {
            Thread.sleep(30000); // 主线程休眠30秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 停止日志记录任务并关闭文件写入器
        logRecorder.stopLogging();

        // 主线程结束,由于守护线程的存在,JVM不会立即关闭
        // 但由于我们调用了scheduler.shutdown(),守护线程中的任务将不再执行
        System.out.println(" 主线程结束,停止写入日志.");
    }
}

四、使用多线程可能带来什么问题

在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会面临非常多的挑战,比如:

  • 上下文切换的问题:频繁的上下文切换会影响多线程的执行速度。
  • 死锁的问题
  • 受限于硬件和软件的资源限制问题:在进行并发编程时,程序的执行速度受限于计算机的硬件或软件资源。

当两个或多个线程无限期地等待一个资源,而这些资源又被其他线程持有时,就会发生死锁。

public class DeadlockExample {
/*
这个死锁大概思路:
1、线程1拿到lock1休眠5s
2、线程1休眠后,线程2拿到lock2
3、线程1休眠结束后。尝试拿lock2,但是lock2被线程2占有
4、同理,线程2休眠结束后,尝试拿lock1,但是lock1又被线程1占有
因此,造成了死锁
*/

    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        new Thread(() -> {
            synchronized (lock1){
                System.out.println(Thread.currentThread().getName()+"已经获得a锁");
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"睡眠5ms结束");
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName()+"已经获得b锁");
                }
            }
        },"线程1").start();
        new Thread(() -> {
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + "已经获得b锁");
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"睡眠5ms结束");
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + "已经获得a锁");
                }
            }
        },"线程2").start();
    }
}

五、说一说sleep()、wait()、join()、yield()的区别

在说这几个方法区别之前,先给大家说一下什么锁池等待池

1.锁池

所有需要竞争同步锁的线程都会放在锁池中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。

2.等待池

当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用notify()notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所以线程放到锁池当中。

sleep跟wait的区别

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

        3.sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。

        4.sleep不需要被唤醒,但是wait需要(不指定时间需要被别人中断)。

        5.sleep一般用于当前线程休眠,或者轮询暂停操作,wait则多用于多线程之间的通信。

        6.sleep会让出CPU执行时间并且强制上下文切换,而wait不一定,wait后还是有机会重新争夺锁继续执行的。

 yield跟join的区别

yield()执行后线程直接进入就绪状态,马上释放cpu的执行权,但是依旧保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行

join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那么线程B会进入到阻塞队列,直到线程A结束或中断线程

给大家举一个简单的例子:t1线程睡4秒,然后执行,之后又调用了join()使主线程进入阻塞,直到t1线程执行完之后主线程才会执行。(注意:是主线程进入阻塞而不是t1阻塞

public static void main(String[] args) throws InterruptedException {
       Thread t1 =  new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(4000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"执行了。。。");
            }
        });
       t1.start();
       t1.join();
       System.out.println(Thread.currentThread().getName()+"执行了。。。");
    }

/*打印结果:
Thread-0执行了。。。 (先执行)
main执行了。。。 (后执行)
*/

六、知道线程中的 run() 和 start() 有什么区别吗?

七、说了这么多,Java程序中如何保证多线程的安全

  • 原子性:在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么都执行,要么都不执行。可以用Java提供了java.util.concurrent.atomic包下的原子类,如AtomicIntegerAtomicLong等,这些类中的方法都是线程安全,或者java.util.concurrent.locks 包下的 Lock 接口提供了比 synchronized 更灵活的锁机制,包括可重入锁、读写锁、定时锁
  • 可见性:Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值
  • 有序性:在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

总结

这期的面试题需要大家多理解多记多背,先理解在背。好了,今天的分享就到这,喜欢的小伙伴记得三连欧😘

参考文章:并发编程&JVM_ΘLLΘ的博客-CSDN博客

举报

相关推荐

0 条评论