多线程技术学习笔记(尚学堂-百战)

发布时间:2024年01月21日

多线程技术学习笔记(尚学堂-百战)


一、多线程介绍

1.进程

? 执行中的程序叫做进程(Process),是一个动态的概念。其实进程就是一个在内存中独立运行的程序空间。如正在运行的写字板程序就是一个进程。

进程具有如下特点:

  • 进程是程序的一次动态执行过程,占用特定的地址空间。
  • 每个进程由3部分组成: cpu、data、code。每个进程都是独立的,保有自己的cpu时间,代码和数据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西,但这样的缺点是:浪费内存,cpu的负担较重。
  • 多任务(Multitasking)操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每个进程独立运行。以进程的观点来看,它会以为自己独占CPU的使用权。

2.线程

? 一个进程可以产生多个线程。同一进程的多个线程也可以共享此进程的某些资源(比如:代码、数据),所以线程又被称为轻量级进程(lightweight process)。

线程特点:

  • 一个进程内部的一个执行单元。它是程序中的一个单一的顺序控制流程(例如在java里面,jvm是进程,里面的主函数main方法就是线程)。
  • 一个进程可拥有多个并行的(concurrent)线程。
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间,可以访问相同的变量和对象,而且它们从同一堆中分配对象并进行通信、数据交换和同步操作。
  • 由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。
  • 线程的启动、中断、消亡,消耗的资源非常少。

3.并发

? 并发是指在一段时间内同时做多个事情。当有多个线程在运行时,如果只有一个CPU,这
种情况下计算机操作系统会采用并发技术实现并发运行,具体做法是采用”时间片轮询算法”,在一个时间段的线程代码运行时,其它线程处于就绪状。这种方式我们称之为并发(Concurrent)。

4.线程执行特点

? 在java代码中,执行的方法f()里面包含另外一个方法g(),当f()中执行到g()时,程序会等g()执行完后继续执行f()方法后续的代码。
? 在线程中,执行的方法f()里面包含一个线程g(),当f()中执行到g()时,程序不会等待g()执行完,而是g()跟f()并列执行,这就是并发状态。

5. 主线程及其子线程

5.1 主线程

? 当Java程序启动时,一个线程会立刻运行,该线程通常叫做程序的主线程(main thread),即main方法对应的线程,它是程序开始时就执行的。
? Java应用程序会有一个main方法,是作为某个类的方法出现的。当程序启动时,该方法就会第一个自动的得到执行,并成为程序的主线程。也就是说,main方法是一个应用的入口,也代表了这个应用的主线程。JVM在执行main方法时,main方法会进入到栈内存,JVM会通过操作系统开辟一条main方法通向cpu的执行路径,cpu就可以通过这个路径来执行main方法,而这个路径有一个名字,叫main(主)线程。

主线程的特点: 它是产生其他子线程的线程,它不一定是最后完成执行的线程,子线程可能在它结束之后还在运行。

5.2 子线程

在主线程中创建并启动的线程,一般称为子线程

二、线程的创建

1. 通过继承Thread类实现多线程

在java中负责实现线程功能的类是java.lang.Thread类

继承Thread类实现多线程的步骤:

  1. 继承Thread类定义线程类。
  2. 重写Thread类中的run()方法。run()方法也称为线程体。
  3. 实例化线程类并通过start()方法启动线程。

代码:

public class TestThread extends Thread{
    /**
     * 构造方法,当线程被实例化后,我们先去获取线程的名字
     */
    public TestThread(){
        System.out.println(this.getName());
    }
    /**
     * 线程的线程体
     */
    public void run(){
        System.out.println(this.getName()+"→"+"线程开始");
        for (int i=0; i<2;i++){
            System.out.println(this.getName()+"→"+i);
        }
        System.out.println(this.getName()+"→"+"线程结束");
    }

    public static void main(String[] args){
        System.out.println("主线程开始");
        TestThread t1 = new TestThread();
        // 启动线程,通过start方法去启动run方法
        t1.start();
        TestThread t2 = new TestThread();
        // 再次启动一个线程
        t2.start();
        System.out.println("主线程结束");
    }
}

结果截图:
在这里插入图片描述

2.通过实现Runnable接口实现多线程

/**
 * 使用接口Runnable来实现多线程
 */
public class TestThread2 implements Runnable{
    /**
     * 当前线程的线程体方法
     */
    @Override
    public void run() {
        // 直接不能获取到线程名称。需要创建Thread对象然后在去获取到当前线程名称
        System.out.println(Thread.currentThread().getName()+"→"+ "线程开始");
        for (int i= 0; i<29;i++){
            System.out.println(Thread.currentThread().getName()+"→"+ i);
        }
        System.out.println(Thread.currentThread().getName()+"→"+ "线程结束");
    }

    public static void main(String[] args) {
        System.out.println("主线程开始");
        TestThread2 testThread2 = new TestThread2();
        // testThread2不能直接执行start(),所以要先被Thread类封装后再才能启动线程
        Thread thread = new Thread(testThread2);
        thread.start();
        Thread thread1 = new Thread(new TestThread2());
        thread1.start();
        System.out.println("主线程结束");
    }
}

3.线程的执行流程

在这里插入图片描述

4.线程的生命周期

在这里插入图片描述
一个线程对象在它的生命周期内,需要经历5个状态。

  • 新生状态(New)

? 用new 关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。

  • 就绪状态

? 处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于 “线程就
绪队列”
,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。

有4种原因会导致线程进入就绪状态:

  1. 新建线程:调用start()方法,进入就绪状态;
  2. 阻塞线程:阻塞解除,进入就绪状态;
  3. 运行线程:调用yield()方法,直接进入就绪状态;
  4. 运行线程:JVM将CPU资源从本线程切换到其他线程。
  • 运行状态

? 在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。

  • 阻塞状态

? 阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。

有4种原因会导致阻塞:

  1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。
  2. 执行wait(方法,使当前线程进入阻塞状态。当使用nofity(方法唤醒这个线程后,它进入就绪状态。
  3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。
  4. join()线程联合,当某个线程等待另一个线程执行结束后,才能继续执行时,使用join(方法。
  • 死亡状态

? 死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个: 一个是正常运行
的线程完成了它run()方法内的全部工作;另一个是线程被强制终止,如通过执行stopo或destroy0)方法来终止一个线程(注: stop(/destroy()方法已经被JDK废弃,不推荐使用)
? 当线程进入死亡状态后,就不能回到其它状态了。

三、线程的使用

1. 终止线程

? 如果我们想在一个线程中终止另一个线程我们一般不使用JDK提供的stop()/destroy方法(它们本身也被JDK废弃了)。通常的做法是提供一个boolean型的终止变量,当这个变量值为false 时,则终止线程的运行。

public class StopThread implements Runnable{
    // 定义一个变量,去判定线程多久死亡
    private boolean flag = true;
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+ "->" + "线程开始");
        int i=0;
        while (flag){
            System.out.println(Thread.currentThread().getName()+"->"+ i++);
            // 休眠的操作
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        // 验证子线程是否将执行内容执行完后才死亡
        System.out.println(Thread.currentThread().getName()+ "->" + "线程结束");
    }

    // 终止方法
    public void stop(){
        this.flag = false;
    }

    public static void main(String[] args) throws IOException {
        System.out.println("主线程开始");
        StopThread stopThread = new StopThread();
        Thread thread = new Thread(stopThread);
        thread.start();
        // 让主线程进入阻塞状态,当按下回车键退出阻塞
        System.in.read();
        // 结束子线程,将变量改为false
        stopThread.stop();
        System.out.println("主线程结束");
    }
}

2. 暂停当前线程执行sleep/yield

? 暂停线程执行常用的方法有sleep()和yield()方法.
这两个方法的区别是:
sleep()方法: 可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。
yield()方法: 可以让正在运行的线程直接进入就绪状态,让出 CPU的使用权。

2.1 sleep方法的使用
public class SleepThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"->" + "线程开始");
        for (int i=0; i<20; i++){
            System.out.println(Thread.currentThread().getName()+"->"+i);
            // 让线程休眠,进入阻塞状态,休眠时间到就回到就绪状态
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(Thread.currentThread().getName()+"->" + "线程结束");
    }

    public static void main(String[] args) {
        System.out.println("主线程开始");
        // 创建SleepThread对象作为Thread的构造参数
        Thread thread = new Thread(new SleepThread());
        thread.start();
        System.out.println("主线程结束");
    }
}
2.2 yield方法的使用

? yield()方法的作用: 暂停当前正在执行的线程,并执行其他线程。
? yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为让步的线程可能被线程调度程序再次选中。

使用yield方法时要注意的几点:

  • yield是一个静态的方法。
  • 调用yield后,yield告诉当前线程把运行机会交给具有相同优先级的线程。
  • yield不能保证,当前线程迅速从运行状态切换到就绪状态。
  • yield 只能是将当前线程从运行状态转换到就绪状态,而不能是等待或者阻塞状态。
public class YieldThread implements Runnable{
    @Override
    public void run() {
        for(int i=0; i<30; i++){
            if (Thread.currentThread().getName().equals("Thread-0")){
                // 如果是Thread-0线程,那么就暂停执行,执行其他线程
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName()+"->"+i);
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new YieldThread());
        Thread thread1 = new Thread(new YieldThread());
        thread.start();
        thread1.start();
    }
}

3. 线程的联合(由并行执行改为串行执行)

3.1 join方法的使用

? join()方法就是指调用该方法的线程在执行完run()方法后,再执行join方法后面的代码,即将两个线程合并,用于实现同步控制。(类似于方法的调用执行流程)
? 当前线程邀请调用方法的线程优先执行,在调用方法的线程执行结束之前,当前线程不能再次执行。线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。

class A implements Runnable{
    @Override
    public void run() {
        for (int i=0; i<10; i++){
            System.out.println(Thread.currentThread().getName()+ "->"+i);
            // 休眠进入阻塞,休眠时间到进入就绪
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class JoinThread {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new A());
        thread.start();

        for (int i=0; i<10; i++){
            // 联合子线程,当主线程为2时,就联合子线程
            if (i==2){
                thread.join();
            }
            System.out.println(Thread.currentThread().getName()+"->"+i);
            Thread.sleep(100);
        }
    }
}

结果截图:
在这里插入图片描述

3.2 线程联合案例
/**
 * 女友吃饭线程
 */
class FatherThread implements Runnable{
    @Override
    public void run() {
        System.out.println("女朋友饿了,但是发现小小修士没有做饭");
        System.out.println("于是女友喊小小修士做饭");
        Thread thread = new Thread(new MatherThread());
        thread.start();
        System.out.println("女友等待小小修士做好饭");
        try {
            thread.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("女友高兴的吃饭,并对小小修士表示了感谢");
    }
}

/**
 * 小小修士做饭线程
 */
class MatherThread implements Runnable{
    @Override
    public void run() {
        System.out.println("小小修士去厨房做饭");
        System.out.println("小小修士做饭需要半个小时");
        for (int i=0; i<3;i++){
            System.out.println("第"+(i+1)*10 +"分钟");
            try {
                sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("小小修士饭做好了");
    }
}

public class JoinDemo {
    public static void main(String[] args) {
        System.out.println("小小修士给女友做饭的故事");
        Thread thread = new Thread(new FatherThread());
        thread.start();
    }
}

结果截图:
在这里插入图片描述

4. Thread类中的其他常用方法

4.1 获取当前线程名称
  1. 方式一

this.getName()获取线程名称,该方法适用于继承Thread实现多线程方式

  1. 方式二

Thread.currentThread().getName()获取线程名称,该方法适用于实现 Runnable接口实现多线程方式。

4.2 设置线程名称
  1. 方式一

通过构造方法设置线程名称
在这里插入图片描述

  1. 方式二

通过setName()方法设置线程名称

4.3 判断当前线程是否存活

? isAlive(方法:判断当前的线程是否处于活动状态。
? 活动状态是指线程已经启动且尚未终止,线程处于正在运行或准备开始运行的状态,就
认为线程是存活的。

四、线程的优先级

? 每一个线程都是有优先级的,我们可以为每个线程定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5。
? Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。
? 注意:线程的优先级,不是说哪个线程优先执行,如果设置某个线程的优先级高。那就是有可能被执行的概率高。并不是优先执行。

在这里插入图片描述

五、守护线程

在Java中有两类线程:

  • User Thread(用户线程):就是应用程序里的自定义线程。
  • Daemon Thread(守护线程):比如垃圾回收线程,就是最典型的守护线程。

? 守护线程(即Daemon Thread),是一个服务线程,准确地来说就是服务其他的线程,这是它的作用。而其他的线程只有一种,那就是用户线程
守护线程的特点:
? 守护线程会随着用户线程死亡而死亡。
? 用户线程,不随着主线程的死亡而死亡。用户线程只有两种情况会死掉.

  1. 在run中异常终止。
  2. 正常把 run执行完毕,线程死亡。

? 守护线程,随着用户线程的死亡而死亡,当用户线程死亡守护线程也会随之死亡。

5.1 守护线程的使用

在这里插入图片描述

六、线程同步

  • 线程冲突现象

? 同一时间动作一去修改数据库表信息(name,password),动作二去读取表的信息。当动作一修改了name后,时间片到了,此线程就会进入就绪状态从而cpu去加载动作二的线程(读取表的信息),此时读取到的对象就已经出现了数据杂乱,线程冲突。

  • 同步问题的提出

? 现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题。比如:教室里,只有一台电脑,多个人都想使用。天然的解决办法就是,在电脑旁边,大家排队。前一人使用完后,后一人再使用。

  • 线程同步的概念

? 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候,我们就需要用到“线程同步”。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

1. 线程冲突案例演示

? 我们以银行取款经典案例来演示线程冲突现象。银行取钱的基本流程基本上可以分为如下几个步骤:
(1)用户输入账户、密码,系统判断用户的账户、密码是否匹配。
(2)用户输入取款金额
(3)系统判断账户余额是否大于取款金额
(4)如果余额大于取款金额,则取钱成功;如果余额小于取款金额,则取钱失败。

/**
 * 账号类
 */
class Account{
    // 账号
    private String accountNo;
    // 账号余额
    private double balance;

    public Account() {
    }

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
}

/**
 * 取款线程
 */
class DrawThread extends Thread{
    // 账号对象
    private Account account;
    // 取款金额
    private double drawMoney;

    public DrawThread(String name, Account account,double drawMoney){
        super(name);
        this.account = account;
        this.drawMoney = drawMoney;
    }
    @Override
    public void run() {
        // 判断当前账号余额是否大于或等于取款金额
        if (this.account.getBalance() >= this.drawMoney){
            System.out.println(this.getName()+"取钱成功!吐出钞票:"+this.drawMoney);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 更新账号余额
            this.account.setBalance(this.account.getBalance()-this.drawMoney);
            System.out.println("余额为:"+this.account.getBalance());
        }else {
            System.out.println(this.getName()+"取钱失败,余额不足");
        }

    }
}
public class DrawMoneyThread {
    public static void main(String[] args) {
        Account account = new Account("1234",1000);
        // 取钱
        new DrawThread("小小修士",account,800).start();
        new DrawThread("小小修士女友",account,800).start();

    }

}

在这里插入图片描述
做了两个同步操作,导致金额为负(原本是想实现取款金额大于账号余额无法取款),线程冲突(线程1取钱的时候切换到线程2取钱,所以线程2执行的时候账号余额还未扣除,所以线程2判断通过),业务场景实现有问题。

2. 修改线程冲突案例演示

  1. 代码中哪一块代码需要做线程同步?
    取钱的线程块要做线程同步
  2. 让哪些线程具有线程同步的特点?
    当多个线程对同一个账号操作时,那么这些线程就应该同步(线程锁,进入等待),互斥的特点。如果不是同一个账号取款,则这些线程并行。

? 综上,改正代码就是将取款线程的run()线程块的执行代码加到synchronized()中去,然后用账号对象为锁对象,当是同一个账号时就进行同步操作。

public void run() {
        // 线程互斥,以当前账号对象为锁对象,当线程是操作同一个账号时就是同步操作
        synchronized (this.account) {
            // 判断当前账号余额是否大于或等于取款金额
            if (this.account.getBalance() >= this.drawMoney) {
                System.out.println(this.getName() + "取钱成功!吐出钞票:" + this.drawMoney);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 更新账号余额
                this.account.setBalance(this.account.getBalance() - this.drawMoney);
                System.out.println("余额为:" + this.account.getBalance());
            } else {
                System.out.println(this.getName() + "取钱失败,余额不足");
            }
        }
    }

在这里插入图片描述

3. 线程同步的使用

3.1 使用this作为线程对象锁

在不同线程中,相同对象中的synchronized会互斥。
语法结构:
在这里插入图片描述

/**
 * 定义修士类
 */
class Programmer{
    private String name;
    public Programmer(String name){
        this.name = name;
    }
    /**
     * 进入修炼场所
     */
    public void computer() throws InterruptedException {
        // 同一个对象线程互斥(线程等待,等同一个对象的前一个线程完成后再执行)
        synchronized (this) {
            System.out.println(this.name + "打开大门");
            Thread.sleep(100);
            System.out.println(this.name + "点燃静神香");
            Thread.sleep(100);
            System.out.println(this.name + "盘腿坐下");
            Thread.sleep(100);
            System.out.println(this.name + "开始修炼");
        }
    }
    /**
     * 修炼
     */
    public void coding() throws InterruptedException {
        // 同一个对象线程互斥(线程等待,等同一个对象的前一个线程完成后再执行)
        synchronized (this) {
            System.out.println(this.name + "静心");
            Thread.sleep(100);
            System.out.println(this.name + "回忆观想图");
            Thread.sleep(100);
            System.out.println(this.name + "带动灵气环绕经脉");
            Thread.sleep(100);
            System.out.println(this.name + "沉迷修炼状态");
        }
    }
}

/**
 * 进入修炼场所的行为线程
 */
class Open extends Thread{
    // 定义修士
    private Programmer programmer;
    public Open(Programmer programmer){
        this.programmer = programmer;
    }
    @Override
    public void run() {
        try {
            this.programmer.computer();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

/**
 * 修炼的行为线程
 */
class Work extends Thread{
    // 定义修士
    private Programmer programmer;
    public Work (Programmer programmer){
        this.programmer = programmer;
    }
    @Override
    public void run() {
        try {
            this.programmer.coding();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
public class TestSyncThread {
    public static void main(String[] args) {
        Programmer programmer = new Programmer("小小修士");
        // 同一个对象线程互斥(线程等待,等同一个对象的前一个线程完成后再执行)
        new Open(programmer).start();
        new Work(programmer).start();
    }
}

在这里插入图片描述

3.2 使用字符串作为线程对象锁

所有线程在执行synchronized时都会同步。
语法结构:
在这里插入图片描述

/**
 * 定义修士类
 */
class Programmer{
    private String name;
    public Programmer(String name){
        this.name = name;
    }

    /**
     * 炼丹
     */
    public void alchemy() throws InterruptedException {
        // 当所有的线程碰到同一个线程锁对象时,将会由并行改为串行
        synchronized ("abc") {
            System.out.println(this.name + "打开炼丹炉");
            Thread.sleep(100);
            System.out.println(this.name + "打开地火开关");
            Thread.sleep(100);
            System.out.println(this.name + "放入材料");
            Thread.sleep(100);
            System.out.println(this.name + "控制火候开始炼丹");
        }
    }
}


/**
 * 炼丹的行为线程
 */
class Alchemy extends Thread{
    // 定义修士
    private Programmer programmer;
    public Alchemy(Programmer programmer){
        this.programmer = programmer;
    }
    @Override
    public void run() {
        try {
            this.programmer.alchemy();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
public class TestSyncThread {
    public static void main(String[] args) {
        Programmer programmer = new Programmer("小小修士");
        Programmer programmer1 = new Programmer("萧炎");
        // 如果不加线程锁,两个修士使用同一个炼丹炉的动作就会并行
        new Alchemy(programmer).start();
        new Alchemy(programmer1).start();

    }
}

在这里插入图片描述

3.3 使用Class作为线程对象锁

在不同线程中,拥有相同Class对象中的synchronized会互斥。
语法结构:
在这里插入图片描述
多个修士去地火房间炼丹属于并行,地火房间只有一个,会发生冲突。如果是不同的类则并行
在这里插入图片描述

/**
 * 定义修士类
 */
class Programmer{
    private String name;
    public Programmer(String name){
        this.name = name;
    }

    /**
     * 领取地火房间号
     */
    public void to() throws InterruptedException {
        int i = 0;
        synchronized (Programmer.class) {
            System.out.println(this.name + "前往办理点");
            Thread.sleep(100);
            System.out.println(this.name + "申请地火房间");
            Thread.sleep(100);
            System.out.println(this.name + "支付灵石");
            Thread.sleep(100);
            System.out.println(this.name + "前往对应房间");
            Thread.sleep(100);
            System.out.println(this.name + "炼完丹,走人");
        }
    }
    /**
     * 炼丹
     */
    public void alchemy() throws InterruptedException {
        // 当所有的线程碰到同一个线程锁对象时,将会由并行改为串行
        synchronized ("abc") {
            System.out.println(this.name + "打开炼丹炉");
            Thread.sleep(100);
            System.out.println(this.name + "打开地火开关");
            Thread.sleep(100);
            System.out.println(this.name + "放入材料");
            Thread.sleep(100);
            System.out.println(this.name + "控制火候开始炼丹");
        }
    }
}
/**
 * 修士领取地火房间号的行为线程
 */
class To extends Thread{
    // 定义修士
    private Programmer programmer;
    public To(Programmer programmer){
        this.programmer = programmer;
    }

    @Override
    public void run() {
        try {
            this.programmer.to();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

/**
 * 炼丹的行为线程
 */
class Alchemy extends Thread{
    // 定义修士
    private Programmer programmer;
    public Alchemy(Programmer programmer){
        this.programmer = programmer;
    }
    @Override
    public void run() {
        try {
            this.programmer.alchemy();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
public class TestSyncThread {
    public static void main(String[] args) {
        Programmer programmer = new Programmer("小小修士");
        Programmer programmer1 = new Programmer("萧炎");
        // 如果不加线程锁,两个修士使用同一个炼丹炉的动作就会并行
        new To(programmer).start();
        new To(programmer1).start();

    }
}

上面代码再定义一个教导炼丹的老师类,那么就不会发生冲突,并行发生。

3.4 使用自定义对象作为线程对象锁

? 在不同线程中,拥有相同自定义对象中的synchronized会互斥(等待)。
语法结构:
在这里插入图片描述
类似于前面银行取钱的案例。

总结:只用线程运行到线程锁这里来之后,如果线程锁定义的参数与此线程一样,那么该线程就互斥,进入等待队列。

4. 死锁及解决方案

“死锁”指的是:
? 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。
? 因此,某一个同步块需要同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。比如,“化妆线程”需要同时拥有“镜子对象”、“口红对象”才能运行同步块。那么,实际运行时,“小丫的化妆线程”拥有了“镜子对象”,“大丫的化妆线程”拥有了“口红对象”,都在互相等待对方释放资源,才能化妆。这样,两个线程就形成了互相等待,无法继续运行的“死锁状态”。

死锁案例:

/**
 * 药材类
 */
class Lipstick{

}

/**
 * 炼丹炉类
 */
class Mirror{

}

/**
 * 炼丹线程类
 */
class Makeup extends Thread{
    // 定义化妆步骤标签,0,药材;1,炼丹炉
    private int flag;
    private String girlName;
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
    @Override
    public void run() {
        this.doMakeup();
    }

    public void setFlag(int flag) {
        this.flag = flag;
    }

    public void setGirlName(String girlName) {
        this.girlName = girlName;
    }

    /**
     * 开始化妆
     */
    public void doMakeup(){
        if(flag == 0){
            // 当有线程拿到口红后,其他线程就需要等待此线程使用完
            synchronized (lipstick){
                System.out.println(this.girlName+"有药材");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 当此线程拿到镜子后,其他线程就需要等待此线程使用完
                synchronized (mirror){
                    System.out.println(this.girlName+"有炼丹炉");
                }
            }
        }else {
            synchronized (mirror){
                System.out.println(this.girlName+"有炼丹炉");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lipstick){
                    System.out.println(this.girlName+"有药材");
                }
            }
        }
    }
}
public class DeadLockThread {
    public static void main(String[] args) {
        Makeup makeup = new Makeup();
        makeup.setFlag(0);
        makeup.setGirlName("小小修士");
        Makeup makeup1 = new Makeup();
        makeup1.setFlag(1);
        makeup1.setGirlName("大大修士");
        makeup.start();
        makeup1.start();
    }
}

死锁问题的解决:

? 死锁是由于“同步块需要同时持有多个对象锁造成"的,要解决这个问题,思路很简单:就是同一个代码块,不要同时持有两个对象锁(不要嵌套使用,并行线程锁)

七、线程并发协作

? 多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。

1. 角色介绍

  • 生产者

生产者指的是负责生产数据的模块

  • 消费者

消费者指的是负责处理数据的模块

  • 缓冲区

消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”。消费者从“缓冲区”拿要处理的数据。
在这里插入图片描述

缓冲区是实现并发的核心,缓冲区的设置有两个好处:

  1. 实现线程的并发协作

? 有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离,解除了生产者与消费者之间的耦合。

  1. 解决忙闲不均,提高效率

? 生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据。

2. 实现生产者与消费者模式

步骤:

  1. 定义缓冲区类,里面有数组、存取方法操作
  2. 定义生产者线程类,将数据对象存进缓冲区(有线程锁、线程释放和线程唤醒操作)
  3. 定义消费者线程类,将数据对象从缓冲区取出(有线程锁、线程释放和线程唤醒操作)
  4. 最后声明一个缓冲区对象,然后执行生产者和消费者的两个线程类。
class ManTou{
    private int id;
    public ManTou(int id){
        this.id = id;
    }
    public int getId(){
        return id;
    }
}

/**
 * 定义缓冲区类
 */
class SyncStack{
    // 定义存放数据的盒子
    private ManTou[] mt = new ManTou[10];
    // 定义操作盒子的索引
    private int index;

    /**
     * 放数据,该方法有wait()方法,当调用这个线程时,如果线程阻塞了就会释放,让其他线程执行,避免线程死锁
     * 有线程锁,如果同时有多个线程来进行操作,将会进入等待队列,等待前面线程执行完毕
     */
    public synchronized void push(ManTou manTou) throws InterruptedException {
        // 判断盒子是否已满,如果满了就释放,让该线程进入阻塞状态
        while(this.index == this.mt.length){
            /**
             *  语法:wait(),该方法必须要在synchronized块中调用
             *  如果当前线程阻塞了,wait()执行后线程会将持有的对象锁释放,进入阻塞状态
             *  其他需要该对象锁的线程就可以继续运行了
             */
            this.wait();
        }
        // 唤醒取数据的线程
        /**
         * 语法: 该方法必须要在synchronized块中调用
         * 该方法会唤醒处于等待状态队列中的一个线程
         */
        this.notify();
        this.mt[this.index] = manTou;
        this.index++;
    }
    /**
     * 取数据
     */
    public synchronized ManTou pop() throws InterruptedException {
        while (this.index ==  0){
            /**
             *  语法:wait(),该方法必须要在synchronized块中调用
             *  如果当前线程阻塞了,wait()执行后线程会将持有的对象锁释放,进入阻塞状态
             *  其他需要该对象锁的线程就可以继续运行了
             */
            this.wait();
        }
        // 唤醒存数据的线程
        /**
         * 语法: 该方法必须要在synchronized块中调用
         * 该方法会唤醒处于等待状态队列中的一个线程
         */
        this.notify();
        this.index--;
        return this.mt[this.index];
    }

}

/**
 * 定义生产者线程类
 */
class ShengChan extends Thread{
    private SyncStack ss;
    public ShengChan(SyncStack ss){
        this.ss = ss;
    }

    @Override
    public void run() {
        for (int i=0; i<10;i++){
            System.out.println("存数据:"+i);
            ManTou manTou = new ManTou(i);
            try {
                this.ss.push(manTou);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
/**
 * 定义消费者线程类
 */
class XiaoFei extends Thread{
    private SyncStack ss;
    public XiaoFei(SyncStack ss){
        this.ss = ss;
    }

    @Override
    public void run() {
        for (int i=0; i<10; i++){
            try {
                ManTou manTou = this.ss.pop();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("取出数据:" + i);
        }
    }
}
public class ProduceThread {
    public static void main(String[] args) {
        SyncStack ss = new SyncStack();
        new ShengChan(ss).start();
        new XiaoFei(ss).start();
    }
}

3. 总结

线程并发协作(也叫线程通信).

生产者消费者模式:
1.生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖。互为条件
2.对于生产者,没有生产产品之前,消费者要进入等待状态。而生产了产品之后,又需要马上通知消费者消费。
3.对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。
4.在生产者消费者问题中,仅有synchronized是不够的。synchronized可阻止并发更新同一个共享资源,实现了同步但是synchronized不能用来实现不同线程之间的消息传递《通信)。

在这里插入图片描述

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