07- 线程高级特性

本文通过票务中心案例深入探讨线程高级特性,包括同步代码锁、synchronized关键字、lock锁的使用,以及线程池的优势。同时,讲解了线程间的通讯问题,wait()和notify()的区别,并给出多线程结合IO流的文件复制案例。
摘要由CSDN通过智能技术生成

票务中心案例

票 100

售票途径 (多线程)

// 票务中心
public class Ticket implements  Runnable{
    // 票的数量为 100 张
    int count = 100;
    @Override
    public void run() {
        // 循环卖票
        while (true){
            if(count > 0){
            // 为了让问题更加突显
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"\t第"+count+"张");
            }else{
                break;
            }
            // 每循环一次, 总票数 -1
            count--;
        }
    }
}

测试类

public class TicketDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket();

        Thread t1 = new Thread(t,"网络售票");
        Thread t2 = new Thread(t,"售票窗口");
        Thread t3 = new Thread(t,"黄牛党");

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

运行结果分析

  • 重复票 一张票卖出两次
  • 卖票顺序不同
  • 负数票

产生该问题的原因

CPU 选择线程的随机性

思考解决方式

在容易出现问题的代码上 上锁

  • 容易出现问题的代码
一个售票员 每卖出一张票, 必须重新设置该票的总数, 中间的过程不允许被其它售票员强行中断
一旦中断, 很容易发生 线程安全问题

代码锁的格式

同步代码锁

synchronized (锁对象){
	// 容易发生问题的代码
 }
对锁对象的要求是, 多个线程的锁对象必须是同一个

代码锁演示

@Override
public void run() {
    while(true){
        synchronized (this){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count <= 0){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"\t第"+count+"张");
            count--;
        }
    }
}

同步方法锁

成员方法

public void run() {
    while(true){
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sellTicket();
    }
}
public synchronized void sellTicket(){
    if(count <= 0){
        return;
    }
    System.out.println(Thread.currentThread().getName()+"\t第"+count+"张");
    count--;
    }

synchronized 存在一定有锁对象存在

成员方法 的锁对象为 this

静态方法

只是把成员方法 添加静态修饰符

静态方法 的锁对象为 该类的字节码对象

使用同步代码锁优劣分析

优势

  • 可以避免线程安全问题的出现

劣势

  • 必须等待一个线程运行完毕同步代码块 之后, 其他线程才可以运行

导致效率变低

速度和安全 对立的双方

对比之前的对象

ArrayList 和 Vector synchronized

StringBuffer (synchronized) 和 StringBuilder

学习完同步之后再看线程状态

在这里插入图片描述

同步代码锁, 把一块区域的代码 加锁, 一次只能允许一条线程 进入加锁的代码

提款机, 从一个人进入提款机面前, 插卡, 输入密码 , 输入提款金额, 把钱放进钱包, 退卡

其他线程 在排队等待 即使CPU 把执行权给了 线程, 但是该线程也无法进入上锁区域

lock锁

lock 锁基本介绍

	Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。 它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition 。 
	锁是用于通过多个线程控制对共享资源的访问的工具。 通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁

基本使用语法

public void sellTicket(){
	// 获得锁。 
    lock.lock();
    if(count <= 0){
        return;
    }
    System.out.println(Thread.currentThread().getName()+"\t第"+count+"张");
    count--;
    // 释放锁
    lock.unlock();
}

问题分析

万一在 lock() 和 unlock() 之间 出现了 异常

将不会执行 unlock() 释放锁

代码优化方式

public void sellTicket(){
	// 获得锁。 
    lock.lock();
    try{
        if(count <= 0){
            return;
        }
        System.out.println(Thread.currentThread().getName()+"\t第"+count+"张");
        count--;
    }finally{
         // 释放锁
    	lock.unlock();
    }
}

和 synchronized 对比

	1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到finally{} 中;

  2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  3)Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

  5)Lock可以提高多个线程进行读操作的效率。

  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

死锁问题

// 场景模拟
张三  和  李四 

在这里插入图片描述

public class MyThread extends Thread{
    public static final Object left = new Object();
    public static final Object right = new Object();
    public boolean boo ;
    public MyThread(){}
    public MyThread(boolean boo) {
        this.boo = boo;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // true  张三开始从左向右走
        if(boo){
            while (true){
                leftRight();
            }
        }else{
            // false  李四开始从右向左走
            while(true){
                rightLeft();
            }
        }
    }
    public void leftRight(){
        synchronized (left){
            System.out.println("张三进入了左侧房间!");
            synchronized (right){
                System.out.println("张三进入了右侧房间!");
            }
        }
    }
    public void rightLeft(){
        synchronized (right){
            System.out.println("李四进入了右侧房间!");
            synchronized (left){
                System.out.println("李四进入了左侧房间!");
            }
        }
    }
}

测试

public class ThreadDemo {
    public static void main(String[] args) {

        MyThread t1 = new MyThread(true);
        t1.start();
        MyThread t2 = new MyThread(false);
        t2.start();
    }
}

线程池

线程的生命周期

用户使用线程的步骤为

创建线程 => 启动线程 => 可运行=运行 => 阻塞或销毁..... 

把线程看做是一辆自行车

用户 想要从家去公司, 想要一辆自行车

买自行车 => 骑自行车 => 销毁自行车

程序的运行结束 是需要销毁已经创建的线程对象的, 根据自行车的案例, 现实生活中有共享单车, 可以很好地优化线程的获取方式

租车 => 用车 => 还车
租用线程 => 使用线程 => 归还线程

线程池

Executor

Interface Executor
界面提供了一种将任务提交从每个任务的运行机制分解的方式,包括线程使用,调度等的Executor 

Executors

static ThreadFactory defaultThreadFactory() 
返回用于创建新线程的默认线程工厂。  
static ExecutorService newCachedThreadPool() 
创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程。  
static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) 
创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程,并在需要时使用提供的ThreadFactory创建新线程。  
static ExecutorService newFixedThreadPool(int nThreads) 
创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程。  
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) 
创建一个线程池,重用固定数量的线程,从共享无界队列中运行,使用提供的ThreadFactory在需要时创建新线程。  
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 
创建一个线程池,可以调度命令在给定的延迟之后运行,或定期执行。  
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) 
创建一个线程池,可以调度命令在给定的延迟之后运行,或定期执行。  

案例

public class Demo1_Pool {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        // 线程池本质上就是一个容器, 该容器存放的内容是 线程 
        // 用户使用线程, 从该容器中自动获取
        ExecutorService pool = Executors.newCachedThreadPool();
        pool.submit(ticket);
        pool.submit(ticket);
        pool.submit(ticket);
    }
}

优势

  • 不需要创建线程类 , 直接使用线程池, 实现业务逻辑和线程分离
一个人 开了一个 淘宝网店卖零食, 需要组建自己的快递团队吗?
人负责好自己的主业就可以了, 不要在快递上分心, 直接把业务打包给快递公司负责
需要很多辆自行车, 直接找共享单车合作
线程池就是属于第三方公司
  • 使用线程池, 可以节约资源, 提高系统效率
同一辆车 可以提供给多人重复使用

线程之间的通讯问题

之前的卖票案例中 多个线程共同争抢CPU资源

但是线程之间不存在依赖关系

两个卖票窗口之间是相互独立的, 虽然有共享资源存在, 两者之间不存在先后关系!

通讯问题引入

存在依赖关系的两个线程之间 比如超市案例 共享同一个超市共享资源的两个线程 , 供货商线程 消费者线程, 如果供货商没有供货,消费者将无法消费, 消费者线程依赖于 供货商线程, 一旦存在依赖关系, 需要线程之间进行通讯

案例资源

共享资源类

public class SharedData {
    private char c;
    // 是否存在 已经生产完成的字符
    private boolean isProduced  = false;

    // 生产字一个符的方法
    public void putShareChar(char c){

        this.c = c;
        // 修改状态
        isProduced = true;
        System.out.println("生产了一个字符! "+c);
    }
    public char getShareChar(){
        // 调用该方法, 是从共享数据中 拿取一个字符
        // 把字符取出之后, 修改状态
        isProduced = false;
        System.out.println("消费了一个字符"+c);
        return c;
    }
}

生产者类

public class Producer extends Thread{
    private SharedData sd;

    public Producer(SharedData sd) {
        this.sd = sd;
    }

    @Override
    public void run() {
        // 生产者 循环 向仓库 放入字符, 生产商品放入仓库
        for(char ch = 'A' ; ch <= 'D' ; ch++){
            // 时间差
            try {
                Thread.sleep((long)(Math.random()*3000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sd.putShareChar(ch);
        }
    }
}

消费者类

public class Consumer extends Thread{
    private SharedData sd;
    public Consumer(SharedData sd) {
        this.sd = sd;
    }

    @Override
    public void run() {
        // 消费者去买东西
        char ch;
        do{
            // 时间差
            try {
                Thread.sleep((long)(Math.random()*3000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ch = sd.getShareChar();
        }while(ch != 'D');
    }
}

测试类

public class Demo {
    public static void main(String[] args) {
        // 共享资源对象
        SharedData sd = new SharedData();

        // 生产者线程
        Producer pro = new Producer(sd);
        Consumer con = new Consumer(sd);
        pro.start();
        con.start();
    }
}

没有同步, 没线程之间也没有通讯

生产者还未生产, 消费者就已经开始消费了

案例优化

public class SharedData {
    private char c;
    // 是否存在 已经生产完成的字符
    private boolean isProduced  = false;

    // 生产字一个符的方法
    public synchronized void putShareChar(char c)  {
        if(isProduced){
            //
            System.out.println("仓库的商品 还未销售完毕, 生产者停止生产");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.c = c;
        // 修改状态
        isProduced = true;
        System.out.println("生产了一个字符! "+c);
        this.notify();
    }
    public synchronized char getShareChar(){
        // 只有当 产品有的时候, 才可以买,
        if(!isProduced){
            // 没有的时候只能等
            System.out.println("生产者还未生产完毕, 请等待");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 调用该方法, 是从共享数据中 拿取一个字符
        // 把字符取出之后, 修改状态
        isProduced = false;
        System.out.println("消费了一个字符"+c);
        this.notify();
        return c;
    }
}

wait 和 notify

wait 等待 挂起

需要被唤醒

notify() 唤醒的方法 可以唤醒正在等待的线程

实际上是属于 Object 类的两个方法

调用两个方法的主体是 共享资源对象, 因为任何类都可以作为共享资源而存在

wait() 和 sleep() 的区别

wait() 在 线程暂停执行之后 会释放锁

sleep() 在线程暂停执行之后, 不会释放锁

类似于 银行提款机,张三在提款机中取钱, 突发疾病 在提款机旁 晕倒
wait()  在晕倒之前把门打开
sleep() 直接晕倒 

线程和IO流的综合案例

使用原始方式复制多文件

package s0805;
import java.io.*;

public class Demo1_copy {
    /*
    // 1- 判断 目标文件夹是否存在, 不存在创建
    // 2- 创建 源文件 输入流对象
    // 3- 创建 目标文件 输出流对象
    // 4- 使用包装流 buffered
    // 5- 使用 for循环 多次进行文件复制
    // 6- 关闭资源
     */
    public void copyFiles(String targetDir,String ...files) throws Exception {
        // 1- 判断 目标文件夹是否存在, 不存在创建
        File dir = new File(targetDir);
        if(!dir.exists()){
            dir.mkdirs();
        }
        // 2- 创建 源文件 输入流对象
        // 使用 for循环
        for(String file : files){
            File sourceFile = new File(file);
            FileInputStream fis = new FileInputStream(sourceFile);
            // 3- 创建 目标文件 输出流对象
            File targetFile = new File(targetDir,sourceFile.getName());
            FileOutputStream fos = new FileOutputStream(targetFile);
            // 4- 使用包装流 buffered
            BufferedInputStream bis = new BufferedInputStream(fis);
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            // 5-  多次进行文件复制
            byte [] bys = new byte[1024];
            int len;
            // 文件开始复制之前 打印一句话
            System.out.println(sourceFile.getName()+"开始复制");
            while( (len = bis.read(bys)) != -1){
                bos.write(bys,0,len);
            }
            System.out.println(sourceFile.getName()+"复制完成");
            // 6- 关闭资源
            bis.close();
            bos.close();
        }
    }
}

测试类

public class Demo2_copytest {
    public static void main(String[] args) throws Exception {

        Demo1_copy copy = new Demo1_copy();
        String dir = "D:\\testCopy";
        String f1 = "D:\\55- java 软件\\Mysql\\mysql-installer-community-5.7.19.0.msi";
        String f2 = "D:\\55- java 软件\\Mysql\\Navicat for MySQL.rar";
        String f3 = "D:\\55- java 软件\\IDEA\\ideaIU-2017.2.6.exe";

        copy.copyFiles(dir,f1,f2,f3);
    }
}

使用多线程 完成多文件复制案例

需求分析

// 1- 新建线程类, 该类主要负责 一个文件的复制工作
// 2- 线程类需要两个属性, 分别表示 源文件, 和目标文件夹
// 3- 在run 方法中  把文件复制的代码 补充完毕, (注意必须内部处理异常)
// 4- 在测试类中, 多文件复制, 每一个文件都需要开启一条线程
// 5- 使用 线程池来优化 线程的逻辑和效率

线程类

package s0805.copyThread;

import java.io.*;
/**
 * 该线程 主要功能是 每一条线程 负责一个文件的复制拷贝工作
 */
public class CopyRunnable implements Runnable{
    // 源文件
    private String file;
    // 目标文件夹
    private String targetDir;
    public CopyRunnable(){}
    public CopyRunnable(String file, String targetDir) {
        this.file = file;
        this.targetDir = targetDir;
    }

    @Override
    public void run() {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try{
            File sourceFile = new File(file);
            FileInputStream fis = new FileInputStream(sourceFile);
            // 3- 创建 目标文件 输出流对象
            File targetFile = new File(targetDir,sourceFile.getName());
            FileOutputStream fos = new FileOutputStream(targetFile);
            // 4- 使用包装流 buffered
            bis = new BufferedInputStream(fis);
            bos = new BufferedOutputStream(fos);
            // 5-  多次进行文件复制
            byte [] bys = new byte[1024];
            int len;
            // 文件开始复制之前 打印一句话
            System.out.println(sourceFile.getName()+"开始复制");
            while( (len = bis.read(bys)) != -1){
                bos.write(bys,0,len);
            }
            System.out.println(sourceFile.getName()+"复制完成");
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            // 6- 关闭资源
            if(bis != null){
                try {
                    bis.close();
                } catch (IOException e) {
                    bis = null;
                }
            }
            if(bos != null){
                try {
                    bos.close();
                } catch (IOException e) {
                    bos = null;
                }
            }
        }
    }
}

文件复制类

public void copyFiles(String targetDir,String ...files) throws Exception {
    // 1- 判断 目标文件夹是否存在, 不存在创建
    File dir = new File(targetDir);
    if(!dir.exists()){
        dir.mkdirs();
    }
    // 2- 创建 源文件 输入流对象
    // 使用 for循环
    for(String file : files){
        // 1- 创建线程对象
        CopyRunnable copyRunnable = new CopyRunnable(file, targetDir);
        // 2- 使用线程池 来执行多条线程
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(copyRunnable);
    }
}

测试类不变

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
资源包主要包含以下内容: ASP项目源码:每个资源包中都包含完整的ASP项目源码,这些源码采用了经典的ASP技术开发,结构清晰、注释详细,帮助用户轻松理解整个项目的逻辑和实现方式。通过这些源码,用户可以学习到ASP的基本语法、服务器端脚本编写方法、数据库操作、用户权限管理等关键技术。 数据库设计文件:为了方便用户更好地理解系统的后台逻辑,每个项目中都附带了完整的数据库设计文件。这些文件通常包括数据库结构图、数据表设计文档,以及示例数据SQL脚本。用户可以通过这些文件快速搭建项目所需的数据库环境,并了解各个数据表之间的关系和作用。 详细的开发文档:每个资源包都附有详细的开发文档,文档内容包括项目背景介绍、功能模块说明、系统流程图、用户界面设计以及关键代码解析等。这些文档为用户提供了深入的学习材料,使得即便是从零开始的开发者也能逐步掌握项目开发的全过程。 项目演示与使用指南:为帮助用户更好地理解和使用这些ASP项目,每个资源包中都包含项目的演示文件和使用指南。演示文件通常以视频或图文形式展示项目的主要功能和操作流程,使用指南则详细说明了如何配置开发环境、部署项目以及常见问题的解决方法。 毕业设计参考:对于正在准备毕业设计的学生来说,这些资源包是绝佳的参考材料。每个项目不仅功能完善、结构清晰,还符合常见的毕业设计要求和标准。通过这些项目,学生可以学习到如何从零开始构建一个完整的Web系统,并积累丰富的项目经验。
资源包主要包含以下内容: ASP项目源码:每个资源包中都包含完整的ASP项目源码,这些源码采用了经典的ASP技术开发,结构清晰、注释详细,帮助用户轻松理解整个项目的逻辑和实现方式。通过这些源码,用户可以学习到ASP的基本语法、服务器端脚本编写方法、数据库操作、用户权限管理等关键技术。 数据库设计文件:为了方便用户更好地理解系统的后台逻辑,每个项目中都附带了完整的数据库设计文件。这些文件通常包括数据库结构图、数据表设计文档,以及示例数据SQL脚本。用户可以通过这些文件快速搭建项目所需的数据库环境,并了解各个数据表之间的关系和作用。 详细的开发文档:每个资源包都附有详细的开发文档,文档内容包括项目背景介绍、功能模块说明、系统流程图、用户界面设计以及关键代码解析等。这些文档为用户提供了深入的学习材料,使得即便是从零开始的开发者也能逐步掌握项目开发的全过程。 项目演示与使用指南:为帮助用户更好地理解和使用这些ASP项目,每个资源包中都包含项目的演示文件和使用指南。演示文件通常以视频或图文形式展示项目的主要功能和操作流程,使用指南则详细说明了如何配置开发环境、部署项目以及常见问题的解决方法。 毕业设计参考:对于正在准备毕业设计的学生来说,这些资源包是绝佳的参考材料。每个项目不仅功能完善、结构清晰,还符合常见的毕业设计要求和标准。通过这些项目,学生可以学习到如何从零开始构建一个完整的Web系统,并积累丰富的项目经验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值