个人理解,如有错误,请海涵
多任务调度
大部分操作系统如Linux、Windos等,都是采用时间片轮转的抢占式调度方式来实现任务调度的。在这种调度方式下,每个进程执行一个任务都会在一短时间后暂停执行,切换其他进程执行任务。由于进程的上下文切换,CPU需要耗费大量的时间来保存该进程的内存资源以及状态,如果进程过多就会导致CPU用于处理进程上下文切换的时间过多导致系统执行效率降低。因此出现了线程,每个进程拥有多个线程,并且共享该进程的所有内存资源。CPU执行的最小单元从进程变为了线程。这样的好处就是当CPU进行任务调度的时候只需要保存线程所具有的内存资源,减少了很多CPU用于处理上下文切换的时间,这样的好处就是增加了CPU用于处理有效计算任务的时间,提高了系统性能。
多线程模型
应用线程不能直接访问CPU资源,而是先映射到一个操作系统线程上再去访问CPU资源,所以在早期单核CPU计算机多对一的线程模型中,多个应用线程(用户线程)对应一个操作系统线程(内核线程),这些应用线程串行执行,如果其中一个线程出现阻塞就会一直占用这个内核线程,导致其他线程无法正常执行。
一对一的线程模型解决了这一个问题,一个用户线程对应一个内核线程来执行任务,其中一个线程的阻塞对其他线程不会产生影响。但是这种线程模型当用户线程过多的时候,会频繁创建、销毁内核线程最终占用大量资源用于创建和销毁内核线程影响到系统性能。这种模型一般会设置内核线程创建数量上限,以免过度浪费资源。
多对多的线程模型解决了这个问题,内核线程可以执行多个用户线程,用户线程可以在多个内核线程中切换执行,实现了一种多路复用的机制,避免了内核线程大量创建所带来的资源浪费。
数据一致性
- 加锁
- CAS机制
同一个进程中的线程共享进程的主内存,而各个线程之间不能实现数据共享。当线程执行时,会从主内存中复制一份数据来进行操作,操作之后的数据在线程之间不具有可见性。
Thread类的使用
start()、run()
每个Thread类的实例在执行时都会对应操作系统的一个内核线程执行,其中Thread类对象实例的执行需要调用Thread的start()方法,对应的内核线程会执行Thread的run()方法,默认这个run()方法不执行任何操作,所以在创建线程时,需要重写run()方法执行业务逻辑。
join()
当主线程调用 join() 方法时,会等待子线程执行完毕再执行下面的操作。
sleep()、yield()
线程调用sleep() 方法时,会休眠指定时间,休眠期间会释放CPU线程资源,让其他线程(不包括调用sleep方法的线程)来抢占CPU资源,调用sleep()方法的线程可能会被中断产生异常,需要try..catch...
线程调用yield()方法时,和sleep()相似,但是释放CPU线程资源后让其他线程抢占CPU线程资源时,包括调用yield()方法这个线程本身,所以有可能还是自己抢占到CPU资源继续执行。
wait()、notify()
这两个方法的实现基于一种消费者和生产者模型,消费者在执行期间没有得到执行所需要的必要数据或其他资源,就调用wait()休眠,等待生产者来唤醒,生产者执行完任务后调用notify()方法唤醒消费者,此时消费者唤醒得到了执行需要的必要资源,线程继续执行。
Runnable接口的使用
由于Java语言的单继承特性,使用继承Thread类创建线程的这种方式其子类不能再继承其他类来实现不同的功能,具有一定的局限性。Runnable应运而生,创建一个Runnable接口的实现类并重写run()方法,在 new Thread()时将这个Runnable接口的实现类作为构造函数的参数传入,调用start()方法启动线程之后,内核线程在调用run()方法时就不再调用Thread默认的run()方法,而是调用Runnable实现类的run()方法。这样Runnable接口的实现类就可以再继承或者实现其他类与接口来扩展功能。
内核线程是如何通过Thread调用到Runnable实现类的run()方法的呢?其实Thread同样实现了Runnable接口,并在内部定义了一个Runnable对象属性,通过构造方法来给这个Runnable对象属性进行赋值。这种实现方式其实是一种静态代理模式,静态代理模式 的定义是与目标对象实现相同的接口并在内部包含一个目标对象的引用,通过构造方法传入实际对象并赋值。所以Thread是Runnable的代理类,代理增强的功能是使用一个子线程而不是使用主线程来执行这个任务。
线程状态
由于线程开启后,并不能获得CPU时间片立即执行,需要等待操作系统调度,所以Thread类定义了一系列线程状态值来表示线程执行的情况。
NEW【新建状态】
当创建了Thread类的实例,但是还没有调用start方法时,该线程处于NEW状态
RUNNABLE【可运行状态】
Thread实例调用start()方法开启线程后,该线程由NEW状态转变为 RUNNABLE 状态,如果该线程立即获取到CPU时间片,内核线程的状态为运行中running,如果该线程没有立即获得时间片,则对应内核线程状态为ready,而这两种内核线程对应到应用线程状态为RUNNABLE
BLOCKED【阻塞状态】
当多个线程访问被锁保护的资源时,没有获取到锁而等待时的状态。当所资源被释放并该线程获取到锁后,从BLOCKED状态转为RUNNABLE状态
WAITING【等待状态】
当前线程调用了wait()方法或者join()方法时,该线程处于WAITING状态。当现车给处于WAITING状态时,该线程不能继续执行,并且如果该线程持有锁资源,就会自动释放锁资源
TIMED_WAITING【超时等待】
和WAITING状态类似,但是具备超时自动唤醒机制,当等待超过指定时间后,线程会自动唤醒并执行
TERMINATED【终止状态】
当前线程执行完毕之后,线程状态为TERMINATED,处于该状态的线程不会再转换为其他状态,Thread对象会被回收
线程安全
由于一个进程的多个线程共享进程资源,当多个线程同时操作同一个资源数据时,会发生线程安全问题
synchronized与互斥锁
-
sychronized可以使用在类的静态方法上、类的成员方法上、代码块上
-
当synchronized在类的静态方法上使用时,需要使用该类的对象本身作为监视器monitor
-
当synchronized在类的成员方法上使用时,需要使用该类对象的实例作为监视器
-
当synchronized在代码块上使用时,可以使用任意一个对象作为监视器
-
-
synchronized关键字可以配合监视器对象的wait()、notify()、join()等方法实现线程协作
-
synchronized使用简单,无须显式加锁和释放锁,由JVM自动加锁和释放
-
synchronized修饰的范围越小,并发性能越好
- 原因是修饰范围越大,竞争同一把锁的线程越多,阻塞线程越多,修饰范围越小,整个进程中被阻塞的线程越少
实现原理
任何一个类被加载时,都需要编译为字节码文件,然后加载到JVM中执行,Thread类被加载时,如果执行到synchronized关键字,则在字节码文件的代码块头部和尾部分别加上monitorerent和monitorexit,也就是使用monitorerent 和 monitorexit 来包围方法或者代码块对应的字节码。
monitorerent指令:当每个线程执行到monitorerent时,会检查monitor的计数是否为0,如果是,则该线程称为monitoe的拥有者,也就是拿到了锁资源,当该线程内部的方法也使用了synchronized关键字,该线程再一次调用该monitor作为锁时,monitor的计数 +1,这也就是synchronized作为可重入锁的实现。
monitorexit指令:当该线程每执行完一次同步方法或者代码块时,monitor的计数 -1,知道计数变为0,释放锁资源。
Volatile关键字和线程可见性
内存模型与线程可见
线程不能直接访问主内存,而是具有自己的独立工作内存,线程执行时复制一份主内存数据作为自己的独立工作内存,每个线程只能访问自己的工作内存,在线程之间不具有可见性。
为了解决线程之间某些数据的可见性,提供了Volatile关键字,工作内存中被Volatile修饰的变量在被修改时,会将这个变量的最新值同步到主内存中,同时其他线程中该变量的副本也会失效,从而需要重新从主内存中加载该变量的值,达到数据在线程之间可见。
Volatile不具备原子性
对于共享变量的复合操作,Volatile并不能保证整个操作的原子性和安全性。如果想要保证原子性,则需要Java并发包下的原子类来实现。
Volatile禁止指令重排
在CPU执行指令时,会将没有依赖关系的指令重新调整执行位置,以提高CPU执行效率。指令重排在单线程中可以完美执行,但在多线程中,可能会因为指令顺序的调整导致整个代码执行出错,所以Volatile禁止指令chong'pai
标签:调用,Thread,线程,内核,相关,执行,CPU From: https://www.cnblogs.com/dev-lurun/p/17280263.html