多线程基础知识点

发布时间:2024年01月06日

1. 进程

一个正在执行中的程序就是一个进程,系统会为这个进程发配独立的【内存资源】。进程是程序的一次执行过程,它有自己独立的生命周期,它会在启动程序时产生,运行程序时存在,关闭程序时消亡。

例如:正在运行的 QQ、IDE、浏览器就是进程。

2. 线程

线程是由进程创建的,是进程的一个实体,是具体干活的人,一个进程可能有多个线程。线程不独立分配内存,而是共享进程的内存资源,线程可以共享 CPU 的计算资源。

一个进程的线程就不能修改另一个线程的数据,隔离性更好,安全性更好。

3. 并发和并行

大部分操作系统的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。CPU 在不同的进程之间轮换,进程又在不同的线程之间轮换,因此线程是 CPU 执行和调度的最小单元。

任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态,等待下一个属于它的时间片的到来。这样每个任务都能得到执行,由于 CPU 的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发。

并发是两个队列交替使用一台咖啡机。

并行是两个队列同时使用两台咖啡机。

4. Java 中常见线程方式

4.1 继承 Thread 类

步骤:

  • 定义类继承 Thread;
  • 重写 Thread 类中的 run 方法;
  • 调用线程的 start 方法:
public class Test01 {
??
    public static void main(String[] args) {
        System.out.println(1);
        new MyThread01().start();
        System.out.println(3);
        try {
          Thread.sleep(100);
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
       System.out.println(4);
?    }?
}

class MyThread01 extends Thread{
    @Override
    public void run() {
        System.out.println(2);
    }
}

4.2 实现 Runnable 接口

步骤:

  • 创建类实现 Runnable 接口
  • 使用 Thread 为这个任务分配线程
  • 调用线程的 start 方法
public class Test02 {

    public static void main(String[] args) {
        System.out.println(1);
        //注意,这里 new 的是 Thread
        new Thread(new MyRun()).start();
        System.out.println(3);
        try {
          Thread.sleep(100);
       } catch (InterruptedException e) {
          e.printStackTrace();
       }
        System.out.println(4);
    }
?
}

class MyRun implements Runnable{
    public void run() {
        System.out.println(2);
    }
}

4.3 实现 Callable 接口

步骤:

  • 创建类实现 Callable 接口
  • 通过 Callable 接口实现类创建 FutureTask
  • 使用 Thread 为这个 FutureTask 分配线程
  • 调用线程的 start 方法
public class Test03 {

     public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println(2);
        FutureTask<Integer> futureTask = new  FutureTask<>(new MyCallable());
        System.out.println(3);
        new Thread(futureTask).start();
        System.out.println(4);
        int result = futureTask.get();
        System.out.println(5);
        System.out.println(result);
        System.out.println(6);
     }

}

class MyCallable implements Callable<Integer> {
    public Integer call() throws Exception {
        Thread.sleep(2000);
        return 1;
    }
}

futureTask.get(); 是一个阻塞的方法,意思就是,这个方法会一直等,主线程会一直等待,这个线程执行完成之后并有了返回值,才会继续执行。

5. 守护线程

Java 提供两种类型的线程: 用户线程和守护线程。

守护线程旨在为用户线程提供服务,并且仅在用户线程运行时才需要。

守护线程对于后台支持任务非常有用,例如垃圾收集,释放未使用对象的内存以及从缓存中删除不需要的数据。大多数 JVM 线程都是守护线程。

要将线程设置为守护线程,我们需要做的就是调用 Thread 的 setDaemon() 方法。

NewThread t = new NewThread();
t.setDaemon(true);
t.start();

6. 线程的生命周期

生命周期可以通俗地理解为“从出生到死亡”的整个过程。线程的生命周期包括从创建到销毁的整个过程。

线程的状态:

  • NEW?- 初始状态,一个新创建的线程,还没开始执行。
  • RUNNABLE?- 可执行的状态,要么是在执行,要么是一切就绪等待执行,例如等待分配 CPU 时间。
  • WAITING?- 等待状态,等待其他的线程去执行特定的动作,没有时间限制。
  • TIMED_WAITING?- 限时等待状态,等待其他的线程去执行特定的动作,这个是在一个指定的时间范围内。
  • BLOCKED?- 阻塞状态,等待锁,以便进入同步块儿。
  • TERMINATED?- 终止状态,线程执行结束。

图片描述

7. 线程常用方法

currentThread()?:该方法是 Thread 类中的类方法,可以用类名调用,方法返回当前正在使用 CPU 资源的线程。

sleep()?:使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。

join()?:让当前线程邀请调用方法的那个线程优先执行,在被邀请的线程执行结束之前当前线程一直处于阻塞状态,不再继续执行。

yield()?:让当前线程直接放弃时间片返回就绪状态。

wait()?:当前线程放弃监视器并进入睡眠状态,直到其他进入同一个监视器的线程调用 notify 为止。

notify()?:唤醒同一监听器中调用 wait 的某一个线程。

notifyAll()?:唤醒同一监听器中所有等待的线程。

8. 线程安全

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

9. 线程同步

线程同步:关键字 synchronized。

使用同步监视器来判断当前代码是否有线程在执行。线程开始执行同步代码块之前,必须先获得对同步监视器的锁。

任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然会释放对同步监视器对象的锁。

Java 程序运行可以使用任何对象来作为同步监视器对象。

synchronized 有三种方式来加锁,分别是:

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

10. synchronized 原理

Synchronized 底层是通过两个方法来完成同步:

  • monitorenter
  • monitorexit

10.1 monitorenter

每个对象有一个监视器锁(monitor)。当 monitor 被占用时就会处于锁定状态,线程执行 monitorenter 指令时尝试获取 monitor 的所有权,过程如下:

  1. 如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为 1,该线程即为 monitor 的所有者。

  2. 如果线程已经占有该 monitor,只是重新进入,则进入 monitor 的进入数加 1.

  3. 如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权。

10.2 monitorexit

执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。

指令执行时,monitor 的进入数减 1,如果减 1 后进入数为 0,那线程退出 monitor,不再是这个 monitor 的所有者。其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权。

11. 死锁

死锁问题:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,而该资源又被其他线程锁定,从而导致每一个线程都得等其它线程释放其锁定的资源,造成了所有线程都无法正常结束。结构如下图所示:

死锁示例代码:

public class MyThread01 implements Runnable{
    static Object garlic = new Object();
    static Object vinegar = new Object();
    int flag = 0;
    @Override
    public void run() {
        if (flag == 0) {
            synchronized (vinegar) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {return;}
                System.out.println("我张三正在吃醋,李四你给我点蒜瓣");
                synchronized (garlic) {
                    System.out.println("我张三终于吃上蒜了,hiahia~");
                }
            }
        }
        if (flag == 1) {
            synchronized (garlic) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
                System.out.println("俺李四正在吃蒜,张三你给我点醋");
                synchronized (vinegar) {
                    System.out.println("我李四终于吃上醋了,hiahia~");
                }
            }
        }
    }

    public static void main(String[] args) {
        MyThread01 ZhangSan = new MyThread01();
        MyThread01 LiSi = new MyThread01();
        ZhangSan.flag = 0;
        LiSi.flag = 1;
        Thread t1 = new Thread(ZhangSan);
        Thread t2 = new Thread(LiSi);
        t1.start();
        t2.start();
    }
}

程序运行结果:

图片描述

文章来源:https://blog.csdn.net/qq_57747969/article/details/135384331
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。