java 多线程

使用 java 多线程

1. 继承 Thread

  • 需要自定义线程类,继承自 java.lang.Thread
  • 线程中执行的代码通过重载的方式放在 public void run()方法中
  • main() 线程中实例化线程对象后得到一个线程
  • 调用 start()方法唤醒线程
  • 此后这个线程与主线程并行,进程中就有了至少 2 个线程
public class ThreadTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

class MyThread extends Thread {
    //线程自己的数据域
    private int field1;
    private int field2;
    @Override
    //线程中执行的代码
    public void run() {
        System.out.println("my thread running");
        int tag = 1;
      // enhanced switch with return value
        String s =
                switch (tag) {
                    case 1 -> "111";
                    case 2 -> "222";
                    default -> "else";
                };
      //
    }
}

2. 实现 Runnable 接口

  • 使用实现了 Runnable 接口的类的实例对象作为构造线程的参数来使用多线程
  • Runnable
class PrimeRun implements Runnable {
  long minPrime;
  PrimeRun(long minPrime) {
    this.minPrime = minPrime;
  }

  public void run() {
    // compute primes larger than minPrime
    // ...
  }
}

// The following code would then create a thread and start it running:
PrimeRun p = new PrimeRun(143);  //实例化一个实现了 Runnable 接口的类的对象
new Thread(p).start(); // 以其作为参数构造一个线程对象

3. 实现 Callable 接口

  • 接口 Callable(java.util.concurrent.Callable)

    1. 是一个函数式接口,这个接口的实现类表示一项可能有返回值或抛出异常的任务。实现此接口需要重写call()方法,任务内容写在 call()方法中
    2. 在接口带有泛型参数时,call()的返回值类型与泛型参数相同
    3. 接口忽略泛型参数时,返回Object类的对象
  • FutureTask(java.util.concurrent.FutureTask)

    1. 表示一项可以取消的异步计算,即一个单独的计算任务
    2. 提供了开始计算、取消计算、查询计算是否完成和查询计算结果的方法
      • run()
      • cancle()
      • get()
      • isDone()
    3. 如果查询计算结果时计算还未完成,查询将阻塞直至计算完成
    4. 计算完成后,将不能再次开始或取消
    5. 带有泛型参数,用于描述计算结果的类型
  • 创造线程的步骤

    1. 创造一个 Callable 接口的实现类
    2. 重写 call()方法
    3. 创造一个 Callable实现类的对象,将其作为参数创造一个 FutureTask类的对象
    4. FutureTask类的对象作为参数创造一个线程

    Callable --> FutureTask --> Thread

  • 注意

    1. FutureTask对象如果先调用了run(),那么将此对象作为参数创造一个线程,在调用线程的start()后,线程也不会执行,除非
  • 实例

public class TestCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // Callable
        Task1 task1 = new Task1();
        // FutureTask
        FutureTask<String> futureTask=new FutureTask<>(task1); // 计算结果的类型由泛型参数 String 指定
        // Thread
        Thread thread = new Thread(futureTask);
        thread.start();
    }
}
class Task1 implements Callable<String>
{
    @Override
    public String call() throws Exception {  //返回类型由接口的泛型参数指定
        System.out.println("Callable task executed");
        String msg = "task return value";
        System.out.println(Thread.activeCount());
        return msg;
    }
}

4. 线程池

线程池使用线程来执行给定的任务,一个线程可以执行多个任务,实现了对线程资源的重复利用,而非执行完一个任务就回收线程资源。能有效提高执行多个短期异步任务的性能。无返回值的任务是实现了 Runnable 接口的类的对象,有返回值的任务是实现了 Callable 接口的类的对象,参见 submit 方法。

线程池的创建

  • 使用工具类 Executors 来创建线程池,返回的是 ExecutorService接口类型

    1. Executors.newCachedThreadPool();

      返回一个自适应的线程池。这个线程池可以按需要增加线程的数量,当有可用的现成的之前创建的线程时会重用现成的线程资源来执行新的线程任务。由于超过 60s 没有被使用的线程将会被终止并回收资源,所以一个长期空闲的此类线程池将不会消耗资源。使用这种类型的线程池可以明显地提升需要执行许多短期异步任务的程序的性能。

    2. Executors.newFixedThreadPool(int nThreads);

      返回一个重用固定数量线程的线程池。如果当所有线程都处于活跃状态时有任务提交,任务将等待在等待队列中,等待有可用的线程。所有线程将存在到显式调用 shutdown() 时。

    3. Executors.newSingleThreadExecutor();

      只有一个线程,依次执行所有提交的任务。

操作线程池

  • 线程池的操作通过接口 ExecutorService的方法来完成

    1. execute(Runnable command)

      将给定的任务加入调度。

    2. submit(Runnable task) submit(Callable<T> task) submit(Runnable task, T result )

      将指定的任务提交线程池执行,返回一个接口 Future 类型的对象表示这个任务。

      接口 Future 有几个方法:

      • get()

        等待任务执行完成并返回结果

      • cancel()

        取消任务的执行

      • isCanceled()

        检查是否被取消

      • isDone()

        检查任务是否执行结束

      对以上几种 submit,对返回的 Future 类型的对象执行 get() 将会依次得到 null、Callable接口中 call() 方法的返回值、参数result

    3. shutdone()

      关闭线程池。

      正在执行任务的线程将继续执行,没有任务执行的线程将被关闭,还未执行的任务会继续被执行,但不再接受新的任务。直到所有已经提交的任务都执行完,线程池才会退出。

    4. shutdoneNow()

      试图尝试中止正在执行的线程(但是不一定成功,对不响应interrupt()的线程将失败),不再执行并返回还未执行的任务,不再接受新的任务,必须等待所有正在执行的任务执行完了才能退出。

    5. awaitTermination(long timeout, TimeUnit unit)

      等待正在执行的任务执行结束,或参数指定的等待时间用尽,或当前线程被中止。

互斥与线程安全

当多个线程之间有共享的数据时,又是需要保证对共享变量的访问是互斥的,即某些共享变量和操作共享变量的代码块只能同时被一个线程所访问。java 使用临界区代码的方式来处理互斥。具体分为同步代码块、同步方法和互斥锁。

同步代码块

同步代码块要求在进入代码块之前需要先获得互斥锁。这个互斥锁是一个任意对象,但是这个作为互斥锁的对象对各个需要进行同步互斥的线程而言需要唯一。这样在某个线程获得互斥锁进入同步代码块(临界区代码)中执行后,其他线程在被调度执行时,在临界区代码处,会由于得不到互斥锁而被阻塞。

对于继承 Thread 类来创造线程类的方式,每个线程都是一个线程类的对象,它们之间的共享数据是线程类的静态数据域,此时可以使用线程类内的静态对象作为互斥锁,也可以使用线程类对应的Class类的对象作为互斥锁,因为这个对象对所有的线程而言都是唯一的。

class MyThread extends Thread {
    //线程自己的数据域
    private int field1;
    private int field2;
    @Override
    //线程中执行的代码
    public void run() {
        System.out.println("my thread running");
        // synchronized code block
        // up to only one of all thread objects of this class can enter this code block at once
        synchronized (this.getClass())
        {
            // do something need to be synchronized
        }
    }
}

对于实现 Runnable 接口来创造类 C,并以类 C 的一个对象 O 作为 Thread 的参数来构造线程对象的方式,此时使用 O 来构造多个线程。对这些线程而言,O 这个对象是唯一的。由于这些线程中执行的代码都在类 C 的 run 方法之中,所以在需要互斥锁时,可以方便地使用 this 作为互斥锁,而不必重新创建一个符合要求的对象。

class C implements Runnable
{
    int a = 0;
    @Override
  // 递增 a 至 50,使用同步代码块,同一个对象造就的线程对象都对 a 进行递增,但是递增的是同一个 a
    public void run() {
        //synchronized code block
        //up to only one of all thread objects created with "new Thread(O)" can enter this code block at once
        // O is a object of class C
        while(a<50)
        synchronized (this)
        {
            System.out.println(Thread.currentThread().getName());
            System.out.println(a++);
            //do something need to be synchronized
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        var O = new C();
        var thread1 = new Thread(O);
        var thread2 = new Thread(O);
        thread1.start();
        thread2.start();
    }
}

同步方法

把同步代码块拿出来变成一个方法,就可以通过将此方法标记为同步方法来实现同步互斥。使用关键字 synchronized来进行标记,此关键字的位置与 static 关键字相同.

此时使用的互斥锁为隐式的 this。对于继承自 Thread 的子类的方式,使用的是类的对象本身,此时保证互斥的只是这一个对象,没有实际意义。但是可以将同步方法作为静态方法,此时由于方法属于类本身,故此时的 this 为类对应的 Class 对象,保证所有这个类的线程对象互斥。对于实现 Runnable 接口的方式,使用的是创建线程对象时使用的对象,即保证所有使用对象 O 创建的线程互斥。

class C implements Runnable
{
    int a = 0;
    public synchronized void synchronizedMethod()
    {
        System.out.println(Thread.currentThread().getName());
        System.out.println(a++);
    }
    @Override
    public void run() {
        //synchronized code block
        //up to only one of all thread objects created with "new Thread(O)" can enter this code block at once
        // O is a object of class C
        while(a<50)
        synchronizedMethod();
    }
}

使用 ReentrantLock

再入锁 ReentrantLock 是 java 中实现了Lock接口(java.util.concurrent.locks.Lock)的一个类。语义和 synchronized 基本相同,用于控制对线程中运行的代码块的互斥访问。但是ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制。

  • 锁的获取和释放

    再入锁通过对象直接调用 lock() 方法获取,代码书写更加灵活。与此同 时, ,比如可以控制 fairness ,也就是公平性,或者利用定义条件等。但是,编码中也需 要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。

    ReentrantLock reentrantLock = new ReentrantLock();
    try {
      reentrantLock.lock();
      /* do something */
    }finally {
      reentrantLock.unlock();
    }
    
  • “再入”表示当一个线程试图获取一个它已经获取的锁时,这个获取动作就自动成功,也就是锁的持有以线程为单位。

  • 再入锁可以设置公平性( fairness ),我们可在创建再入锁时选择是否是公平的。

    ReentrantLock fairLock = new ReentrantLock(true);

    这里所谓的公平性是指在竞争场景中,当公平性为真时,会倾向于将锁赋予等待时间最久的线程。公平性是减少线程 “ 饥饿 ” (个别线程长期等待锁,但始终无法获取)情况发生的一 个办法。

  • 性能比较

    synchronized 和 ReentrantLock 的性能不能一概而论,早期版本 synchronized 在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优 于 ReentrantLock。

同步与线程通信

某些情况下,一个线程中的某些操作必须等另外的线程的某些操作完成后才能进行,这时需要进行线程间的同步。同步也通过互斥锁的原理来进行,完成同步使用的同步方法有:

  • wait()

    立即释放 synchronized 的同步监视器并阻塞在同步监视器的等待队列中直至被唤醒,然后进行线程调度。

  • ``notify(),notifyAll()`

    唤醒 synchronized 的同步监视器的等待队列中的一个或所有线程,使得这些线程能够参与调度。然后继续本线程的执行。本线程的执行状态不会因为调用 notify()notifyAll()而发生改变

值得注意的是,这些同步方法都需要放在 synchronized 的代码块或方法中

线程方法

  • start()

    加载并启动线程,调用本地平台的 start

  • public void run()

    线程要执行的代码,需要重写来使线程具有特定的功能

  • currentThread()

    获取进程中当前正在执行的线程,是 Thread 类的静态方法

  • getName()

    获取线程的名字

  • setName("MyThread")

    设置线程的名字,默认为 Thread-0、Thread-1…

  • yield()

    主动放弃本次CPU使用权,重新进行线程调度,调用本地平台的 yield

  • join()

    在线程A的执行代码中调用线程B的join方法后,线程A将被阻塞,直至线程B执行完后才会继续参与调度

  • sleep(long millitime)

    当前正在执行的线程立即放弃CPU使用权并在接下来的一段时间(单位ms)内不参与调度。是 Thread 类的静态方法,原因是执行到当前线程代码中的 sleep 时 CPU 一定在执行当前线程,不需要也不能指明睡眠哪一个线程。调用本地平台的 sleep

  • isAlive()

    判断线程是否还存活。

线程调度

线程的优先级等级

  • MAX_PRIORITY 10
  • MIN_PRIORITY 1
  • NORM_PRIORITY5

调度相关的方法

  • getPriority()

    获取线程的优先级

  • setPriority(int )

    设置线程的优先级

java 的线程调度策略

  • 早期的JVM线程调度策略

    1. JVM使用抢占的、基于优先权的调度策略;
    2. 每个线程都有优先级,JVM总是选择最高优先级的线程执行;
    3. 若果两个线程具有相同的优先级,则采用FIFO的调度顺序。

    在早期的java1.1中,JVM自己实现线程调度,而不依赖于底层的平台。绿色线程(用户级线程是JVM使用的唯一的线程模型(至少是在solaris平台上),其线程调度采用的应该就是上述这种调度策略。

    绿色线程的执行时间由线程本身来控制,线程自身工作告一段落后,要主动告知系统切换到另一个线程上。其特点是实现简单,不需要专门的硬件支持,切换操作对线程自身来说是预先可知的。

    因为绿色线程库是用户级的,并且Solaris一次只能处理一个绿色线程,即Java运行时采用多对一的线程模型,所以会导致如下问题:(1)Java应用程序不能与Solaris环境中的多线程技术互操作,就是说Solaris管理不了Java线程;(2)Java线程不能在多处理机上并行执行;(3)Java 应用不能享用操作系统提供的并发性。由于绿色线程的这些限制,在java1.2之后的版本中放弃了该模型,而采用本地线程(Native threads,是指使用操作系统本地的线程库建立和管理的线程),即将Java线程连接到本地线程上,主要由底层平台实现线程的调度[3]。

  • 依托底层平台的Java线程调度策略

    Java语言规范和Java虚拟机规范是Java的重要文档,可惜的是他们都没有说明Java线程的调度问题。或许从Java的角度看,线程并不是Java最基本的内容。毕竟Thread类也仅仅是Java一个特定的类而已。

    终于在Java SE 8 API规范的Thread类说明中算是找到了线程调度的有关描述:每个线程有一个优先级(从1级到10级),较高优先级的线程比低优先级线程先执行。程序员可以通过Thread.setPriority(int)设置线程的优先级,默认的优先级是NORM_PRIORITY。Java SE 还声明JVM可以任何方式实现线程的优先级,甚至忽略它的存在。Java将线程调度交给底层的操作系统去做。通常而言,线程的优先级设置得越高,线程被调度执行的机会就越大,而这个机会可能是一次调度执行的时间片更大(Linus平台)或(和)被调度执行的机会(次数)更多。但是**只是机会更大,不能保证一定被更多地执行或高优先级的线程在一定在低优先级线程之前执行。**线程方法中的sleep、yield等也只是一个建议,具体是否真正睡眠或放弃本次CPU使用权还依赖于具体平台的实现。

Solaris平台上的JVM线程调度策略

先说Solaris本身的线程调度。在第9版之前,Solaris 采用M:N的线程模型,即M个本地线程映射到N个内核级线程(LWP,LWP和内核级线程是一一对应的)上。当本地线程连接到一个LWP上时,它才有机会获得CPU使用权。虽然Solaris提供了改变LWP的优先权的系统调用,但是由于本地线程与LWP的连接是动态的、不固定的。一个本地线程过一会儿可能会连接到另一个LWP上。因而Solaris没有可靠的方法改变本地线程的优先权。

再说Java线程。既然Java底层的运行平台提供了强大的线程管理能力,Java就没有理由再自己进行线程的管理和调度了。于是JVM放弃了绿色线程的实现机制,将每个Java线程一对一映射到Solaris平台上的一个本地线程上,并将线程调度交由本地线程的调度程序。由于Java线程是与本地线程是一对一地绑在一起的,所以改变Java线程的优先权也不会有可靠地运行结果。

img

尽管如此,Solaris早期版本还是尽量实现了基本的用户模式下的抢占。系统维护这一条戒律:就绪队列上任何线程的优先级必须小于等于正在运行的线程,否则,优先级最低的正在运行的线程将被剥夺运行的机会,即将其对应的LWP让给优先级高的本地线程。在如下三种情况下会发生线程的抢占:

(1)当正在运行的本地线程降低了其优先级,使其小于就绪队列中的某个线程的优先级
(2)当正在运行的本地线程增加了就绪队列中某个线程的优先级,使其高于正在运行的线程的优先级
(3)当就绪队列中新加入了一个优先级高于正在运行的线程的优先级,例如,某个高优先级的线程被唤醒。

Java线程的唤醒、优先级设置是由JVM实现的,但线程的调度(与LWP的连接)则是由本地线程库完成。操作系统(Solaris)可以依据自己的原则改变LWP的优先级,例如,通过动态优先级实现分时,这是线程库和JVM都无法干预的。

Solaris 9之后,使用了1:1的线程模型,即本地线程与LWP一对一地绑在一起,本地线程库也失去了直接干预线程调度的机会(指为本地线程选择连接LWP)。Java线程也就通过本地线程与LWP终生地一对一地绑在一起。这样可以通过改变本地线程或Java线程的优先级来影响LWP的优先级,从而影响系统的CPU调度。但具体的CPU分配策略还是Solaris做出的,JVM仅起辅助的作用。

Windows平台上的Java线程调度策略

在Windows下,Java线程一对一地绑定到Win32线程(相当于Solaris的native线程)上。当然Win32线程也是一对一地绑定到内核级线程上,所以Java线程的调度实际上是内核完成的。Java虚拟机可以做的是通过将Java线程的优先级映射到Win32线程的优先级上,从而影响系统的线程调度决策。

Windows内核使用了32级优先权模式来决定线程的调度顺序。优先权被分为两类:可变类优先权包含了1-15级,不可变类优先权(实时类)包含了16-31级。调度程序为每一个优先级建一个调度队列,从高优先级到低优先级队列逐个查找,直到找到一个可运行的线程。

Win32将进程(process)分为如下6个优先级类:

  • REALTIME_PRIORITY_CLASS
  • HIGH_PRIORITY_CLASS
  • ABOVE_NORMAL_PRIORITY_CLASS
  • NORMAL_PRIORITY_CLASS
  • BELOW_NORMAL_PRIORITY_CLASS
  • IDLE_PRIORITY_CLASS

为区分进程内线程的优先级,每个优先级类又包含6个相对优先级:

  • TIME_CRITIAL
  • HEGHEST
  • ABOVE_NORNAL
  • NORMAL
  • BELOW_NORMAL
  • LOWEST
  • IDLE
    这样每个Win32线程属于某个优先级类(由该线程所属的进程决定),并具有进程内的某个相对优先级,其对应的内核级线程的优先级如下表所示:

img

当把Java 线程绑定到Win32线程时,需要将Java线程的优先级映射到Win32线程上。Java 6在Windows的实现中将Java线程的优先级按下表所示映射到Win32线程的相对优先级上。

img

当JVM将线程的优先级映射到Win32线程的优先级上之后,线程调度的工作就是Win32和Windows内核的事儿了。

Windows采用基于优先级的、抢占的线程调度算法。调度程序保证总是让具有最高优先级的线程运行。一个线程仅在如下四种情况下才会放弃CPU:(1)被一个更高优先级的线程抢占;(2)结束;(3)时间片到;(4)执行导致阻塞的系统调用。当线程的时间片用完后,降低其优先级;当线程从阻塞变为就绪时,增加线程的优先级;当线程很长时间没有机会运行时,系统也会提升线程的优先级。Windows区分前台和后台进程,前台进程往往获得更长的时间片。以上这些措施体现了Windows基于动态优先级、分时和抢占的CPU调度策略。调度策略很复杂,考虑了线程执行过程的各个方面,再加上系统运行环境的变化,我们很难通过线程运行过程的观察理清调度算法的全貌。在本文开头的例子说明了这一点。

由于Java线程到Windows内核线程一对一的绑定方式,所以我们看到的Java线程的运行过程实际上反映的是Windows的调度策略。

请注意,尽管Windows采用了基于优先级的调度策略,但不会出现饥饿现象。其采取的主要措施是:优先级再高的的线程也会在运行一个时间片之后放弃CPU,并且降低其优先级,从而保证了低优先级线程也有机会运行。

Linux中Java线程调度

同Windows一样,在Linux上Java线程一对一地映射到内核级线程上。不过Linux中是不区分进程和线程的,同一个进程中的线程可以看作是共享程度较高的一组进程。Linux也是通过优先级来实现CPU分配的,应用程序可以通过调整nice值(谦让值)来设置进程的优先级。nice值反映了线程的谦让程度,该值越高说明这个线程越有意愿把CPU让给别的线程,nice的值可以由线程自己设定。所以JVM需要实现Java线程的优先级到nice的映射,即从区间[1,10]到[19, -20]的映射。把自己线程的nice值设置高了,说明你的人品很谦让,当然使用CPU的机会就会少一点。

linux调度器实现了一个抢占的、基于优先级的调度算法,支持两种类型的进程的调度:实时进程的优先级范围为[0,99],普通进程的优先级范围为[100,140]。

img

进程的优先权越高,所获得的时间片就越大。每个就绪进程都有一个时间片。内核将就绪进程分为活动的(active)和过期的(expired)两类:只要进程的时间片没有耗尽,就一直有资格运行,称为活动的;当进程的时间片耗尽后,就没有资格运行了,称为过期的。调度程序总是在活动的进程中选择优先级最高的进程执行,直到所有的活动进程都耗尽了他们的时间片。当所有的活动进程都变成过期的之后,调度程序再将所有过期的进程置为活动的,并为他们分配相应的时间片,重新进行新一轮的调度。所以Linux的线程调度也不会出现饥饿现象。

在Linux上,同Windows的情况类似,Java线程的调度最终转化为了操作系统中的进程调度。

总结

从以上Java在不同平台上的实现来看,只有在底层平台不支持线程时,JVM才会自己实现线程的管理和调度,此时Java线程以绿色线程的方式运行。由于目前流行的操作系统都支持线程,所以JVM就没必要管线程调度的事情了。应用程序通过setPriority()方法设置的线程优先级,将映射到内核级线程的优先级,影响内核的线程调度。

目前的Java的官方文档中几乎不再介绍有关Java线程的调度算法问题,因为这确实不是Java的事儿了。尽管程序中还可以调用setPriority(),提请JVM注意线程的优先级,但你千万不要把这事儿太当真。Java中所谓的线程调度仅是底层平台线程调度的一个影子而已。

由于Java是跨平台的,因此要求Java的程序设计不能对Java线程的调度方法有任何假设,即程序运行的正确性不能依赖于线程调度的方法。所以说程序员最好不要过分关心底层平台是如何实现线程调度的,呵呵!只要知道他们是并发运行的就可以了,甚至不必在意线程的优先级,因为优先级也不靠谱。正如Joshua Bloch在他的书《Effective Java》中给出的第72条忠告:任何依赖线程调度器来达到正确性或性能要求的程序,很有可能都是不可移植的[5]。当然,世界上没有绝对的事情。

如果程序员一定要规范线程的执行顺序,应该使用线程的同步操作wait(), notify()等显式实现线程之间的同步关系,才能保证程序的正确性。

java 的线程调度策略部分的内容转自
https://www.jianshu.com/p/3f6b26ee51ce

有部分删改

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值