[Java面试基础]几道常见的多线程编程题

1.线程安全的单例模式

场景引入:

什么是线程安全?如何判断一个方法是线程安全的?方法只要加了锁就是线程安全的吗?

当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

并不是只要加了锁就是安全的,只有满足了那把锁的对象是锁同一个对象才是线程安全的。比如synchronized关键字中:

  • synchronized修饰非static方法等同于synchronized(this){};
  • synchronized修饰static方法等同于synchronized(xxx.class);

并发编程三要素(线程的安全性问题体现在):

  • 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
  • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
  • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

出现线程安全问题的原因:

  • 线程切换带来的原子性问题

  • 缓存导致的可见性问题

  • 编译优化带来的有序性问题

解决办法:

  • JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
  • synchronized、volatile、LOCK,可以解决可见性问题
  • Happens-Before 规则可以解决有序性问题
class Singleton{
    private volatile static Singleton uniqueInstance;
    private Singleton(){}
    
    public Singleton getUniqueInstance(){
        if(uniqueInstance == null) {
            synchronized (Singleton.class) {
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

问:为什么要两次判断uniqueInstance == null

  • 第一次判断对象是否已经创建实例化过,没有实例化过才进入加锁代码,否则直接返回实例(提高效率)
  • 第二次的话,如果没有没这判断,假设两个线程t1、t2同时经过了第一次判断,进入了6、7行之间,这时t1获得锁,创建了一个实例刚好释放后,t2立即获得锁,就会再一次创建一个uniqueInstance。

问:为什么要使用volatile关键字呢?

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance,即调用invokespecial构造方法为其成员变量赋值
  3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。(使用了半初始化状态的对象)

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境 下也能正常运行

2.手写一个死锁

场景引入:

什么是死锁?

死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

比如,线程1 持有资源A,线程 2持有资源B,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

死锁有哪些产生条件?如何避免死锁?

  1. 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
  2. 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞
/**
 * 手写死锁
 */
public class Main {

    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();

        Thread t1 = new Thread(()->{
            synchronized (obj1){
                System.out.println("got obj1");
                //为了保证实验发生,阻塞当前线程让给另个线程运行
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                synchronized (obj2){
                    System.out.println("got obj2");
                }
            }
        });

        Thread t2 = new Thread(()->{
            synchronized (obj2){
                System.out.println("got obj2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj1){
                    System.out.println("got obj1");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

3.手写一个生产者消费者模式

sleep() 和 wait() 有什么区别?

  • 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
  • 是否释放锁:sleep() 不释放锁;wait() 释放锁。
  • 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  • 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?

Java中,任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。

wait(), notify()和 notifyAll()这些方法在同步代码块中调用

有的人会说,既然是线程放弃对象锁,那也可以把wait()定义在Thread类里面啊,新定义的线程继承于Thread类,也不需要重新定义wait()方法的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂。

综上所述,wait()、notify()和notifyAll()方法要定义在Object类中。

为什么调用wait方法时要放在while中?

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。

wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。

public class ProducerAndConsumer<T> {
    private LinkedList<T> list = new LinkedList<>();
    private static int MAX = 10;
    private int size = 0;

    public synchronized void put(T t) throws InterruptedException {
        while(list.size() == MAX){
            this.wait();
        }

        list.add(t);
        size ++;
        this.notifyAll(); //通知消费者线程可以消费了
    }

    public synchronized T get() throws InterruptedException {
        T t = null;
        while (list.size() == 0){
            this.wait();
        }
        t = list.removeFirst();
        size --;
        this.notifyAll();
        return t;
    }

}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值