java并发编程第一天-java内存模型与线程

684 阅读12分钟

进程和线程

进程: 程序在数据集合上的一次运行活动,是操作系统进行资源分配和调度的基本单位。

线程: 进程的一个执行路径,一个进程中有多个线程,进程中的多个线程共享进程的资源,线程是 CPU 分配的基本单位。

在 Java 中,启动 main 函数就是启动了一个 jvm 进程, main 函数所在的线程就是 这个进程中的一个线程,称为主线程。

在 Java 虚拟机中,所有线程共享方法区和堆中的资源,每个线程有私有的程序计数器和栈区域。


上下文切换

多线程编程中,线程的个数一般大于 CPU 的个数,而 CPU 同一时刻只能执行一个线程。

CPU 资源的分配采用了了时间片轮转策略,也就是给每个线程分配个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU ,让其他线程占用这就是上下文切换

上下文切换的时机举例:

  • 当前线程的 CPU 时间片使用完处于就绪状态时
  • 当前线程被其他线程中断时

多线程一定比单线程快吗?

不一定,当任务量不多时,单线程比多线程更快,因为多线程上下文切换回带来额外的开销


线程的状态

java语言中定义了 6 种线程状态,可以通过不同的方法在6种不同的线程之间转换

  1. New (初始) : 线程创建后且未启动时
  2. Runnable (运行) :包括 Running (当前线程正在执行) 和 Ready (还没有执行,处于就绪状态,等着 cpu 调度为其分配时间片)
  3. Waiting (无限期等待):处于这种状态的线程不会 CPU 调度分配时间片,它们要等待被其他线程显式唤醒
  4. Timed Waiting (限期等待):处于这种状态的线程不会 CPU 调度分配时间片,一段时间后会自动唤醒,无需其他线程显示唤醒
  5. BLOCKED (阻塞状态):当获取对象资源监视器锁时,该对象锁已被其他线程占用,线程会进入阻塞状态
  6. Terminated : 线程终止状态

这张图很重要,上面的方法会在后面一一出现


java内存模型

工作内存与主内存

Java 内存模型规定了所有的变量(指实例变量、常量、静态变量)都在主内存中,每个线程还有自己的工作内存。

线程的工作内存中,保存了该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间的传值要通过主内存来完成。

这里的工作内存,是抽象的概念,实际上并不存在。从硬件层次可以认为,主内存位于主存储器上, 工作内存涵盖了 高速缓存寄存器,控制器,运算器和其他硬件等 (学过计组的都懂)


内存间交互操作

java内存模型定义了 8 种内存间交互操作,每一种操作都是原子性的。

  1. lock : 作用与主内存中的变量,把一个变量标示为一条线程独占的状态
  2. unlock : 作用与主内存中的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  3. read : 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中
  4. load : 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  5. use : 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  6. assign : 作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  7. store : 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用
  8. write: 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。

把一个变量从主内存拷贝到工作内存,需要执行: read 和 load 操作。

把变量从工作内存同步到主内存,需要执行 store 和 write 操作

需要知道的规则限定:

  • 对一个变量进行 lock 操作,会清空工作内存中此变量的值,执行引擎使用这个变量时,需要重新从主内存中读取

  • 对一个变量进行 unlock 操作之前,必须先把变量同步到主内存。


java中创建线程的三种方式

继承Thread

public class ThreadTest1 {
    public static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("my first thread");
        }
        // 线程的run方法执行完毕后,线程处于Terminated状态。
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread(); //线程处于new状态
        myThread.start(); // 调用start方法后,线程处于Ready状态,当CPU调度该线程时,线程处于Running状态
    }
}

好处:

获取当前线程直接用 this 就可以,不需要使用 Thread.currentThread() 方法

坏处:

Java 只支持单继承,继承了这个类就不能继承别的类了


实现Runnable接口

public class ThreadTest2 {
    public static class RunableTask implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "  2020-5-24,学习多线程第一天");
        }
    }

    public static void main(String[] args) {
        RunableTask task = new RunableTask();
        new Thread(task,"勤能补拙").start(); // 给thread起名字
        new Thread(task,"笨鸟先飞").start();
    }
}

好处: 解决了继承问题

实现Callable接口

直接继承 Thread 和实现 Runnable 有个共同的缺点,无法获取返回值

public class ThreadTest3 {
    public static class MyTask implements Callable<String>{
        @Override
        public String call() throws Exception {
            System.out.println("第三种创建线程方式");
            Thread.sleep(2000); //线程休眠2000ms
            return "77777"; // 返回一个String
        }
    }

    public static void main(String[] args) {
        // 创建异步任务
        FutureTask<String> futureTask = new FutureTask<>(new MyTask());
        new Thread(futureTask).start(); //启动线程
        try {
            String res = futureTask.get(); //当线程执行完毕后才会得到结果,否则线程处于WAIT状态
            System.out.println(res); // 打印返回值
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("执行结束");
    }
}


线程的通知与等待

Object类 包含线程的通知与等待函数

wait函数

wait 函数在使用前必须获取对象的监视器锁,否则会抛出 IllegalMonitorStateException

如何获得对象的监视器锁呢? 使用 synchronized (后面再详解)关键字

synchronized 修饰代码块,用共享变量作为参数

// 当对象的监视器锁被线程持有时,其他尝试获取对象监视器锁的线程会被阻塞
synchronized(obj){
   // 同一时间只有一个线程能获取对象的监视器锁,只有当前线程释放对象锁后,其他线程才能尝试获取锁
}

wait 函数能让当前线程进入 WAITNG 状态,并释放对象的监视器锁(其他尝试获取该锁的线程会有一个获得锁),直到发生以下事件才返回:

  • 其他线程调用了该共享对象的 notify 或者 notifyAll 方法
  • 其他线程调用了该线程的 interrupt 方法,该线程抛出 InterruptedException 异常后返回

notify&notifyAll

notify 函数在使用前也必须获取对象的监视器锁,否则会抛出 IllegalMonitorStateException。

同一对象上调用 notify/notifyAll 方法,就可以唤醒对应对象 monitor(监视器锁) 上等待的线程了。notify和notifyAll 的区别在于前者只随机唤醒 monitor 上的一个线程,而 notifyAll 则唤醒所有的线程


例子1

    private static Object resource = new Object(); //定义一个共享对象

    public static class NotifyTest implements Runnable{
        @Override
        public void run() {
            synchronized (resource) { // 使用wait和notify前必须获取对象的监视器锁
                String name = Thread.currentThread().getName(); // 获取当前线程名称
                System.out.println(name + "进入wait状态,释放锁资源");
                try {
                    resource.wait();
                    System.out.println(name + "被唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(name + "执行结束,释放锁资源");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new NotifyTest(),"t1");
        Thread t2 = new Thread(new NotifyTest(),"t2");
        Thread t3 = new Thread(new NotifyTest(),"t3");
        t1.start();
        t2.start();
        t3.start();
        Thread.sleep(1000); //等待前面的线程执行完
        // 此时线程t1、t2、t3 都处于wait状态
        synchronized (resource){
           // resource.notify(); //随机随机唤醒一个线程
            resource.notifyAll(); // 唤醒所有线程
        }
    }

这里 t2 比 t3 先start,但是 t3 比 t2 先进入 Running 状态: 先进入就绪状态不一定先执行,调度程序没有挑选到你,你就永远是就绪状态。这段程序每次运行的结果顺序可能都不一样

大家可以自己试一下 notify ,多运行几次看看结果。


例子2

当前线程调用共享变量的 wait 方法只会释放当前共享变量上的锁,如果当前线程还持有其他贡献变量的锁,则不会释放

private static Object resourceA = new Object();
    private static Object resourceB = new Object();  // 定义两个共享变量

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            String name = Thread.currentThread().getName();
            synchronized (resourceA){
                System.out.println(name + " get resourceA lock");
                synchronized (resourceB){
                    System.out.println(name + " get resourceB lock");
                    System.out.println(name + " release resourceA lock");
                    try {
                        resourceA.wait(); //释放resourceA的监视器锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"t1").start();  // 线程名为t1
        
        Thread.sleep(1000); // 保证t1先进入Running状态
        
        new Thread(()->{
            String name = Thread.currentThread().getName();
            synchronized (resourceA){
                System.out.println(name + " get resourceA lock");
                System.out.println(name + " try get resourceB lock");
                synchronized (resourceB){
                    System.out.println(name + " get resourceB lock");
                    System.out.println(name + " release resourceA lock");
                    try {
                        resourceA.wait(); //释放resourceA的监视器锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"t2").start();  // 线程名为t2
    }

t1 先获取 rescouceA 的对象锁,然后获取 recouceB 的对象锁(此时线程 t2 还未启动),然后线程 t1 释放对象 A 的锁资源,进入 wati 状态。 线程 t2 启动后获取 rescouceA 的对象锁,但不能获取 resourceB 的对象锁,因为线程 t1 并没有释放 resourceA 的对象锁。


join

在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行。比如多个线程加载资源,需要等待多个线程全部加载完毕再向下处理。

Thread类 有一个 join 方法可以完成这个功能

t.join,会让当前线程进入 wait 状态,直到 t线程 执行完毕再向下执行。

public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 over");
        });

        Thread t2 = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2 over");
        });

        t1.start();
        t1.join(); // 当前线程(主线程)进入wait,必须等线程t1执行完后才向下执行

        t2.start();
        t2.join();  // 当前线程(主线程)进入wait,必须等线程t2执行完后才向下执行

        System.out.println("main over"); 
    }
}

sleep

Thread 类有个静态方法 sleep ,当一个执行中的线程调用了 Thread 的 sleep 方 法后,调用线程会暂时让出指定时间内 CPU 的执行权,也就是在这期间不参与 CPU 的调度 , 但是锁资源并不释放

public class SleepTest {
    private static Object resource = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1= new Thread(()->{
            synchronized (resource) {
                try {
                    System.out.println("t1 get lock");
                    Thread.sleep(10000); // 进入wait状态,但不会释放锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1 over");
            }
        });
        Thread t2= new Thread(()->{
            synchronized (resource) {
                try {
                    System.out.println("t2 get lock");
                    Thread.sleep(10000); // 进入wait状态,但不会释放锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2 over");
            }
        });
        t1.start();
        t2.start();
    }
}

注释写的很清楚了,就不帖结果图了


yield

Thread类有个静态方法 yield ,能让当前正在执行的线程让出时间片,重新处于就绪状态,让 CPU 进行下一轮的 CPU 调度。不是很常用

public class YieldTest {
    static class MyTask implements Runnable{
        @Override
        public void run() {
            for (int i=0;i<3;i++){
                String name = Thread.currentThread().getName();
                if (i==0){
                    Thread.yield(); // 当i=0时,当前线程放弃时间片,cpu进行下一轮调度
                }
                System.out.println(name + " start " + i);
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new MyTask(),"t1").start();
        new Thread(new MyTask(),"t2").start();
        new Thread(new MyTask(),"t3").start();
    }
}

调用 yield 方法时,只是让出当前线程剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时仍可能调度到当前线程执行 。


interrupt

interrupt 是中断的意思,但并不是真的中断线程,只是将线程的中断标志位设为true,线程仍然会继续运行

Thread 类有几个与 interrupt 相关的函数

// 中断线程
public void interrupt() {
    ...
}

//  判断当前线程是否被中断,不清除中断标志位
public boolean isInterrupted() {
    return isInterrupted(false);
}

// 静态方法,判断当前线程是否被中断,并清除中断标志位
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

// 判断当前线程是否被中断,根据ClearInterrupted判断是否清除中断标志位,true代表清除
private native boolean isInterrupted(boolean ClearInterrupted);

清除中断标志位就是将中断标志位设为 false 。下面来看个例子

public class InterrputTest {
    static class MyThread extends Thread {
        @Override
        public void run() {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {  //换成这个试试 Thread.interrupted()
                    System.out.println("线程被中断");
                } else {
                    System.out.println("线程在运行");
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        Thread.sleep(1000);
        myThread.interrupt();
    }
}

先输出 1s 的 '线程在运行',然后一直输出'线程被中断',大家把if中的判断条件换成 Thread.interrupted()试试看结果有什么不一样。

ps : 线程在 sleep() 中或者 wait() 中如果被中断,会抛出 InterruptedException 异常。


守护线程和用户线程

Java 中线程分为两类, daemon 线程(守护线程) 和 user 线程

main 函数所在的线程就是一个用户线程,其实在 JVM 内部同时还启动了许多守护线程,比如垃圾回收线程。

区别是当最后一个用户线程结束时, JVM 就会正常退出,不管守护线程是否结束。

public class DaemonTest {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            for (;;){} // 无限循环
        });

        t.setDaemon(true); // 设置线程为守护线程,注释掉该行看看有啥区别
        t.start();
    }
}

虽然有一个无限循环线程,但是他是守护线程,不影响 JVM 的退出,


原创不易,求点赞