声明

多线程对我来说可能是比较薄弱的地方(那会上课在打盹),
然后实际开发中又用的比较少(其实一直都知道多线程的重要性,主要是懒),
这里的话就是自己这些天对多线程问题的一些整理和自我理解。

其实内容也大多都是参考别人的文档,然后加上自己的理解。

问题(1-10)

①并行和并发有什么区别?

解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
解释二: 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
解释三

并行:指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。

并行

并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,
使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。

并发

(其实这个问题的话可以说是明白多线程的基础问题,但其实很多人都不一定清楚真正的解释方法。包括我

②线程和进程的区别?

进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。线程又叫轻量级进程。
线程的划分小于进程,线程是隶属于某个进程的。进程是程序的一种动态形式,是CPU,内存等资源占用的基本单位,而线程是不能占有这些资源的。
进程之间相互独立,通信比较困难,而线程之间共享一块内存区域,通信比较方便。
进程在执行过程中包含比较固定的入口,执行顺序,出口,而线程的这些过程会被应用程序所控制。

进程

进程

线程

线程

其实简单来说就是,进程就像一个工厂,一个公司(操作系统)会有多个工厂部署在不同地方,所以他们之间不能互相交流,而且他们所生产的产品
也不一样;而线程就像是工厂里的某一条生产线,生产线与生产线之间可以互相交流,而且他们的生产只会受该工厂负责人的管理。

③守护线程是什么?

守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他的线程只有一种,那就是用户线程。所以java里线程分2种,

  • 前提
  1. 守护线程,比如垃圾回收线程,就是最典型的守护线程。
  2. 用户线程,就是应用程序里的自定义线程。
  • 守护线程的机制
  1. 守护线程,专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕,那么jvm就会退出(即停止运行)——此时,连jvm都停止运行了,守护线程当然也就停止执行了。
  2. 再换一种说法,如果有用户自定义线程存在的话,jvm就不会退出——此时,守护线程也不能退出,也就是它还要运行,干嘛呢,就是为了执行垃圾回收的任务啊。
  3. 守护线程又被称为“服务进程”“精灵线程”“后台线程”,是指在程序运行是在后台提供一种通用的线程,这种线程并不属于程序不可或缺的部分。 通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的“保姆”。

④创建线程有哪几种方式?

(其实像这种问题没必要出现在这个文章的,但由于我总是忘记单词英盲,所以还是写在这。忏悔

  1. 继承Thread类,重写run方法;
  2. 实现Runnable接口,重写run方法,但是比继承Thread类好用,实现接口还可以继承类,避免了单继承带来的局限性;
  3. 使用Executor框架创建线程池。Executor框架是juc里提供的线程池。
  • 补充线程(Thread)相关的方法
  1. start():启动线程并执行相应的run()方法

  2. run():子线程要执行的代码放入run()方法中

  3. currentThread():静态的,调取当前的线程

  4. getName():获取此线程的名字

  5. setName():设置此线程的名字

  6. yield():调用此方法的线程释放当前CPU的执行权(很可能自己再次抢到资源)

  7. join():在A线程中调用B线程的join() 方法,表示:当执行到此方法,A线程停止执行,直至B线程执行完毕, A线程再接着join()之后的代码执行

  8. isAlive():判断当前线程是否还存活

  9. sleep(long l):显式的让当前线程睡眠l毫秒 (只能捕获异常,因为父类run方法没有抛异常)

  10. 线程通信(方法在Object类中):wait() notify() notifyAll()

  11. getPriority():返回线程优先值 setPriority(int newPriority):改变线程的优先级设置线程的优先级(非绝对,只是相对几率大些)

⑤说一下 runnable 和 callable 有什么区别

  • 相同点
  1. 两者都是接口;(废话
  2. 两者都可用来编写多线程程序;
  3. 两者都需要调用Thread.start()启动线程
  • 不同点
  1. 两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
  2. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

⑥线程有哪些状态?

线程状态有 5 种:新建,就绪,运行,阻塞,死亡

  1. 线程 start 方法执行后,并不表示该线程运行了,而是进入就绪状态,意思是随时准备运行,但是真正何时运行,是由操作系统决定的,代码并不能控制(玄学)。
  2. 同样的,从运行状态的线程,也可能由于失去了 CPU 资源,回到就绪状态,也是由操作系统决定的。这一步中,也可以由程序主动失去 CPU 资源,只需调用 yield 方法。
  3. 线程运行完毕,或者运行了一半异常了,或者主动调用线程的 stop 方法,那么就进入死亡。死亡的线程不可逆转。
  4. 下面几个行为,会引起线程阻塞:

主动调用 sleep 方法。时间到了会进入就绪状态 主动调用 suspend 方法。主动调用 resume 方法,会进入就绪状态。

调用了阻塞式 IO 方法。调用完成后,会进入就绪状态。 试图获取锁。成功的获取锁之后,会进入就绪状态。 线程在等待某个通知。其它线程发出通知后,会进入就绪状态

⑦sleep() 和 wait() 有什么区别?

  1. 同步锁的对待不同:

**sleep()**后,程序并不会不释放同步锁。
**wait()**后,程序会释放同步锁。

  1. 用法的不同:

sleep()**可以用时间指定版来使他自动醒过来。如果时间不到你只能调用interreput()来强行打断。wait()可以用notify()**直接唤起。

⑧notify()和 notifyAll()有什么区别?

锁池

假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法 (或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池

假设一个线程A调用了某个对象的 wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。

而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

⑨线程的 run()和 start()有什么区别?

调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run() ;直接调用run()方法,无法达到启动多线程的目的,相当于主线程线性执行Thread对象的run()方法。

一个线程对线的 start()方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常; run() 方法没有限制。

⑩创建线程池有哪几种方式?

  1. newCachedThreadPool() ,它是用来处理大量短时间工作任务的线程池,具有几个鲜明特点:

    它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
    如果 线程闲置时间超过60秒,则被终止并移除缓存;

    长时间闲置时,这种线程池,不会消耗什么资源。其内部使用
    SynchronousQueue 作为工作队列。

  2. newFixedThreadPool(int nThreads) ,重用指定数目( nThreads )的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动线程数目,将在工作队列中等待空闲线程出现;

    如果工作线程退出,将会有新的工作线程被创建,以补足指定数目nThreads。

  3. newSingleThreadExecutor() ,它的特点在于工作线程数目限制为1,操作一个无界的工作队列,所以它保证了所有的任务都是被顺序执行,最多会有一个任务处于活动状态,并且不予许使用者改动线程池实例,因此可以避免改变线程数目。

  4. newSingleThreadScheduledExecutor()newScheduledThreadPool(int corePoolSize) ,创建的是个 ScheduledExecutorService ,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。

  5. newWorkStealingPool(int parallelism) ,这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建 ForkJoinPool ,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

问题(11-20)

⑪线程池都有哪些状态?

  1. RUNNING :线程池一旦被创建,就处于 RUNNING 状态,任务数为 0,能够接收新任务,对已排队的任务进行处理。

  2. SHUTDOWN :不接收新任务,但能处理已排队的任务。调用线程池的 **shutdown() ** 方法,线程池由 RUNNING 转变为 SHUTDOWN 状态。

  3. STOP :不接收新任务,不处理已排队的任务,并且会中断正在处理的任务。调用线程池的 shutdownNow() 方法,线程池由( RUNNINGSHUTDOWN ) 转变为 STOP 状态。

  4. TIDYINGSHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态,会执行 terminated()
    法。

    线程池中的 terminated() 方法是空实现,可以重写该方法进行相应的处理。

    线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池就
    会由 SHUTDOWN 转变为 TIDYING 状态。

    线程池在 STOP 状态,线程池中执行中任务为空时,就会由 STOP 转变为
    TIDYING 状态。

  5. TERMINATED :线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会由 TIDYING 转变为 TERMINATED状态。

⑫线程池中 submit()和 execute()方法有什么区别?

  1. submit(Callable task)submit(Runnable task, T result)submit(Runnable task) 归属于 ExecutorService 接口。

  2. execute(Runnable command) 归属于Executor接口。ExecutorService继承了 Executor

⑬在 java 程序中怎么保证多线程的运行安全?

参考文章:http://www.jasongj.com/java/thread_safe/

  • 线程的安全性问题主要体现在以下几个特性:
  1. 原子性 :一个或者多个操作在 CPU 执行的过程中不被中断的特性。

关于原子性,一个非常经典的例子就是银行转账问题 :比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。

  1. 可见性 :一个线程对共享变量的修改,另外一个线程能够立刻看到。

CPU从主内存中读数据的效率相对来说不高,现在主流的计算机中,都有几级缓存。每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。

  1. 有序性 :程序执行的顺序按照代码的先后顺序执行。

    boolean started = false; // 语句1
    long counter = 0L; // 语句2
    counter = 1; // 语句3
    started = true; // 语句4

从代码顺序上看,上面四条语句应该依次执行,但实际上JVM真正在执行这段代码时,并不保证它们一定完全按照此顺序执行。

处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。

CPU不按照我的代码顺序执行代码,那怎么保证得到我们想要的效果呢?实际上,完全可以放心,CPU虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。

  • 导致的原因如下:
  1. 缓存导致的可见性问题
  2. 线程切换带来的原子性问题

编译优化带来的有序性问题解决办法:

JDK Atomic 开头的原子类、 synchronizedLOCK ,可以解决原子性问题 synchronizedvolatileLOCK ,可以解决可见性问题 Happens-Before 规则可以解决有序性问题。