@Java | Thread & synchronized - [ 多线程 基本使用]

本文及后续相关文章梳理一下关于多线程和同步锁的知识,平时只是应用层面的了解,由于最近面试总是问一些原理性的知识,虽说比较反感这种理论派,但是为了生计也必须掌握一番。(PS:并不是说掌握原理不好,但是封装就是为了更好的应用,个人感觉没必要为了学习而学习,比较倾向于行动派,能将原理应用到实际才算参透,本文也仅仅是背书而已)

知识点

  • 进程:进程就是一段程序的执行过程,指在系统中能独立运行并作为资源分配的基本单位,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。
  • 线程:线程是进程中的一个实体,作为系统调度和分派的基本单位。Linux下的线程看作轻量级进程。
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止
多进程是指操作系统能同时运行多个任务(程序)
多线程是指在同一程序中有多个顺序流在执行

多线程使用说明

如何创建线程

  • 实现Runnable接口
  • 继承Thread类
  • 通过Callable和Future创建线程

1. 通过实现Runnable接口创建并运行线程

- 实现Runnable接口

Public class A implements Runnable {
    public void run () {    // 必须实现run方法
        // 线程执行的业务代码
    }
}
上面实现了 Runnable接口并通过 run方法包装了需要通过线程执行的代码

- 运行线程
通过实现了Runnable接口的类的实例创建线程对象(Thread)来运行线程,常用的Thread构造方法:

Thread(Runnable threadOb,String threadName);
其中 threadOb 是一个实现 Runnable接口的类的 实例threadName指定线程的名字

调用线程类中的start()方法运行线程 new Thread(threadOb,threadName).start();(可创建多个线程对象运行同一个Runnable接口实例的run方法,实现资源共享

PS: start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为 可运行态(Runnable),等待CPU分配调度执行

2. 继承Thread类创建并运行线程

- 继承Thread类

public class A extends Thread {
    @Override
    public void run() {    // 重写run方法
        // 线程执行的业务代码
    }
}
上面继承了 Runnable接口并通过重写 run方法包装了需要通过线程执行的代码,通过源码可以看到 Thread类也是实现了 Runnable接口的,所以本质上和上一种方式无太大区别,不同的是Thread类不适合 共享资源线程实现

- 运行线程

同样是调用线程类中的start()方法运行线程,此时线程类为继承Thread的类

3. 通过Callable和Future创建并运行线程

- 实现Callable接口

public class A implements Callable {
    @Override  
    public T call() throws Exception  // 实现call()方法
    {  
        //  线程执行的业务代码
    }  
}
创建 Callable接口的实现类 (通过泛型制定线程执行结束后的返回值类型),并实现 call()方法,该 call()方法将作为线程执行体,并且有 返回值(返回值类型为 Callable接口泛型制定的类型)

- 使用FutureTask类来包装Callable对象

FutureTask ft = new FutureTask<>(callableObj);
其中 callableObjCallable实现类的实例,使用 FutureTask类来包装 Callable对象,该 FutureTask对象封装了该 Callable对象call()方法的返回值

- 运行线程

通过FutureTask类的实例创建线程对象(Thread)来运行线程,此时应用的Thread`构造方法:

Thread(FutureTask futureObj,String threadName);
其中 futureObj 是一个 FutureTask 类的 实例threadName指定线程的名字

调用线程类中的start()方法运行线程 new Thread(threadOb,threadName).start();
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

使用须知

  1. 多线程执行的时候是无序的,即谁先获取到CPU资源就可以先执行,随机性比较大
  2. 如果start()方法重复调用,会出现java.lang.IllegalThreadStateException异常
  3. 直接继承Thread类和实现接口方式创建线程的区别

    • 直接继承Thread类方便快捷,适合相对单一无序的多线程执行
    • 实现接口方式适合多个相同的程序代码的线程去处理同一个资源
    • 实现接口方式可以避免java中的单继承的限制
    • 实现接口方式增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
    • 线程池只能放入实现Runablecallable类线程,不能直接放入继承Thread的类
  4. Java程序运行首先会启动一个JVM进程,然后至少启动两个线程,即main线程垃圾收集线程

Thread常用方法说明

  1. sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),可用TimeUnit.MILLISECONDS.sleep方法替换
  2. join(): 等待调用join()的线程终止

    • 使用方式

      `Thread t = new AThread(); t.start(); t.join();`
      join()的作用是将线程加入到 当前线程中,只有执行完join()调用线程才能执行后面的代码
    • 使用场景
      正常情况下主线程不依赖子线程执行完成而结束,当主线程需要在子线程完成之后再结束时,可使用此方法
  3. yield(): 暂停当前正在执行的线程对象,并执行其他线程

    • 使用说明
      yield()只是将线程从运行状态转到可运行状态(start()方法执行后的状态),不会导致线程转到等待/睡眠/阻塞状态
    • 使用场景
      yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中

      PS: yield()方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行
  4. setPriority(): 更改线程的优先级,优先级高的线程会获得较多的运行机会
    优先级静态常量MIN_PRIORITY=1,NORM_PRIORITY=5,MAX_PRIORITY=10

    • 使用方式

      Thread t1 = new Thread("t1");
      Thread t2 = new Thread("t2");
      t1.setPriority(Thread.MAX_PRIORITY);
      t2.setPriority(Thread.MIN_PRIORITY);
    • 使用说明
      Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
      每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
      线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
      JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式
  5. interrupt(): 将线程对象的中断标识设成true

    • 使用说明

      • 中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupted方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位;如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现
      • 每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断
      • 通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用
    • 使用方式

      • 在调用阻塞方法时正确处理InterruptedException异常
      • 设置中断监听(另一种方式)

        Thread t1 = new Thread( new Runnable(){
               public void run(){
                   // 若未发生中断,就正常执行任务
                   while(!Thread.currentThread.isInterrupted()){
                       // 正常任务代码……
                   }
           
                   // 中断的处理代码……
                   doSomething();
               }
           } ).start();
      • 触发中断

        t1.interrupt();
    • interrupt方法使用参考链接
      大闲人柴毛毛
  6. wait(): 主动释放对象锁,同时本线程休眠,直到有其它线程调用对象的notify()唤醒该线程,重新获取对象锁并执行(wait()方法属于Object中的方法,并不属于Thread类)
  7. notify(): 唤醒调用notify()对象的线程,notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行

Thread几个方法比较

1.sleep()yield()的区别

  • sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行
  • sleep方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield方法使当前线程让出CPU占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为'退让',它把运行机会让给了同等优先级的其他线程
  • sleep方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得CPU占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep方法,又没有受到IO阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行

2.wait()sleep()区别

共同点:

  • 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回
  • wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态,从而使线程立刻抛出InterruptedException
  • 如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
需要注意的是, InterruptedException是线程自己从内部抛出的,并不是 interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出 InterruptedException。但是,一旦该线程进入到 wait()/ sleep()/ join()后,就会立刻抛出 InterruptedException

不同点:

  • sleep(),yield()等是Thread类的方法
  • wait()和notify()等是Object的方法
  • 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法
  • wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

简单来说: 

  • sleep()睡眠时,保持对象锁,仍然占有该锁;
  • 而wait()睡眠时,释放对象锁。
  • wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)

sleep()方法

  • sleep()使当前线程进入停滞状态(阻塞当前线程),让出CPU的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
  • sleep()是Thread类的Static的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并没有被释放,其他线程无法访问这个对象(即使休眠也持有对象锁
  • 在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级

wait()方法

  • wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;
  • wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
  • wait()必须放在synchronized block中,否则会在程序runtime时扔出java.lang.IllegalMonitorStateException异常。

你可能感兴趣的:(java,thread,synchronized,多线程,同步锁)