java - 多线程

155 阅读11分钟

这是我参与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 实际就是在操作系统中启动了一个进程

线程的几种状态

image.png

  • 新建状态(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关键字

image.png 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方法来终止线程。