这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」
时间片轮转机制(RR调度)
时间片轮转法(Round-Robin,RR)主要用于分时系统中的进程调度。为了实现轮转调度,系统把所有就绪进程按先入先出的原则排成一个队列。新来的进程加到就绪队列末尾。每当执行进程调度时,进程调度程序总是选出就绪队列的队首进程,让它在CPU上运行一个时间片的时间。时间片是一个小的时间单位,通常为10~100ms数量级。当进程用完分给它的时间片后,系统的计时器发出时钟中断,调度程序便停止该进程的运行,把它放入就绪队列的末尾;然后,把CPU分给就绪队列的队首进程,同样也让它运行一个时间片,如此往复。
进程调度
进程调度程序总是选择就绪队列中的第一个进程,也就是说按照先来先服务原则调度,但一旦进程占用处理机则仅使用一个时间片。在使用先一个时间片后,进程还没有完成其运行,它必须释放出处理机给下一个就绪的进程,而被抢占的进程返回到就绪队列的末尾重新排队等待再次运行。
处理器同一个时间只能处理一个任务。处理器在处理多任务的时候,就要看请求的时间顺序,如果时间一致,就要进行预测。挑到一个任务后,需要若干步骤才能做完,这些步骤中有些需要处理器参与,有些不需要(如磁盘控制器的存储过程)。不需要处理器处理的时候,这部分时间就要分配给其他的进程。原来的进程就要处于等待的时间段上。经过周密分配时间,宏观上就象是多个任务一起运行一样,但微观上是有先后的,就是时间片轮换
实现原理
时间片轮转算法的基本思想是,系统将所有的就绪进程按先来先服务算法的原则,排成一个队列,每次调度时,系统把处理机分配给队列首进程,并让其执行一个时间片。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序根据这个请求停止该进程的运行,将它送到就绪队列的末尾,再把处理机分给就绪队列中新的队列首进程,同时让它也执行一个时间片
实现多线程的几种方式
- 通过继承thread类来实现
public static void main(String[] args) {
new MyThread("A").start();
new MyThread("B").start();
}
public class MyThread extends Thread{
private String mThreadName;
public MyThread(String threadName){
mThreadName = threadName;
}
@Override
public void run() {
super.run();
for (int i = 0; i < 10; i++) {
System.out.println(mThreadName+"运行,i="+i);
try {
sleep((long) (Math.random()*10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 实现runnable接口来实现
public static void main(String[] args) {
new Thread(new MyRunnable("A")).start();
new Thread(new MyRunnable("B")).start();
}
public class MyRunnable implements Runnable{
private String mThreadName;
public MyRunnable(String threadName){
mThreadName = threadName;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(mThreadName+"运行,i="+i);
try {
sleep((long) (Math.random()*10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 使用callable + FutureTask
CallableTask worker = new CallableTask();
FutureTask<Integer> task = new FutureTask<Integer>(worker);
new Thread(task).start();
public class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int hours = 5;
int amount = 0;
while(hours>0){
System.out.println(" i am working ,rest is "+ hours);
amount++;
hours--;
Thread.sleep(1000);
}
return amount;
}
}
实现 Runnable 接口比继承 Thread 类所具有的优势:
- 可以避免java中的单继承的限制
- 线程池只能放入实现 Runable 或 callable 类线程,不能直接放入继承 Thread 的类
在 java 中,每次程序运行至少启动两个线程, 一个是 main 线程, 一个是垃圾回收线程。因为每当使用 java 命令执行一个类的时候,实际上都会启动一个 JVM ,每一个 JVM 实际就是在操作系统中启动了一个进程
线程的几种状态
-
新建状态(New) :新创建了一个线程对象
-
就绪状态(Runnable) :线程对象创建后,其他线程调用了该对象的 start() 方法,该状态的线程位于可运行的线程池中,变为可运行状态,这个时候,只要获取了 cpu 的执行权,就可以运行,进入运行状态。
-
运行状态(Running) : 就绪状态的线程从 cpu 获得了执行权之后,便可进入此状态,执行 run() 方法里面的代码。
-
阻塞状态(Blocked) :阻塞状态是线程因为某种原因失去了 cpu 的使用权,暂时停止运行,一直等到线程进入就绪状态,才有机会转到运行状态,阻塞一般分为下面三种:
- 等待阻塞 :运行的线程执行了 wait() 方法, JVM 会把该线程放入线程等待池中,(wait() 会释放持有的锁 )
- 同步阻塞:运行的线程在获取对象的同步锁时,如果该同步锁被其他线程占用,这时此线程是无法运行的,那么 JVM 就会把该线程放入锁池中,导致阻塞
- 其他阻塞:运行的线程执行 sleep() 或者 join() 方法,或者发出了 I/O 请求,JVM 会把该线程置为阻塞状态,当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程会重新进入就绪状态,(注意:sleep() 是不会释放本身持有的锁的)
-
死亡状态(Dead) :线程执行完了之后或者因为程序异常退出了 run() 方法,结束该线程的生命周期。
Thread的start()和run()的区别
调用 start() 方法
当线程创建完毕以后,线程处于初始状态,如果调用线程的start()
方法,java
虚拟机会调用该线程的 run
方法,从而使线程进入可运行状态,等待 cpu
分配时间片以后得到执行,线程进入运行状态,如果线程得到执行,那么run()
方法会在子线程中运行,运行完毕以后,线程进入结束状态。
调用 run() 方法
但是如果直接调用run()
方法,只是单纯的执行线程中的run()
方法,而不会使线程进入可执行状态,run
方法里面的代码还是运行在主线程的。
sleep(),yield()和wait()的区别
sleep()在指定的毫秒数内让正在执行的线程休眠(暂停执行)
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
wait() 方法 暂停线程,释放 cpu 控制权,同时释放对象锁的控制
- wait()方法是Object类里的方法;
- 当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;
- wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
- wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。
yield() 方法 暂停当前正在执行的线程对象,并执行其他线程
yield() 应该做的是让当前运行线程回到可运行状态(就绪状态),以允许具有相同优先级的其他线程获得运行机会。因此,使用 yield() 的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证 yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
yield() 只是给相同优先级的线程让出 cpu 资源,如果没有相同优先级的线程,那么它还是会得到执行
sleep 和 wait 的区别
- wait只能在同步(synchronize)环境中被调用,而sleep不需要。
- 进入wait状态的线程能够被notify和notifyAll线程唤醒,但是进入sleeping状态的线程不能被notify方法唤醒。
- wait通常有条件地执行,线程会一直处于wait状态,直到某个条件变为真。但是sleep仅仅让你的线程进入睡眠状态。
- wait方法在进入wait状态的时候会释放对象的锁,但是sleep方法不会。
- wait方法是针对一个被同步代码块加锁的对象,而sleep是针对一个线程。
线程安全需要保证几个基本特征
- 原子性:相关操作不会中途被其他线程干扰,一般通过同步机制实现
- 可见性:一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被理解为将线程本地状态反应到主内存上,volatile就是负责保证可见性的。
- 有序性:保证线程内串行语义,避免指令重排等。
volatile关键字
volatile修饰的变量具有可见性、有序性,不能保证原子性。
由于java的内存模型的原因,线程在修改了共享变量后并不会立即把修改同步到内存中,而是会保存到线程的本地缓存中。
volatile关键字修饰的变量在线程修改后会立刻同步到主内存中,使该修改对其他线程可见
Synchronized关键字
- sychronized 是java中的内置锁,可以限制线程对代码块或者方法的访问
- sychronized可以修饰类方法,实例方法,代码块
- 在执行sychronized方法或代码块时,线程需要先获取被修饰对象的锁。一次只能有一个线程可以获取到一个对象的锁,同一个线程可以多次获取同一个对象的锁(可重入锁)
- sychronized 不能响应中断,当一个线程在等待锁的时候,调用该线程的interrupt是不起作用的
- 锁的获取和释放是隐式的,进入同步sychronized blocks后会获取锁,离开sychronized blocks后会释放锁
Obejct类的wait/notify方法
- wait/notify是用于线程同步的方法
- wait方法会使得当前线程放弃调用对象的监控,并使当前线程进入等待。直到调用了该对象的notify方法或者notifyAll方法(语法上是这样设计,但存在例外)
- 可以多次调用对象的wait方法,notify方法只会随机释放一个wait方法等待,与调用顺序无关。如果要释放所有的wait调用可以调用notifyAll方法
- 调用wait的线程有可能会存在interrupt,虚假唤醒的情况,导致wait方法返回,但实际并没有调用对象的notify方法。在使用时通常会搭配一个lock flag和loop使用
volatile和synchronized的区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
- synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
停止线程的方法
- 使用volatile关键字修饰变量的方法终止
public static void main(String[] args) throws Exception{
KeyWordStopThread task = new KeyWordStopThread("arrom");
new Thread(task).start();
Thread.sleep(3000);
task.isExit = true;
}
public class KeyWordStopThread implements Runnable{
private String name;
public volatile boolean isExit = false;//退出的标记
public KeyWordStopThread(String name){
this.name = name;
}
@Override
public void run() {
while (!isExit){
try {
Thread.sleep(1000);
System.out.println("thread:"+name+" runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程终止");
}
}
-
使用interrupt方式终止
-
使用Stop方法终止
可以直接使用thread.stop()来强行终止线程,但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用stop方法来终止线程。