基础概念
什么是进程和线程
进程是程序运行资源分配的最小单位
,是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程是 CPU 调度的最小单位,必须依赖于进程而存在
,与同属一个进程的其 他的线程共享进程所拥有的全部资源。
CPU核心数和线程数的关系
目前主流 CPU 都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是 1:1 对应关系,也 就是说四核 CPU 一般拥有四个线程。但 Intel 引入超线程技术后,使核心数与线程数形成 1:2 的关系。
CPU时间片轮转机制
如果在时间片结束时进程还在运行,则 CPU 将被剥夺并分配给另一个进程。 如果进程在时间片结束前阻塞,则 CPU 当即进行切换。
并行和并发
并发:
指应用能够交替执行不同的任务,比如单 CPU 核心下执行多线程并非是 同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不 断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已。
并行:
指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话, 这两件事情可以同时执行。
区别:
前者是交替执行,一个是同时执行。
高并发编程的意义、好处和注意事项
- 充分利用 CPU 的资源
- 加快响应用户的时间
- 可以使你的代码模块化,异步化,简单化
注意事项
- 线程之间的安全性
- 线程之间的死锁
- 线程太多了会将服务器资源耗尽形成死机宕机
认识Java里的线程
Java 程序天生就是多线程的
一个 Java 程序从 main()方法开始执行,就会开启以下线程:
- main线程,用户程序入口
- Reference Handle,清除 Reference 的线程
- Finalizer,调用对象finalize方法的线程
- Signal Dispatcher,分发处理发送给 JVM 信号的线程
- Attach Listener,内存 dump,线程 dump,类信息统计,获取系统属性等
- Monitor Ctrl-Break,监控 Ctrl-Break 中断信号
线程的启动与中止
启动方式:
- 继承Thread类,然后调用start;
- 实现Runnable接口,然后交给Thread运行;
Thread 和 Runnable 的区别:
Thread才是Java里对线程的唯一抽象,Runnable只是对任务(业务逻辑)的抽象。Thread可以接受任意一个Runnable的实例并执行.
中止:
线程自然中止
,要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。stop
,暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop(),但是这些API是过期的,也就是不建议使用的。
,以stop方法为例,在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
中断:
安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作,中断好比其他线程对该线程打了个招呼,“A,你要中断了”,不代表线程A会立即停止自己的工作,同样的A线程完全可以不理会这种中断请求。因为java里的线程是协作式的,不是抢占式的。线程通过检查自身的中断标志位是否被置为true来进行响应。
线程通过方法 isInterrupted()来进行判断是否被中断
,也可以调用静态方法 Thread.interrupted()来进行判断当前线程是否被中断,不过 Thread.interrupted() 会同时将中断标识位改写为 false
。
如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、 thread.wait 等),则在线程在检查中断标示时如果发现中断标识为 true,则会在 这些阻塞方法调用处抛出 InterruptedException 异常
。
不建议自定义一个取消标志位来中止线程的运行
。因为 run 方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。这种情况下,使用中断会更好,因为:
1.一般的阻塞方法,如 sleep 等本身就支持中断的检查。
2.检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可 以避免声明取消标志位,减少资源的消耗。【处于死锁状态的线程无法被中断
】
深入理解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。
线程的优先级
在 Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范围从 1~10,在线程构建的时候可以通过 setPriority(int)方法来修改优先级,默认优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。
设置线程优先级时,针对频繁阻塞(休眠或者 I/O 操作)的线程需要设置较高优先级
,而偏重计算(需要较多 CPU 时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占
。在不同的 JVM 以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。
守护线程
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。
这意味着,当一个 Java 虚拟机中不存在非 Daemon 线程的时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置为Daemon 线程。我们一般用不上,比如垃圾回收线程就是 Daemon 线程。
Daemon 线程被用作完成支持性工作,但是在 Java 虚拟机退出时 Daemon 线程中的 finally 块并不一定会执行。在构建 Daemon 线程时,不能依靠 finally块中的内容来确保执行关闭或清理资源的逻辑。
线程间共享和协作
线程间共享
synchronized 内置锁
概述:
Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量 访问的可见性和排他性,又称为内置锁机制。
对象锁和类锁:
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态 方法或者一个类的class 对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个 class 对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的 class 对象。类锁和对象锁之间也是互不干扰的。
线程间协作
等待/通知机制
概述:
是指一个线程 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):
对于超时时间更细粒度的控制,可以达到纳秒;
尽可能用 notifyall(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我 们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程
Q:调用 yield() 、sleep()、wait()、notify()等方法对锁有何影响?
A:yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。 调用 wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait 方法后面的代码。 调用 notify()系列方法后,对锁无影响,线程只有在 syn 同步代码执行完后才 会自然而然的释放锁,所以 notify()系列方法一般都是 syn 同步代码的最后一行。
线程的状态
Java中线程的状态分为6种:
初始化状态(NEW) :
新建一个线程对象,但还没有调用start()
方法;运行(RUNNING):
Java线程中将就绪(ready)和运行中(running)两种状态统称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start方法,该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready),就绪状态的线程获取CPU时间片后变成运行中状态(running)
。阻塞(BLOCKED):
表示线程阻塞于锁。【仅对应于synchronized关键字】等待(WAITING):
进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)超时等待(TIME_WAITING):
该状态不同于WAITING,它可以在指定的时间后自行返回。终止(TERMINATED):
表示该线程已经执行完毕。
死锁
概念
指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,他们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁。
死锁产生的条件
死锁的发生必须具备以下四个必要条件:
互斥:
指进程对所分配到的资源进行排他性使用,即在一段时间内某资源只由一个进程占用,如果此时还有其他进程请求资源,则请求者只能等待,直到占有资源的进程用完释放。请求和保持:
指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程阻塞,但又对自己已获得的其他资源保持不放。不剥夺:
指进程已获取的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。环路等待:
指在发生死锁时,必然存在一个进程—资源的环形链,即进程集合{P0,P1,P2…Pn}中的P0正在等待一个P1占用的资源,P1正在等待P2占用的资源,…,Pn正在等待已被P0占用的资源。
打破死锁的方式
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。
只要打破四个必要条件之一就可以有效预防死锁的发生。
打破互斥条件:
改造独占性资源为虚拟资源,大部分资源已无法改造。打破不可抢占条件:
当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。打破占有且申请条件:
采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。打破循环等待条件:
实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。
避免死锁常见的算法有有序资源分配法、银行家算法。
死锁的危害
- 线程不工作了,但是整个程序还是活着的;
- 没有任何的异常信息可以供我们检查;
- 一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序;
活锁
概念:
两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
活锁解决办法:
每个线程休眠随机数,错开拿锁的时间。
线程饥饿
概念:
低优先级的线程,总是拿不到执行时间。