多线程

53 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情

概念

  • 程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
  • 进程(process):是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程,具有生命周期。
  • 线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。

  • 并行:多个CPU同时执行多个任务。
  • 并发:一个CPU (采用时间片) “同时” 执行多个任务。

多线程是并发还是并行?

在CPU资源比较充足的时候,一个进程内的多线程,可以被分配到不同的CPU资源,这是并行;

在CPU比较繁忙,资源不足的时候,操作系统只为一个含有多线程的进程分配一个CPU资源,这是并发。

所以并行和并发都有可能,取决于操作系统分配的CPU。

线程的调度

同优先级线程使用时间片策略(简单来说就是执行a一会会,再执行b一会会)。


对高优先级,使用优先调度的抢占式策略:

线程创建时继承父线程的优先级。低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用。

  • getPriority() :返回线程优先值
  • setPriority(int newPriority) :改变线程的优先级

线程的创建

方式一:继承Thread类

  1. 定义子类继承Thread类。
  2. 子类中重写Thread类中的run()方法。
  3. 创建子类对象,即创建了线程对象。
  4. 调用线程对象start()方法。

方式二:实现Runnable接口

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run()方法。
  3. 把子类对象作为参数传入给Thread类的构造器中。
  4. 调用Thread类的start()方法。

相对于方式一的好处:

  • 避免了单继承的局限性。
  • 多个线程可以共享同一个接口实现类的对象,适合多个程序处理同一份数据。

方式三:实现Callable接口

  1. 定义子类实现Callable接口。

  2. 子类中重写call()方法,可以有返回值。

  3. 把子类对象作为参数传给FutureTask的构造器中。

  4. 将FutureTask对象作为参数传入给Thread类的构造器中。

    • FutrueTask是Futrue接口的唯一的实现类
    • FutureTask同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
  5. 调用Thread类的start()方法。

  6. 通过FutureTask对象的get()方法获取call()的返回值

相对于方式二的好处:

  • call()可以有返回值,可以通过FutureTask类获取返回结果,支持泛型。
  • call()可以抛出异常。

方式四:线程池

提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。

JDK 5.0起提供了线程池相关API:ExecutorServiceExecutors

  • ExecutorService:线程池接口。常见子类ThreadPoolExecutor。

    • void execute(Runnable command) :执行任务/命令,没有返回值,用来执行Runnable。
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,用来执行Callable。
  • Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池。

    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池。
    • Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池。
    • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池。

常用方法

  • static Thread currentThread():返回当前线程
  • void start(): 启动线程,并执行对象的run()方法
  • String getName():返回线程的名称
  • void setName(String name):设置该线程名称
  • static void yield():暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
  • join() :当前线程调用其他线程的join()方法,当前线程将被阻塞,直到其他线程执行完为止
  • static void sleep(long millis):睡眠millis毫秒数,睡眠时间内线程是阻塞状态

线程的生命周期

image.png

线程同步

  • 同步代码块

    • 自己指定锁(同步监视器)
  • 同步方法

    • 非静态方法的同步监视器是this
    • 静态方法的同步监视器是当前类本身,.class
  • Lock锁

    • 从JDK 5.0开始,可以通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
    • ReentrantLock 类实现了Lock,可以调用lock()unlock() 进行加锁和释放锁

Lock和同步(synchronized)的对比:

Lock是手动开启和关闭锁;synchronized自动释放锁。


同步释放锁的情况:

  • 同步方法、同步代码块执行结束。
  • 同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  • 同步代码块、同步方法中出现了异常。
  • 同步代码块、同步方法中执行了当前线程对象的wait()方法,当前线程暂停,并释放锁。

同步监视器(锁)

  • 任意对象都可以作为同步锁。
  • 确保使用共享数据的多个线程共用一把锁

死锁问题

不同的线程分别拿着对方的锁,都在等待对方释放自己需要的锁。

解决:尽量避免嵌套同步。

线程通信

  • wait():使当前线程进入阻塞状态,并释放锁。
  • notify():唤醒被wait()的一个线程,如果有多个线程,则唤醒优先级高的。
  • notifyAll():唤醒被wait()的所有线程。

以上3个方法定义在Object类中,必须在同步代码块或同步方法中。必须是锁调用,否则报IllegalMonitorStateException异常。


sleep()wait()的异同:

相同点:都可以使当前线程进入阻塞状态。

不同点:

  • Thread类中声明sleep(),Object类中声明wait()
  • wait()必须在同步代码块或同步方法中调用。
  • wait()会释放同步监视器。

ThreadLocal

ThreadLocal是线程的局部变量,也就是说一个ThreadLocal对象只有自身线程可以访问。

  • Object get():获取该线程局部变量的值

  • void set(Object value):给该线程局部变量赋值。

  • public void remove():将当前线程局部变量的值删除。

  • protected Object initialValue():返回该线程局部变量的初始值。