Java进阶 —— 多进程并发

news2024/11/25 20:49:03

前言

        在系统学完Java的面向对象编程之后,我们需要认真地来学习Java并发编程,我们在学习计算机操作系统的时候也都了解过进程、线程和协程的概念。在这篇文章中荔枝主要会梳理有关线程创建、线程生命周期、同步锁和死锁、线程通信和线程池的知识,并给出相应的精简示例,希望能帮助有需要的小伙伴们哈哈哈~~~


文章目录

前言

一、基础概念

二、创建线程的三种方式

2.1 通过继承Thread类来启用

2.2 实现Runnable接口来实现

2.3 实现Callable接口

三、Thread类的相关方法

四、生命周期

五、同步锁和死锁

5.1 同步锁 

5.1.1 synchronized加锁的两种方式 

5.1.2 Lock

5.2 死锁

六、线程通信

6.1 传统的线程通信

6.2 使用Condition来控制线程通信

6.3 使用阻塞队列来控制线程通信

七、线程池

7.1 ExecutorService类使用示例

7.2 Java8中的ForkJoinPool

总结


一、基础概念

进程 

        我们知道CPU是主机上的中央核心处理器,CPU的核数代表着主机能在一个瞬间同时并行处理的任务数,单核CPU只能在内存中并发处理任务。而在现有的操作系统中,几乎都支持进程这个概念。进程是程序的在内存中的一次执行过程,具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位

线程

        线程在程序中是独立的、并发的执行流,与分隔的进程相比隔离性会更小,线程之间共享内存、文件句柄和其它的进程应有的状态。线程比进程具有更高的性能,这是由于同一进程中的线程具有共性。简单理解,多线程是进程中并行执行的多个子程序。

并发性和并行的区别

        并行是指在同一时刻,有多条指令在多个处理器上同时执行;而并发是指在同一时刻只能执行,但是通过多进程快速轮换执行可以达到同时执行的效果。CPU主频就代表着这些进程之间频繁切换的速度。


二、创建线程的三种方式

2.1 通过继承Thread类来启用

Java语言中JVM允许程序运行多个线程并通过java.lang.Thread类来实现。

Thread类的特性
        每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体,并通过该Thread对象的start()方法来启动线程。

流程:

  1. 定义子类继承Thread类;
  2. 子类中重写Thread类中的run方法;
  3. 创建Thread子类对象,即创建了线程对象;
  4. 调用线程对象start方法:启动线程,调用run方法。

 具体代码示例

首先构建一个继承Thread类的子类

//继承Thread类的方式实现多线程
public class TestThread extends Thread{
    @Override
    public void run(){
        System.out.println("多线程运行的代码");
    }
}

 调用线程

public class Test{
    public static void main(String[]args){
        Thread t = new TestThread();
        t.start();   //启动线程
    }
}

2.2 实现Runnable接口来实现

流程

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。
  5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。 

 实现Runnable接口

public class TestRunnable implements Runnable{
    @Override
    public void run(){
        System.out.println("实现Runnable接口运行多线程");
    }
}

实现多线程

public class Test{
    public static void main(String[]args){
        Thread t = new Thread(new TestRunnable);
        //带有线程名称的实例化线程对象。可以通过Thread.currentThread().getName()获取
        //Thread t = new Thread(new TestRunnable,"the FirstThread");
        t.start();   //启动线程
    }
}

与继承Thread类的区别

  • 继承Thread:线程代码存放Thread子类run方法中。重写run方法
  • 实现Runnable:线程代码存在接口的子类的run方法。实现run方法 

实现Runnable接口方法的好处

        实现Runnable接口方法通过继承Runnable接口避免了当继承的局限性,同时也使得多个线程可以同时共享一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。 

2.3 实现Callable接口

        在前面通过实现Runnable接口创建多线程时,Thread类的作用就是把run方法包装成线程的执行体。而从Java5以后,Java提供了一个Callable接口中的call()方法作为线程执行体,同时call()方法可以有返回值,也可以抛出异常。

public class Test{
    public static void main(String[]args){
        //创建callable对象
        ThirdThread tt = new ThirdThread();
        //使用FutureTask来包装Callable对象
        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
            ...
            ...
        });
        new Thread(task,"有返回值的线程").start();
        try{
            //获取线程返回值
            System.out.println("子线程的返回值" + task.get());
        }catch (EXception ex){
            ex.printStackTrace();    
        }
    }
}

Callable接口实现类和Runnable接口实现类的区别在于是否有参数返回! 


三、Thread类的相关方法

常用方法如下:

  • void start():启动线程,并执行对象的run(0方法
  • run():线程在被调度时执行的操作
  • String getName():返回线程的名称
  • void setName(String name):设置该线程名称
  • static currentThread():返回当前线程 
public class Test{
    public static void main(String[]args){
        TestRun r1 = new TestRun();
        Thread t1 = new Thread(r1);
        //为线程设置名称
        t1.setName("线程t1");

        t1.start();   //启动线程
        System.out.println(t1.getName()); //若没指定,系统默认给出的线程名称是Thread-0....
    }
}
public class TestRun implements Runnable{
    @Override
    public void run(){
        System.out.println("实现Runnable接口运行多线程");
    }
}

线程优先级

线程的优先级设置增加了线程的执行顺序靠前的概率,是用一个数组1-10来表示的,默认的优先级是5。涉及的方法有:getPriority()和setPriority()

//获取优先级
t1.getPriority();
//设置优先级
t1.setPriority(10);

线程让步

static void yield()线程让步,即暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,则跳过。

Thread.yield();

线程阻塞

join():当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。

try{
    //获取线程返回值
    t1.join();
}catch (EXception ex){
    ex.printStackTrace();    
}

线程睡眠

try{
    Thread.sleep(1000);//当前线程睡眠1000毫秒
}catch(InterruptedException e)(
    e.printStackTrace();
}

线程生命结束

t1.stop();

判断当前线程是否存活

t1.isAlive();

四、生命周期

线程从创建、启动到死亡经历了一个完整的生命周期,在线程的生命周期中一般要经历五种状态:新建——就绪——运行——阻塞——死亡。

  • 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;
  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,也就是在执行.start()方法后;
  • 运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态,run()方法定义了线程的操作和功能,此时run()方法的代码开始执行;
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态;
  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止 。

 ​​​​​

线程可能以如下三种方法结束:

  • run或call方法执行完成后
  • 线程抛出一个未捕获的Exception或Error
  • 直接调用了stop()方法

五、同步锁和死锁

5.1 同步锁 

        多线程模式的提出势必就会带来线程同步的问题,在保证数据一致性上,我们需要为线程加上同步锁。Java中对于多线程安全的问题提出了同步机制,即在方法声明的时候加入synchronized关键字来修饰或者直接使用synchronized来锁一个demo

5.1.1 synchronized加锁的两种方式 

synchronized同步锁关键字修饰 

//使用synchronized同步锁关键字修饰需要同步执行的方法体
public synchronized void drawing(int money){
    需要同步执行的代码
}

注意:

        在普通方法上加同步锁synchronized,锁的是整个对象,不是某一个方法。如果是不同对象的话那么就是不同的锁。静态的方法加synchronized对于所有的对象都是同一个锁

synchronized锁一段demo

使用这种方法来锁指向this的代码块使用的都是同一个同步锁。如果改成方法对象的话比如Account对象的话就是不同的同步锁。

synchronized(this){ //表示当前的对象的代码块被加了synchronized同步锁
    demo...
}

5.1.2 Lock

        相比于上面的synchronized相应的锁操作,Lock提供了更为广泛的锁操作。其中包括ReadWriteLock(读写锁)和ReentrantLock(可重入锁),ReadWriteLock提供了ReentrantReadWriteLock的实现类。在Java8中引入了一个新的StampedLock类替代了传统的ReentrantReadWriteLock并给出了三种锁模式:Write、ReadOptimistic和Reading。

ReentrantLock 实现demo 

class x{
    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    //...
    //定义需要保证线程安全的方法
    public void m(){
        lock.lock();
        try{
            //需要保证线程安全的demo
        }
        finally{
            lock.unlock();
        }
    }
}

5.2 死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
解决方法

  • 专门的算法、原则,比如加锁顺序一致
  • 尽量减少同步资源的定义,尽量避免锁未释放的场景

六、线程通信

        当我们手动开启并在控制台中输出两个线程的运行过程的时候,程序并不能每次都准确的控制两个线程的轮换执行的先后次序,所以Java中也提供了一些机制来保证线程的协调运行。在传统的Java中,基于同步锁synchronized关键字提供了借助于Object类的wait()、notify()和notifyAll()方法来控制线程的阻塞情况,而之后也出现了基于Condition和阻塞队列BlockingQueue来控制线程阻塞的情况。

6.1 传统的线程通信

Object类中提供的wait()、notify()和notifyAll()方法必须由一个同步监视器对象来调用,所以这三种方法必须基于同步锁synchronized关键字

  • wait():该方法会导致当前线程进入等待状态,直到其它的线程调用notify()或notifyAll()方法来唤醒该线程,wait方法有三种形式:不带时间参数(等待唤醒)、带毫秒时间参数(时间到自动唤醒)和带毫微秒的时间参数(时间到自动唤醒)。调用wait方法当前线程会释放对同步监视器的锁定。
  • notify():唤醒该同步监视器上等待的单个线程,这种选择是按照优先级最高的来唤醒结束其等待状态。
  • notifyAll():唤醒等待的所有线程。
//使用时直接调用方法就行,但必须是在有synchronized修饰的方法内去调用才可
wait();
notify();
notifyAll();

6.2 使用Condition来控制线程通信

        对于程序不使用synchronized关键字来保证同步锁,而是采用Lock对象来保证同步,Java中提供了Condition类来保证线程通信。Contidion类中提供了类似于synchronized关键字中的三种方法:await()、signal()和signalAll(),替代了同步监视器的功能。

  • await():类似于wait方法,会使得当前线程进入等待状态,直到其它线程调用signal()或signalAll()来唤醒。
  • signal():唤醒单个线程。
  • signalAll():唤醒多个线程。
//显示定义Lock对象
Lock lock = new ReentrantLock();
//获取Condition
Condition cond = lock.newCondition();
//需要同步的方法中加锁
public void fun(){
    //加锁过程
    lock.lock();
    try{
        if(条件) cond.await(); //线程进入等待
        else{
            //唤醒其他线程
            cond.signalAll();
        }
    }catch(InterruptedException e){
        e.printStrackTrace();
    }finally{
        //锁的释放
        lock.unlock();
    }
}

6.3 使用阻塞队列来控制线程通信

        除了上述两种方法,Java5中还提供了BlockingQueue接口来作为线程同步的工具。它的工作原理是这样滴:当生产者往BlockingQueue接口中放入元素直至接口队列满了,线程阻塞;消费者从BlockingQueue接口队列中取元素直至队列空了,线程阻塞。BlockingQueue接口继承了Queue接口并提供了如下三组方法。

  •  在队列尾部添加元素:add(E e)、offer(E e)、put(E e),当队列已满的时候,这三个方法分别会抛出异常、返回false和阻塞线程。
  • 在队列头部删除并返回删除元素:remove()、poll()、take()方法。当该队列已空时,这三个方法分别会抛出异常、返回false和阻塞线程。
  • 在队列头部取出但不删除元素:element()和peek(),当该队列已空时,分别会抛出异常和返回false

在Java7之后,阻塞队列出现了新增,分别是:ArrayBlockingQueue、LinkedBlockingQueue、priorityBlockingQueue、SynchornizedQueue和DelayQueue这五个类。 


七、线程池

        系统启动一个新线程的成本是比较高的,尤其是当系统本身已经有大量的并发线程时,会导致系统性能急剧下降,甚至会导致JVM崩溃,因此我们通常采用线程池来维护系统的并发线程。与数据库连接池类似的时,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动后一个空闲的线程来执行它们的run()或call()方法,当运行结束后,该线程不会死亡而是返回线程池中进入空闲等待状态。

        ExecutorService代表尽快执行线程的线程池,程序只需要将一个Runnable对象或Callable对象传给线程池,就会尽快执行线程任务;ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池。

7.1 ExecutorService类使用示例

使用线程池的步骤如下:

  • 调用Executors类的静态工厂方法调用创建一个ExecutorService对象,该对象就代表着一个线程池;
  • 创建Runnable实现类或Callable实现类的实例,作为线程执行的任务;
  • 调用ExecutorService对象的submit()方法来提交Runnable或者Callable对象实例;
  • 结束任务时,调用ExecutorService对象的shutdown()方法来关闭线程池;
//开启6个线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(6);
//创建Runnable实现类
Runnable target = ()->{...}

//提交线程任务到线程池
pool.submit();

//关闭线程
pool.shutdown();

        用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列并不再接受新任务,线程池中的任务依次执行完毕后线程死亡;或者调用线程池的shutdownNow()方法来直接停止所有正在执行的活动任务。

7.2 Java8中的ForkJoinPool

        计算机发展到现在其实基本的硬件都支持多核CPU,为了更好地利用硬件设备的资源,Java中提供了一个ForkJoinPool来支持将一个任务拆分成多个小任务并行计算。ForkJoinPool是ExecutorService的实现类,是一个特殊的线程池。

构造器的两种方法

  • ForkJoinPool(int num):创建一个包含num个并行线程的ForkJoinPool;
  • ForkJoinPool():以Runtime.availableProcessors()方法的返回值作为parallelism参数(上面我写成了num)来创建改线程池

实现通用池的两个静态方法

  • ForkJoinPool commonPool():改方法返回一个通用池,通用池的状态不会受到shutdown()等方法的影响,System.exit(0)除外。
  • int getCommonPoolParallelism():该方法返回通用池的并行级别

注意:

        ForkJoinPool.submit(ForkJoinTask task) ,其中ForkJoinTask代表着一个可以并行和合并的任务,他有两个抽象的子类:RecursiveAction和RecursiveTask,分别代表着有返回值和无返回值的任务。

class PrintTask extends RecursiveAction{
    ...
    @Override
    protected void compute(){
        ......
        //分割任务
        PrintTask t1 = new PrintTask(start,middle);
        PrintTask t2 = new PrintTask(middle,end);
        //并行执行子任务
        t1.fork();
        t2.fork();
    }
}

public class Test{
    public static void main(String[]args) throws Exception{
        //实例化通用池对象
        ForkJoinPool pool = new ForkJoinPool();
        pool.submit(new PrintTask(0,1000));
        //线程等待完成
        pool.awaitTermination(2,TimeUnit.SECONDS);
        //关闭线程池
        pool.shutdown();
    }
}

总结

        现有的所有企业都采用的是多线程并发的方式来开发的,也要求我们能够应对在高并发场景下保证系统服务的高可用的要求,所以多线程和异步编程我们必须牢牢掌握。这几章可能会比较枯燥,难度也会比较大,荔枝也是啃了一段时间嘿嘿嘿,在学这部分之前一定要把面向对象学好,要不然会晕哈哈哈~~~

今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/785114.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

android app控制ros机器人一

android开发app&#xff0c;进而通过控制ros机器人&#xff0c;记录开发过程 查阅资料&#xff1a; rosjava使用较多&#xff0c;已经开发好的app也有开源的案例 rosjava GitHub https://github.com/ros-autom/RobotCA https://github.com/ROS-Mobile/ROS-Mobile-Android…

因果推断(一)合成控制法(SCM)

因果推断&#xff08;一&#xff09;合成控制法&#xff08;SCM&#xff09; 在互联网时代&#xff0c;产品迭代速度越来越快&#xff0c;营销活动也越来越多。分析师因此需要快速的量化每次迭代或每次营销的效果&#xff0c;探索改变与结果之间的因果关系&#xff0c;并将优秀…

idea如何解决导入的项目不是Maven工程(文件下面没有蓝色的方格)二

简介&#xff1a; Maven项目导入&#xff0c;idea不识别项目 解决方法&#xff1a; 选中pom.xml -- 右键 -- Add as Maven Project

使用Python搭建代理服务器- 爬虫代理服务器详细指南

搭建一个Python爬虫代理服务器可以让你更方便地管理和使用代理IP。下面是一个详细的教程来帮助你搭建一个简单的Python爬虫代理服务器&#xff1a; 1. 首先&#xff0c;确保你已经安装了Python。你可以在官方网站(https://www.python.org/)下载并安装最新版本的Python。 2. 安…

Spring 中简单存取 Bean 的相关注解

目录 前言存储 Bean 对象五大类注解方法注解&#xff08;Bean&#xff09; 获取 Bean 对象 (Autowired)属性注入多个同类型 Bean 注入怎么办&#xff1f; Setter 注入构造方法注入&#xff08;官方推荐&#xff09; 前言 之前我们存储获取 Bean 的操作很繁琐&#xff0c;需要将…

在职硕士|2023级中国社科院-美国杜兰大学合办双证能源管理硕士(MME)

金融硕士 在职硕士|2023级中国社科院-美国杜兰大学合办双证能源管理硕士&#xff08;MME&#xff09; 中国社会科学院大学与美国杜兰大学合作举办的能源管理专业硕士学位教育项目&#xff08;UCASS-Tulane Master of Management in Energy&#xff0c;简称MME&#xff09;于2…

《人工智能安全》课程总体结构

1 课程内容 人工智能安全观&#xff1a;人工智能安全问题、安全属性、技术体系等基本问题进行了归纳整理。人工智能安全的主要数据处理方法&#xff0c;即非平衡数据分类、噪声数据处理和小样本学习。人工智能技术赋能网络空间安全攻击与防御&#xff1a;三个典型实例及攻击图…

mybatis_配置之属性优化

概念 别名优化&#xff1a; 类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置&#xff0c;意在降低冗余的全限定类名书写。例如&#xff1a; 在xml文件里为SQL映射文件中定义返回值类型的属性起个别名 之后直接使用User进行使用 核心配置文件&#xff1a; MyBa…

常见面试题分享1

一、对JVM的了解 1.1 什么是JVM&#xff1f; JVM&#xff08;Java Virtual Machine&#xff09;&#xff0c;俗称Java虚拟机。它是一个虚构出来的计算机&#xff0c;是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java语言的一个非常重要的特点就是与平台的无关性。…

商城体系之产商品系统

本文主要讲解商城体系下产商品系统的设计。商城系统可以拆分成多个业务中台和多个应用服务。 1、产商品系统业务架构 产商品系统作为商城重要的基础信息组成部分&#xff0c;主要划分为产品信息和商品信息&#xff0c;产品信息保持最原始的产品基础属性和内容&#xff0c;商品…

下拉框可筛选可树状多选组件

实际效果图片 父页面 <el-form-item label"转发&#xff1a;" :label-width"formLabelWidth" class"formflex_item"><el-select ref"select" :clearable"true" clear"clearSelect" remove-tag"r…

day2 驱动开发 c语言

通过驱动开发给pcb板子点灯。 u-boot已经提前移植到了emmc中。 灯也是一种字符型设备。 编程流程需要先注册设备&#xff0c;然后创建结点&#xff0c;然后操作电灯相关寄存器 应用层直接调用read write来打开字符设备进行操作。 这样写会造成无法处理内核页面请求的虚拟地址…

SpringBoot中java操作excel【EasyExcel】

EasyExcel 处理Excel&#xff1b;简单记录&#xff0c;方便日后查询&#xff01; 官方文档&#xff1a; Easy Excel (alibaba.com) 一、EasyExcel概述 Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存&#xff0c;poi有一套…

前端食堂技术周刊第 91 期:2023 npm 状态、TC39 会议回顾、浏览器中的 Sass、React 18 如何提高应用程序性能

美味值&#xff1a;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f; 口味&#xff1a;茶椰生花 食堂技术周刊仓库地址&#xff1a;https://github.com/Geekhyt/weekly 大家好&#xff0c;我是童欧巴。欢迎来到前端食堂技术周刊&#xff0c;我们先来看下…

js基础-练习三

九九乘法表&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthsc, initial-scale1.0"><title>九九乘法表</title><style&g…

5.9 Bootstrap 警告框(Alert)插件

文章目录 Bootstrap 警告框&#xff08;Alert&#xff09;插件用法选项方法事件 Bootstrap 警告框&#xff08;Alert&#xff09;插件 警告框&#xff08;Alert&#xff09;消息大多是用来向终端用户显示诸如警告或确认消息的信息。使用警告框&#xff08;Alert&#xff09;插件…

基于 Flink SQL CDC 数据处理的终极武器

文章目录 一、传统的数据同步方案与 Flink SQL CDC 解决方案1.1 Flink SQL CDC 数据同步与原理解析1.2 基于日志的 CDC 方案介绍1.3 选择 Flink 作为 ETL 工具 二、 基于 Flink SQL CDC 的数据同步方案实践2.1 CDC Streaming ETL2.2 Flink-CDC实践之mysql案例 来源互联网多篇文…

Redis—分布式系统

Redis—分布式系统 &#x1f50e;理解分布式&#x1f50e;分布式—应用服务与数据库服务分离引入更多的应用服务节点理解负载均衡 引入更多的数据库服务节点缓存分库分表 微服务 &#x1f50e;常见概念应用(Application) / 系统(System)模块(Module) / 组件(Component)分布式(D…

nvm 安装 Node 报错:panic: runtime error: index out of range [3] with length 3

最近在搞 TypeScript&#xff0c;然后想着品尝一下 pnpm&#xff0c;但是 pnmp 8.x 最低需要 Node 16.x&#xff0c;但是电脑上暂时还没有该版本&#xff0c;通过 nvm list available 命令查看可用的 Node 版本&#xff1a; nvm list available# 显示如下 | CURRENT | …

【C++进阶】:继承

继承 一.继承的概念和定义1.概念2.定义 二.基类和派生类对象赋值转换三.继承中的作用域四.派生类的默认成员函数五.继承与友元六.继承与静态成员七.复杂的菱形继承及菱形虚拟继承1.二义性2.原理 八.总结 一.继承的概念和定义 1.概念 继承(inheritance)机制是面向对象程序设计使…