@x-power
2023-02-01T05:38:59.000000Z
字数 5006
阅读 609
面试 线程/进程
多线程的五种状态: 新建状态, 就绪状态, 运行状态, 阻塞状态, 死亡状态.
新建状态: 当new 一个线程的时候, 程序还没有运行其中的run代码.
就绪状态: 一个新创建的线程并不会自动开始, 要想执行线程, 必须调用其start方法., 之后线程处于就绪状态, 位于可运行线程池中, 等待被线程调度选中,
运行状态: 就绪状态的线程获得了CPU的时间片, 执行程序代码.
阻塞: 阻塞是因为线程由于某种原因放弃了CPU的使用权, 让出了时间片, 暂停运行, 直到线程再次进入可运行状态, 才有机会再次获得CPU时间片.
其他阻塞:运行的线程执行Thread.sleep, 或者join方法,或者是翻出了I/O请求,置为阻塞状态. 当sleep状态超时,join等待线程种植
死亡: 线程run, main方法执行结束, 或者异常原因推出了退出了run方法.
sleep方法需要制定等待的时间, 他可以让当前正在执行的线程在制定的时间内暂停, 进入阻塞状态, 该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行的机会. 但是sleep不会释放锁的标志, 如果有synchronize同步代码块, 其他线程依然不能获得锁,不能访问共享数据.
wait()方法需要和notify()以及notifyAll两个方法一起介绍, 这三个方法用于协调多个线程对共享数据的存取, 所以必须要在synchronize语句块内使用, 也就是说, 调用wait(), notify()和notifyAll的任务调用在这些方法前必须拥有对象的锁. 注意, 他们都是Object类的方法,而不是Thread类的方法.
wait和sleep方法的不同之处在于, wait方法会释放对象的"锁标志". 当调用某一对象的wait方法后,会使当前线程暂停执行, 并将当前线程放入对象等待池中, 直到调用了notify方法后, 将对象从等待池中移除任意一个线程并放入锁标志等待池中, 只有锁标志等待池中的线程可以获得锁标志, 他们随时准备争夺锁的拥有权, 当调用了某个对象的notifyAll方法, 会将对象等待池中的所有线程都移动到该对象的锁标志等待池.
此外, wait, notify ,以及notifyAll只能在synchronize内使用, 但是如果使用的是ReenTrantLock实现同步, 该如何达到这三个方法的效果呢? 解决方式就是使用ReenTrantLock.newCondition获取一个Condition类对象, 然后Condition的await,signal以及signalAll分别对应上面的三个方法.
yield方法和sleep方法类似, 也不会释放"锁标志", 区别在于,她没有参数, 即yield方法被执行之后, 当前线程进入就绪状态.所以执行yield的线程有可能在进入到 这种可执行状态之后马上又被执行, 另外yield方法只能使同优先级或者更高优先级的线程得到执行机会, 这也和sleep不同
join方法会使当前线程等待调用join方法的线程结束之后才会继续执行.
Thread的子类, 并重写该类的run方法, 该run方法的方法体就代表了线程要完成的任务, 因此把run成为执行体.Thread子类的实例, 创建线程对象.start方法来启动该线程.Runnable接口的实现类, 并重写接口的run方法,该run方法的方法体同样是该线程的执行体.Runnable实现类的实例, 并依此实例作为Thread的target来创建Thread对象, 该Thread才是真正的线程对象 . start方法去启动线程.Callable接口的实现类, 并实现call方法, 该call方法将作为线程执行体, 并且有返回值.Callable实现类的实例, 使用FutureTask类来包装Callable对象, 该FutureTask对象封装了该Callable对象的call方法的返回值.FutureTask对象作为Thread对象的target创建并启动新线程.Runnable接口或Callable接口,还可以继承其他类. 在这种方式下,多个线程可以共享一个target对象, 所以非常适合多个相同线程来处理同一份资源的情况, 从而可以将CPU,代码, 数据分开,形成清晰的模型, 较好的体现了面向对象的思想.劣势: 编码稍微复杂.
CountDownLatch内部维护了一个整数n(n>=0), 在当前线程初始化CountDownLatch的时候指定其值. 当前线程调用CountDownLatch的await方法阻塞当前线程, 等待其他调用CountDownLatch对象的CountDown方法的线程执行完毕, 其他线程调用该CountDownLatch的CountDown方法会将n-1, 知道所有线程执行完毕, 当前线程则回复运行.
import java.util.Random;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class CountDownLatchDemo {private static final JackMa demo = new JackMa();public static void main(String[] args) throws InterruptedException {ExecutorService exec = Executors.newFixedThreadPool(100);for (int i=0; i<10; i++){exec.submit(demo);}// 等待检查JackMa.getLatch().await();// 发射火箭System.out.println("Fire!");// 关闭线程池exec.shutdown();}}class JackMa implements Runnable{static CountDownLatch getLatch() {return latch;}private static final CountDownLatch latch = new CountDownLatch(10);@Overridepublic void run() {// 模拟检查任务try {Thread.sleep(new Random().nextInt(10) * 1000);System.out.println("check complete");} catch (InterruptedException e) {e.printStackTrace();} finally {//计数减一//放在finally避免任务执行过程出现异常,导致countDown()不能被执行latch.countDown();}}}
在
Executors类里面提供了一些静态工厂, 生成一些常用的线程池.
newFixThreadPool: 创建固定大小的线程池, 线程池的大小一旦达到最大值,就会保持不变, 如果某个线程因为执行异常而结束, 那么线程池会补充一个新的线程.newCachedThreadPool: 创建一个可以缓存的线程池, 如果线程池的大小超过了处理任务所需的线程, 那么就会回收部分空闲的线程(60S 不执行任务), 当任务数量增加的时候, 此线程池又可以只能的添加新线程来处理任务, 此线程池不会对线程池大小做限制, 线程池的大小依赖于操作系统和JVM能创建的最大线程数的大小.newSingleThreadExecutor: 创建一个单线程的线程池, 这个线程池只有一个线程在工作, 也就是相当于单线程串行执行所有任务, 如果这个唯一的线程因为异常而结束的时候, 那么会有一个新的线程去替代它, 此线程池保证所有任务的执行顺序按照任务的提交顺序执行.newScheduledThreadPool: 创建一个大小无限的线程池, 此线程池支持定时以及周期性执行任务的需求.newSingleThreadScheduledExecutor: 创建一个单线程的线程池, 此线程池支持定时以及周期性执行任务的需求.在JVM底层volatile是采用"内存屏障"来实现的.
缓存一致性协议(MESI协议) 它确保每个缓存中使用的共享变量的副本是一致的.其核心思想如下: 当某个CPU在写数据的时候, 如果发现操作的变量是共享变量, 则会通知其他的CPU告知该变量的缓存是无效的, 因此其他CPU在读取该变量时, 发现其无效会从主存中加载数据. CPU的临时寄存器, 会把一些使用频率比较高的数据放到寄存器中, 以减少读数据方面的瓶颈.
指令重拍: 编译器或者CPU对操作指令进行重排序, 在一些特定的情况下,指令重排可能会给代码造成一些不可预料的后果.
在计算机执行指令的顺序在经过程序编译器编译之后形成指令序列, 一般而言, 这个指令序列是会输出确定的结果, 以确保每一次的执行都有确定的结果. 但是一般情况下, CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化, 在某些情况下这个指令优化会带来一些执行的逻辑问题. 主要原因在于代码逻辑之间存在一定的先后顺序, 在并发执行的情况下, 会发生二义性, 即按照不同的执行顺序得到不同的执行结果.
数据依赖性: 不同的程序指令之间的顺序是不允许进行交互的, 即可以称这些程序指令之间存在数据依赖性.
| 名称 | 代码示例 | 说明 |
|---|---|---|
| 写读 | a=1;b=a; |
写一个变量之后, 再读这个位置 |
| 读写 | a=b;b=1; |
读一个变量之后, 再写这个变量 |
可以发现这里的每一组指令之中都有写操作, 这个写操作的位置是不允许变化的, 否则会带来不一样的执行结果.
编译器将不会对存在数据依赖性的程序指令进行重排, 这里的依赖性仅仅指单线程情况下的数据依赖性; 多线程并发情况下, 此规则将失效.
同步代码块是使用
monitorenter和monitorexit指令实现的, 同步方法(在这看不出来需要看JVM底层实现) 依靠的是方法修饰符上的ACC_SYNCHRONIZED.
synchronized 和 lock 的用法区别 synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized 可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。lock(显示锁):需要显示指定起始位置和终止位置。一般使用 ReentrantLock 类做为锁,多个线程中必须要使用一个 ReentrantLock 类做为对象才能保证锁的生效。且在加锁和解锁处需要通过 lock() 和 unlock() 显示指出。所以一般会在 finally 块中写 unlock() 以防死锁。