【JavaEE】线程安全问题

目录

一.线程安全问题

1.什么是线程安全

2.线程不安全的原因

3.如何解决线程安全问题?

3.1synchronized的使用方式

3.2解决示例自增带来的线程安全问题

(1)对代码块进行加锁

 (2)对方法进行加锁

4.synchronized的特性

5.死锁

5.1两个线程两把锁

5.2N个线程M把锁

​编辑 5.3造成死锁的必要条件

5.4如何避免出现死锁?

6.volatile关键字

7.wait和notify

7.1wait

7.2notify

notify和notifyAll的区别 

小练习

 7.3wait和sleep的区别


 在上一篇中,我们讲解了线程以及如何在java中创建线程,但在多线程之间存在着线程安全问题,本篇我们就围绕线程安全问题来展开。

一.线程安全问题

1.什么是线程安全

线程安全是指在多线程环境下,共享数据的访问和操作不会引起不正确的结果。具体来说,线程安全的程序能够正确地处理多个线程同时访问共享数据的情况,保证数据的一致性和正确性。当两个或者多个线程同时访问共享的数据时,导致数据不一致,称为线程安全问题

2.线程不安全的原因

  1. 线程在操作系统中是随机调度、抢占式执行的
  2. 多个线程同时修改同一个变量
  3. 修改操作不具"原子性"
  4. 内存可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。
  5. 指令重排序:计算机系统在执行程序时,为了提高程序性能,可能会对指令进行重新排序的操作。

示例:我们这里来利用两个线程来让count累加,每个线程体中循环次数为5w次。如下:

package Threads.threadtext;

/**
 * Demo 类用于演示线程安全问题。
 * 本类中,两个线程同时增加一个静态变量 count 的值,以展示并发情况下可能出现的不一致问题。
 */
public class Demo {
    // count 用于演示线程安全问题,初始值为 0。
    public static int count=0;//静态变量

    /**
     * 程序入口。
     * 创建两个线程,每个线程执行 50000 次 count 的增加操作。
     * 随后等待两个线程执行完毕,并打印最终的 count 值。
     * 期望输出为 100000,但实际运行结果可能因并发问题而小于该值。
     *
     * @param args 命令行参数
     * @throws InterruptedException 如果线程在等待时被中断
     */
    public static void main(String[] args) throws InterruptedException{
        // 创建线程 t1,负责增加 count 变量的值
        Thread t1=new Thread(()->{
            for(int i=0;i<50000;i++){
                count++;
            }
        });

        // 创建线程 t2,同样负责增加 count 变量的值
        Thread t2=new Thread(()->{
            for(int i=0;i<50000;i++){
                count++;
            }
        });

        // 启动线程 t1 和 t2
        t1.start();
        t2.start();

        // 等待线程 t1 和 t2 执行完毕
        t1.join();
        t2.join();

        // 打印最终的 count 值
        System.out.println(count);
    }
}

在上面的代码中,我们期望的count的是10w,但实际真的有10w吗?我们可以运行一下。

我们可以看到最后count的值为89931,这与我们期望的值相差挺大。但其实我们在每次运行之后,显示的答案都会不同,这主要是与线程在cpu中是抢占式执行、随机调度有关

在上图中,要执行count++过程,从cpu上要分为三个指令

指令是cpu执行的最基本单位,线程要调度,也至少需要将当前的指令执行完):

  1. 在内存中读取数据到cpu寄存器里(load)
  2. 把cpu寄存器里的数据+1              (add)
  3. 把寄存器里的数据写回到内存中  (save)

 

由于count++是三条指令,且线程在cpu中是抢占式执行、随机调度的,所以可能会出现cpu刚执行一条指令或者2个指令、3个指令被调走的情况。基于这些情况,当两个线程同时进行对count++时,就会出现线程安全问题。

以下是根据时间轴来画指令的执行情况:

若按照以上这种调度顺序执行,则可以得到10w,但这个概率非常小,由于是随机调度的,因此在计算时会产生很多其他的执行顺序,以下是列举的一些执行顺序:

在上图中,这几种执行顺序最终都不能让count的值为10w。

3.如何解决线程安全问题?

我们可以用synchronized关键字来对代码块或方法进行加锁。

3.1synchronized的使用方式

1)对指定代码块进行加锁

    synchronized(锁对象){
        代码块
    }

2)对指定方法进行加锁

    synchronized 权限修饰符 (static) 返回值 方法名(参数){
        代码块
    }

当进入synchronized修饰的代码块时,相当于加锁;退出synchronized修饰的代码块,相当于解锁。

注意:

  • 若synchronized修饰的是一个静态的方法,就相当于针对当前类对象进行加锁。
  • 若synchronized修饰的是一个普通的方法,就相当于针对this进行加锁。

锁对象的作用:锁对象可以是任意的Object/Object子类的对象,锁对象是谁并不重要,重要的是两个或多个线程的锁对象是否是同一个。若是同一个,则会出现锁竞争/锁冲突。反之,若不是针对同一个锁对象加锁,则不会出现。 

使用锁,本质上就是将线程从并行执行-->串行执行,这样来解决线程安全问题。

3.2解决示例自增带来的线程安全问题

(1)对代码块进行加锁

对上述的示例进行加锁,这里我们实例一个Object类对象locker作为锁对象,即:


/**
 * Demo 类用于演示线程安全问题。
 * 本类中,两个线程同时增加一个静态变量 count 的值,以展示并发情况下可能出现的不一致问题。
 */
public class Demo {
    // count 用于演示线程安全问题,初始值为 0。
    public static int count = 0;//静态变量
    static Object locker = new Object();//锁对象

    /**
     * 程序入口主方法。
     * 创建两个线程,每个线程循环50000次,对静态变量count进行自增操作。
     * 使用synchronized关键字确保在同一时间只有一个线程可以访问count变量,以演示线程安全。
     *
     * @param args 命令行参数
     * @throws InterruptedException 如果线程在等待、通知或唤醒过程中被中断
     */
    public static void main(String[] args) throws InterruptedException {
        // 创建线程 t1,负责增加 count 变量的值
        Thread t1 = new Thread(()->{
            for(int i=0;i<50000;i++){
                synchronized (locker){
                    count++;
                }
            }
        });

        // 创建线程 t2,同样负责增加 count 变量的值
        Thread t2 = new Thread(()->{
            for(int i=0;i<50000;i++){
                synchronized (locker){
                    count++;
                }
            }
        });

        // 启动线程 t1 和 t2
        t1.start();
        t2.start();

        // 等待线程 t1 和 t2 执行完毕
        t1.join();
        t2.join();

        // 打印最终的 count 值
        System.out.println(count);
    }
}

可以看到,在加锁之后,count的值能达到我们所期望的。

 (2)对方法进行加锁
class Counter {
    public int count = 0;
    synchronized public void add(){
        count++;
    }
}

public class Demo1 {

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        // 创建线程 t1,负责增加 count 变量的值
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (counter) {
                    counter.add();
                }
            }
        });

        // 创建线程 t2,同样负责增加 count 变量的值
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (counter) {
                    counter.add();
                }
            }
        });

        // 启动线程 t1 和 t2
        t1.start();
        t2.start();

        // 等待线程 t1 和 t2 执行完毕
        t1.join();
        t2.join();

        // 打印最终的 count 值
        System.out.println(counter.count);
    }
}

通过对类的实例对象的方法进行加锁<==>对类的实例对象进行加锁,也可以写成:

class Counter {
    public int count = 0;
    public void add(){
        synchronized (this){
            count++;
        }
    }
}

对静态方法进行加锁<==>对该类的类对象进行加锁


class Counter {
    public static int count = 0;
    synchronized public static void add(){
        count++;
    }
}

public class Demo1 {

    public static void main(String[] args) throws InterruptedException {
        // 创建线程 t1,负责增加 count 变量的值
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (Counter.class) {
                    Counter.add();
                }
            }
        });

        // 创建线程 t2,同样负责增加 count 变量的值
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (Counter.class) {
                    Counter.add();
                }
            }
        });

        // 启动线程 t1 和 t2
        t1.start();
        t2.start();

        // 等待线程 t1 和 t2 执行完毕
        t1.join();
        t2.join();

        // 打印最终的 count 值
        System.out.println(Counter.count);
    }
}
class Counter {
    public static int count = 0;
    public static void add(){
        synchronized (Counter.class){
            count++;
        }
    }
}

为什么说是对该类的类对象进行加锁?

静态方法在java中是不需要创建类的实例对象就能进行调用。可以由该类本身或者该类的对象的引用来引用。

类对象:一个类只有一个类对象;

一个类对象中包含:

  1. 类有哪些属性,都是啥名字,啥类型,权限
  2. 类的方法有哪些,都是啥名字,参数,类型,权限
  3. 类自身继承了哪个类,实现了哪些接口等等

4.synchronized的特性

1.互斥性:synchronized会引起互斥效果,当某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待。

假设现在有三个人准备上厕所,但是厕所只有一个,那么当第一个人进去厕所后,其余的人要等待前面的人上完厕所才能上。即一个线程先上了锁,其他线程只能等待这个线程释放,这个等待的过程称为“阻塞等待”。

2.可重入synchronized同步块对同一条线程来说是可重入的,不会出现死锁的情况。

 我们来看个例子:

class Demo1 {
    static Object locker = new Object();
    public static void main(String[] args) throws InterruptedException {
        
        Thread t1 = new Thread(()->{
            synchronized (locker){
                synchronized (locker){
                    System.out.println("t1");
                }
            }
        });
        t1.start();
    }
}

 在这个例子中,线程t1首先会对locker对象进行加锁,但是在给locker加完一次之后,再加一次锁,就会失败,为什么?第二次加锁需要第一次加锁释放之后才能加锁,但第一次加锁释放需要执行完synchronized修饰的代码块,很遗憾的是,由于第二次加锁在发现对象已经有锁之后,会进行阻塞等待状态,直到第一个锁被释放后才能进行加锁。从而造成了“死锁”。

但在java中,synchronized锁具有可重入性,即:在同一线程中对同一个锁对象加多次锁,不会造成死锁。

那如果在一个线程中,对同个锁对象加多次锁,什么时候才能释放?

可重入锁的内部,包含了:“线程持有者”和“计数器”。

在一个线程,如果某个线程加锁的时候,发现锁已经被占用,且占用的恰好是自己,那么仍然可以继续获取到锁,并让计数器自增。解锁时计数器递减为0时,才能真正释放锁。

5.死锁

如果没有snychronized的可重入性,那么如果针对一个锁连续加多次锁,就会出现死锁的情况。

常见的死锁有两种:

  1. 两个线程两把锁
  2. N个线程M把锁

5.1两个线程两把锁

现有两个线程t1和t2,线程t1已经获得了锁1,线程2已经获得了锁2,同时,线程t1想要获得锁2,线程t2想要获得锁1,就会发生死锁。

class Demo2{
    //锁对象
    private static Object locker1=new Object();
    private static Object locker2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker1) {
                System.out.println("t1获得锁1");
                //让线程睡眠1秒,是为了让两个线程都能先拿到锁,如果没有sleep,执行结果就不可控,可能就会出现某个线程一次拿了两个锁,另一个线程还在执行,无法构成死锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1获得锁2");
                }
            }
        });

        Thread t2=new Thread(()->{
            synchronized (locker2) {
                System.out.println("t2获得锁2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker1){
                    System.out.println("t2获得锁1");
                }
            }

        });
        t1.start();
        t2.start();
    }
}

上述代码在运行之后,会出现两个线程一直僵持的状态

也可以打开jconsole进行查看两个线程的状态,可以看到两个线程都是BLOCKED状态

 

5.2N个线程M把锁

这就得提到一个经典的问题:哲学家进餐问题 。

有5个哲学家共用同一张桌子,分别坐在周围的五张椅子上,在桌子上有5只碗和5根筷子,他们只有两个行为:思考和进餐,在思考的时候,不需要任何动作,在进餐的时候需要分别拿起左右最近的筷子进餐。而且哲学家非常的固执,如果他拿到了筷子,如果没吃到东西是不会放下筷子的。

这些哲学家进餐时间是不定的,如果一个哲学拿到左手边的筷子,此时需要右手边的筷子,但右边的筷子也被另一位哲学家拿了,那么他们就会陷入僵持状态,谁也不让谁。若当5个哲学家同时拿起了筷子,那么就会造成5个哲学家一直吃不到东西。

那么如何解决上述问题,让每个哲学家都能进餐呢? 

我们给哲学家进行编号,从第一个哲学家开始,先拿起左边的筷子,再看看右边的筷子是否有使用,若没有就拿起来。同理,往后的每个哲学家都是先拿起左边的筷子,右边的筷子若有哲学家使用就进行等待。

当1号哲学家用完之后,此时2号哲学家就能进行就餐,以此类推,当4号哲学家进完餐之后,5号就可以拿起他左右边的筷子进行就餐。

 5.3造成死锁的必要条件

  1. 锁是互斥的(锁的基本特性)。当一个线程1被上锁之后,另一个线程2想要上锁,就会进入阻塞状态,等到线程1释放锁。
  2. 锁是不可被抢占的(锁的基本特性)。线程1拿到了锁A,如果线程1不主动释放锁A,线程2就不能把锁A抢过去。
  3. 请求和保持(代码结构)。一个已经有锁线程请求获取另一个锁,但同时又不释放现有的锁。
  4. 循环等待/环路等待/循环依赖(代码结构)。如小白正在玩电脑,同时也想玩手机,而小黑正在玩手机,同时也想玩电脑。但两个人谁也不让谁,都想同时玩电脑和手机,就陷入了僵局。

5.4如何避免出现死锁?

互斥和不可抢占是锁的基本特性,我们可以通过避免代码结构“嵌套锁”,但在某些场景下需要用到。所以最好避免死锁的是破坏循环等待。在加多把锁的时候,先加编号小的锁,再加编号大的锁,且所有的线程都要遵循这一个规则。

class Demo2{
    //锁对象
    private static Object locker1=new Object();
    private static Object locker2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker1) {
                System.out.println("t1获得锁1");
                //让线程睡眠1秒,是为了让两个线程都能先拿到锁,如果没有sleep,执行结果就不可控,可能就会出现某个线程一次拿了两个锁,另一个线程还在执行,无法构成死锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1获得锁2");
                }
            }
        });

        Thread t2=new Thread(()->{
            synchronized (locker1) {
                System.out.println("t2获得锁1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t2获得锁2");
                }
            }

        });
        t1.start();
        t2.start();
    }
}

6.volatile关键字

volatile关键字修饰的变量,保证了“内存的可见性”,在编译器判断是否要进行优化时,通过volatile能够让编译器知道当前的变量不需要进行优化。

示例:

class Demo12{
    static int i=0;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.println("t1 线程开始");
            while(i==0){
                ;
            }
            System.out.println("t1 线程结束");
        });
        Thread t2=new Thread(()->{
            System.out.println("请输入i的值");
            Scanner scanner=new Scanner(System.in);
            i=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

当我们在输入一个值后,发现程序并没有因此停止

这是为什么呢?

我们都知道变量是存储在内存中的,当我们需要使用这个变量的时候,cpu寄存器会从内存中将该变量的数据读取过来,再将cpu寄存器获取到的变量的值读取到我们的代码中,但是从内存中读取变量的速度远小于直接从寄存器中读取变量的速度。当需要对某个变量进行多次的读取时,编译器会自动优化:即从内存中读取到一次的值存放到cpu寄存器中,剩下的读取操作则直接从寄存器中读取即可。

因此,在这里,当我们启动线程t1,t2时,t1内部的循环可能已经进行成千上万次甚至更多,那么此时编译器就会对其进行优化,直接从cpu寄存器中读取,以此来提高运行速度。所有,当我们在线程t2输入一个值后,线程t1中的变量并不能获取到内存中已经修改的值,就会一直执行下去。

 这就是“内存不可见性”,当在一个线程中对某个变量进行修改,由于编译器的优化,在另外的线程中不能获取到内存中已修改的值。

因此,我们需要使用volatile关键字来修饰变量,让编译器知道这个变量不能进行优化,每次操作都需要从内存中获取到值,而不是直接从cpu寄存器中获取。

/**
 * Demo12 类用于演示两个线程之间的交互。
 * 其中一个线程等待另一个线程输入并设置共享变量 i 的值。
 */
class Demo12{
    /**
     * 共享变量 i,使用 volatile 关键字修饰以确保多线程环境下的可见性。
     */
    volatile static int i=0;
    
    /**
     * 程序入口点。
     * 创建两个线程,一个负责等待直到 i 被赋值,另一个负责从用户输入中获取 i 的值。
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        // 创建线程 t1,负责等待直到 i 的值被设置
        Thread t1=new Thread(()->{
            System.out.println("t1 线程开始");
            while(i==0){
                // 通过空循环等待 i 的值被设置
            }
            System.out.println("t1 线程结束");
        });
        // 创建线程 t2,负责从用户输入中获取 i 的值
        Thread t2=new Thread(()->{
            System.out.println("请输入i的值");
            Scanner scanner=new Scanner(System.in);
            i=scanner.nextInt();
        });
        // 启动两个线程
        t1.start();
        t2.start();
    }
}

虽然使用volatile关键字能够保证线程之间内存是可见的,但是不具有原子性。但使用synchronized能够保证原子性。

示例:

class Demo{
    static volatile int count=0;

    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            for (int i=0;i<5000;i++){
                count++;
            }
        });
        Thread t2=new Thread(()->{
            for (int i=0;i<5000;i++){
                count++;
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("count="+count);
    }
}

 

可以看到,即使使用volatile,也不能让count的值为1w。

7.wait和notify

由于线程之间是抢占式执行、随机调度的,因此线程执行的先后顺序是无法确定的。但在实际开发中我们有时候希望能够协调多个线程之间的先后执行顺序。

wait和notify都是Object中的方法,任意的Object类对象都可以使用者两个方法

7.1wait

wait可以让调用的线程进入阻塞状态。

wait一共会做三件事:

  1. 释放锁
  2. 进入阻塞状态,等待通知
  3. 收到通知之后,唤醒线程,并重新尝试获取锁

我们来看个例子,既然wait和notify都是object中的方法那么能够直接使用吗?

class Demo14{
    static Object lock=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.println("t1 线程开始");
            try {
                lock.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println();
        });
        t1.start();
    }
}

 我们在前面提到了wait需要做三件事,第一件事就是释放锁,说明在使用wait时,需要给他加锁才能够使用。

class Demo14{
    static Object lock=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.println("t1 线程开始");
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println();
        });
        t1.start();
    }
}

 当我们运行上面的代码之后,可以看到线程t1现在是处于WAITTING(不超时的等待),但由于现在没有其他的线程去notify,因此t1会一直等下去。

7.2notify

使用notify去通知t1线程并让wait去唤醒线程。

/**
 * Demo14 类用于演示使用对象锁和等待/通知机制进行线程通信的简单示例。
 */
class Demo14{
    /**
     * 用于线程同步的锁对象。
     */
    static Object lock=new Object();
    
    /**
     * 程序入口点。
     * 创建两个线程,一个线程负责等待,另一个线程负责通知。
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        /* 创建线程 t1,该线程打印开始信息后进入同步块并等待 */
        Thread t1=new Thread(()->{
            System.out.println("t1 线程开始");
            synchronized (lock){
                try {
                    /* t1 线程释放锁并等待,直到被通知 */
                    lock.wait();
                } catch (InterruptedException e) {
                    /* 将中断异常转换为运行时异常并抛出 */
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1 线程结束");
        });
        
        /* 创建线程 t2,该线程读取用户输入后进入同步块并唤醒等待中的线程 */
        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            scanner.nextInt();
            synchronized (lock){
                /* t2 线程唤醒等待中的线程后离开同步块 */
                lock.notify();
            }
            System.out.println("t2结束");
        });
        
        /* 启动两个线程 */
        t1.start();
        t2.start();
    }
}

这里我们设置从控制台输入,方便观察等待线程被唤醒时的现象。

notify和notifyAll的区别 

notify()是用于通知单个线程,notifyAll()是通知所有线程

 7.3wait和sleep的区别

wait默认是“死等”,wait也提供了带参数的版本,指定超时时间,若wait达到了最大的时间,notify还没有通知就不会继续等待下去,而是会继续执行。

wait和sleep有着本质区别:

  1. wait是为了提前唤醒线程;而sleep是固定时间的阻塞,不涉及唤醒,但sleep可以被interrupt唤醒,但调用interrupt是终止线程。
  2. wait需要搭配synchronized使用,wait会先释放锁,并同时等待;sleep和锁无关,如果不加锁sleep可以正常使用,但如果加了锁,sleep也不会释放锁,而是拉着锁一起睡眠,其他线程无法拿到锁。
  3. wait是Object的⽅法sleep是Thread的静态⽅法.

小练习

利用线程,按照顺序打印出ABC

 这里需要用到两个object对象,为什么呢?

如果我们使用同一个object,那么打印B和C的线程的顺序就是随机的,这就不符合我们的预期。可以去实验一下,使用同一个锁对象,可能就会导致打印顺序不确定。线程调度是由操作系统决定的,我们不能保证线程总是按照预期的顺序被调度。这里就不过多说。

class Demo16{
    static Object lock1=new Object();
    static Object lock2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.print("A");
            synchronized (lock1){
                lock1.notify();
            }
        });
        Thread t2=new Thread(()->{
            synchronized (lock1){
                try {
                    lock1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.print("B");
            synchronized (lock2){
                lock2.notify();
            }
        });
        Thread t3=new Thread(()->{
            synchronized (lock2){
                try {
                    lock2.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("C");
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

 打印10个ABC

class Demo16{
    static Object lock1=new Object();
    static Object lock2=new Object();
    static Object lock3=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            for(int i=0;i<10;i++) {
                System.out.print("A");
                synchronized (lock1) {
                    lock1.notify();
                }
                synchronized (lock3) {
                    try {
                        lock3.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<10;i++) {
                synchronized (lock1) {
                    try {
                        lock1.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print("B");
                synchronized (lock2) {
                    lock2.notify();
                }
            }
        });
        Thread t3=new Thread(()->{
            for(int i=0;i<10;i++) {
                synchronized (lock2) {
                    try {
                        lock2.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("C");
                synchronized (lock3) {
                    lock3.notify();
                }
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

<think>好的,我现在需要回答用户关于JavaEE工程师面试常见问题及答案的查询。首先,我要根据用户提供的引用内容,特别是引用[2]、[4]中的信息,整理出相关的问题和答案。同时,用户还提到需要符合特定的格式要求,比如行内数学表达式用$...$,独立公式用$$...$$,但看起来当前问题可能不涉及数学公式,所以可能不需要处理这点,但还是要留意。 用户提供的引用中,引用[2]提到了Java中的正则表达式操作,涉及String类的方法和Pattern类。引用[4]提到了类加载器,如bootstrap、extension、AppClassLoader以及双亲委派模式。引用[1]、[3]、[5]更多是关于面试资料和图片,可能作为补充引用。 接下来,我需要组织这些问题,确保覆盖用户提到的JavaEE工程师面试常见问题。常见问题通常包括Java基础、集合框架、多线程、JVM、框架知识(如Spring)、数据库、Web服务等。不过根据用户提供的引用,可能重点在Java基础、类加载机制、正则表达式等方面。 需要确保回答结构清晰,分点列出问题和答案,并在引用处添加引用标识。例如,关于正则表达式的问题可以引用[2],类加载器的问题引用[4]。同时,用户可能需要更多问题,所以需要结合常见面试题补充内容,但需注意只使用提供的引用内容,或者明确说明其他内容为补充。 需要注意用户要求生成相关问题,在回答末尾添加“相关问题”部分,可能包括用户引用中没有涵盖但相关的话题,如JVM内存模型、Spring框架等,但如果有引用的话优先使用引用中的内容。例如,引用[4]提到了双亲委派模式,可以生成相关问题如双亲委派机制的原理。 另外,用户提到的JavaEE可能涉及Servlet、JSP、EJB等技术,但引用中未明确提到,可能需要根据常识补充,但需注意是否有引用支持。如果没有,可能需要避免深入,或者指出常见问题但无引用。 总结步骤: 1. 分析用户提供的引用内容,提取相关面试问题。 2. 结构化回答,分点列出问题和答案,附上引用标记。 3. 生成相关问题部分,基于已有引用和常见JavaEE面试主题。 4. 确保符合格式要求,如引用标识、代码块(如果有的话)等。 现在开始组织内容: 首先,从引用[2]中提取正则表达式相关的问题,如Java中如何支持正则表达式,答案包括String类的方法和Pattern类。引用[4]中的类加载器问题,如三种类加载器和双亲委派模式。此外,可能还需要包括其他常见问题,如JVM内存模型、集合框架、多线程等,但需注意是否有引用支持。根据用户提供的引用,可能没有,所以可能需要仅基于现有引用回答问题,并生成相关问题。 可能的问题列表: 1. Java中如何支持正则表达式操作?[引用2] 2. JVM的类加载器有哪些?双亲委派模式是什么?[引用4] 3. 解释Java中的多线程机制。 4. 什么是Servlet的生命周期? 5. Spring框架的核心概念有哪些? 但根据现有引用,只有前两个问题有直接引用支持,其他问题可能需要补充,但用户提供的引用中没有相关信息。因此,可能需要仅回答有引用支持的问题,并生成相关问题基于现有引用。 因此,回答部分可能包括: 1. 正则表达式操作(引用2) 2. 类加载器和双亲委派(引用4) 3. 可能还有引用4中的其他问题,如JVM内存模型,但引用4只提到了类加载器,所以需要确认。 可能需要参考其他常见问题,但用户提供的引用有限,所以可能只能根据现有引用构造答案,然后生成相关问题。 因此,最终回答可能包括: JavaEE工程师面试常见问题及答案: 1. Java中如何支持正则表达式操作? 答案:Java中的String类提供了支持正则表达式的方法,如matches(), replaceAll(), replaceFirst(), split()。此外,可以使用Pattern类表示正则表达式对象,提供丰富的API进行操作[^2]。 2. JVM的类加载器有哪些?双亲委派模式是什么? 答案:JVM主要有三种类加载器:Bootstrap ClassLoader(负责加载核心类库)、Extension ClassLoader(加载扩展库)、Application ClassLoader(加载应用类路径)。双亲委派模式指类加载时先委派给父加载器尝试加载,只有父加载器无法加载时,子加载器才自己加载,确保类的一致性和安全性[^4]。 相关问题: 1. 如何自定义类加载器? 2. 正则表达式中Pattern和Matcher类的作用? 3. 双亲委派模式的优缺点? 4. Java内存模型的主要组成部分? 5. 解释Java中的垃圾回收机制。 需要注意,用户提供的引用中可能没有所有答案,所以生成的问题可能需要用户进一步研究,但根据引用内容,前两个问题有支持,其他问题可能超出引用范围,但属于常见面试题,可以生成。</think>### JavaEE工程师面试常见问题及答案 #### 1. Java中如何支持正则表达式操作? Java通过`String`类和`Pattern`类提供正则表达式支持。 - `String`类的方法: - `matches()`:判断字符串是否匹配正则表达式。 - `replaceAll()`/`replaceFirst()`:替换匹配的子串。 - `split()`:按正则表达式分割字符串。 - `Pattern`类:通过`Pattern.compile()`编译正则表达式,生成`Matcher`对象进行匹配、查找等操作。 #### 2. JVM的类加载器及双亲委派模式 - **类加载器**: - **Bootstrap ClassLoader**:加载核心类库(如`rt.jar`)。 - **Extension ClassLoader**:加载扩展库(`jre/lib/ext`目录)。 - **Application ClassLoader**:加载应用类路径(`-classpath`指定路径)。 - **双亲委派模式**: 类加载器收到加载请求时,先委派给父加载器处理,若父加载器无法完成,子加载器才尝试加载。此机制确保类的一致性(如避免重复加载`java.lang.Object`)和安全性。 #### 3. JavaEE中Servlet的生命周期 1. **初始化**:容器调用`init()`方法,仅执行一次。 2. **请求处理**:调用`service()`方法,根据请求类型分派到`doGet()`/`doPost()`等。 3. **销毁**:容器调用`destroy()`方法,释放资源。 #### 4. Spring框架的核心概念 - **IoC(控制反转)**:对象的创建和依赖注入由容器管理。 - **AOP(面向切面编程)**:通过代理实现日志、事务等横切关注点。 - **Bean作用域**:如`singleton`、`prototype`等。 #### 5. 数据库连接池的作用 - **资源复用**:避免频繁创建/关闭连接,提升性能。 - **连接管理**:限制最大连接数,防止资源耗尽。 - **常见实现**:如HikariCP、Tomcat JDBC Pool。 ```java // 示例:使用Java正则表达式验证邮箱格式 import java.util.regex.Pattern; public class EmailValidator { public static boolean isValid(String email) { String regex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; return Pattern.matches(regex, email); } } ```
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小猪同学hy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值