多线程篇

1.Java中实现多线程的几种方法

  • 继承Thread类

  • 实现Runnable接口

  • 实现Callable接口

  • 线程池方式创建

2.使用Thread、Runnable和Callable创建线程的优缺点

  • 采用继承Thread类的方式创建线程的优缺点

    • 优点:直接使用this即可获取当前线程,编程简单

    • 缺点:已经继承了Thread类,无法再继承其他父类了

  • 采用实现Runnable、Callable接口的方式创建线程的优缺点以及区别

    • 优点:线程类只是实现了Runnable或者Callable接口,还可以继承其他类。这种方式下,多个线程可以给共享一个target对象,所以比较适合多个相同线程来处理同一份资源的情况。

    • 缺点:编程较为复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。

    • 区别:

      • Runnable通过重写run方法实现,Callable通过call方法实现

      • Runnable可以提交给Thread来包装下直接启动一个线程来执行,Callable一般都是提交给ExcuteService来执行

      • Runnable的任务无返回值,Callable的任务执行后有返回值

      • call方法可以抛出异常,run方法不可以

      • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。

3.如何停止一个正在运行的线程

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  • 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法
  • 使用interrupt方法中断线程

4.sleep()和wait()有什么区别

  • sleep()方法属于Thread类中的;而wait()方法属于Object类中的。
  • sleep()方法导致程序暂停执行指定的时间,让出cpu给其他线程,但是它的监控状态依然保持着,当指定时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁;而调用wait()方法,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。

5.volatile 是什么?可以保证有序性吗?

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,那么保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。 什么叫保证部分有序性,即当程序执行到volatile变量的读写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行。 使volatile一般用于状态标记量和单例模式的双检锁。

 

6.为什么wait和notify方法要在同步块中调用?

  • 只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法

  • 如果不这么做会抛出IllegalMonitorStateException异常

  • 避免wait()和notify()之间产生竞态条件 wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此线程必须在某个对象的同步方法或同步代码块中才能够调用该对象的wait()方法。 在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁,因此,必须在某个对象的同步方法或同步代码块中才能够调用该对象的notify()和notifyAll()方法。 调用wait()方法的原因通常是调用线程希望某个特殊的状态或变量被设置之后再继续执行。 调用notify()和notifyAll()方法的原因通常是调用线程希望告诉其他等待线程"特殊状态已被设置"。 这个状态作为线程间通信的通道,他必须是一个可变的共享状态或变量。

7.Thread类中的yield方法有什么作用?

yield()方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且保证当前线程放弃CPU占用而不能保证其他线程一定能占用CPU,执行yield()的线程有可能在进入暂停状态后马上又被执行。

8.线程、进程的区别

  • 进程是一个"执行中的程序",是系统进行资源分配和调度的一个独立单位。

  • 线程时进程的一个实体,一个进程拥有多个线程,线程间共享地址空间和其他资源,所以通信和同步等操作线程比进程更加容易。

  • 线程上下文的切换比进程上下文切换要快很多

    • 进程切换时,涉及到当前进程的CPU环境的保存和新被调度运行进程的CPU环境的设置。

    • 线程切换仅需要保存和设置少量寄存器内容,不涉及存储管理方面的操作。

9.产生死锁的四个必要条件,且如何避免死锁

  • 互斥条件:一个资源每次只能被一个线程使用。

  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。

  • 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺。

  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源。

如何避免死锁:

  • 一次性申请所有的资源,破坏 "占有且等待" 条件

  • 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,破坏 "不可抢占" 条件

  • 按序申请资源,破坏 "循环等待" 条件

10.说说ThreadLocal原理?

ThreadLocal可以理解为线程本地变量,它会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行,做到了线程之间互相隔离,相比于synchronized的做法时用空间换时间。

ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry数组本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了包i村key-value键值对的能力。

弱引用的目的是为了防止内存泄漏,如果是强引用,那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。

但是这样还是会存在内存泄漏的问题,假如key和ThreadLocal对象被回收之后,Entry中就存在key为null,但是value有值的Entry对象,但是永远无法访问,同样除非线程结束运行才会被回收。

但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的。

11.引用类型有哪些?有什么区别?

  • 强引用:指的就是代码中普遍存在的赋值方式,比如A a = new A();这种。强引用关联的对象,永远不会被GC回收。

  • 软引用:可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。

  • 弱引用:可以用WeakReference来描述,它的强度比软引用更低一点,弱引用对象无论内存是否足够,下一次GC的时候一定会被回收

  • 虚引用:被称为欢迎引用,是最弱的引用关系,可以用PhantomReference来描述,它必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。

12.常用线程池有哪些

  • newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行,支持定时以及周期性执行任务的需求。

  • newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,知道线程达到线程池的最大大小。

  • newCacheThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(JVM)能够创建的最大线程大小。

  • newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。

13.简述一下你对线程池的理解

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

14.线程池的原理以及核心参数

首先线程池有几个核心的参数概念

  • 最大线程数--maximumPoolSize

  • 核心线程数--corePoolSize

  • 活跃时间--keepAliveTime

  • 阻塞队列--workQueue

  • 拒绝策略--RejectedExecutionHandler 当提交一个新任务到线程池时,具体执行流程如下:

  1. 当我们提交任务,线程池会根据核心线程数(corePoolSize)大小创建若干任务数量线程执行任务

  2. 当任务的数量超过核心线程数(corePoolSize)数量,后续任务将会进入阻塞队列(workQueue)阻塞排队

  3. 当阻塞队列(workQueue)也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待活跃时间(keepAliveTime)之后被自动销毁。

  4. 如果达到最大线程数(maximumPoolSize),阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理。

15.线程池的拒绝策略有哪些

  • AbortPolicy:直接丢弃任务,抛出异常,这是默认策略。

  • CallRunsPolicy:只是调用者所在的线程来处理任务。

  • DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务。

  • DiscardPolicy:直接丢弃任务,也不抛出异常。

16.什么是Daemon线程?它有什么意义?

所谓后台(daemon)线程,也叫守护线程,是指在程序运行的时候后台提供一种通用服务的线程,并且这个线程并不属于线程中不可或缺的部分。 因此所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止 必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。注意:后台进程在不执行finally子句的情况下就会终止其run()方法。 比如:JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程。

 

posted @ 2023-04-12 19:26  XIAOBAI001  阅读(24)  评论(0)    收藏  举报