Java线程

Java线程

标签(空格分隔): 面试 线程/进程


1. sleep join yield有什么区别

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

  • 新建状态: 当new 一个线程的时候, 程序还没有运行其中的````run```代码.

  • 就绪状态: 一个新创建的线程并不会自动开始, 要想执行线程, 必须调用其start方法., 之后线程处于就绪状态, 位于可运行线程池中, 等待被线程调度选中,

  • 运行状态: 就绪状态的线程获得了CPU的时间片, 执行程序代码.

  • 阻塞: 阻塞是因为线程由于某种原因放弃了CPU的使用权, 让出了时间片, 暂停运行, 直到线程再次进入可运行状态, 才有机会再次获得CPU时间片.

  • 等待阻塞: 运行的线程执行wait, JVM会将其放入等待队列当中.

  • 同步阻塞: 运行的线程在获取对象的同步锁的时候, 若该同步锁被其他线程占用, 则JVM会将其放入锁池当中.

  • 其他阻塞:运行的线程执行Thread.sleep, 或者join方法,或者是翻出了I/O请求,置为阻塞状态. 当sleep状态超时,join等待线程种植

  • 死亡: 线程run, main方法执行结束, 或者异常原因推出了退出了run方法.


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类创建线程

  • 定义Thread的子类, 并重写该类的run方法, 该run方法的方法体就代表了线程要完成的任务, 因此把run成为执行体.
  • 实例化Thread子类的实例, 创建线程对象.
  • 调用线程对象的start方法来启动该线程.

2.2 通过Runnable接口创建线程类

  • 定义Runnable接口的实现类, 并重写接口的run方法,该run方法的方法体同样是该线程的执行体.
  • 创建Runnable实现类的实例, 并依此实例作为Threadtarget来创建Thread对象, 该Thread才是真正的线程对象 .
  • 调用上述真正的线程对象的start方法去启动线程.

2.3 通过Callable和Future创建线程

  • 创建Callable接口的实现类, 并实现call方法, 该call方法将作为线程执行体, 并且有返回值.
  • 创建Callable实现类的实例, 使用FutureTask类来包装Callable对象, 该FutureTask对象封装了该Callable对象的call方法的返回值.
  • 调用FutureTask对象作为Thread对象的target创建并启动新线程.

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

  • 优势: 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类. 在这种方式下,多个线程可以共享一个target对象, 所以非常适合多个相同线程来处理同一份资源的情况, 从而可以将CPU,代码, 数据分开,形成清晰的模型, 较好的体现了面向对象的思想.

劣势: 编码稍微复杂.


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

  • 优势: 编写简单
  • 劣势: 无法继承其他父类.

3 周边


3.1 CountDownLatch

CountDownLatch内部维护了一个整数n(n>=0), 在当前线程初始化CountDownLatch的时候指定其值. 当前线程调用CountDownLatchawait方法阻塞当前线程, 等待其他调用CountDownLatch对象的CountDown方法的线程执行完毕, 其他线程调用该CountDownLatchCountDown方法会将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);
    @Override
    public 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();
        }
    }
}

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对操作指令进行重排序, 在一些特定的情况下,指令重排可能会给代码造成一些不可预料的后果.

  • 在计算机执行指令的顺序在经过程序编译器编译之后形成指令序列, 一般而言, 这个指令序列是会输出确定的结果, 以确保每一次的执行都有确定的结果. 但是一般情况下, CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化, 在某些情况下这个指令优化会带来一些执行的逻辑问题. 主要原因在于代码逻辑之间存在一定的先后顺序, 在并发执行的情况下, 会发生二义性, 即按照不同的执行顺序得到不同的执行结果.

  • 数据依赖性: 不同的程序指令之间的顺序是不允许进行交互的, 即可以称这些程序指令之间存在数据依赖性.

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

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


4.2 synchronize实现原理

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


4.3 synchronized 与 lock 的区别

  • synchronizedlock 的用法区别
  • synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized 可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
  • lock(显示锁):需要显示指定起始位置和终止位置。一般使用 ReentrantLock 类做为锁,多个线程中必须要使用一个 ReentrantLock 类做为对象才能保证锁的生效。且在加锁和解锁处需要通过 lock()unlock() 显示指出。所以一般会在 finally 块中写 unlock() 以防死锁。

posted @ 2019-09-25 21:10  X-POWER  阅读(221)  评论(0编辑  收藏  举报