【java】多线程

一.认识线程(Thread)

1. 1) 线程是什么

⼀个线程就是⼀个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码,main()⼀般被称为主线程(Main Thread)。

1. 2) 为啥要有线程

首先, “并发编程” 成为 “刚需”.

  • 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.
  • 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做⼀些其他的工作, 也需要用到并发编程. 其次,
    虽然多进程也能实现 并发编程, 但是线程比进程更轻量.
  • 创建线程比创建进程更快.
  • 销毁线程比销毁进程更快.
  • 调度线程比调度进程更快.

最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)
关于线程池我们后面再介绍. 关于协程的话题我们此处暂时不做过多讨论.

1.3) 进程和线程的区别

1.进程是包含线程的. 每个进程至少有⼀个线程存在,即主线程。
2.进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间.
3.进程是系统分配资源的最小单位,线程是系统调度的最小单位。
4.⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带走(整个进程崩溃)

1.4) Java的线程和操作系统线程的关系

线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了⼀些API供用户使用(例如Linux的pthread库) 例如:Java标准库Thread的类可以视为是对操作系统提供的API进行了进⼀步的抽象和封装.

二.创建线程

方法1:继承Thread类

继承Thread来创建⼀个线程类,直接使用this就表示当前线程对象的引用

class MyThread extends Thread { 
    @Override
    public void run() {
        System.out.println("这⾥是线程运⾏的代码");
    }
}
public class Test {
    public static void main(String[] args)  {
        MyThread t = new MyThread();
        t.start();
    }
}

运行结果:

图片[1]-【java】多线程

方法2:实现Runnable接口

实现Runnable接口,this表示的是 MyRunnable 的引用.需要使用Thread.currentThread()

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("这⾥是线程运⾏的代码");
    }
}
public class Test {
    public static void main(String[] args)  {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

运行结果:

图片[1]-【java】多线程

方法3:匿名内部类创建Thread子类对象

public class Test {
    public static void main(String[] args)  {
        // 使⽤匿名类创建 Thread ⼦类对象
        Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("使⽤匿名类创建 Thread ⼦类对象");
            }
        };
    }
}

方法4:匿名内部类创建Runnable子类对象

public class Test {
    public static void main(String[] args)  {
        // 使⽤匿名类创建 Runnable ⼦类对象
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
            }
        });
    }
}

方法5:lambda表达式创建Runnable子类对象

public class Test {
    public static void main(String[] args)  {
        // 使⽤匿名类创建 Runnable ⼦类对象
        // 使⽤ lambda 表达式创建 Runnable ⼦类对象
        Thread t3 = new Thread(() -> System.out.println("使⽤匿名类创建 Thread ⼦类对象"));
        Thread t4 = new Thread(() -> {
            System.out.println("使⽤匿名类创建 Thread ⼦类对象");
        });
    }
}

三.Thread类及其方法

Thread 类是 JVM 用来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关联。而Thread 类的对象就是用来描述⼀个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

3.1Thread的常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group, Runnable target)线程可以被用来分组管理,分好的组即为线程组
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

3.2Thread的几个常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID是线程的唯⼀标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的⼀个情况,下面我们会进⼀步说明
  • 优先级高的线程理论上来说更容易被调度到

关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有非后台线程结束后,才会结束运行。 是否存活,即简单的理解,为run方法是否运行结束了

public class Test {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(1 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我即将死去");
        });
        System.out.println(Thread.currentThread().getName() + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName() + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName() + ": 优先级: " + thread.getPriority());
        System.out.println(Thread.currentThread().getName() + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName() + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName() + ": 被中断: " + thread.isInterrupted());
        thread.start();
    }
}
    

运行结果:

图片[3]-【java】多线程

3.3获取当前线程引用

方法说明
public static Thread currentThread();返回当前线程对象的引用
public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName());
    }
}

3.4休眠当前线程

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程 millis 毫秒
public static void sleep(long millis, int nanos) throws InterruptedException可以更高精度的休眠当前线程 millis 毫秒和 nanos 纳秒
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
    }
}

四:线程的状态

4.1线程状态

  1. NEW(新建)
    • 线程已被创建,但尚未启动。
    • 处于这个状态的线程还没有开始执行。
  2. RUNNABLE(可运行)
    • 线程正在JVM中执行,或者正在等待CPU时间片以便执行。
    • 这个状态可以分为两种情况:
      • 正在工作中:线程正在使用CPU执行。
      • 即将开始工作:线程已准备好执行,但正在等待CPU时间片。
  3. BLOCKED(阻塞)
    • 线程因等待一个监视器锁而暂停执行。
    • 线程将一直等待,直到它能够获取到所需的锁。
  4. WAITING(等待)
    • 线程正在等待另一个线程执行特定的操作(如通知或中断)。
    • 线程不会自动唤醒,必须等待其他线程调用 notify()notifyAll() 方法。
  5. TIMED_WAITING(定时等待)
    • 线程在等待另一个线程执行操作或超时。
    • 线程将在指定的时间后自动唤醒,或者在其他线程调用 notify()notifyAll() 方法时唤醒。
  6. TERMINATED(终止)
    • 线程已完成执行。
    • 线程已经运行完毕或被其他线程中断。

4.2线程状态和状态转移的意义

图片[4]-【java】多线程

4.3观察线程的状态和转移

示例1:NEW(新建) 、 RUNNABLE(可运行) 、 TERMINATED(终止) 状态的转换

public class ThreadStateTransfer {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 1000_0000; i++) {
            }
        }, "李四");
        System.out.println(t.getName() + ": " + t.getState());;
        t.start();
        while (t.isAlive()) {
            System.out.println(t.getName() + ": " + t.getState());;
        }
        System.out.println(t.getName() + ": " + t.getState());;
    }
}

运行结果

图片[5]-【java】多线程
图片[6]-【java】多线程

示例2: WAITING(等待) 、 BLOCKED(阻塞) 、 TIMED_WAITING (定时等待)状态的转换

public static void main(String[] args) {
        final Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    while (true) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("hehe");
                }
            }
        }, "t2");
        t2.start();
    }

使用jconsole可以看到t1的状态是TIMED_WAITING,t2的状态是BLOCKED

结论:

1.BLOCKED表示等待获取锁,WAITING和TIMED_WAITING表示等待其他线程发来通知.
2.TIMED_WAITING线程在等待唤醒,但设置了时限;WAITING线程在无限等待唤醒

五:多线程带来的的风险-线程安全(重点)

5.1 线程安全的概念

线程安全指的是在多线程环境下,代码的运行结果符合预期,即与单线程环境下的运行结果相同。这意味着程序在多线程环境中能够正确处理共享数据,而不会导致数据不一致或其他意外行为。

5.2 线程不安全的原因

线程不安全的主要原因是线程调度的随机性。这种随机性导致程序在多线程环境下的执行顺序存在变数,使得程序的行为难以预测。程序员必须确保代码在任意执行顺序下都能正常工作,以保证线程安全。

5.3 线程的几大特性

5.3.1 原子性

原子性指的是代码在执行过程中不会被其他线程打断,从而保证了操作的完整性。这有时也被称为同步互斥,意味着操作是互斥的,即同一时间只能有一个线程执行该操作。

5.3.2 可见性

可见性指的是一个线程对共享变量的修改能够及时被其他线程看到。这是通过Java内存模型(JMM)来实现的,JMM定义了主内存和工作内存之间的交互规则,确保了变量值的可见性。

  • Java内存模型(JMM):JMM是Java虚拟机规范中定义的,目的是屏蔽不同硬件和操作系统的内存访问差异,以实现Java程序在各种平台下的一致并发效果。
  • 主内存与工作内存:线程之间的共享变量存储在主内存中。每个线程都有自己的工作内存,用于存储共享变量的副本。线程读取或修改共享变量时,需要在主内存和工作内存之间进行数据同步。

5.3.3 指令重排序

指令重排序是指编译器或CPU为了优化性能,可能会改变指令的执行顺序。在单线程环境下,这种优化通常不会影响程序的逻辑。但在多线程环境下,由于线程间的执行顺序更加复杂,编译器很难预测代码的执行效果,因此激进的重排序可能导致优化后的逻辑与之前不等价。

  • 代码重排序示例
  • 1. 去前台取下U盘 2. 去教室写10分钟作业 3. 去前台取下快递
  • 在单线程情况下,JVM或CPU可能会按1->3->2的方式执行,以减少前台的访问次数。但在多线程环境下,这种重排序可能会导致问题,因为多线程的代码执行复杂度更高,编译器难以在编译阶段预测代码的执行效果。

指令重排序是一个复杂的话题,涉及到CPU和编译器的底层工作原理,需要更深入的讨论和理解。

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容