1. 什么是线程?

一个程序可以看作是一个进程,线程是进程的一个实体单位,是一条独立的执行路径,线程是操作系统能够进行运算调度的最小单位,可以使用多线程对运算进行提速。
例如:一个线程完成一个任务需要100ms,那么我们可以使用多个线程来共同完成这个任务,例如10个线程来执行,只需要10ms。

image.png

2. 线程和进程有什么区别?

一个程序即为一个进程,线程是进程的实体单元,一个线程为一条独立的执行路径。
进程:每个进程都有独立的代码和数据空间(进程上下文),都有一个堆和方法区,进程间的切换会有较大的开销,一个进程包含1—n个线程。(进程是资源分配的最小单位)
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。

3. 线程实现的方式有几种(四种)?

1、继承Thread类
2、实现Runable接口,重写run()方法【推荐使用】
3、实现Callable接口,重写call()方法,产生线程,结合FutureTask获取线程的结果。
4、利用线程池创建多个线程,ThreadPoolExector(核心线程池数、阻塞队列、最大线程池数、等待时间、时间单元,拒绝策略)方法

4. 什么是线程安全和线程不安全?

线程的安不安全,其本质是内存的安全问题。线程安全是指当多个线程访问一个对象时,如果不进行额外的同步操作,调用这个对象的行为可以获得正确的结果的话,即是线程安全的。简单来说,如果线程没有加锁,那么就是不安全的;反之,如果加锁了,那么线程就是安全的。
线程安全问题都是由全局变量及静态变量引起的主要是多线程同时操作导致实体对象数据的不正确。
线程安全:
多个线程共同执行任务访问同一个资源对象,线程安全采用了加锁同步机制,当前线程进行数据访问时,其他线程不能进行访问,只有当前线程读取完成后,其他线程才能进行访问。
线程不安全:
多个线程共同执行任务访问同一个资源对象,线程没有采用了加锁同步机制,多个线程会同时对这个数据进行访问操作,导致数据没有按照预期进行操作。

5. 什么是多线程?优缺点?

什么是多线程?
多线程:是指从软件或者硬件上实现多个线程的并发技术。Cpu是通过时间片的方式为进程分配cpu处理时间的,例如我们从网上下载文件的时候,如果仅仅使用单线程,那么我们需要按照下载-显示进度-保存的顺序依次进行,不仅会拖慢速度,而且会浪费cpu的内存。利用多线程可以实现下载文件的一边下载、一边显示进度,一边保存,充分利用了cpu资源空间。
多线程的好处:

  1. 1. 使用多线程可以把程序中占据时间长的任务放到后台去处理,如图片、视屏的下载
  2. 1. 发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好

多线程的缺点:

  1. 大量的线程降低代码的可读性;
  1. 更多的线程需要更多的内存空间
  1. 当多个线程对同一个资源出现争夺时候要**注意线程安全的问题。**

6. 线程的五个状态(五种状态,创建、就绪、运行、阻塞和死亡)?

阻塞:
等待阻塞:运行的线程执行wait()方法,该线程会进入“等待池”中,“等待池”中的线程不会去竞争同步锁,只有调用notify()方法才会重新去竞争同步锁(把线程从等待池中放入”锁池”中)
同步阻塞:多线程操作时竞争对象的同步锁时,若该锁被其他线程占用,则jvm会将该线程放入“锁池”中;
其他阻塞:调用的Thread类的sleep()方法或者Object的join()方法。
wait()、sleep()方法、join()方法、yield()方法:
yield()方法执行后线程进入就绪状态,马上释放cpu执行权,但依然保留CPU的执行资格;
join()方法 执行后线程进入阻塞状态,一般是通过B中调用A的join()方法,此时线程B进入阻塞队列,知道线程A结束或中断。
1、线程与多线程 - 图2

7. 一个线程如果出现了运行时异常会怎么样?

如果一个线程出现运行时异常,那么如果这个异常如果没有被捕获的话,该线程即会中止!
Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。
补充,针对java和C++中对于线程异常的不同处理方式

  - java:存在用户线程(User Thread)和守护线程(Daemon Thread),当所有用户线程都退出了,守护线程退出,进而应用程序退出;这就是为什么"main线程"退出后,其他线程还正在运行。也是其他"子线程"被异常终止了或者退出了,其他线程正常运行。

  - c++:main属于主进程,当子线程抛出异常时,最后抛到主线程中,导致主进程crash,进而结束应用程序,相应的Thread被销毁(thread对象被销毁)

8. 如果同步块内的线程抛出异常会发生什么?

如果这个异常没有被捕获的话,这个线程就停止执行了。
另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放.

9. ThreadPool(线程池)用法与优势?

1、为什么要用线程池:
提高响应速度,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
2、new Thread 缺点
每次new Thread新建对象性能差。
线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
缺乏更多功能,如定时执行、定期执行、线程中断。
3、ThreadPool 优点
减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)
减少在创建和销毁线程上所花的时间以及系统资源的开销
如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存
4、线程池的核心方法使用ThreadPoolExecutor创建线程池
核心线程池数corePoolSize
工作队列 workQueue
最大线程池数 maximumcorePoolSize
保持存活时间 keepAliveTime
时间单位 Timesunits
创建线程的工厂ThreadFactory
拒绝策略 Handler RejectedExecutionHandler(接口)
5、线程池有哪几种拒绝策略
1、callRunsPolicy 调用者策略:当触发调用者策略时,只要当前线程池没有停止,就由提交任务的当前线程进行处理。(一般在不允许失败、对性能要求不高,并发量较小的场景使用)
2、AbortPolicy 中止策略:触发中止策略时,直接抛出拒绝当前执行的异常,即中断此次线程。(默认使用中止策略)
3、DicardPolicy 丢弃策略:直接静悄悄的丢弃这个任务,不触发任何动作。
6、利用Executors类创建的四种线程池,在线程池中都会创建大量的线程,导致资源的堆积【不推荐使用】
//Exector创建的四种线程池
Executors._newFixedThreadPool
(5);
Executors.newCachedThreadPool();
Executors.newScheduledThreadPool(5);
Executors.newSingleThreadExecutor();

10. 什么是多线程的上下文切换?

即使是单核CPU也支持多线程执行,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)
上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换 回这个任务时,可以再次加载这个任务的状态。

  • 从任务保存到再加载的过程就是一次上下文切换.

    11. Java中用到的线程调度算法是什么

    抢占式。一个线程用完CPU之后,操作系统会根据线程优先级线程饥饿情况等数据算出一个总的优先级分配下一个时间片给某个线程执行。
    协同式。线程的执行由自己来控制的,一个线程执行完毕后通知其线程来执行。

    12. 如何保证多线程下 i++ 结果正确?

    解释i++和++i为什么是线程不安全的:
    i++的执行过程:
    1、先是寄存器从主内存中读取i,到线程的工作内存副本(线程的工作内存区)。
    2、然后在寄存器中进行+1操作,
    3、最后+1后的值从工作内存中回传到主内存中,
    因为没有保证原子操作,万一出现第3步没有执行完而线程的CPU时间片已经用完,导致操作失败,所以并不安全。
    如何保证i++和++i线程安全:
    1、利用CAS技术(compare and swap),通过总线加锁的方式,指令能保证CPU寄存器和内存交换数据是原子操作。
    2、利用ReentrantLock 进行同步加锁
    3、利用synchronized给对象加锁,但是影响运行效率

    13、线程池中阻塞队列的作用?为什么线程池的操作都是先添加队列而不是先创建最大线程?

    线程池中阻塞队列的作用:
    1)一般的队列只能保证作为一个有限长度的缓冲区,如果超过了缓冲长度,就无法保留当前的任务,而阻塞队列能够通过阻塞保留当前想要继续入队的线程任务。
    2)阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源
    阻塞队列自带阻塞和唤醒的功能,不需要额外的处理。无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活。
    为什么线程池的操作都是先添加队列而不是先创建最大线程:
    线程池创建新的线程需要获得mainlock全局锁,会影响并发效率,所以阻塞队列把第一步获取核心线程池和第三步获取最大线程池数隔离开来,起一个缓冲的作用。
    添加到阻塞队列,可以避免频繁的线程的创建和回收。

    14、线程池中线程复用的原理?

    线程池将线程和任务解耦,线程是线程,任务是任务,不像之前我们创建线程Thread,必须绑定一个对应的任务。
    线程池中线程的复用即线程池中同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread进行了一个封装,并不是每次执行任务都会调用Thread.run()方法,而是让每个线程去执行一个“循环任务”,不断检查是否还有任务需要执行,如果有就直接执行。