美文网首页
面试题之多线程

面试题之多线程

作者: 东方欲晓_莫道君行早 | 来源:发表于2021-03-28 15:12 被阅读0次

    (一) 并发编程三要素

    1.定义
    • 原子性: 一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
    • 有序性: 程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
    • 可见性: 一个县城对共享变量的修改,另一个线程能够立刻看到。
    2.处理方案:
    • synchronized和Lock能确保原子性,能让多线程执行代码的时候依次按顺序执行,自然就具有有序性。
    • 而volatile关键字也可以解决这个问题,volatile 关键字可以保证有序性,让处理器不会把这行代码进行优化排序。
    3.volatile 相关关联点

    (1)定义

    • volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略

    (2)volatile 的特性

    • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
      禁止进行指令重排序。(实现有序性)
      只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性

    (二) 创建线程的方式以及对比

    1.方式
    • 1)继承Thread类创建线程类
    • 2)通过Runnable接口创建线程类
    • 3)通过Callable和Future创建线程
    • 4)通过线程池创建
    2.对比

    Runnable与Callable接口实现基本一致。这两种方式优缺点:

    • (a)线程类还可以继承其他类
    • (b)多个线程可以共享一个target,非常适合多个相同的线程处理同一份资源的情况。从而可以将CPU,代码,数据分开,形成清晰的模型,较好的体现了面向对象的编程思想。
    • (c)缺点是编程复杂一些,如果需要访问当前线程,需要访问Thread.currentThread()

    采用Thread

    • (a)缺点是不能继承其他父类
    • (b)优点是编码简单,当前线程this即可。

    (三)线程的五种基本状态

    1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
    2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
    3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。

    就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

    4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。

    根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    • a.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    • b.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    • c.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

    5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    (四)几种方法的比较

    • Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入阻塞,但不释放对象锁,millis后线程自动苏醒进入可运行状态。作用:给其它线程执行机会的最佳方式。

    • Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。

    • t.join()/t.join(long millis),当前线程里调用其它线程1的join方法,当前线程阻塞,但不释放对象锁,直到线程1执行完毕或者millis时间到,当前线程进入可运行状态。

    • obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。

    • obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

    (五)线程池

    1.通过Excutors创建的四种线程池

    • newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
    
    
    • newFixedThreadPool :创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
    • newScheduledThreadPool :创建一个定长线程池,支持定时及周期性任务执行。
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
    
    
    • newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    

    2.线程池常见参数

    public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) { }
    
    • corePoolSize:核心线程数量,会一直存在,除非allowCoreThreadTimeOut设置为true
    • maximumPoolSize:线程池允许的最大线程池数量
    • keepAliveTime:线程数量超过corePoolSize,空闲线程的最大超时时间
    • unit:超时时间的单位
    • workQueue:工作队列,保存未执行的Runnable 任务
    • threadFactory:创建线程的工厂类
    • handler:当线程已满,工作队列也满了的时候,会被调用。被用来实现各种拒绝策略。

    3.线程池的拒绝策略
    如何实现自定义策略:通过实现 RejectedExecutionHandler 接口

    jdk内置的四种拒绝策略:

    • AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
    • CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会丢弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量。
    • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
    • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
      4.通过Excutors创建线程池的弊端(引用阿里的开发手册)

    阿里巴巴Java开发手册,明确指出不允许使用Executors静态工厂构建线程池
    原因如下:
    线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
    说明:Executors返回的线程池对象的弊端如下:
    1:FixedThreadPool 和 SingleThreadPool:
    允许的请求队列(底层实现是LinkedBlockingQueue)长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
    2:CachedThreadPool 和 ScheduledThreadPool
    允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

    (六)乐观锁与悲观锁

    乐观锁

    总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。

    • version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
      核心SQL代码:
      update table set x=x+1, version=version+1 where id=#{id} and version=#{version};

    • CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

    悲观锁

    总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁。

    相关文章

      网友评论

          本文标题:面试题之多线程

          本文链接:https://www.haomeiwen.com/subject/yfvjhltx.html