Skip to main content

java面经-并发篇

·1063 words·5 mins
WFUing
Author
WFUing
A graduate who loves coding.
Table of Contents

1. JAVA如何开启线程?怎么保证线程安全?
#

线程和进程的区别:

  • 进程是操作系统的进行资源分配的最小单元
  • 线程是操作系统的进行任务分配的最小单元

如何开启线程?

  1. 继承Thread类,重写run方法;
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread running");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
  1. 实现Runnable接口,实现run方法;
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable running");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}
  1. 实现Callable接口,实现call方法。通过FutureTask创建一个线程;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<String> {
    @Override
    public String call() {
        return "MyCallable running";
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}
  1. 通过线程池来开启线程。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("MyTask running");
    }
}

public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        executor.execute(new MyTask());
        executor.shutdown();
    }
}

怎么保证线程安全?

加锁:

  1. JVM 提供的锁,也就是 sychronized 关键字
  2. JDK 提供的各种锁

2. volatilesychronized 有什么区别? volatile 能不能保证线程安全?DCL(Double Check Lock)单例为什么要加 volatile
#

volatilesychronized 有什么区别?

  • sychronized 关键字用来加锁;
  • volatile 只是保持变量的线程可见性,通常使用于一个线程写,多个线程读的场景。

volatile 能不能保证线程安全?

不能。volatile 关键字只能保证线程可见性,不能保证原子性。

public class VolatileDemo {
    private volatile boolean flag = false;

    public void toggleFlag() {
        flag = !flag;
    }

    public void printFlag() {
        System.out.println("Flag is: " + flag);
    }

    public static void main(String[] args) {
        VolatileDemo demo = new VolatileDemo();

        // 线程1:不断修改flag的值
        Thread thread1 = new Thread(() -> {
            while (true) {
                demo.toggleFlag();
                try {
                    Thread.sleep(1000); // 等待1秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 线程2:不断读取并打印flag的值
        Thread thread2 = new Thread(() -> {
            while (true) {
                demo.printFlag();
                try {
                    Thread.sleep(1000); // 等待1秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();
    }
}
Flag is: false
Flag is: true
Flag is: false
Flag is: true
Flag is: false
Flag is: true
...

DCL(Double Check Lock)单例为什么要加 volatile

volatile 防止指令重排。在DCL中,防止高并发下情况下,指令重排造成的线程安全问题。

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
        // 私有构造函数,防止外部实例化
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

如果没有使用 volatile 关键字修饰 instance,那么编译器和处理器可能会对上述代码进行重排序。重排序后的执行顺序可能是:

  1. 创建一个新的 Singleton 实例。
  2. 将引用赋值给 instance
  3. 进行对象初始化。

在这种情况下,如果有另一个线程在调用 getInstance() 方法时,可能会得到一个未完全初始化的对象,从而导致程序出现错误。

而使用了 volatile 关键字修饰 instance 后,可以禁止指令重排序,确保对象的初始化操作发生在对象引用赋值操作之前,从而避免了上述问题。

JVM 中 as-if-serial 原则 和 happens-before 原则

Integer i = 8;
  1. 分配内存
  2. 初始化
  3. 建立指针对应关系

3. JAVA 线程锁机制是怎样的?偏向锁、轻量级锁、重量级锁有什么区别?锁机制是如何升级的?
#

JAVA 的锁就是在对象的 Markword 中记录一个锁状态。四个不同的锁状态:

  • 无锁
  • 偏向锁
  • 轻量级锁
  • 重量级锁

无锁:是一种并发编程的理想状态,表示在多个线程访问共享资源时不需要进行任何同步操作。在无锁状态下,所有线程都可以顺利地完成对共享资源的访问,不会发生阻塞或争用。

偏向锁:是一种针对加锁对象只有一个线程访问的情况进行优化的锁机制。当一个线程获取了偏向锁后,如果再次访问同一个锁对象,无需进行任何同步操作,可以直接进入临界区,从而减少了不必要的竞争和上下文切换。

偏向锁

轻量级锁(自旋锁):是针对多个线程交替执行临界区代码的情况进行优化的锁机制。当一个线程尝试获取轻量级锁时,会将对象头部的标志位设置为偏向锁或轻量级锁,并将当前线程的ID保存到对象头中。如果其他线程也想获取该锁,则会进行自旋等待,避免进入重量级锁的阻塞状态。

轻量级锁

重量级锁(需要操作系统来组织):是针对多个线程同时访问临界区代码的情况进行优化的锁机制。当多个线程争用同一个锁时,轻量级锁无法解决冲突,会升级为重量级锁。在重量级锁中,线程会进入阻塞状态,等待锁的释放,从而保证了临界区的互斥访问。

重量级锁

锁之间的关系

  • -XX:UseBiasedLocking : 是否打开偏向锁
  • -XX:BiasedLockingStartupDelay : 默认是4秒

程序首先检查偏向锁是否启用,然后在等待4秒后启动多个线程来竞争同一个锁对象。如果 -XX:UseBiasedLocking 打开,并且 -XX:BiasedLockingStartupDelay 设置为默认的4秒,则程序会在启动后4秒钟开始使用偏向锁。如果偏向锁启用,则第一个获取锁的线程将会获得偏向锁,并且其他线程会在竞争锁时自旋等待。

4. 谈谈你对 AQS 对理解。AQS 如何实现可重入锁?
#

AQS(AbstractQueuedSynchronizer) 是一个 JAVA 线程同步的框架。是 JDK 中很多锁工具的核心实现框架。

在 AQS 中,维护了一个信号量 state 和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而 state 就像一个红绿灯,用来控制线程排队或者放行。在不同的场景下,有不同意义。

在可重入锁的场景下,state 就用来表示加锁的次数。0 表示无锁,每加一次锁,state 就加1.释放锁 state 就减 1。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        new Thread(ReentrantLockExample::outerMethod).start();
        new Thread(ReentrantLockExample::outerMethod).start();
    }

    public static void outerMethod() {
        lock.lock();
        try {
            System.out.println("Outer method is called by " + Thread.currentThread().getName());
            innerMethod();
        } finally {
            lock.unlock();
        }
    }

    public static void innerMethod() {
        lock.lock();
        try {
            System.out.println("Inner method is called by " + Thread.currentThread().getName());
        } finally {
            lock.unlock();
        }
    }
}

5. 有 A、B、C 三个线程,如何保证三个线程同时执行?如何在并发情况下保证三个线程依次执行?如何保证三个线程有序交错进行?
#

CountDownlatchCylicBarrierSemaphore

  1. CountDownLatch(倒计时门闩):

CountDownLatch 是一种同步辅助工具,它允许一个或多个线程等待其他线程执行完一组操作后再继续执行。它被初始化为一个计数值,每次调用 countDown() 方法都会将计数值减一。等待线程可以调用 await() 方法来阻塞,直到计数值变为零。通常用于某个线程需要等待其他线程执行完特定任务后才能继续执行的情况。

案例:假设有一个主线程需要等待多个子线程完成某个任务后才能进行后续操作,可以使用 CountDownLatch。以下是一个简单的示例:

import java.util.concurrent.CountDownLatch;

public class SimultaneousExecutionExample {
    public static void main(String[] args) throws InterruptedException {
        int numberOfThreads = 3;
        CountDownLatch startLatch = new CountDownLatch(1);

        for (int i = 0; i < numberOfThreads; i++) {
            new Thread(() -> {
                try {
                    startLatch.await(); // 等待主线程释放信号
                    System.out.println(Thread.currentThread().getName() + " is running");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }

        // 主线程释放信号,让所有线程同时执行
        startLatch.countDown();
    }
}
Thread-0 is running
Thread-2 is running
Thread-1 is running
  1. CyclicBarrier(循环屏障):

CyclicBarrier 是一种同步屏障,它允许一组线程互相等待,直到所有线程都到达指定的屏障点后再继续执行。它初始化时指定了线程数,每个线程调用 await() 方法来表示自己已经到达屏障,当所有线程都到达时,屏障就会打开,所有线程可以继续执行。与 CountDownLatch 不同,CyclicBarrier 可以重用。

案例:假设有一个任务需要分成多个子任务并行执行,然后在所有子任务执行完成后合并结果,可以使用 CyclicBarrier。以下是一个示例:

import java.util.concurrent.CyclicBarrier;

public class SequentialExecutionExample {
    public static void main(String[] args) {
        int numberOfThreads = 3;
        CyclicBarrier barrier = new CyclicBarrier(numberOfThreads);

        for (int i = 0; i < numberOfThreads; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " is waiting");
                    barrier.await(); // 等待其他线程到达屏障
                    System.out.println(Thread.currentThread().getName() + " is running");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
Thread-0 is waiting
Thread-1 is waiting
Thread-2 is waiting
Thread-0 is running
Thread-1 is running
Thread-2 is running
  1. Semaphore(信号量):

Semaphore 是一种用于控制对共享资源访问的同步辅助工具。它维护一组许可证,线程需要通过 acquire() 方法获取许可证,通过 release() 方法释放许可证。初始化时指定了许可证数量,每次调用 acquire() 时,许可证数量减一,如果许可证数量为零,则调用线程会被阻塞。通常用于控制对有限资源(如连接数、线程数等)的访问。

案例:假设有一个连接池,最多只能同时有3个线程获取连接,其他线程需要等待连接释放后才能获取,可以使用 Semaphore。以下是一个示例:

import java.util.concurrent.Semaphore;

public class OrderedInterleavedExecutionExample {
    public static void main(String[] args) {
        Semaphore semaphore1 = new Semaphore(1);
        Semaphore semaphore2 = new Semaphore(0);
        Semaphore semaphore3 = new Semaphore(0);

        new Thread(() -> {
            try {
                while (true) {
                    semaphore1.acquire();
                    System.out.println("Thread A is running");
                    semaphore2.release();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                while (true) {
                    semaphore2.acquire();
                    System.out.println("Thread B is running");
                    semaphore3.release();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                while (true) {
                    semaphore3.acquire();
                    System.out.println("Thread C is running");
                    semaphore1.release();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
Thread A is running
Thread B is running
Thread C is running
Thread A is running
Thread B is running
Thread C is running
...

6. 如何对一个字符串快速进行排序?
#

Fork/Join 框架:是 Java 中用于并行执行任务的一个框架。它提供了一种将大型任务拆分成更小的子任务,并将这些子任务并行执行的机制。Fork/Join 框架的核心是工作窃取(work-stealing)算法,即当一个线程执行完自己的任务后,会去其他线程的任务队列中窃取任务来执行,从而实现了任务的动态负载均衡。Fork/Join 框架通常用于处理递归分治算法的任务,比如归并排序、快速排序等。在 Java 中,Fork/Join 框架主要通过 ForkJoinPool、ForkJoinTask 和 RecursiveTask 等类来实现。

import java.util.Arrays;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;

public class StringMergeSortExample {

    public static void main(String[] args) {
        String[] array = {"banana", "apple", "orange", "grape", "kiwi", "peach"};
        ForkJoinPool pool = new ForkJoinPool();
        pool.invoke(new MergeSortTask(array));
        System.out.println(Arrays.toString(array));
    }

    static class MergeSortTask extends RecursiveAction {
        private final String[] array;
        private final int low;
        private final int high;

        public MergeSortTask(String[] array) {
            this(array, 0, array.length - 1);
        }

        public MergeSortTask(String[] array, int low, int high) {
            this.array = array;
            this.low = low;
            this.high = high;
        }

        @Override
        protected void compute() {
            if (low < high) {
                int mid = (low + high) >>> 1;
                MergeSortTask leftTask = new MergeSortTask(array, low, mid);
                MergeSortTask rightTask = new MergeSortTask(array, mid + 1, high);
                invokeAll(leftTask, rightTask);
                merge(array, low, mid, high);
            }
        }

        private void merge(String[] array, int low, int mid, int high) {
            String[] temp = new String[high - low + 1];
            int i = low, j = mid + 1, k = 0;
            while (i <= mid && j <= high) {
                if (array[i].compareTo(array[j]) <= 0) {
                    temp[k++] = array[i++];
                } else {
                    temp[k++] = array[j++];
                }
            }
            while (i <= mid) {
                temp[k++] = array[i++];
            }
            while (j <= high) {
                temp[k++] = array[j++];
            }
            System.arraycopy(temp, 0, array, low, temp.length);
        }
    }
}

在这个示例中,我们定义了一个 MergeSortTask 类,继承自 RecursiveAction,用于执行归并排序。在 compute() 方法中,我们首先检查待排序的数组范围是否需要分解成子任务,如果需要,则创建两个子任务并调用 invokeAll() 方法并行执行。然后,在子任务执行完成后,再调用 merge() 方法进行合并。最后,我们通过创建 ForkJoinPool 实例并调用 invoke() 方法来启动任务。


💬评论