[关闭]
@x-power 2023-02-01T05:38:59.000000Z 字数 5006 阅读 559

Java线程

面试 线程/进程


1. sleep join yield有什么区别

多线程的五种状态: 新建状态, 就绪状态, 运行状态, 阻塞状态, 死亡状态.


1.1 sleep

sleep方法需要制定等待的时间, 他可以让当前正在执行的线程在制定的时间内暂停, 进入阻塞状态, 该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行的机会. 但是sleep不会释放锁的标志, 如果有synchronize同步代码块, 其他线程依然不能获得锁,不能访问共享数据.


1.2 wait

wait()方法需要和notify()以及notifyAll两个方法一起介绍, 这三个方法用于协调多个线程对共享数据的存取, 所以必须要在synchronize语句块内使用, 也就是说, 调用wait(), notify()notifyAll的任务调用在这些方法前必须拥有对象的锁. 注意, 他们都是Object类的方法,而不是Thread类的方法.

waitsleep方法的不同之处在于, wait方法会释放对象的"锁标志". 当调用某一对象的wait方法后,会使当前线程暂停执行, 并将当前线程放入对象等待池中, 直到调用了notify方法后, 将对象从等待池中移除任意一个线程并放入锁标志等待池中, 只有锁标志等待池中的线程可以获得锁标志, 他们随时准备争夺锁的拥有权, 当调用了某个对象的notifyAll方法, 会将对象等待池中的所有线程都移动到该对象的锁标志等待池.

此外, wait, notify ,以及notifyAll只能在synchronize内使用, 但是如果使用的是ReenTrantLock实现同步, 该如何达到这三个方法的效果呢? 解决方式就是使用ReenTrantLock.newCondition获取一个Condition类对象, 然后Conditionawait,signal以及signalAll分别对应上面的三个方法.


1.3 yield

yield方法和sleep方法类似, 也不会释放"锁标志", 区别在于,她没有参数, 即yield方法被执行之后, 当前线程进入就绪状态.所以执行yield的线程有可能在进入到 这种可执行状态之后马上又被执行, 另外yield方法只能使同优先级或者更高优先级的线程得到执行机会, 这也和sleep不同


1.4 join

join方法会使当前线程等待调用join方法的线程结束之后才会继续执行.


2. 创建线程的方式以及实现


2.1 继承Thread类创建线程


2.2 通过Runnable接口创建线程类


2.3 通过Callable和Future创建线程


2.4 采用Runnable Callable接口的方式创建多线程时

劣势: 编码稍微复杂.


2.5 使用继承Thread类的方法创建多线程


3 周边


3.1 CountDownLatch

CountDownLatch内部维护了一个整数n(n>=0), 在当前线程初始化CountDownLatch的时候指定其值. 当前线程调用CountDownLatchawait方法阻塞当前线程, 等待其他调用CountDownLatch对象的CountDown方法的线程执行完毕, 其他线程调用该CountDownLatchCountDown方法会将n-1, 知道所有线程执行完毕, 当前线程则回复运行.

  1. import java.util.Random;
  2. import java.util.concurrent.CountDownLatch;
  3. import java.util.concurrent.ExecutorService;
  4. import java.util.concurrent.Executors;
  5. public class CountDownLatchDemo {
  6. private static final JackMa demo = new JackMa();
  7. public static void main(String[] args) throws InterruptedException {
  8. ExecutorService exec = Executors.newFixedThreadPool(100);
  9. for (int i=0; i<10; i++){
  10. exec.submit(demo);
  11. }
  12. // 等待检查
  13. JackMa.getLatch().await();
  14. // 发射火箭
  15. System.out.println("Fire!");
  16. // 关闭线程池
  17. exec.shutdown();
  18. }
  19. }
  20. class JackMa implements Runnable{
  21. static CountDownLatch getLatch() {
  22. return latch;
  23. }
  24. private static final CountDownLatch latch = new CountDownLatch(10);
  25. @Override
  26. public void run() {
  27. // 模拟检查任务
  28. try {
  29. Thread.sleep(new Random().nextInt(10) * 1000);
  30. System.out.println("check complete");
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. } finally {
  34. //计数减一
  35. //放在finally避免任务执行过程出现异常,导致countDown()不能被执行
  36. latch.countDown();
  37. }
  38. }
  39. }

3.2 线程池

Executors类里面提供了一些静态工厂, 生成一些常用的线程池.

  1. newFixThreadPool: 创建固定大小的线程池, 线程池的大小一旦达到最大值,就会保持不变, 如果某个线程因为执行异常而结束, 那么线程池会补充一个新的线程.
  2. newCachedThreadPool: 创建一个可以缓存的线程池, 如果线程池的大小超过了处理任务所需的线程, 那么就会回收部分空闲的线程(60S 不执行任务), 当任务数量增加的时候, 此线程池又可以只能的添加新线程来处理任务, 此线程池不会对线程池大小做限制, 线程池的大小依赖于操作系统和JVM能创建的最大线程数的大小.
  3. newSingleThreadExecutor: 创建一个单线程的线程池, 这个线程池只有一个线程在工作, 也就是相当于单线程串行执行所有任务, 如果这个唯一的线程因为异常而结束的时候, 那么会有一个新的线程去替代它, 此线程池保证所有任务的执行顺序按照任务的提交顺序执行.
  4. newScheduledThreadPool: 创建一个大小无限的线程池, 此线程池支持定时以及周期性执行任务的需求.
  5. newSingleThreadScheduledExecutor: 创建一个单线程的线程池, 此线程池支持定时以及周期性执行任务的需求.

4. 锁


4.1 volatile实现原理

在JVM底层volatile是采用"内存屏障"来实现的.

缓存一致性协议(MESI协议) 它确保每个缓存中使用的共享变量的副本是一致的.其核心思想如下: 当某个CPU在写数据的时候, 如果发现操作的变量是共享变量, 则会通知其他的CPU告知该变量的缓存是无效的, 因此其他CPU在读取该变量时, 发现其无效会从主存中加载数据. CPU的临时寄存器, 会把一些使用频率比较高的数据放到寄存器中, 以减少读数据方面的瓶颈.

指令重拍: 编译器或者CPU对操作指令进行重排序, 在一些特定的情况下,指令重排可能会给代码造成一些不可预料的后果.

名称 代码示例 说明
写读 a=1;b=a; 写一个变量之后, 再读这个位置
读写 a=b;b=1; 读一个变量之后, 再写这个变量

可以发现这里的每一组指令之中都有写操作, 这个写操作的位置是不允许变化的, 否则会带来不一样的执行结果.
编译器将不会对存在数据依赖性的程序指令进行重排, 这里的依赖性仅仅指单线程情况下的数据依赖性; 多线程并发情况下, 此规则将失效.


4.2 synchronize实现原理

同步代码块是使用monitorentermonitorexit指令实现的, 同步方法(在这看不出来需要看JVM底层实现) 依靠的是方法修饰符上的ACC_SYNCHRONIZED.


4.3 synchronized 与 lock 的区别


添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注