【Android面试】2023最新面试专题九:Java并发编程(四)

16 什么是守护线程?你是如何退出一个线程的?

这道题想考察什么?

是否了解守护线程与真实场景使用,是否熟悉线程退出该如何操作的本质区别?

考察的知识点

守护线程与线程退出的概念在项目中使用与基本知识

考生应该如何回答

守护线程

在开发过程中,直接创建的普通线程为用户线程,而另一种线程,也就是守护线程,通过setDaemon(true)将一个普通用户线程设置为守护线程。

守护线程,也叫Daemon线程,它是一种支持型、服务型线程,主要被用作程序中后台调度以及支持性工作,跟上层业务逻辑基本不挂钩。Java中垃圾回收线程就是一个典型的Daemon线程。

public class DaemonThreadTest {
    
        
    public static void main(String[] args) {
    
            
        // 创建线程        
        Thread daemonThread = new Thread("Daemon") {
    
                
            @Override            
            public void run() {
    
                    
                super.run();                
                // 循环执行5次(休眠一秒,输出一句话)                
                for (int i = 0; i < 5; i++) {
    
                        
                    try {
    
                            
                        Thread.sleep(1000);                    
                    } catch (InterruptedException e) {
    
                            
                        e.printStackTrace();                    
                    }                    
                    System.out.println("守护线程->" + Thread.currentThread().getName() + "正在执行");                		  }            
            }        
        };        
        // 设置为守护线程,必须在线程启动start()方法之前设置,否则会抛出IllegalThreadStateException异常  
        daemonThread.setDaemon(true);        
        // 启动守护线程        
        daemonThread.start();        
        // 这里是主线程逻辑        
        // 循环执行3次(休眠一秒,输出一句话)        
        for (int i = 0; i < 3; i++) {
    
                
            try {
    
                    
                Thread.sleep(1000);            
            } catch (InterruptedException e) {
    
                    
                e.printStackTrace();            
            }            
            System.out.println("普通线程->" + Thread.currentThread().getName() + "正在执行");        
        }    
    }
}

在主线程里创建守护线程,然后一起执行,主线程的业务逻辑是每隔1s,输出一句log,总共循环三次,而守护线程里也是隔1s输出一句log,不过想要循环5次。其中需要注意的是设置为守护线程,必须在线程启动start()方法之前设置,否则会抛出IllegalThreadStateException异常,意思就是运行中的线程不能设置成守护线程的。最后我们运行一下main方法,看看控制台的输出结果如下:

控制台的输出普通线程->
main正在执行守护线程->
Daemon正在执行守护线程->
Daemon正在执行普通线程->
main正在执行守护线程->
Daemon正在执行普通线程->
main正在执行BUILD SUCCESSFUL in 3s2 actionable tasks: 2 executed

从上面的日志来看,两个线程都只是输出了3次,然后就执行结束,这其实就是守护线程的特点,用一句话概括就是,当守护线程所守护的线程结束时,守护线程自身也会自动关闭。

线程如何退出

要知道当Thread的run方法执行结束后,线程便会自动退出,生命周期结束,这属于线程正常退出的范畴。在实际的开发过程中,还存在另一种业务场景,因为某种原因,需要停止正在运行的线程,那该怎么办呢?

在Thread中提供了一个stop方法,stop方法是JDK提供的一个可以强制关闭线程的方法,但是不建议使用!而且Android针对JDK中的Thread进行了修改,在Android的Thread中调用stop方法会直接抛出异常:

@Deprecated
public final synchronized void stop(Throwable obj) {
    
    
	throw new UnsupportedOperationException();
}

那么当我们需要停止运行的线程时,可以通过标识位的方式实现:

使用标志位退出线程

线程执行run方法过程中,我们可以通过一个自定义变量来决定是否还需要退出线程,若满足条件,则退出线程,反之继续执行:

下面代码:注释已经加的很详细了,直接从日志的输出结果来看,很完美的停止了子线程的运行,很OK

public class StopThreadTest {
    
        
    // 定义标志位,使用volatile,保证内存可见    
    private static volatile boolean stopFlag = false;    
    public static void main(String[] args) {
    
            
        // 创建子线程        
        Thread thread = new Thread() {
    
                
            @Override            
            public void run() {
    
                    
                super.run();                
                // 循环打印运行日志                
                while (!stopFlag) {
    
                        
                    System.out.println(currentThread().getName() + " is running");                
                }                
                // 退出后,打印退出日志                
                if (stopFlag) {
    
                        
                    System.out.println(currentThread().getName() + " is stop");                
                }            
            }        
        };        
        thread.start();        
        // 让子线程执行100ms后,将stopFlag置为true        
        try {
    
                
            Thread.sleep(100);        
        } catch (InterruptedException e) {
    
                
            e.printStackTrace();        
        }        
        stopFlag = true;    
    }
}
// logcat日志// Thread-0 is running// Thread-0 is running// Thread-0 is running// Thread-0 is running// Thread-0 is running// Thread-0 is running// Thread-0 is running// Thread-0 is running// Thread-0 is running// Thread-0 is running// Thread-0 is stop
使用interrupt方法中断线程

使用interrupt方法来中断线程,本质上也是标志位的方式。跟上面的原理一样,只不过是Thread类内部提供的罢了。其他线程通过调用某个线程的interrupt()方法对其进行中断操作,被中断的线程则是通过线程的isInterrupted()来进行判断是否被中断。

public class StopThreadTest {
    
        
    public static void main(String[] args) {
    
            
        // 创建子线程        
        Thread thread = new Thread() {
    
                
            @Override            
            public void run() {
    
                    
                super.run();                
                // 通过isInterrupted()方法判断线程是否已经中断                
                while (!isInterrupted()) {
    
                        
                    // 打印运行日志                    
                    System.out.println(currentThread().getName() + " is running");                
                }                
                if (isInterrupted()) {
    
                        
                    // 打印退出日志                    
                    System.out.println(currentThread().getName() + " is stop");                
                }            
            }        
        };        
        thread.start();        
        // 让子线程执行100ms后,将stopFlag置为true        
        try {
    
                
            Thread.sleep(100);        
        } catch (InterruptedException e) {
    
                
            e.printStackTrace();        
        }        
        // 通过调用interrupt方法去改变标志位        
        thread.interrupt();    
    }
}

17 sleep 、wait、yield与join的区别,wait 的线程如何唤醒它?(字节跳动)

这道题想考察什么?

  1. 是否了解Java中线程相关的知识点
  2. 线程的生命周期

考察的知识点

  1. Java中线程的相关概念
  2. Sleep、yield、wait和jion函数的区别
  3. 多线程并发相关的知识点

考生应该如何回答

sleep 、wait、yield与join的区别

sleep、yield与join是线程方法,而wait则是Object方法:

  • sleep **,释放cpu资源,不释放锁资源,**如果线程进入sleep的话,释放cpu资源,如果外层包有Synchronize,那么此锁并没有释放掉。

  • wait,**释放cpu资源,也释放锁资源,**一般用于锁机制中 肯定是要释放掉锁的,因为notify并不会立即调起此线程,因此cpu是不会为其分配时间片的,也就是说wait 线程进入等待池,cpu不分时间片给它,锁释放掉。

  • yield:让出CPU调度,Thread类的方法,类似sleep只是不能由用户指定暂停多长时间 ,并且yield()方法只能让同优先级的线程有执行的机会。 yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。调用yield方法只是一个建议,告诉线程调度器我的工作已经做的差不多了,可以让别的相同优先级的线程使用CPU了,没有任何机制保证采纳。

  • join:一种特殊的wait,当前运行线程调用另一个线程的join方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。 注意该方法也需要捕捉异常。

wait 的线程如何唤醒

执行wait方法会导致当前线程进入Blocked状态,可以调用执行wait方法的对象的notify或者notifyAll方法唤醒:

public class Main {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        var q = new TaskQueue();
        var ts = new ArrayList<Thread>();
        for (int i=0; i<5; i++) {
    
    
            var t = new Thread() {
    
    
                public void run() {
    
    
                    // 执行task:
                    while (true) {
    
    
                        try {
    
    
                            String s = q.getTask();
                            System.out.println("execute task: " + s);
                        } catch (InterruptedException e) {
    
    
                            return;
                        }
                    }
                }
            };
            t.start();
            ts.add(t);
        }
        var add = new Thread(() -> {
    
    
            for (int i=0; i<10; i++) {
    
    
                // 放入task:
                String s = "t-" + Math.random();
                System.out.println("add task: " + s);
                q.addTask(s);
                try {
    
     Thread.sleep(100); } catch(InterruptedException e) {
    
    }
            }
        });
        add.start();
        add.join();
        Thread.sleep(100);
        for (var t : ts) {
    
    
            t.interrupt();
        }
    }
}

class TaskQueue {
    
    
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
    
    
        this.queue.add(s);
        this.notifyAll();
    }

    public synchronized String getTask() throws InterruptedException {
    
    
        while (queue.isEmpty()) {
    
    
            this.wait();
        }
        return queue.remove();
    }
}

在addTask()方法中调用了this.notifyAll()而不是this.notify(),使用notifyAll()将唤醒所有当前正在this锁等待的线程,而notify()只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。

之所以使用notifyAll,是因为可能有多个线程正在getTask()方法内部的wait()中等待,使用notifyAll()将一次性全部唤醒。通常来说,notifyAll()更安全。有些时候,如果我们的代码逻辑考虑不周,用notify()会导致只唤醒了一个线程,而其他线程可能永远处于Blocked状态。


18 sleep是可中断的么?(小米)

这道题想考察什么?

是否能够在真实场景中合理运用sleep

考察的知识点

线程管理

考生应该如何回答

sleep是可中断的。

/**
 * Causes the currently executing thread to sleep (temporarily cease
 * execution) for the specified number of milliseconds, subject to
 * the precision and accuracy of system timers and schedulers. The thread
 * does not lose ownership of any monitors.
 *
 * @param  millis
 *         the length of time to sleep in milliseconds
 *
 * @throws  IllegalArgumentException
 *          if the value of {@code millis} is negative
 *
 * @throws  InterruptedException
 *          if any thread has interrupted the current thread. The
 *          <i>interrupted status</i> of the current thread is
 *          cleared when this exception is thrown.
 */
// BEGIN Android-changed: Implement sleep() methods using a shared native implementation.
public static void sleep(long millis) throws InterruptedException {
    
    
    sleep(millis, 0);
}

在现场中


19 怎么保证线程按顺序执行?如何实现线程排队 ?(金山)

这道题想考察什么?

是否了解多个线程顺序启动的方式有哪些与真实场景使用,是否熟悉多个线程顺序启动在工作中的表现是什么?

考察的知识点

多个线程顺序启动的方式有哪些的概念在项目中使用与基本知识

考生应该如何回答

Q:假设有A、B两个线程,B线程需要在A线程执行完成之后执行。

A:可以在启动B线程之前,调用A线程的join方法,让B线程在A线程执行完成之后启动。

Thread t1 = new Thread() {
    
    
	@Override
	public void run() {
    
    
          System.out.println("执行第一个线程任务!");    
	}
};
t1.start();
t1.join(); //阻塞等待线程1执行完成
Thread t2 = new Thread() {
    
    
	@Override
	public void run() {
    
    
		System.out.println("执行第二个线程任务!");
	}
};
t2.start();

Q: 假设有A、B两个线程,其中A线程中执行分为3步,需要在A线程执行完成第二步之后再继续执行B线程的代码怎么办?

A:可以使用wait/notify,在B线程中wait,A线程执行完成第二步之后执行notify通知B线程继续执行。

Object lock = new Object();
Thread t1 = new Thread() {
    
    
	@Override
	public void run() {
    
    
		System.out.println("第一步执行完成!");
		System.out.println("第二步执行完成!");
         synchronized (lock) {
    
    
			lock.notify();
         }
		System.out.println("第三步执行完成!");
	}
};

Thread t2 = new Thread() {
    
    
	@Override
	public void run() {
    
    
		synchronized (lock) {
    
    
			try {
    
    
				lock.wait();
			} catch (InterruptedException e) {
    
    
				e.printStackTrace();
			}
		}
		System.out.println("执行第二个线程任务!");
	}
};
t2.start(); //注意必须先启动t2线程,否则可能t2在notify之后才wait!
t1.start();

Q:假设有A、B、C三个线程,其中A、B线程执行分为三步,C线程需要在A线程执行完第二步后执行一部分代码然后继续等待B线程都执行完第二步时才能执行,怎么办?

A:可以借助CountDownLatch闭锁来完成:

CountDownLatch countDownLatch = new CountDownLatch(2);
Thread t1 = new Thread() {
    
    
    @Override
    public void run() {
    
    
		System.out.println("t1:第一步执行完成!");
		System.out.println("t1:第二步执行完成!");
		countDownLatch.countDown();
		System.out.println("t1:第三步执行完成!");
	}
};

Thread t2 = new Thread() {
    
    
	@Override
    public void run() {
    
    
		System.out.println("t2:第一步执行完成!");
		System.out.println("t2:第二步执行完成!");
        countDownLatch.countDown();
		System.out.println("t2:第三步执行完成!");
    }
};
Thread t3 = new Thread() {
    
    
	@Override
    public void run() {
    
    
	try {
    
    
        countDownLatch.await();
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
	System.out.println("执行第三个线程任务!");
};
t3.start();
t2.start();
t1.start();

20 非阻塞式生产者消费者如何实现(字节跳动)

这道题想考察什么?

是否了解非阻塞式生产者消费者与真实场景使用,是否熟悉非阻塞式生产者消费者

考察的知识点

非阻塞式生产者消费者的概念在项目中使用与基本知识

考生应该如何回答

生产者消费者模式在日常生活中,生产者消费者模式特别常见。比如说我们去麦当劳吃饭,在前台点餐,付完钱后并不是直接给你汉堡薯条啥的,而是给你一张小票,你需要前去取餐处等待,后厨加工完的餐食都直接放入取餐处,机器叫号提醒,客户凭小票取餐。

在这里插入图片描述

上面取餐的场景其实就是一个典型的生产者消费者模型,具备3个部分:生产者、消费者、缓冲区。后厨就相当于生产者,客户就是消费者,而取餐台是两者之间的一个缓冲区。再转到我们平时开发过程中,经常会碰到这样子的场景:某个模块负责产生数据,这些数据由另一个模块来负责处理。产生数据的模块,就称为生产者,而处理数据的模块,就称为消费者。当然如果只抽象出生产者和消费者,还不是正儿八经的生产者消费者模式,还需要一个缓冲区,生产者生产数据到缓冲区,消费者从缓冲区拿数据去消费。服务器端经常使用的消息队列设计就是参照生产者消费者模型。但这个时候有的同学就会好奇的问一句,干嘛需要缓冲区呢,生产完直接给消费者不是更加简单吗?在复杂的系统中,这中间的缓冲区必不可少,作用明显。

  • 解耦。这是最显而易见的,如果生产者直接将数据交给消费者,那么这两个类必然会有依赖,消费者的改动都会影响到生产者。当两者之间加入缓存区之后,生产者与消费者之间彻底解耦了,各有所职,互不依赖。
  • 平衡生产与消费能力。在多线程的环境下,如果生产者生产数据速度很快,消费者来不及消费,那么缓冲区便是生产者数据暂存的地方,生产者生产完一个数据后直接丢在缓冲区,便可以去生产下一个数据,消费者自己从缓冲区拿数据慢慢处理,这样生产者无需因为消费者的处理能力弱而浪费资源。当然,反之也一样。

阻塞与非堵塞

什么是堵塞?通俗的话来讲,就是一件事没干完,就只能在这等待,不允许去做其他的事。在程序世界里,阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。而非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程,线程让出CPU。

要求实现非堵塞式生产消费模式,所以缓冲区我们就不能使用便捷的堵塞队列,只能使用一般的集合代替,类似ArrayList等。很明显会引起两个问题。

**第一,并发问题。**类似ArrayList这些普通集合是线程不安全的,当生产者、消费者线程同时操作(数据入队、数据出队)缓冲区时,必然会引起ConcurrentModificationException。当然这个问题解决方案有很多种,比如使用synchronized、ReentrantLock等锁机制,也可以使用线程安全的集合,当然线程安全的集合底层也是锁机制。

**第二,线程通信问题。**非堵塞式的意思就是生产者与消费者去操作缓冲区,只是尝试去操作,至于能不能得到想要的结果,他们是不管的,并不会像堵塞队列那样死等。那么生产者与消费者之间的协作与通信,比如缓冲区没数据时通知生产者去生产;缓冲区有数据后,通知消费者去消费;当缓冲区数据满了让生产者休息。这里我们可以使用wait、notify/notifyAll方法。这些方法使用过程中注意以下几点基本就可以了。

  • wait() 和 notify() 使用的前提是必须先获得锁,一般配合synchronized关键字使用,即在synchronized同步代码块里使用 wait()、notify/notifyAll() 方法。
  • 当线程执行wait()方法时候,会释放当前持有的锁,然后让出CPU,当前线程进入等待状态。
  • 当notify()方法执行时候,会唤醒正处于等待状态的线程,使其继续执行,notify()方法不会立即释放锁,锁的释放要看同步代码块的具体执行情况。notifyAll()方法的功能也是类似。
  • notify()方法只唤醒一个等待线程并使该线程开始执行。所以如果有多个线程等待,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll()方法会唤醒所有等待线程,至于哪一个线程将会第一个处理取决于操作系统的实现。

经过分析,非堵塞式生产者消费者模式实现为:

/**
 * 实现非堵塞式生产者消费者模式
 */     
public class ProducerConsumerDemo {
    
    

    /**
     * 定义队列最大容量,指缓冲区最多存放的数量
     */
    private static int MAX_SIZE = 3;

    /**
     * 缓冲区队列,ArrayList为非堵塞队列,线程不安全
     * static修饰,全局唯一
     */
    private static final List<String> list = new ArrayList<>();

    public static void main(String[] args) {
    
    
        //创建生产者线程
        Producer producer = new Producer();
        //创建消费者线程
        Consumer consumer = new Consumer();
        //生产者线程开启
        producer.start();
        //消费者线程开启
        consumer.start();
    }

    /**
     * 生产者线程
     */
    static class Producer extends Thread {
    
    

        @Override
        public void run() {
    
    
            //具体实现...
        }
    }

    /**
     * 消费者线程
     */
    static class Consumer extends Thread {
    
    

        @Override
        public void run() {
    
    
            //具体实现...
        }
    }
}

类的大致结构如上所示,很简单也很清晰。我们实现了两个线程,一个代表生产者,一个代表消费者,缓冲区使用非阻塞式队列ArrayList,所以它是线程不安全的,为了能更加清晰的看出生产者消费者执行流程,缓冲区大小设置成较小的3。

生产者线程

/**
 * 生产者线程
 */
static class Producer extends Thread {
    
    

        @Override
        public void run() {
    
    
            //使用while循环执行run方法
            while (true) {
    
    
                try {
    
    
                    //生产者 sleep 300ms, 消费者 sleep 500ms,模拟两者的处理能力不均衡
                    Thread.sleep(300);
                } catch (InterruptedException e1) {
    
    
                    e1.printStackTrace();
                }

                //第1步:获取队列对象的锁,与消费者持有的锁是同一把,保证线程安全
                synchronized (list) {
    
    
                    //第2步:判断缓冲区当前容量
                    //第2.1步:队列满了就不生产,等待
                    while (list.size() == MAX_SIZE) {
    
    
                        System.out.println("生产者 -> 缓冲区满了,等待消费...");
                        try {
    
    
                            //使用wait等待方法,内部会释放当前持有的锁
                            list.wait();
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                    }
                    //第2.2步:队列未满就生产一个产品
                    list.add("产品");
                    System.out.println("生产者 -> 生产一个产品,当前队列大小:" + list.size());

                    //唤醒其他线程,这里其他线程就是指消费者线程
                    list.notify();
                }
            }
        }
    }

生产者线程负责生产数据。只要开始执行生产流程,第一步先获取list对象锁,也就意味着当前只有生产者线程可操作缓冲区,保证线程安全。第二步它会先检查一下当前缓存区的容量,如果缓存区已经满了,那生产者无需再去生产新的数据,调用wait方法进行等待,这个过程会释放list对象锁。如果缓冲区没满,就直接生产一个产品,并通过notify方法唤醒消费者线程。

消费者线程

/**
 * 消费者线程
 */
static class Consumer extends Thread {
    
    

        @Override
        public void run() {
    
    
            //使用while循环执行run方法
            while (true) {
    
    
                try {
    
    
                    Thread.sleep(500);
                } catch (InterruptedException e1) {
    
    
                    e1.printStackTrace();
                }

                //第1步:获取队列对象的锁,与生产者持有的锁是同一把,保证线程安全
                synchronized (list) {
    
    
                    //第2步:判断缓冲区当前容量
                      //第2.1步:队列空了,等带
                    while (list.size() == 0) {
    
    
                        System.out.println("消费者 -> 缓冲区空了,等待生产...");
                        try {
    
    
                            //使用wait等待方法,内部会释放当前持有的锁
                            list.wait();
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                    }
                    //第2.2步:队列不为空,消费一个产品
                    list.remove(0);
                    System.out.println("消费者 -> 消费一个产品,当前队列大小:" + list.size());

                    //唤醒其他线程,这里其他线程就是指生产者线程
                    list.notify();
                }
            }
        }
    }

消费者线程负责消费数据,它的实现与生产者相似。第一步也是先获取list对象锁,避免并发异常。第二步检查当前缓冲区容量时,这里与生产者正好相反。如果缓冲区已经空了,没有数据可消费了,它会使用wait方法进行等待。如果缓冲区没空,则去消费一个产品,并且调用notify方法唤醒生产者线程去生产。
执行ProducerConsumerDemo的main方法,抓取方法执行日志。从logcat日志可以看出,生产者与消费者互相协作,有条不紊的进行生产与消费操作,没有引起并发异常问题。

//logcat日志(截取了部分)
生产者 -> 生产一个产品,当前队列大小:1
消费者 -> 消费一个产品,当前队列大小:0
生产者 -> 生产一个产品,当前队列大小:1
生产者 -> 生产一个产品,当前队列大小:2
消费者 -> 消费一个产品,当前队列大小:1
生产者 -> 生产一个产品,当前队列大小:2
消费者 -> 消费一个产品,当前队列大小:1
生产者 -> 生产一个产品,当前队列大小:2
生产者 -> 生产一个产品,当前队列大小:3
消费者 -> 消费一个产品,当前队列大小:2
生产者 -> 生产一个产品,当前队列大小:3
生产者 -> 缓冲区满了,等待消费...
消费者 -> 消费一个产品,当前队列大小:2
生产者 -> 生产一个产品,当前队列大小:3
生产者 -> 缓冲区满了,等待消费...
消费者 -> 消费一个产品,当前队列大小:2
生产者 -> 生产一个产品,当前队列大小:3
生产者 -> 缓冲区满了,等待消费...
消费者 -> 消费一个产品,当前队列大小:2
生产者 -> 生产一个产品,当前队列大小:3
生产者 -> 缓冲区满了,等待消费...
消费者 -> 消费一个产品,当前队列大小:2
...

最后

此面试题会持续更新,请大家多多关注!!!

有需要这份面试题的朋友可以扫描下方二维码即可免费领取!!!
(扫码还可以享受ChatGPT机器人的服务哦!!!!)

猜你喜欢

转载自blog.csdn.net/datian1234/article/details/131576844