前言- CPU竞争策略
操作系统中,CPU竞争有很多种策略。Unix系统使用的是时间片算法,而Windows则属于抢占式的。
在时间片算法中,所有的进程排成一个队列。操作系统按照他们的顺序,给每个进程分配一段时间,即该进程允许运行的时间。如果在 时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程 序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。
抢占式操作系统,就是说如果一个进程得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU 。因此可以看出,在抢 占式操作系统中,操作系统假设所有的进程都是“人品很好”的,会主动退出 CPU 。
在抢占式操作系统中,假设有若干进程,操作系统会根据他们的优先级、饥饿时间(已经多长时间没有使用过 CPU 了),给他们算出一 个总的优先级来。操作系统就会把 CPU 交给总优先级最高的这个进程。当进程执行完毕或者自己主动挂起后,操作系统就会重新计算一 次所有进程的总优先级,然后再挑一个优先级最高的把 CPU 控制权交给他。
1-线程的优先级
在java线程中,可以在构造线程时通过setPriority()方法设定线程的优先级,优先级为从1-10的整数(默认为5),优先级越高系统分配的时间就越多;这里有一个设置优先级的一个常用经验知识:对于频繁阻塞的线程(经常休眠,IO操作等)需要设置较高的优先级,因为这些经常阻塞的线程即使设置为较高的优先级,但是在大部分时间里,处于阻塞状态,会让出CPU;而对于偏重计算的(将会长时间独占CPU)线程设置为较低的优先级,防止其他线程的不会长时间得不到执行。
Thread thread = new Thread(job);
thread.setPriority(10);
thread.start();
注意:但是在很多系统下面对线程优先级的设置可能无效(如类unix的分时系统)
2-线程的状态
给定一个时刻,线程只能处于6种状态其中的一种状态
- NEW:初始状态,线程被构建,但是还没有调用start()方法。
- RUNNABLE:运行状态,java线程将操作系统中的就绪状态和运行两种状态笼统的称作运行中。
- BLOCKED:阻塞状态,特指线程阻塞于锁(synchronized关键字修饰的方法或者方法块),并将该线程加入同步队列中。
- WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的动作(比如通知或者中断),并将该线程加入等待队列中。需要注意的是调用LockSupport.park()方法和Thread.join()会使得线程进入这个状态,而不是阻塞状态。
- TIME_WAITING:超时等待状态,该状态是WAITING状态的超时版本,它可以在指定的时间自行返回。一般由带有超时设置的方法调用引起。
- TERMINATED:终止状态,表示当前线程已经执行完毕。
注意上图 Object.join()有误,应改成Thread.join()
线程start()和run()方法的区别
- thread.start()方法
调用此方法将会由操作系统任务调度器在新创建的线程中执行run()方法,可能不会立刻执行,由任务调度器调度,但是一定是在新创建的线程中执行。重复调用start方法将抛出异常IllegalThreadStateException
- thread.run()方法
Thread实现了Runnable接口,默认实现是调用target的run方法,调用此方法并不会再新创建的线程去执行run方法,只会在调用Thread.run()方法的线程本地执行,和调用一个普通对象的一个方法效果一样,可以被重复调用。
线程方法
- Thread.sleep(long n)静态方法
- 当n = 0 时,thread 线程主动放弃自己CPU控制权,进入就绪状态。这种情况下只能调度优先级相等或者更高的线程,低优先级的线程很有能永远得不到执行,当没有符合条件的线程时,当前会一直占用CPU,造成CPU满载。
- 当n > 0 时,Thread线程将会被强制放弃CPU控制权,并睡眠n毫秒,进入阻塞状态。这种情况下所有其他任意优先级就绪的线程都有机会竞争CPU控制权。无论有没有符合的线程,都会放弃CPU控制权-,因此CPU占用率较低。
- 上述1、2是从线程调度的角度分析的,无论1、2,都不会释放对象的锁,也就是说如果有synchronized方法块,其他线程仍然不能访问共享数据,该方法抛出中断异常。
- thread.join()
使得调用thread.join()语句的线程等待thread线程的执行完毕,才从这个语句返回,并继续这个线程,该方法也需要捕获中断异常。这个方法含有超时重载版本
- Thread.yield()静态方法
将thread线程放入就绪队列中,而不是同步队列,由操作系统去调度。如果没有找到其他就绪的线程,则当前线程继续运行,比thread.sleep(0)速度快,只能让相同优先级的线程得以运行。
重点分析join方法的实现
如何实现join方法的语义?
- ** 方法内部调用Object.wait()方法进行等待。**
- 当线程终止时,会调用线程自身的notifyAll()方法,通知所有等待在该线程对象监视器上的线程。
属于经典的等待/通知模式
例子
解析:假设有两个线程A、B,在B中调用方法A.join,由于join是同步方法,线程B排他获取方法所属的对象监视器锁,即线程对象A的监视器锁;线程B获取线程A的对象监视器锁成功后,在join方法内部,调用的是this.wait()方法,即在线程B在线程A对象上等待并释放线程A上的对象监视器锁。
方法内部有两个循环判断:
- join(0):Object.wait(0),在第一个while循环里始终对线程A是否终止进行判断,如果还在运行,则使线程B等待,直到被通知或者中断,当被唤醒时还得去判断线程A是否终止,如果终止则在获取监视器锁后从join方法返回继续代码,否则继续等待。
- join(millis > 0) : Object.wait(millis)分析方法和上面基本一样,只不过加了超时返回,即从wait方法返回时判断是否超时,如果超时则在获取对象锁后跳出循环,从join方法返回继续执行。
对象方法
object.wait(),object.notify(),object.notifyAll()
- 这3个方法在使用之前都要获取object对象的锁,即在
synchronized(object){ object.wait();}
- 调用wait()方法后,线程状态将由running变为waiting,并将当前线程放置到等待队列中,并释放object上的锁。
- notify() 和notifyAll()方法调用后,等待线程依旧不会从wait方法返回,而是将等待队列的一个或全部的线程移动到同步队列中,被移动的线程状态变为blocked,然后通知线程从同步方法块返回,并释放object上锁,只有下一次锁的竞争中,等待线程成功获取到object上的锁,才从wait方法返回。
3-线程的创建
提供了三个方法来创建Thread
- 继承Thread类来创建线程类,重写run()方法作为线程执行体。
缺点:
线程类继承了Thread类,无法在继承其他父类。
因为每条线程都是一个Thread子类的实例,因此多个线程之间共享数据比较麻烦。
- 用实现了Runnable接口的对象作为target来创建线程对象。
推荐,用来将没有返回值和不会抛出异常的方法体run()传递给线程去执行
- 用实现了Callable接口的对象作为task提交给线程池ExecutorService 通过submit方法来提交执行
推荐,用来将有返回值和会抛出异常的方法体run()传递给线程去执行
4-线程中断
中断是一种线程之间通信机制,但是中断不像其名字一样会让线程中断,而是线程通过循环判断查看中断标志位,来及时的查看中断状态并采取下一步的操作。
- 其他线程通过该线程的interrupt()方法对其进行中断操作。
- 线程通过调用自身的isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()将清除当前线程的中断标志并返回之前线程的中断状态;如果该线程已经处于终结状态,无论是否中断,则调用该对象的isInterrupted()都将返回false。
- 抛出InterruptedException异常的方法,比如Thread.sleep(),这些方法在抛出异常之前,Java虚拟机会先将该线程的中断标志位清除,然后再抛出InterruptedException异常,这时在调用isInterrupted()方法进行判断将返回false。
5-等待/通知的经典线程间通信范式
- 等待方遵循如下原则
- 获取对象的锁。
- 如果条件不满足,那么调用对象的wait()方法,被通知后还要检查条件。
- 条件满足则执行对应的逻辑 。
synchronized(object对象){
while(条件不满足){
object.wait();
}
对应的处理逻辑;
}
- 通知方遵循如下原则
- 获取对象的锁。
- 改变条件
- 通知所有等待在对象上的线程
synchronized(object对象){
改变条件
object.notifyAll();
}
6-ThreadLocal
线程本地变量,是一个以TreadLocal变量为键,任意对象为值的存储结构,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本,ThreadLocal将变量的范围限制在一个线程的上下文当中,使得变量的作用域为线程级别。
- ThreadLocal仅仅是个变量访问的入口;
- 每一个Thread对象都有一个ThreadLocalMap对象,这个ThreadLocalMap持有所有已经初始化的ThreadLocal值对象的引用;
- 只有在线程中调用ThreadLocal的set(),或者get()方法时都会在当前线程中绑定这个变量,否则不会绑定。第一次get()方法调用将会进行初始化(如果set方法没有调用过),而且初始化每个线程值进行一次。
- 初始化方法
允许对默认初始化方法进行重写
// 默认初始化方法
protected T initialValue(){
return null;
}
ThreadLocal源码分析
- set()
// ThreadLocal.java
public void set(T value) {
//1.首先获取当前线程对象
Thread t = Thread.currentThread();
//2.获取该线程对象的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map不为空,执行set操作,以当前threadLocal对象为key
//实际存储对象为value进行set操作
if (map != null)
map.set(this, value);
//如果map为空,则为该线程创建ThreadLocalMap
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
//线程对象持有ThreadLocalMap的引用
return t.threadLocals;
}
// Thread.java
ThreadLocal.ThreadLocalMap threadLocals = null;
- get()
public T get() {
//1.首先获取当前线程
Thread t = Thread.currentThread();
//2.获取线程的map对象
ThreadLocalMap map = getMap(t);
//3.如果map不为空,以threadlocal实例为key获取到对应Entry,然后从Entry中取出对象即可。
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
//如果map为空,也就是第一次没有调用set直接get
//(或者调用过set,又调用了remove)时,为其设定初始值
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();//获取初始值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
场景一:为每一个线程分配一个递增无重复的ID
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadLocalDemo {
public static void main(String []args){
for(int i=0;i<5;i++){
final Thread t = new Thread(){
@Override
public void run(){
System.out.println("当前线程:"+Thread.currentThread().getName()
+",已分配ID:"+ThreadId.get());
}
};
t.start();
}
}
static class ThreadId{
//一个递增的序列,使用AtomicInger原子变量保证线程安全
private static final AtomicInteger nextId = new AtomicInteger(0);
//线程本地变量,为每个线程关联一个唯一的序号
private static final ThreadLocal threadId =
new ThreadLocal() {
@Override
protected Integer initialValue() {
//相当于nextId++,由于nextId++这种操作是个复合操作而非原子操作,
//会有线程安全问题(可能在初始化时就获取到相同的ID,所以使用原子变量
return nextId.getAndIncrement();
}
};
//返回当前线程的唯一的序列,如果第一次get,会先调用initialValue,后面看源码就了解了
public static int get() {
return threadId.get();
}
}
}
说明:ThreadID是线程共享的,所以需要原子类来保证线程访问的安全性,而ThreadID的成员变量threadId是线程封闭的,只是线程本地变量初始化时需要访问原子类(多个线程同时访问引起 )
场景二:web开发中,为每一个连接创建一个ThreadLocal保存session信息,如果web服务器使用线程池技术(比如Tomcat)进行线程复用,则每一次连接都要重新的set,以保证session为本次连接的信息。当session结束,调用remove方法,将线程本地变量从线程的ThreadLocalMap中移除。
7-等待超时
主要学习的是剩余时间的计算
等待超时模式的经典模式
// 同步方法
public synchronized Object get(long millss) throws InterruptedException {
// 获取将来时间点
long future = System.currentTimeMillis)() + millis;
// 初始化剩余时间为millis,从这可以看出超时等待时间并不是十分的严格
long remaining = millis;
// 超时等待判断,当返回值的结果不满足并且剩余时间小于0时,从循环退出
while((result == null) && remaining > 0){
wait(remaining);
remaining = future - System.currentTimeMillis();
}
return result;
}
参考链接
https://hacpai.com/article/1488015279637