1、什么是线程和进程
进程是操作系统在程序运行时进行资源分配的最小单位
- 其中的资源包括:CPU、内存空间、磁盘 IO 等,同一进程中的多条线程共享该进程中的资源,而进程和进程之间是相互独立的。进程是具有一定独立功能的程序,它是关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程是 CPU 调度的最小单位,必须依赖于进程而存在
- 线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。它只拥有一点在运行中必不可少的资源(程序计数器,一组寄存器和栈),但是它可以与其他线程共享同一个进程的资源。
2、CPU核心数和线程数的关系
- 多核心:也指单芯片多处理器(Chip Multiprocessors 简称CMP),cmp 的思想是将大规模并行处理器中的smp对称多处理器集成到同一芯片内,各个处理器并行的执行不同的进程。称为并行处理。
- 多线程:Simultaneous Multithreading 简称 SMT。让同一个处理器上的多个线程同步执行并共享处理器的执行资源。
- 核心数、线程数:一般情况下它们是 1:1 对应关系,也就是说四核 CPU 一般拥有四个线程。但 Intel 引入超线程技术后,使核心数与线程数形成 1:2 的关系
3、时间片轮转机制
- 时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称 RR 调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
- 如果在时间片结束时进程还在运行,则 CPU 将被剥夺并分配给另一个进程。 如果进程在时间片结束前阻塞或完成,则 CPU 当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。
- 时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切换( processwitch),有时称为上下文切换( context switch),需要 5ms, 再假设时间片设为 20ms,则在做完 20ms 有用的工作之后,CPU 将花费 5ms 来进行进程切换。CPU 时间的 20%被浪费在了管理开销上了。
- 结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了 CPU 效率: 而设得太长又可能引起对短的交互请求的响应变差。将时间片设为 100ms 通常是一个比较合理的折衷。
4、并发和并行
并发
- 指应用能够交替执行不同的任务,比如单 CPU 核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",只是计算机的速度太快,我们无法察觉到而已.
并行
- 指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边听音乐, 这两件事情可以同时执行
两者区别:一个是交替执行,一个是同时执行.
5、高并发编程的意义,好处和注意事项
好处
(1)充分利用 CPU 的资源
(2)加快响应用户的时间
(3)可以使你的代码模块化,异步化,简单化注意事项
(1)线程之间的安全性
(2)线程之间的死锁
(3)线程太多了会将服务器资源耗尽形成死机当机
6、认识Java里的线程
[6] Monitor Ctrl-Break //监控 Ctrl-Break 中断信号的
[5] Attach Listener //内存 dump,线程 dump,类信息统计,获取系统属性等
[4] Signal Dispatcher // 分发处理发送给 JVM 信号的线程
[3] Finalizer // 调用对象 finalize 方法的线程
[2] Reference Handler//清除 Reference 的线程
[1] main //main 线程,用户程序入口
电脑不同可能显示数量有所差异。
7、线程的启动和终止
启动方式有:
- X extends Thread 然后X.start
- X implements Runnable 然后交给Thread运行
两者的区别
- Thread 才是 Java 里对线程的唯一抽象。
- Runnable 只是对任务(业务逻辑) 的抽象。Thread 可以接受任意一个 Runnable 的实例并执行。
中止
线程自然终止
- 要么是 run 执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
过期方法
- 暂停、恢复和停止操作对应在线程 Thread 的 API 就是 suspend()、resume() 和 stop()。但是这些 API 是过期的,也就是不建议使用的。不建议使用的原因主要有:以 suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如 锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、 resume()和 stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法
中断
- 安全的中止则是其他线程通过调用某个线程 A 的 interrupt()方法对其进行中断操作, 中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程 A 会立即停止自己的工作,同样的 A 线程完全可以不理会这种中断请求。 线程通过检查自身的中断标志位是否被置为 true 来进行响应,
- 线程通过方法 isInterrupted()来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()来进行判断当前线程是否被中断,不过 Thread.interrupted() 会同时将中断标识位改写为 false。
- 如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、 thread.wait 等),则线程在检查中断标示时如果发现中断标示为 true,则会在这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为 false。
- 不建议自定义一个取消标志位来中止线程的运行。因为 run 方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为, 一、一般的阻塞方法,如 sleep 等本身就支持中断的检查, 二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断.
深入理解run和start方法
- Thread类是Java里对线程概念的抽象,可以这样理解:我们通过new Thread() 其实只是 new 出一个 Thread 的实例,还没有与操作系统真正的线程挂起钩。 只有执行了 start()方法后,才实现了真正意义上的启动线程。
- start()方法让一个线程进入就绪队列等待分配 cpu,分到 cpu 后才调用实现的 run()方法,start()方法不能重复调用,如果重复调用会抛出异常。 而 run 方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,也可以被单独调用。
其他的线程相关方法
- yield()方法:使当前线程让出 CPU 占有权,但让出的时间是不可设定的。也不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行 yield( ) 的线程不一定就会持有锁,我们完全可以在释放锁后再调用 yield 方法。 所有执行 yield() 的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。
join 方法
- 把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。 比如在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续执行线程 B。(此处为常见面试考点)
守护线程
- Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非 Daemon 线程的时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置为 Daemon 线程。我们一般用不上,比如垃圾回收线程就是 Daemon 线程。Daemon 线程被用作完成支持性工作,但是在 Java 虚拟机退出时 Daemon 线程中的 finally 块并不一定会执行。在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑。
线程的状态
8、线程间的共享和协作
synchronized 内置锁
- Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
对象锁和类锁
- 对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的 class 对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个 class 对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。 有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的 class 对象。类锁和对象锁之间也是互不干扰的
volatile,最轻量的同步机制
volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。参见代码:
`public class volatileCase{ private static Boolean ready; private static int number; }`
不加 volatile 时,子线程无法感知主线程修改了 ready 的值,从而不会退出循环, 而加了 volatile 后,子线程可以感知主线程修改了 ready 的值,迅速退出循环。 但是 volatile 不能保证数据在多个线程下同时写时的线程安全。 volatile 最适用的场景:一个线程写,多个线程读。
9、ThreadLocal
与 Synchronized 的比较
- 他们都用于解决多线程并发访问。可是 threadlocal 与 synchronized 有本质的区别。synchronized 是利用锁的机制,使变量或代码块在某一时刻仅仅能被一个线程访问。而 Threadlocal 为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的共享。
使用
- ThreadLocal 类接口只有4个方法:
- void set(Object value) 设置当前线程的线程局部变量的值。
- public Object get() 该方法返回当前线程所对应的线程局部变量。
- public void remove() 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK 5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显示调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
- protected Object initialValue() 返回该线程局部变量的初始值,是为了让子类覆盖而设计的。他是一个延迟调用方法,在线程第一次调用 get () 或 set(Object) 时才执行,并且仅执行1次。ThreadLocal 中的缺省实现直接返回一个null。
- public final static ThreadLocal< String > RESOURCE = new Threadlocal < String >(); RESOURCE 代表一个能够存放string 类型的 ThreadLocal 对象。此时不论什么,一个线程并发访问这个变量,对它进行写入、读取操作,都是线程安全的。
实现解析
上面先取到当前线程,然后调用 getMap 方法获取对应的 ThreadLocalMap,它是 threadLocal 的静态内部类,然后 thread 类中有一个这样类型成员,所以 getMap 是直接返回 thread 的成员
可以看到有个 Entry 内部静态类,它继承了 WeakReference,总之它记录了两个信息,一个是 ThreadLocal<?>类型,一个是 Object 类型的值。getEntry 方法 则是获取某个 ThreadLocal 对应的值,set 方法就是更新或赋值相应的 ThreadLocal 对应的值。private Entry getEntry(ThreadLocal< ? > key){ int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; private void set(ThreadLocal < ? > key,Object value){...} }
回顾我们的 get 方法,其实就是拿到每个线程独有的 ThreadLocalMap 然后再用 ThreadLocal 的当前实例,拿到 Map 中的相应的 Entry,然后就可以拿到相应的值返回出去。当然,如果 Map 为空,还会先进行 map 的创建,初始化等工作。
引发的内存泄露分析
- Object o = new Object();
这个 o ,我们称之为对象引用,而 new Object() 我们称之为在内存中产生了一个对象实例。当写下 o = null 时,只是表示 o 不再指向堆中 object 的对象实例,不代表这个对象实例不存在了。强引用
- 就是指在程序代码之中普遍存在的,类似“Object obj=new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。
软引用
- 是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象, 在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行 第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。
弱引用
- 也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱 引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时, 无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在 JDK 1.2 之 后,提供了 WeakReference 类来实现弱引用。
虚引用
- 也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象 实例被收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了 PhantomReference 类来实现虚引用。
内存泄漏的现象分析
根据我们前面对 ThreadLocal 的分析,我们可以知道每个 Thread 维护一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。仔细观察 ThreadLocalMap,这个 map 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。 因此使用了 ThreadLocal 后,引用链如图所示
图中的虚线表示弱引用。这样,当把 threadlocal 变量置为 null 以后,没有任何强引用指向 threadlocal 实例,所以 threadlocal 将会被 gc 回收。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块 value 永远不会被访问到了,所以存在着内存泄露。
只有当 thread 结束以后,current thread 就不会存在栈中,强引用断开, Current Thread、Map value 将全部被 GC 回收。最好的做法是在不需要使用 ThreadLocal 变量后,都调用它的 remove()方法,清除数据。
其实考察 ThreadLocal 的实现,我们可以看见,无论是 get()、set()在某些时候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()方法中显式调用了 expungeStaleEntry 方法。
为什么使用弱引用而不是强引用?
- key 使用强引用:引用 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 的对象实例不会被回收,导致 Entry 内存泄漏。
- key 使用弱引用:引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 的对象实例也会被回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 都有机会被回收。
- 比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障
- 因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。
- 总结
JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。
JVM 利用调用 remove、get、set 方法的时候,回收弱引用。
当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、 get、set 方法,那么将导致内存泄漏。
10、线程间的协作
- 线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值, 而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程, 而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了 “做什么”(what)和“怎么做”(How),简单的办法是让消费者线程不断地循环检查变量是否符合预期在 while 循环中设置不满足的条件,如果条件满足则退出 while 循环,从而完成消费者的工作。却存在如下问题:
- 1) 难以确保及时性。 2)难以降低开销。如果降低睡眠的时间,比如休眠 1 毫秒,这样消费者能更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费。
等待/通知机制
- 是指一个线程 A 调用了对象 O 的 wait()方法进入等待状态,而另一个线程 B 调用了对象 O 的 notify()或者 notifyAll()方法,线程 A 收到通知后从对象 O 的 wait() 方法返回,进而执行后续操作。上述两个线程通过对象 O 来完成交互,而对象上的 wait()和 notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
- notify(): 通知一个在对象上等待的线程,使其从 wait 方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入 WAITING 状态。
- notifyAll(): 通知所有等待在该对象上的线程
- wait() 调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用 wait()方法后,会释放对象的锁
- wait(long) 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n 毫秒,如果没有通知就超时返回
- wait (long,int) 对于超时时间更细粒度的控制,可以达到纳秒
等待和通知的标准范式
等待方遵循如下原则
1)获取对象的锁。 2)如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件。 3)条件满足则执行对应的逻辑。
synchronized(对象){ while(条件不满足){ 对象.wait(); } 对应的处理逻辑 }
通知方遵循如下原则。
1)获得对象的锁。 2)改变条件。 3)通知所有等待在对象上的线程。
synchronized(对象){ 改变条件 对象.notifyAll(); }
在调用 wait()、notify()系列方法之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法、notify()系列方法,进入 wait()方法后,当前线程释放锁,在从 wait()返回前,线程与其他线程竞争重新获得锁,执行 notify()系列方法的线程退出调用了 notifyAll 的 synchronized 代码块的时候后,他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
notify 和 notifyAll 应该用谁
- 尽可能用 notifyall(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程
面试题
调用 yield()、sleep()、wait()、notify()等方法对锁有何影响?
- yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。
- 调用 wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行 wait 方法后面的代码。
- 调用 notify() 系列方法后,对锁无影响,线程只有在 syn 同步代码执行完后才会自然而然的释放锁,所以 notify() 系列方法一般都是 syn 同步代码的最后一行。