多线程基础入门

参考链接:www.cnblogs.com/GrimMjxCl/p...

1.JAVA线程的分类

  • 用户线程: 用户定义的线程,当然包括main线程(main线程不是一个daemon线程,因为如果我再main线程中创建一个用户线程,并且打出日志,我们会发现这样一个问题,main线程运行结束了,但是我们的线程任然在运行)

  • daemon线程:运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。daemon线程是为我们创建的用户线程提供服务的线程,比如说jvm的GC等等,这样的线程有一个非常明显的特征: 当用户线程运行结束的时候,daemon线程将会自动退出

2.线程优先级属性priority

java 中的线程优先级的范围是1~10,默认的优先级是5,10最高

  1. 有时间片轮循机制时:"高优先级线程"被分配CPU的概率高于"低优先级线程"。根据时间片轮循调度,所以能够并发执行。无论是是级别相同还是不同,线程调用都不会绝对按照优先级执行,每次执行结果都不一样,调度算法无规律可循,所以线程之间不能有先后依赖关系。

  2. 无时间片轮循机制时:高级别的线程优先执行,如果低级别的线程正在运行时,有高级别线程可运行状态,则会执行完低级别线程,再去执行高级别线程。如果低级别线程处于等待、睡眠、阻塞状态,或者调用yield()函数让当前运行线程回到可运行状态,以允许具有相同优先级或者高级别的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

3.JAVA线程的几种状态

  • NEW(新建):新创建了一个线程对象,但还没有调用start()方法,例如Thread thread = new Thread();

  • RUNNABLE(可运行):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为"运行",包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在运行,也可能正在等待系统资源,如等待CPU为它分配时间片,如等待网络IO读取数据获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)

  • BLOCKED(阻塞):表示线程阻塞于锁。当一个线程要进入synchronized语句块/方法时,如果没有获取到锁(waiting to enter monitor)会变成BLOCKED,直到另一个线程走完临界区或发生了相应锁对象的wait()操作后,它才有机会去争夺进入临界区的权利。处于BLOCKED状态的线程,即使对其调用thread.interrupt()也无法改变其阻塞状态,因为interrupt()方法只是设置线程的中断状态,即做一个标记,不能唤醒处于阻塞状态的线程。ReentrantLock.lock()操作后进入的是WAITING状态,其内部调用的是LockSupport.park()方法

  • WAITING(无限期等待):处于这种状态的线程不会被CPU分配时间片,它们要等待被显式地唤醒,否则会处于无限期等待的状态

    scss 复制代码
      这种状态通常是指一个线程拥有对象锁后进入到相应的代码区域后,调用相应的"锁对象"的wait()方法操作后该线程会进入WAITING状态
    
     调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内
    
     以下方法会让线程陷入无限期等待状态:
    
        obj.wait();
    
        thread.join();
    
        LockSupport.park();  --  驻留
    
        注意:
    
        LockSupport.park(Object blocker) 会挂起当前线程,参数blocker是用于设置当前线程的"volatile Object parkBlocker 成员变量"
    
        parkBlocker 是用于记录线程是被谁阻塞的,可以通过LockSupport.getBlocker()获取到阻塞的对象,用于监控和分析线程用的。
  • TIMED_WAITING(超时等待):处于这种状态的线程也不会被分配CPU执行时间,不过无需等待被其它线程显示的唤醒,在一定时间 之后它们会由系统自动的唤醒

    ini 复制代码
     以下方法会让线程进入TIMED_WAITING限期等待状态:
    
        thread.sleep(timeout);
    
        obj.wait(timeout);
    
        thread.join(timeout);
    
        LockSupport.parkUntil(deadline);  --  驻留
    
        LockSupport.parkNanos(timeout);
  • TERMINATED(结束):RUNNABLE线程run方法走完或者异常退出

4.JAVA线程状态流转图

5.JAVA创建线程的几种方式

继承Thread类重写run方法:java使用Thread类代表线程,所有的线程对象都必须是Thread或者其子类的实例,每个线程的作用是完成一定任务,实际上是就是执行一段程序流(一段顺序执行的代码)

继承Thread类创建线类

  1. 定义Thread类的子类 并重写该类的Run方法,该run方法的方法体就代表了线程需要完成的任务
  2. 创建Thread类的实例,即创建了线程对象
  3. 调用线程的start方法来启动线程

实现Runnable接口:使用Runnable接口创建线程类

  1. 定义Runnable接口的实现类,并重写它的Run方法,run方法同样是该线程的执行体!
  2. 创建Runnable实现类的实例,并将此实例作为Thread的target创建一个Thread对象,该Thread对象才是真正的线程对象!
  3. 调用start方法启动该线程

Runnable对象仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程的对象依旧是Thread实例,只是线程实例负责执行其target的run()方法

实现Callable接口:可以返回执行结果,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果

使用线程池:ThreadPoolExecutor类

6.阻塞与等待的区别

  • 阻塞:当一个线程试图获取对象锁(非java.util.concurrent库中的锁,即synchronized),而该锁被其他线程持有,则该线程进入阻塞状态。它的特点是使用简单,由JVM调度器来决定唤醒自己,而不需要由另一个线程来显式唤醒自己,不响应中断。

  • 等待:当一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态,是在等待一段时间或者唤醒动作的发生,进入"等待"状态是主动的。如主动调用Object.wait(),如无法获取到ReentrantLock,主动调用LockSupport.park(),如主线程主动调用 subThread.join(),让主线程等待子线程执行完毕再执行。离开"等待"状态是因为其它线程发生了唤醒动作或者到达了等待时间

7.sleep()和yield()的区别

sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。

sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。

sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。

8.notify()和notifyAll()的区别

notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争

  1. notify只会随机选取一个处于该对象锁等待池中的线程进入锁池去竞争获取锁的机会,具体唤醒哪一个线程由虚拟机控制;
  2. notifyAll会让所有处于该对象锁等待池的线程全部进入锁池去竞争获取锁的机会;

9.wait和sleep的区别

类的不同:sleep()来自Thread,wait()来自Object

释放锁:sleep() 不释放锁;wait() 释放锁,wait通常用于线程时交互,sleep通常被用于暂停执行

用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒

10.Thread.yield();不响应中断

csharp 复制代码
public static native void yield(); //Thread类中的静态方法

使当前正在运行的线程让步,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。

调用yield后当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态。

使用该方法,可以防止线程对CPU的过度使用,提高系统性能

11.Thread.sleep(long timeout); 响应中断

java 复制代码
public static native void sleep(long millis) throws InterruptedException;  

Thread类中的静态native方法,响应中断,使当前正在运行的线程休眠,进入TIMED_WAITING状态,并释放CPU资源,但不会释放对象锁,当睡眠的时间结束或者中断才会重新进入就绪状态,因此sleep()方法不能保证该线程睡眠到期后就开始执行。

12.为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?

当一个线程需要调用对象的wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify()方法。同样的,当一个线程需要调用对象的notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

13.为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?

Java的每个对象中都有一个锁(monitor,也可以成为监视器) 并且wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在Java的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是Object类的一部分,这样Java的每一个类都有用于线程间通信的基本方法

14.thread.join();响应中断

java 复制代码
public final void join() throws InterruptedException {
     join(0);
}
public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (millis == 0) {
            while (isAlive()) { //只要子线程是活的,主线程就不停的等待
                wait(0); //让当前在CPU上运行的线程进入无限期等待状态
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

主线程会等待子线程结束之后才能继续运行,通常用于在main()主线程内,等待其它线程完成再结束main()主线程

  • thread.join(); //当前线程里调用其它线程thread的join方法,当前线程等待thread线程执行完成才会执行,当前线程会进入WAITING无限期等待状态。

  • thread.join(timeout); //当前线程等待thread线程执行完成才会执行,最多等待time时间,当前线程会进入TIME_WAITING

wait()的作用是让"当前线程"等待,而这里的"当前线程"是指当前在CPU上运行的线程。所以,虽然是调用子线程的wait()方法,但是它是通过"主线程"去调用的;所以,休眠的是主线程,而不是"子线程"!

15.obj.wait();响应中断

java 复制代码
public final native void wait(long timeout) throws InterruptedException; //指定等待时间

public final void wait() throws InterruptedException {wait(0);} //无限期等待

Object类的方法,只能在同步方法或同步代码块中使用,它使得当前正持有该对象的锁的线程等待(即暂停),并释放对象锁,以便其它线程能够获取该对象的锁,当前线程会进入WAITING

obj.wait();线程进入WAITING等待状态并释放CPU资源和对象锁

obj.wait(timeout);指定等待时间,线程进入TIME_WAITING超时等待状态并释放CPU资源和对象锁。如果指定时间内没有notify或者notifyAll唤醒它,则时间到了后自动唤醒,如果时间还没到但是有notify或者notifyAll唤醒它,则会提前唤醒

一个线程T被唤醒可能有以下四种情况,但是唤醒之后线程T需要重新去抢临界区的锁才能把代码真正执行下去:

  • 其它的线程调用 obj.notify(),且当前线程 T,正好是被选中唤醒的
  • 其它的线程调用 obj.notifyAll()唤醒所有obj上的等待线程
  • 其它线程中断T
  • 指定的等待时间(timeout)超时,时间精度会有些误差

调用wait()、notify()以 及notifyAll()时需要注意的细节,如下。

  1. 使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
  2. 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
  3. notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或 notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。
  4. notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll() 方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为 BLOCKED。
  5. 从wait()方法返回的前提是获得了调用对象的锁。

从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从 wait()方法返回时能够感知到通知线程对变量做出的修改。

16.thread.interrupt();

中断:Java中断机制是一种协作机制,通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断

scss 复制代码
interrupt()中断:将线程的中断标志位置位

如果线程sleep()、wait()、join()等处于阻塞状态,那么线程会定时检查中断状态位如果发现中断状态位为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断状态位清除,即重新设置为false。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。

如果线程正在运行、争用synchronized、lock()等,那么是不可中断的,他们会忽略。

可以通过以下三种方式来判断中断:

  1. isInterrupted()此方法只会读取线程的中断标志位,并不会重置。
  2. interrupted()此方法读取线程的中断标志位,并会重置。
  3. throw InterruptException抛出该异常的同时,会重置中断标志位。

thread.interrupt(); //实例方法 如果正在运行wait(),sleep(),join()这三个方法阻塞了线程,那么将会使得线程抛出InterruptedException异常,这是一个中断阻塞的过程。如果是其它的正在运行的状态,那么将不会有任何影响,也不会中断线程,或者抛出异常,只会会打上一个中断线程的标志,是否中断线程,将由程序控制。

boolean interrupted = Thread.interrupted(); //静态方法,返回当前线程是否有中断标志并清除当前线程的中断标志:就是如果当前线程已中断,第一次调用这个方法的返回值是true,第二次调用这个方法的返回值为false,因为调用方法时,会清除它的中断标志

boolean interrupted = thread.isInterrupted(); //实例方法 返回该线程是否有中断标志

中断的使用场景有以下几个:

  • 点击某个桌面应用中的取消按钮时;
  • 某个操作超过了一定的执行时间限制需要中止时;
  • 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;
  • 一组线程中的一个或多个出现错误导致整组都无法继续时;
  • 当一个应用或服务需要停止时。

17.LockSupport.park()和unpark()

java 复制代码
public class LockSupport {

    private LockSupport() {}

    private static final Unsafe UNSAFE;

    //可以响应中断,但是不会抛出InterruptedException
    public static void park() {//获取许可,因为许可默认是被占用的,调用park()时获取不到许可,线程进入阻塞状态
        UNSAFE.park(false, 0L);
    }
    public static void unpark(Thread thread) { //释放许可
        if (thread != null) UNSAFE.unpark(thread);
    }
}
相关推荐
中草药z2 分钟前
【Spring】深入解析 Spring 原理:Bean 的多方面剖析(源码阅读)
java·数据库·spring boot·spring·bean·源码阅读
信徒_10 分钟前
常用设计模式
java·单例模式·设计模式
神仙别闹15 分钟前
基于C#实现的(WinForm)模拟操作系统文件管理系统
java·git·ffmpeg
小爬虫程序猿16 分钟前
利用Java爬虫速卖通按关键字搜索AliExpress商品
java·开发语言·爬虫
组合缺一21 分钟前
Solon v3.0.5 发布!(Spring 可以退休了吗?)
java·后端·spring·solon
程序猿零零漆23 分钟前
SpringCloud 系列教程:微服务的未来(二)Mybatis-Plus的条件构造器、自定义SQL、Service接口基本用法
java·spring cloud·mybatis-plus
猿来入此小猿25 分钟前
基于SpringBoot在线音乐系统平台功能实现十二
java·spring boot·后端·毕业设计·音乐系统·音乐平台·毕业源码
愤怒的代码39 分钟前
Spring Boot对访问密钥加解密——HMAC-SHA256
java·spring boot·后端
带多刺的玫瑰39 分钟前
Leecode刷题C语言之切蛋糕的最小总开销①
java·数据结构·算法
栗豆包1 小时前
w118共享汽车管理系统
java·spring boot·后端·spring·tomcat·maven