多线程

185 阅读9分钟

前言

随着计算机技术的发展,人们对并发编程的需求越来越高,从而引入了进程,然而进程的创建、销毁、调度均需要很大的开销(资源分配/回收的开销比较大),因此又引入了更轻量级的进程即线程。

进程和线程的关系

  1. 进程是程序的执行实例,是一个独立的运行环境,拥有独立的内存空间、文件描述符等系统资源。一个进程可以包含多个线程。
  2. 线程是进程内执行任务的最小单位。一个进程可以拥有多个线程,这些线程共享进程的内存空间和系统资源,共享内存空间意味着在一个线程中创建的变量,在另外的线程中也可以使用;共享文件描述符意味着在线程1中打开的文件等,在线程2、3、4中也可以直接使用。
  3. 线程存在于进程的上下文中,它们共享进程的地址空间和资源。因此,多线程程序的开发相对于多进程程序来说更加高效和灵活,因为线程之间的通信和数据共享更加简单快速。
  4. 不同进程之间的通信一般需要使用进程间通信(Inter-Process Communication, IPC)机制,如管道、消息队列、共享内存等。而线程之间的通信则可以直接读写共享内存变量来实现。
  5. 线程是操作系统调度执行的基本单位。

举例说明:

当前任务:生产包子 进程向系统申请资源,包括房子,电力,人力,生产包子的机器等资源。 当进程中只有一个线程时,相当于只有部分房子、电力、人力、生产包子的机器在工作,其余空闲,当多线程同时工作时,会尽可能的利用到所有的资源,从而实现高效率。

进程控制块(PCB Process Control Block)

PCB是用以记录与进程相关信息的主存区,是进程存在的唯一标志。

PCB中的信息:

1. 进程标识符(Process Identifier,PID)

作用:用于唯一的标识一个进程

2. 处理机状态

处理机状态信息,也被称为处理机的上下文,主要是由处理机的各种寄存器中的内容组成的。

其也是中断现场的保留区,当进程被切换时,处理机状态信息必须被保存在相应的PCB中,以便进程在重新执行时能再从断点继续执行。

3. 进程调度信息

在OS进行调度时,必须了解进程的状态以及有关进程调度的信息,这些信息包括:

① 进程状态

就绪、执行、阻塞等,是进程调度和对换的依据;

② 进程优先级

是分配CPU 的重要依据

③ 其他信息

CPU已执行时间总和,进程已等待CPU时间的总和等;

④ 事件

即阻塞原因(进程从执行状态转变为阻塞状态的原因)

4. 进程控制信息

① 程序和数据的首地址

② 进程同步和通信机制

③ 资源清单

④ 链接指针

PCB 的三种组织方式:

① 线性方式

系统中所有PCB都组织在一张线性表中,表的首地址存放在内存专用区

② 链接方式

具有相同状态进程的PCB分别通过PCB中的链接字链接成一个队列。

③ 索引方式

系统根据所有进程状态的不同,建立几张索引表,并把索引表的首地址存放在专用的内存中,在每个索引表的表目中,记录具有相应的状态的PCB在PCB表中的首地址。

操作系统调度线程时,线程间“抢占式执行”。

实现多线程的方式

  1. 继承Thread类,重写run方法。
package thread;


class MyThread extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("hello world");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();

        while(true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 实现runnable接口,重写run方法
// 实现Runnable接口,重写run方法
    //Runnable的作用是描述“一个要执行的任务”,而run方法内部描述了任务的具体内容。
    class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("hello runnable");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        Runnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        t.start();

        while(true){
            System.out.println("hello world");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

实现runnable接口,重写run方法可以实现解耦合的作用。

  1. 使用匿名内部类,重写run方法
public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                while(true){
                    System.out.println("hello inner thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
        while(true){
            System.out.println("hello world");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 使用匿名内部类实现runnable接口,重写run方法
public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello inner runnable");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
        while(true){
            System.out.println("hello world");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. 使用lambda表达式来实现(推荐使用的方式) Lambda表达式的基本语法如下:

(parameters) -> expression

(parameters) -> { statements; }

其中,参数列表(parameters)指定了Lambda表达式需要接收的参数,可以包含多个参数,也可以为空。箭头(->)将参数列表与Lambda表达式的主体分隔开来。

Lambda表达式的主体可以是一个表达式(expression)或一个代码块(block)。如果只有一个表达式,可以直接写在箭头后面,并且该表达式的结果将自动成为Lambda表达式的返回值。如果主体是一个代码块,则需要用花括号将代码块括起来,并且需要手动指定返回值(如果有返回值的话)。

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(true){
                System.out.println("hello lambda");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        while(true){
            System.out.println("hello world");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

lambda表达式的一个示例

public class Test {
    public static void main(String[] args) {
        interfaceTest interfaceTest =(int a,int b) -> {
            System.out.println(a+b);
        };
        interfaceTest.get(1,3);

    }


}
interface interfaceTest{
    public abstract void get(int a,int b);
}

给线程起名字:

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello myThread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"myThread");
        t.start();
    }
}

image.png 关于后台线程:

代码里手动创建的线程默认都是前台线程,包括main线程,其他JVM自带的线程均为后台线程,我们可以通过setDaemon()来将一个线程设置成后台线程。

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello myThread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"myThread");
        t.setDaemon(true);
        t.start();
    }
}

image.png

此时进程的结束只取决于其他前台线程的结束时间,在本例中,也就是main方法结束时进程就结束了。

isAlive()方法描述的是线程中的任务是否完成,未完成时返回true,完成后返回false,与创建的变量t的存活时间无关。

线程终止:

1.通过控制标志位来控制线程

public class ThreadDemo7 {
    private static boolean flag = true;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (flag) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;
    }
}

自定义变量这种方法,不能及时响应,尤其是sleep执行的时间比较久的情况下。

  1. 使用Thread自带的标志位来控制终止。
public class ThreadDemo8 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            //isInterrupted 为 true 代表收到终止命令
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt();
    }
}

image.png

interrupt会做两件事:

1 将t线程内部的中断标志位(Boolean)设置为true

2 如果t线程在sleep,那么就触发sleep中的异常,唤醒sleep

但是sleep在被唤醒的时候,其会将刚才的标志位重新设置成为false,即清空标志位。

sleep清空标志位的作用:即在收到中断请求后,是否真的要终止程序,由后续代码来决定。

image.png

image.png

等待一个线程

public class ThreadDemo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("hello thread");
            }
        });
        t.start();

        System.out.println("join之前");
        t.join();
        System.out.println("join之后");
    }
}

image.png

若在main线程执行join时,t线程已经执行结束,那么main线程则无需等待t线程。

image.png

image.png

Thread类中的一些常用方法:

public static Thread currentThread()

在哪个线程中调用就返回哪个线程的实例。

public static void sleep()

让线程休眠,本质是让这个线程不参与cpu的调度了。

Java中线程的状态:

NEW 创建了thread对象,但是还没有调用satrt,也就是说在内核中还没有创建pcb

TERMINATED 表示内核中的pcb已经执行完毕了,但是thread对象还没有销毁。

RUNNABLE 可运行的 (一种是正在cpu中执行,一种是在就绪队列中,随时可以被调用执行)

WAITING、TIMED_WAITING、BLOCKED 都代表阻塞状态,只是引起阻塞的原因不同。

public class ThreadDemo2 {
    public static void main(String[] args) {
        for (Thread.State state: Thread.State.values()
             ) {
            System.out.println(state);
        }
    }
}

image.png

状态转换:

image.png

多线程带来的风险:线程安全

//线程安全问题
public class ThreadDemo5 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2= new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("count = " + counter.count);

    }


}

class Counter{
    public int count = 0;

    public void add(){
        this.count++;
    }
}

image.png

代码的执行结果并不是预期的10_0000。

为何会出现这种情况:

image.png

image.png

image.png

出现线程安全问题的原因:

  1. 根本原因:抢占式执行,随机调度
  2. 代码结构性问题:多个线程修改同一个变量
  3. 原子性:一个操作要么全部执行成功并且对外部环境不可见,要么完全不执行,没有中间状态。
  4. 内存可见性:当一个线程或进程对共享的变量进行修改后,另一个线程或进程能否立即看到这个修改的结果。
  5. 指令重排序:由于指令重排序的存在,可能会导致对共享变量的读写操作在不同线程或进程中的顺序出现差异,进而引发内存可见性问题。

通过加锁将非原子的操作转化为“原子”的操作

Synchronized 关键字

用于实现线程间的同步,确保共享资源的访问是有序的,从而避免出现并发访问的问题。

synchronized的使用方法:

1.修饰普通方法:锁对象是this

2.修饰静态方法:锁对象是类对象

3.修饰代码块:显示/手动指定锁对象

synchronized的特性:

1)互斥

synchronized 会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象 synchronized就会阻塞等待.

● 进入synchronized修饰的代码块,相当于加锁

● 退出synchronized修饰的代码块,相当于解锁

2)可重入

一个线程针对同一个对象,连续进行两次加锁是否会有问题,如果没有问题,那么该锁称为“可重入锁”,如果有问题,那么称为“不可重入锁”。

Java标准中的线程安全类:

多线程中不安全:

image.png

多线程中安全:

image.png

还有的虽然没有加锁,但是没有涉及到“修改”,仍然是线程安全的:

String

死锁:

出现的原因:

  1. 一个线程,一把锁,连续锁两次,如果锁是不可重入锁,那么就会导致死锁。 Java中提供的synchronized和reentrantlock都是可重入锁,因此不存在这个问题。

  2. 两个线程,两把锁,首先t1和t2分别对对象A和对象B进行加锁,然后分别尝试获取对象B和对象A,此时就会导致死锁。

public class ThreadDemo8 {
    public static void main(String[] args) {
       Object A = new Object();
       Object B = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (A){
                //确保对A加锁成功
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                synchronized (B){
                    System.out.println("线程1获得了A和B两个对象");
                }
            }

        });
        Thread t2 = new Thread(() -> {
            synchronized (B){
                //确保对B加锁成功
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                synchronized (A){
                    System.out.println("线程2获得了A和B两个对象");
                }
            }

        });
        t1.start();
        t2.start();
    }
}

打开jconsole,可以查看到线程的运行状态:

image.png

  1. 多个线程,多把锁的情况:典型问题:哲学家就餐问题:

image.png

image.png

当每个人都拿起自己左手边的筷子时就会出现极端情况,死锁。

解决办法:给锁编号,然后规定一个固定的顺序,比如从小到大给锁编号。

对于刚才的哲学家就餐问题中,1号哲学家拿起1号筷子,2号拿起2号,以此类推,到了第五个哲学家时,按照规定,他只能拿自己面前较小的那把锁,也就是1号筷子,此时1号筷子正在被使用,所以5号哲学家就只能进入阻塞等待,此时4号哲学家面前两把筷子都在就可以吃到面条,等四号吃完,3、2、1号哲学家也可以就餐,1号就餐结束后,5号哲学家也可以就餐了。

死锁的四个条件:互斥使用、不可抢占、请求和保持、循环等待

Volatile 关键字 只能修饰变量

内存可见性问题:

public class ThreadDemo9 {
    public static void main(String[] args) {
        MyCounter myCounter = new MyCounter();
        Thread t1 = new Thread(() -> {
            while(myCounter.flag == 0){

            }
            System.out.println("线程1循环结束!");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            myCounter.flag = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

class MyCounter{
    public int flag = 0;
}

执行结果:

image.png

循环等待,程序并未结束。

这就是一个典型的内存可见性问题,在一个线程中对一个变量进行读取,在另外一个线程中对相同的变量进行修改,此时读到的变量可能是未修改的。(根本原因是在多线程环境下,jvm/编译器在进行优化时产生了误判,也就是在每次循环比较的时候,并未真正的重新从内存中获取flag的值)。

解决办法:volatile关键字,给flag变量加上volatile关键字相当于告诉编译器该变量是“易变的”,此后每次使用该变量时都会重新从内存中加载,从而避免内存可见性问题。

class MyCounter{
    public volatile int flag = 0;
}

修改代码后的运行结果:

image.png

wait 和 notify

wait会干三件事:

  1. 释放锁
  2. 阻塞等待
  3. 等接到notify的通知时,重新尝试获取锁,然后继续执行

wait和notify只有对相同的对象使用时才能发挥作用。

public class threadDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (object){
                System.out.println("wait 之前");
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait 之后");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (object){
                object.notify();
            }
        });

        t1.start();
        Thread.sleep(500);
        System.out.println("notify 之前");
        t2.start();
        System.out.println("notify 之后");
    }
}

image.png

练习题:三个线程依次打印A、B、C。