JavaOOP 第十章 多线程

Java 多线程

文章目录

  • Java 多线程
        • 一、学习目标
        • 二、进程和线程
          • 进程
          • 线程
        • 三、多线程
          • 1、Java 多线程编程
          • 2、进程与线程的关系
        • 四、`java.lang.Thread`
        • 五、主线程
        • 六、线程的创建和启动
        • 七、继承Thread类创建线程
          • 使用继承Thread方式实现
        • 八、线程休眠
          • 线程休眠的作用:
          • 继承Thread类创建单线程
        • 九、继承Thread类创建多线程
        • 十、实现Runnable接口创建线程
        • 十、扩展 实现Callable接口和线程池
        • 十一、线程的状态
        • 十二、线程调度
        • 十三、线程优先级
        • 十四、线程的强制运行
        • 十五、线程的礼让
        • 十六、比较sleep()方法和yield()方法
        • 十七、多线程共享数据引发的问题
        • 十八、线程同步
        • 十九、同步代码块
        • 二十、同步方法
        • 二十一、线程安全的类型
        • 二十二、本章总结

一、学习目标
  1. 理解线程的基本概念
  2. 掌握线程创建和启动
  3. 掌握线程调度的常用方法
  4. 掌握线程同步的方式
  5. 理解线程安全的类型
二、进程和线程
  1. 进程
    • 应用程序的执行实例
    • 有独立的内存空间和系统资源
  2. 线程
    • CPU调度和分派的基本单位
    • 进程中执行运算的最小单位,可完成一个独立的顺序控制流程

    JavaOOP 第十章 多线程_第1张图片

三、多线程
1、Java 多线程编程
  1. 如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为“多线程” 多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
  2. 多个线程交替占用CPU资源,而非真正的并行执行
  3. Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
  4. 这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
  5. 多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。同时简化了编程模型,给用户带来良好的体验
2、进程与线程的关系

JavaOOP 第十章 多线程_第2张图片

四、java.lang.Thread

支持多线程编程

常用方法

方法 描述 类型
Thread() 创建Thread对象 构造方法
Thread(Runnable target) 创建Thread对象,target为run()方法被调用的对象 构造方法
Thread(Runnable target , String name) 创建Thread对象,target为run()方法被调用的对象,name为新线程的名称 构造方法
void run() 执行任务操作的方法 实例方法
void start() 使该线程开始执行,JVM将调用该线程的run()方法 实例方法
void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行) 静态方法
Thread currentThread() 返回当前线程对象的引用 静态方法

五、主线程

主线程:

  • Java程序启动时,一个线程立即随之启动,通常称之为程序的主线程
  • main()方法即为主线程入口
  • 产生其他子线程的线程
  • 必须最后完成执行,因为它执行各种关闭动作

使用Thread类的方法获取主线程信息

package com.aiden.demo1;

public class MainThreadTest {
    /**
     * 演示示例1:显示主线程名
     */
    public static void main(String[] args) {
        Thread t = Thread.currentThread();
        System.out.println("当前线程:" + t.getName());
        t.setName("MainThread");//设置线程名
        System.out.println("当前线程:" + t.getName());
    }
}

运行结果

当前线程:main
当前线程:MainThread
六、线程的创建和启动

Java中创建线程的方式

  • 继承java.lang.Thread
  • 实现java.lang.Runnable接口
  • 实现Callable接口
  • 使用线程池

使用线程的步骤

JavaOOP 第十章 多线程_第3张图片

七、继承Thread类创建线程
使用继承Thread方式实现

步骤:

  • 自定义线程类继承自Thread类
  • 重写run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程

代码:

//继承自Thread类
public class MyThread extends Thread {
    //省略成员变量和成员方法代码......
    //重写Thread类中run()方法
    @Override
    public void run() {  
        //线程执行任务的代码
    }
}
public class ThreadTest {
    public static void main(String[] args)
        MyThread myThread = new MyThread();
        myThread.start();//启动线程
    }
}
八、线程休眠
线程休眠的作用:
  • 让线程暂时睡眠指定时长,线程进入阻塞状态
  • 睡眠时间过后线程会再进入可运行状态

语法:

public static void sleep(long millis)

示例:

public class WaitDemo {
    public static void sleepBySec(long s) {
        for (int i = 0; i < s; i++) {
            System.out.println(i + 1 + "秒");
            try {
				Thread.sleep(1000); 
            } catch (InterruptedException e) {
	 			e.printStackTrace();
            }
        }
    }
}

注意事项:

  1. millis 为休眠时长,以毫秒为单位
  2. 调用sleep( )方法需处理InterruptedException异常

继承Thread类创建单线程

需求:

星沐生态农场果树成熟了,一位果农采摘5棵苹果树,输出采摘进度,使用继承Thread类的方式创建线程类

实现代码:

package com.aiden.demo2;
//继承Thread类创建单线程
public class WorkThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始采摘苹果树:");
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "进度:第" + (i + 1) + "棵");
            try {
                sleep(1000);//休眠1000毫秒也就是1秒钟
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "已完成采摘任务!");
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        WorkThread thread = new WorkThread();
        thread.setName("果农A");
        thread.start();
    }
}

运行结果:

果农A开始采摘苹果树:
果农A进度:第1棵
果农A进度:第2棵
果农A进度:第3棵
果农A进度:第4棵
果农A进度:第5棵
果农A已完成采摘任务!

注意事项:

  1. 已启动的线程对象不能重复调用start()方法,否则会抛出IllegalThreadStateException异常
  2. 如果调用sleep()方法控制线程休眠时间的线程,被其他线程中断,则会产生InterruptedException异常。
九、继承Thread类创建多线程

继承Thread类创建多线程

  1. 多个线程交替执行,不是真正的“并行”
  2. 线程每次执行时长由分配的CPU时间片长度决定

示例:

  • 两位果农同时进行采摘,并显示采摘进度

分析:

  • 每位果农作为一个线程,开启新的线程

代码:

package com.aiden.demo3;

public class WorkThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始采摘苹果树:");
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "进度:第" + (i + 1) + "棵");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "已完成采摘任务!");
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        WorkThread t1 = new WorkThread();
        t1.setName("果农A");
        WorkThread t2 = new WorkThread();
        t2.setName("果农B");
        //调用 start()方法后,每个线程独立完成各自的操作,相互间没有影响,并行执行
        thread.start();
        thread2.start();
    }
}

运行结果:

果农B开始采摘苹果树:
果农B进度:第1棵
果农A开始采摘苹果树:
果农A进度:第1棵
果农A进度:第2棵
果农B进度:第2棵
果农B进度:第3棵
果农A进度:第3棵
果农A进度:第4棵
果农B进度:第4棵
果农A进度:第5棵
果农B进度:第5棵
果农B已完成采摘任务!
果农A已完成采摘任务!

常见问题

  • 启动线程是否可以直接调用run()方法?

代码:

public class ThreadTest {
    public static void main(String[] args) {
        WorkThread t1 = new WorkThread();
        t1.setName("果农A");
        WorkThread t2 = new WorkThread();
        t2.setName("果农B");
        //用run()替换start(),属于单线程执行模式
        thread.run();
        thread2.run();
    }
}

运行结果:

main开始采摘苹果树:
main进度:第1棵
main进度:第2棵
main进度:第3棵
main进度:第4棵
main进度:第5棵
main已完成采摘任务!
main开始采摘苹果树:
main进度:第1棵
main进度:第2棵
main进度:第3棵
main进度:第4棵
main进度:第5棵
main已完成采摘任务!

注意:

  • 线程对象调用start()方法是启动线程,run()方法是实例方法,在实际应用中切不要混淆

JavaOOP 第十章 多线程_第4张图片

十、实现Runnable接口创建线程

Runnable接口

  • Runnable接口位于java.lang包
  • 只提供一个抽象方法run()的声明

实现步骤

  1. 定义MyRunnable类实现Runnable接口
  2. 实现run()方法,编写线程执行体
  3. 创建线程对象,调用start()方法启动线程

需求:

  • 使用实现Runnable接口的方式创建线程,实现多人采摘

实现代码:

package com.aiden.demo4;

//实现Runnable接口方式创建线程类
public class WorkThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始采摘苹果树:");
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "进度:第" + (i + 1) + "棵");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "已完成采摘任务!");
    }
}
public class RunnableTest {
    public static void main(String[] args) {
        WorkThread work1 = new WorkThread();
        Thread thread1 = new Thread(work1);
        WorkThread work2 = new WorkThread();
        Thread thread2 = new Thread(work2);
        thread1.start();
        thread2.start();
    }
}

运行结果:

Thread-0开始采摘苹果树:
Thread-1开始采摘苹果树:
Thread-0进度:第1Thread-1进度:第1Thread-0进度:第2Thread-1进度:第2Thread-0进度:第3Thread-1进度:第3Thread-1进度:第4Thread-0进度:第4Thread-1进度:第5Thread-0进度:第5Thread-0已完成采摘任务!
Thread-1已完成采摘任务!

比较两种创建线程的方式

继承Thread类

  1. 编写简单,可直接操作线程
  2. 适用于单继承

实现Runnable接口

  1. 避免单继承局限性
  2. 便于共享资源

经验:

  • 推荐使用实现Runnable接口方式创建线程
十、扩展 实现Callable接口和线程池

实现Callable接口

  • 与使用Runnable相比, Callable功能更强大些
  • 实现的call()方法相比run()方法,可以返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果
  • Future接口可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutureTask是Futrue接口的唯一的实现类
  • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future
  • 得到Callable的返回值

示例:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author Aiden
 */
public class CallableTest implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) {
        CallableTest callableTest = new CallableTest();
        FutureTask<Integer> futureTask = new FutureTask<>(callableTest);
        new Thread(futureTask, "方式三").start();

        try {
            Integer sum = futureTask.get();
            System.out.println("返回值Sum:"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

运行结果:

Callable方式:0
Callable方式:1
Callable方式:2
Callable方式:3
Callable方式:4
Callable方式:5
Callable方式:6
Callable方式:7
Callable方式:8
Callable方式:9
返回值Sum:45

使用线程池:

  • 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

好处:

  1. 提高响应速度(减少了创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  3. 便于线程管理

代码示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author Aiden
 */
public class ThreadPool implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor executor = (ThreadPoolExecutor) pool;
        /**
         * corePoolSize:核心池的大小
         * maximumPoolSize:最大线程数
         * keepAliveTime:线程没任务时最多保持多长时间后会终止
         */
        executor.setCorePoolSize(5);

        executor.execute(new ThreadPool());
        executor.execute(new ThreadPool());
        executor.execute(new ThreadPool());
        executor.execute(new ThreadPool());
    }
}

运行结果:

pool-1-thread-1:0
pool-1-thread-4:0
pool-1-thread-4:1
pool-1-thread-4:2
pool-1-thread-4:3
pool-1-thread-4:4
pool-1-thread-3:0
pool-1-thread-2:0
pool-1-thread-3:1
pool-1-thread-1:1
pool-1-thread-3:2
pool-1-thread-3:3
pool-1-thread-3:4
pool-1-thread-2:1
pool-1-thread-1:2
pool-1-thread-2:2
pool-1-thread-1:3
pool-1-thread-1:4
pool-1-thread-2:3
pool-1-thread-2:4
十一、线程的状态

通常,线程的生命周期有五种状态

JavaOOP 第十章 多线程_第5张图片

处于运行状态的线程会让出CPU控制权

  • 线程运行完毕
  • 有比当前线程优先级更高的线程抢占了CPU
  • 线程休眠
  • 线程因等待某个资源而处于阻塞状态
十二、线程调度

指按照特定机制为多个线程分配CPU的使用权

每个线程执行时都具有一定的优先级

常用的线程操作方法

说 明
int getPriority() 返回线程的优先级
void setPrority(int newPriority) 更改线程的优先级
boolean isAlive() 测试线程是否处于活动状态
void join() 进程中的其它线程必须等待该线程终止后才能执行
void interrupt() 中断线程
void yield() 暂停当前正在执行的线程对象,并执行其他线程
十三、线程优先级

线程优先级

  • 线程优先级由1~10表示,1最低,默认优先级为5
  • 优先级高的线程获得CPU资源的概率较大
  • 使用Thread类的静态常量设置线程的优先级
    • MAX_PRIORITY:值是10,表示优先级最高
    • MIN_PRIORITY:值是1,表示优先级最低
    • NORM_PRIORITY:值是5,表示普通优先级

需求:

  • 假设有3位果农采摘苹果树,每位果农采摘50棵苹果树,间隔为10毫秒,果农采摘速度有低、中、高的级别区分,输出果农的采摘进度

代码实现:

package com.aiden.demo6;

//实现Runnable接口方式创建线程类
public class WorkThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始采摘苹果树:");
        for (int i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName() + "进度:第" + (i + 1) + "棵" + ",优先级:" + Thread.currentThread().getPriority());
            try {
                Thread.sleep(10);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "已完成采摘任务!");
    }
}

测试运行:

public class PriorityTest {
    public static void main(String[] args) {
        //通过构造方法设置线程名
        Thread t1 = new Thread(new WorkThread(), "果农A");
        Thread t2 = new Thread(new WorkThread(), "果农B");
        Thread t3 = new Thread(new WorkThread(), "果农C");
        //设置线程优先级
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.NORM_PRIORITY);
        t3.setPriority(Thread.MIN_PRIORITY);
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

果农B开始采摘苹果树:
果农C开始采摘苹果树:
果农A开始采摘苹果树:
果农C进度:第1,优先级:1
果农B进度:第1,优先级:5
果农A进度:第1,优先级:10
果农A进度:第2,优先级:10
果农B进度:第2,优先级:5
果农C进度:第2,优先级:1
//.................
果农A进度:第49,优先级:10
果农C进度:第49,优先级:1
果农B进度:第49,优先级:5
果农A进度:第50,优先级:10
果农B进度:第50,优先级:5
果农C进度:第50,优先级:1
果农A已完成采摘任务!
果农B已完成采摘任务!
果农C已完成采摘任务!

注意:

  • 尽管为线程设定了不同的优先级,但实际上并不能精确控制这些线程的执行先后顺序,在不同的计算机或同一计算机不同时刻中运行本程序,都会得到不同的执行序列。
十四、线程的强制运行

使当前线程暂停执行,等待其他线程结束后再继续执行本线程

join()方法的重载方法

public final void join()
public final void join(long mills)
public final void join(long mills, int nanos)

millis:以毫秒为单位的等待时长
nanos:要等待的附加纳秒时长
需处理InterruptedException异常

需求:

农场为果商和商超各供应10车水果,同时发货。当向散户果商供应了5箱水果后,暂停向散户果商供货,优先供应大型商超。完成紧急任务后,继续正常发货

主线程代表果商,子线程代表大型商超

实现步骤

  1. 代表果商的主线程和代表大型商超的子线程交替执行
  2. 向果商供应了5车水果后,执行t.join()方法,子线程会夺得CPU使用权,优先执行
  3. 子线程全部执行完毕后,代表果商的主线程恢复运行

实现代码

package com.aiden.demo7;

//大型商超采购线程
public class JoinThread implements Runnable {
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "采购进度:第" + (i + 1) + "车");
        }
    }
}
//测试类
public class JoinThreadTest {
    public static void main(String[] args) {
        //创建子线程并启动
        Thread t = new Thread(new JoinThread(), "大型商超");
        t.start();
        Thread.currentThread().setName("果商");//修改主线程名称
        //正常采购
        for (int i = 0; i < 10; i++) {
            if (i == 5) {
                try {
                    //阻塞主线程,子线程强制执行
                    t.join();//需处理InterruptedException异常
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "采购进度:第" + (i + 1) + "车");
        }
    }
}

运行结果

果商采购进度:第1车
大型商超采购进度:第1车
果商采购进度:第2车
大型商超采购进度:第2车
果商采购进度:第3车
大型商超采购进度:第3车
果商采购进度:第4车
大型商超采购进度:第4车
大型商超采购进度:第5车
果商采购进度:第5车
大型商超采购进度:第6车
大型商超采购进度:第7车
大型商超采购进度:第8车
大型商超采购进度:第9车
大型商超采购进度:第10车
果商采购进度:第6车
果商采购进度:第7车
果商采购进度:第8车
果商采购进度:第9车
果商采购进度:第10
十五、线程的礼让

线程的礼让

  • 暂停当前线程,允许其他具有相同优先级的线程获得运行机会
  • 该线程处于就绪状态,不转为阻塞状态
  • yield()方法定义

注意事项:

  • 只是提供一种可能,但是不能保证一定会实现礼让

语法:

public static void yield()

问题:

  • 假设有3个小朋友来农场品尝苹果,每人5块。所有苹果放在一个盘子里,每人品尝完一块后,都会把下一次品尝的机会让给其他小朋友

分析:

  • 调用Thread.yield()方法
  • 当每个小朋友品尝完一块苹果后,都会把下一次机会让给其他小朋友
  • 执行Thread.yield()方法后,多个线程间交替执行较为频繁,可以提高程序的并发性

代码实现:

package com.aiden.demo8;

public class ChildThread implements Runnable {
    public void run() {
        for (int i = 0; i < 5; i++) {
            Thread.yield();//线程礼让
            System.out.println(Thread.currentThread().getName() + "品尝:第" + (i + 1) + "块");
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ChildThread(), "Child1");
        Thread t2 = new Thread(new ChildThread(), "Child2");
        Thread t3 = new Thread(new ChildThread(), "Child3");
        t1.start();
        t2.start();
        t3.start();
    }
}
十六、比较sleep()方法和yield()方法

共同点

  • Thread类的静态方法
  • 会使当前处于运行状态的线程放弃CPU使用权,将运行机会让给其他线程

不同点

  • sleep()方法会给其他线程运行机会,不考虑其他线程的优先级,因此较低优先级线程可能会获得运行机会
  • yield()方法只会将运行机会让给相同优先级或者更高优先级的线程
  • 调用sleep()方法需处理InterruptedException异常,而调用yield()方法无此要求
十七、多线程共享数据引发的问题

多线程共享数据引发的问题

  • 商场里,两位顾客同时都需要到同一个试衣间试衣服,而试衣间同一时刻只能容纳一人,怎么办?

示例:

  • 为了保证水果新鲜,仓库最多暂存3车水果。当仓库的水果全部都发货后,会有运输车将果农们采摘的水果成箱地运送到仓库

    JavaOOP 第十章 多线程_第6张图片

**分析: **编码实现果园供货和仓库发货的过程

  1. 定义仓库类Storage
  2. 定义果商类BusiThread,创建果商线程
  3. 定义果农类FarmerThread,创建果农线程
  4. 定义测试类,创建线程对象
package com.aiden.demo9;

//1.定义仓库类
public class Storage {
    public final int MAX_COUNT = 3; //最大库存3车
    private int count = 0;  //库存量

    public int getCount() {
        return this.count;
    }

    //向果商供货一箱
    public void get() {
        if (count > 0) {
            count--;
            try {
                Thread.sleep(500);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":采购了第" + (MAX_COUNT - count) + "车水果。");
        } else {
            System.out.println(Thread.currentThread().getName() + ":未能采购到水果,库存已空。");
        }
    }

    //果园向仓库供货
    public void put() {
        if (count == 0) {
            count = MAX_COUNT;
            try {
                Thread.sleep(500);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":已向仓库发送" + count + "车水果。");
        }
    }
}
//2.定义果商类BusiThread,创建果商线程
package com.aiden.demo9;

//果商类
public class BusiThread implements Runnable {
    
    Storage storage;//仓库

    public BusiThread(Storage storage) {
        this.storage = storage;
    }
    
    @Override
    public void run() {
        while (true) {
            this.storage.get();//仓库供应一车水果
            try {
                Thread.sleep(500);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}
//3.定义果农类FarmerThread,创建果农线程
package com.aiden.demo9;

//果农类
public class FarmerThread implements Runnable {
    private Storage storage;

    public FarmerThread(Storage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        while (true) {
            storage.put();//向仓库运送一车水果
            try {
                Thread.sleep(500);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

package com.aiden.demo9;

//测试类
public class Test {
    public static void main(String[] args){
        Storage storage = new Storage(); //创建仓库
        Thread busi1 = new Thread(new BusiThread(storage),"莲花超市");
        Thread busi2 = new Thread(new BusiThread(storage),"果果香水果店");
        Thread farmer = new Thread(new FarmerThread(storage),"果农老王");
        farmer.start();
        busi1.start();
        busi2.start();
    }
}

运行结果:

果农老王:已向仓库发送1车水果。
莲花超市:采购了第2车水果。   //问题1
果果香水果店:采购了第2车水果。//问题2
莲花超市:采购了第0车水果。
果果香水果店:采购了第0车水果。
果农老王:已向仓库发送1车水果。//问题
莲花超市:采购了第2车水果。
果果香水果店:采购了第2车水果。
莲花超市:采购了第0车水果。
果果香水果店:采购了第0车水果。
果农老王:已向仓库发送2车水果。//问题
莲花超市:采购了第2车水果。
果果香水果店:采购了第2车水果。
果果香水果店:未能采购到水果,库存已空。
莲花超市:采购了第1车水果。
果农老王:已向仓库发送1车水果。
果果香水果店:采购了第2车水果。
莲花超市:采购了第0车水果。
果果香水果店:采购了第1车水果。
果农老王:已向仓库发送2车水果。
莲花超市:采购了第2车水果。
果果香水果店:采购了第0车水果。
莲花超市:采购了第0车水果。
果农老王:已向仓库发送2车水果。

发现的问题:

  1. 不是从第一车水果开始供货
  2. 出现果果香水果店和莲花超市共同采购同一车水果的情况
  3. 果农老王应该运输3车水果到仓库,但数据显示只发送了2车

……
多个线程操作同一共享资源时,带来数据不安全问题的原因

  • Storage类中

  • 存在多个线程共同操作的变量count

  • get()方法和put()方法中,存在修改数据和显示数据两步操作

    JavaOOP 第十章 多线程_第7张图片

多个线程操作同一共享资源时,将引发数据不安全问题。如何解决?

  • 使用线程同步技术
十八、线程同步

线程同步:

  • 当两个或多个线程需要访问同一资源时,需要以某种顺序来确保该资源某一时刻只能被一个线程使用

  • 相当于将线程中需要一次性完成不允许中断的操作加上一把锁,以解决冲突

  • 使用synchronized关键字,为当前的线程声明一把锁

    JavaOOP 第十章 多线程_第8张图片

实现方式

  1. 同步代码块
  2. 同步方法
十九、同步代码块

使用synchronized关键字修饰的代码块

语法:

synchronized(syncObject) {
    //需要同步的代码
}

解决果园供货和仓库发货过程中访问冲突问题示例:

//仓库类
public class Storage {
    public final int MAX_COUNT = 3; //最大库存10车
    private int count = 0;  //库存量

    public int getCount() {
        return this.count;
    }
    //向果商供货一箱
    public void get() {    
        synchronized (this) {//获得当前Storage类对象的锁
            if (count > 0) {
                count--;
                try {
                    Thread.sleep(500);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":采购了第" + (MAX_COUNT - count) + "车水果。");
            } else {
                System.out.println(Thread.currentThread().getName() + ":未能采购到水果,库存已空。");
            }
        }
    }

    //果园向仓库供货
    public void put() {
        synchronized (this) {获得当前Storage类对象的锁
            if (count == 0) {
                count = MAX_COUNT;
                try {
                    Thread.sleep(500);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":已向仓库发送" + count + "车水果。");
            }
        }
    }
}

运行结果:

果农老王:已向仓库发送3车水果。
果果香水果店:采购了第1车水果。
莲花超市:采购了第2车水果。
果果香水果店:采购了第3车水果。
果农老王:已向仓库发送3车水果。
果果香水果店:采购了第1车水果。
莲花超市:采购了第2车水果。
果果香水果店:采购了第3车水果。

提示:

//一般情况下,只有获得锁的线程可以操作共享数据
//执行完同步代码块中所有代码后,才会释放锁,使其他线程获得锁
二十、同步方法

使用synchronized修饰的方法控制对类成员变量的访问

语法1:

访问修饰符 synchronized 返回类型 方法名(参数列表){……}

语法2:

synchronized 访问修饰符 返回类型 方法名(参数列表){……}

解决果园供货和仓库发货过程中访问冲突问题

public synchronized void get() {
    // 省略修改数据的代码
    // 省略显示数据的代码
}

public synchronized void put() {
    // 省略修改数据的代码
    // 省略显示数据的代码
}

线程同步的特征

  • 不同的线程在执行以同一个对象作为锁标记的同步代码块或同步方法时,因为要获得这个对象的锁而相互牵制
  • 多个并发线程访问同一资源的同步代码块或同步方法时
    1. 同一时刻只能有一个线程进入synchronized(this)同步代码块
    2. 当一个线程访问一个synchronized(this)同步代码块时,其他synchronized(this)同步代码块同样被锁定
    3. 当一个线程访问一个synchronized(this)同步代码块时,其他线程可以访问该资源的非synchronized(this)同步代码
  • 如果多个线程访问的不是同一共享资源,无需同步

经验:

使用同步代码块和同步方法完成线程同步,二者的实现结果没有区别
不同点:

  • 同步方法便于阅读理解
  • 同步代码块更精确地限制访问区域,会更高效
二十一、线程安全的类型

线程安全的类型

方法是否同步 效率比较 适合场景
线程安全 多线程并发共享资源
非线程安全 单线程
  • 如果程序所在的进程中,有多个线程同时运行,每次运行结果和单线程时运行结果是一样的,且其他变量的值也和预期相同,则当前程序是线程安全的

  • 查看ArrayList类的add()方法定义

    public boolean add(E e) {
        ensureCapacityInternal(size + 1); //集合扩容,确保能新增数据
        elementData[size++] = e;//在新增位置存放数据
        return true;
    }
    
  • ArrayList类的add()方法为非同步方法

  • 当多个线程向同一个ArrayList对象添加数据时,可能出现数据不一致问题

  • ArrayList为非线程安全的类型

经验:

  • 为达到安全性和效率的平衡,可以根据实际场景选择合适的类型

常见类型对比

Hashtable && HashMap

HashTable

  • 继承关系
    • 实现了Map接口,Hashtable继承Dictionary类
  • 线程安全,效率较低
  • 键和值都不允许为null

HashMap

  • 继承关系
    • 实现了Map接口,继承AbstractMap类
  • 非线程安全,效率较高
  • 键和值都允许为null

StringBuffer && StringBuilder

  • 前者线程安全,后者非线程安全
  • 在单线程环境下,StringBuilder执行效率更高
二十二、本章总结

JavaOOP 第十章 多线程_第9张图片

你可能感兴趣的:(Java,OOP,java,后端)