线程相关的知识归纳整理
CPU的核心数和线程数的关系
CPU的核心数和线程数一般是1:1的关系,Intel推出的超线程技术能使电脑的核心数和线程数的比达到 1:2 (基于一个物理核心模拟两个逻辑核心),即是一个4核的CPU同时可运行4个线程,如果使用了超线程技术就可以同时运行8个线程,linux 系统下一个进程最大创建1000个线程,windows系统下一个进程最大创建2000个线程)
CPU时间片轮转机制(RR调度)
操作系统把所有就绪进程按先入先出的原则排成一个队列。新来的进程加到就绪队列末尾。每当执行进程调度时,进程调度程序总是选出就绪队列的队首进程,让它在CPU上运行一个时间片的时间。时间片是一个很小的时间单位,通常为10~100ms数量级。当进程用完分给它的时间片后,系统的计时器发出时钟中断,调度程序便停止该进程的运行,把它放入就绪队列的末尾;然后把CPU分给就绪队列的队首进程,同样也让它运行一个时间片,如此往复。(当前时间片执行完成后,线程还未完成,就会保存资源,切换到下一个时间片 执行下一个线程,这个过程叫上下文切换 大概消耗20000个cpu时间周期,一个cpu时间周期大约为 执行一个1+1 操作)
进程和线程的定义和区别
-
进程:进程是程序运行资源分配的最小单位
进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。 - 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动
-
进程是系统进行资源分配和调度的一个独立单位。
进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。 -
线程:线程是CPU调度的最小单位,必须依赖于进程而存在
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。一个进程中有一个线程存活,那么进程就是存活的,因为线程共享它所依赖的进程的资源.
并行和并发
- 并行:以一个具有超线程的4核CPU而言,最大可并行8个线程。
- 并发:以一个具有超线程的4核CPU而言,在1秒钟内可执行100个时间片,并发 能力就是100,并发必须依赖于单位时间,否则没有意义。
- 二者区别:一个是同时执行,一个是交替执行
高并发编程的意义,好处和注意事项(java里的程序天生就是多线程的)
- 意义:更充分的利用cpu资源(特别是多核cpu)
- 好处:加快响应用户的时间,能让程序执行的更快,也能让代码模块化,异步化,简单化。
-
注意事项:
1)程安全性,主要是多个线程共享数据时可能会产生于期望不相符的结果
2)线程之间的死循环,为了解决线程之间的安全性引入了Java的锁机制,而一不小心就会产生Java线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。
3)线程过多时会使得CPU频繁切换,花在调度上时间太多,可能造成死机宕机等问题。
4)线程过多还会消耗过多内存。(一个线程基本内存配置为1M)
认识Java里的线程
- Java里的程序天生就是多线程的,JDK中唯一能代表线程的就是Thread, Runnable和Callable只是对任务的一个抽象接口,不是线程。
- 一个Java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上Java程序天生就是多线程程序,因为执行main()方法的是一个名称为main的线程。
[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线程,用户程序入口
线程的生命周期图
线程的声明周期线程的使用
-
通过继承实现一个线程
image.png -
通过接口实现
image.png -
不同实现的使用
image.png - 一个线程Thread对象,只能执行一次start()方法。
- run()方法和start()方法的区别:
1)run()方法只是一个普通方法,主要是业务逻辑实现的地方
2)start()方法才是将thread和系统的线程关联并执行的方法,执行start()才是一个线程创建启动的标志。
线程的停止方式
- 一个线程在不干预的情况下,执行完成run方法中的代码就会停止。
- 已过时的停止线程的方法:
stop(),destroy() 停止过于粗暴,调用该方法会立马杀死线程,可能资源没有被释放,容易造成资源泄漏
suspend() 挂起线程的方法(容易造成死锁所以被弃用了)
resume() 将挂起线程恢复。 -
目前可使用的停止线程的方法,interrupt() 打断线程,可能不能正常中断。需要使用 interrupted() 和 isInterrupted()来判断。使用interrupt中断线程 需要在线程中判断interrupted标志自己中断。(interrupted() 判断一次后 第二次 interrupt的状态会变成false)
image.png - 如果一个线程处于了阻塞状态(如线程调用了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方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,可以被单独调用。
其他的线程方法
-
join()方法:可以实现让线程顺序执行,普通情况下执行两个线程,线程都是交替执行 时间片轮转机制(RR调度)
image.png
使用join()方法使得线程2在线程1执行完成后执行:在线程2中传入线程1,并且线程1调用join()方法进行插队,最后运行结果是线程1先执行完毕再执行线程2.
image.png - yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源,所有执行yield()的线程有可能在进入到可执行状态后马上又被执行。
-
等待/通知机制
是指一个线程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):对于超时时间更细粒度的控制,可以达到纳秒。
等待和通知方的使用标准范式:
等待方的书写范式 通知方的书写范式
线程间的共享
线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,包括数据之间的共享,协同处理事情。这将会带来巨大的价值。
Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
- 对象锁和类锁:
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。
线程间的协作
线程之间相互配合,完成某项工作,比如:一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了“做什么”(what)和“怎么做”(How),简单的办法是让消费者线程不断地循环检查变量是否符合预期在while循环中设置不满足的条件,如果条件满足则退出while循环,从而完成消费者的工作。却存在如下问题:
1)难以确保及时性。
2)难以降低开销。如果降低睡眠的时间,比如休眠1毫秒,这样消费者能更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费。
ThreadLocal
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值, ThreadLocal往往用来实现变量在线程之间的隔离。threadLocal类接口很简单,只有4个方法,我们先来了解一下:
• void set(Object value)
设置当前线程的线程局部变量的值。
• public Object get()
该方法返回当前线程所对应的线程局部变量。
• public void remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
• protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>(){
@Override
protected String initialValue(){
return “string的初始值”;
}
};
RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。
显式锁
-
Lock接口和synchronized的比较
我们一般的Java程序是靠synchronized关键字实现锁功能的,使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。synchronized属于Java语言层面的锁,也被称之为内置锁。synchronized这种机制,一旦开始获取锁,是不能中断的,也不提供尝试获取锁的机制。
而Lock是由Java在语法层面提供的,锁的获取和释放需要我们明显的去获取,因此被称为显式锁。并且提供了synchronized不提供的机制。
Lock的特性描述
Lock接口和核心方法
在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。
private Lock lock = new ReentrantLock();
public void lockUse(){
lock.lock();
try {
//执行业务代码
goodsInfo.changeNumber(60);
}finally {
//使用try finally 是为了保证即使业务代码报错 也能释放锁
lock.unlock();
}
}
lock的方法解析
可重入锁ReentrantLock、所谓锁的公平和非公平
-
可重入锁
synchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁。ReentrantLock在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。 -
公平锁和非公平锁
如果在时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。
ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。事实上,公平的锁机制往往没有非公平的效率高。原因是,在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程A持有一个锁,并且线程B请求这个锁。由于这个锁已被线程A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此会再次尝试获取锁。与此同时,如果C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样的情况是一种“双赢”的局面:B获得锁的时刻并没有推迟,C更早地获得了锁,并且吞吐量也获得了提高。
读写锁ReentrantReadWriteLock
之前提到锁(synchronized和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量
import java.util.concurrent.locks.*;
public class UsWrLock implements GoodsService {
private GoodsInfo goodsInfo;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock write = lock.writeLock();
private final Lock read = lock.readLock();
@Override
public GoodsInfo getGoodsInfo() {
read.lock();
try {
//读操作 返回商品信息
SleepTools.ms(5);
return this.goodsInfo;
}finally { read.unlock(); }
}
@Override
public void sellGoods(int sellNumber) {
write.lock();
try {
//写操作,改变商品信息
SleepTools.ms(5);
goodsInfo.changeNumber(sellNumber);
}finally { write.unlock(); }
}
public UsWrLock(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
}
Condition接口
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。
用Lock和Condition实现等待通知, condition.signal()只能随机唤醒一个等待线程,condition.signalAll()唤醒所有等待线程。如下是一份使用condition实现的等待通知模式的代码:
public class ExpressCond {
public final static String CITY = "ChengDu";
private int km;/*快递运输里程数*/
private String site;/*快递到达地点*/
private Lock lock = new ReentrantLock();
private Condition siteCond = lock.newCondition();
private Condition kmCond = lock.newCondition();
public ExpressCond(int km, String site) {
this.km = km;
this.site = site;
}
/* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
public void changeKm(int mileage){
lock.lock();
try {
km = mileage;
kmCond.signalAll();
}finally {
lock.unlock();
}
}
/* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
public void changeSite(String cityName){
lock.lock();
try {
this.site = cityName;
siteCond.signalAll();
}finally {
lock.unlock();
}
}
/*当快递的里程数大于100时更新数据库*/
public void waitKm(){
//TODO
lock.lock();
try {
while (km < 100){
try {
kmCond.await();
System.out.println("check KM thread["+Thread.currentThread().getId() +"] is be notifed.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
System.out.println("the Km is "+this.km+",I will change db");
}
/*当快递到达目的地时通知用户*/
public void waitSite(){
lock.lock();
try {
while(CITY.equals(this.site)) {
try {
siteCond.await();
System.out.println("check Site thread["+Thread.currentThread().getId() +"] is be notifed.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
System.out.println("the site is "+this.site+",I will call user");
}
}
- 使用
public class TestCond {
private static ExpressCond express = new ExpressCond(0,ExpressCond.CITY);
/*检查里程数变化的线程,不满足条件,线程一直等待*/
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
/*检查地点变化的线程,不满足条件,线程一直等待*/
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++){
new CheckSite().start();
}
for(int i=0;i<3;i++){
new CheckKm().start();
}
Thread.sleep(1000);
express.changeKm(102);//快递里程变化
express.changeSite("北京天安门");//快递里程变化
}
}
-
结果
condition使用结果