Java多线程——生命周期、并发、临界资源问题

简介: 是进程执行的最小单元。这样说是不是很抽象?比方拿我们的浏览器来说,一个浏览器可以同时下载几幅图片,可以边听歌的同时边聊天、边播放视频的同时还可以打印文件,这每一个行为我们都可看作是一个不同的线程,不同的线程可以同时运行。

进程是什么?


对程序进行占用各种资源的集合。我们打开自己电脑的任务管理器就能看到不同的进程在不停的进行切换,我们可以把每一个.exe文件的执行认为是一个进程


e9bc335b11829a74812cd7b064255630.png


线程是什么?


是进程执行的最小单元。这样说是不是很抽象?比方拿我们的浏览器来说,一个浏览器可以同时下载几幅图片,可以边听歌的同时边聊天、边播放视频的同时还可以打印文件,这每一个行为我们都可看作是一个不同的线程,不同的线程可以同时运行。


那这样做(同时运行)有什么优点呢?(为什么要有多线程?)


随着技术发展,操作系统演变的会为每个进程分配CPU资源,由于CPU运行速度非常快,线程之间频繁切换,使得我们看起来所有的线程都在同一个时刻执行


那什么时候需要使用多线程?


1、程序需要同时执行两个或多个任务


2、需要一些后台运行的程序时


当我们在访问浏览器看网页的时候,我们正在等待图片下载完成,等着百度服务器那边给我们返回结果,那等待的这个过程中其实浏览器是一直占着CPU的,在等待的间隙我们就可以让我们的浏览器先干点其他的事情释放掉CPU资源,不要一直霸占着,等百度服务器把结果响应给我们之后再继续回来执行,这样既提高了程序处理速度,也提高了CPU的利用率


那线程和进程的关系是什么?


我们每打开一个程序就在计算机里增加了一个进程,计算机要给程序分配不同的系统资源,比方说CPU,每个进程所需要的cpu大小也不一样,比方说谷歌浏览器、xmind占的cpu资源比较少就少分配一些,游戏软件或者idea这种就需要分配多一些cpu资源。而一个软件也可能会干不同的事情,比方说QQ,听歌的同时还能QQ截图、收消息,而听歌、截图等这些都可以看成是一个一个的线程


注:进程和进程之间内存空间不共享,但线程和线程共享堆内存和方法区,但是栈内存独立,一个线程一个栈,如:


阿里巴巴和京东他们就可以看作是两个不同的进程,他们之间不共享资源


那线程和进程之间有什么区别?


进程:每个进程都有独立的代码和数据空间,进程间切换开销大


线程:同一个进程中的线程共享资源,每个线程有独立的运行栈和PC,线程切换开销小


我们通过一张图来表示一下它们之间的关系:


一个进程正在运行时至少有一个线程正在运行,比方说main()方法


781db3e27934040d9a29ffe964203d7d.png


线程的生命周期有哪些?


一个线程也是有生到死的过程的,一共有5个线程,我们来看看它们分别都做了些什么吧


309c27e614be2b466fcaba87e8c290b6.png


当我们的程序执行需要两种资源:CPU(谁干活)、内存(要占的地方)


目标就是怎么让我们的cpu在不同程序之间快速切换,怎么用进程实现并发?看似并发实际是切换很快有一定顺序


接下来我们来看看线程具体是怎么使用的吧!


实战演练:


线程的创建


方法一:继承Thread,重写run()方法


public class ThreadCreate {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        //myThread.run();
        System.out.println("主线程执行结束");
    }
}
class MyThread extends Thread {
    //重写run方法,需要并发的任务写到run方法中
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程1--" + i);
        }
    }
}


start()和run()的区别?


继承重写run()方法,run方法就被当成了一个普通方法来运行,在jvm就只有main方法这一个主线程,需要等到run方法体逻辑走完才能执行下面的逻辑。没有实现多线程


缺点:java是单继承,继承Thread类重写run()方法,无法继承其他的类


start方法会产生新线程,用来启动线程,当调用start方法后此时线程变为了就绪态,说明具有争夺CPU的权利,当获得CPU之后便开始执行run方法体内逻辑。实现多线程


8d417417f7b4f241dbcd7b0c2eb04bc3.png


方法二:使用Runnable接口


好处:上面我们讲到有由于java是单继承,为了支持多继承的特点,我们可以实现Runnable接口


package com.example;
public class ThreadCreate {
    public static void main(String[] args) {
        Runnable r1 = () -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("子线程2--" + i);
            }
        };
        Thread t2 = new Thread(r1);
        t2.start();
        System.out.println("主线程执行结束");
    }
}


下面我们再来一起看看线程常用的一些设置


线程的常用方法——线程命名、优先级、礼让


class MyThreads extends  Thread{
    public MyThreads(String name){
        this.setName(name);
    }
}


1、线程命名


public class ThreadName {
    public static void main(String[] args) {
        //方法一:重写构造函数,将名称通过实例化对象的同时传入复制
        MyThreads thread = new MyThreads("young");
        //方法二:通过set方法设置
        Thread t1= new Thread();
        t1.setName("t1");
        //方法三:自带的无参构造函数设置
        Thread t2= new Thread("t2");
        //方法四:实例化线程对象的同时,通过构造方法对线程进行命名
        Thread t3= new Thread(()->{},"t3");
    }
}


2、线程优先级


优先级:1-10,一个线程的缺省优先级是5


  • Thread.MIN_PRIORITY =1
  • Thread.MAX_PRIORITY=10
  • Thread.NORM_PRIORITY=5(默认)


注意:线程的优先级只是说明了线程可以抢到CPU的概率,并不是说就能抢到CPU或者谁的优先级高就先把这个线程执行完


public class ThreadLevel {
    public static void main(String[] args) {
        Runnable r = () -> {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        };
        //创建连个线程分别叫t1和t2
        Thread t1 = new Thread(r, "Thread-1");
        Thread t2 = new Thread(r, "Thread-2");
        //设置线程t1的优先级比t2的优先级高
        t1.setPriority(1);
        t2.setPriority(10);
        t1.start();
        t2.start();
    }
}


3、线程礼让


让出当前的运行状态的线程释放自己CPU资源,运行态——>就绪态


public class ThreadYield {
    public static void main(String[] args) {
        Runnable r= new Runnable() {
            @Override
            public void run() {
                for (int i=0;i<10;i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                    if(i==3){
                        Thread.yield();
                    }
                }
            }
        };
        Thread t1= new Thread(r,"t1-");
        Thread t2= new Thread(r,"t2-");
        t1.start();
        t2.start();
    }
}


4、线程睡眠


sleep单位是毫秒,每间隔2秒执行一次


class MyThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程执行---" + i);
            try {
                //sleep单位是毫秒,每间隔2秒执行一次
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


public class ThreadSleep {
    public static void main(String[] args) {
        MyThread2 myThread = new MyThread2();
        myThread.start();
        System.out.println("主线程执行结束");
    }
}


sleep和wait区别?


1、sleep():休眠了自己可以醒过来。使用sleep是别的线程也不可以访问锁定的对象


2、wait():休眠了自己无法醒过来。需要用notify()方法使线程醒过来。使用wait时别的线程可以访问锁定的对象


3、stop方法。泼凉水,直接把进程杀死。比方说正在打开一个程序,stop方法,这个程序可能造成程序就一直关不上了。很粗暴!!


O.waite()方法会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁


O.notify()方法只会通知,不会释放之前占有的o对象的锁。


什么时候数据在多线程并发的环境下会存在安全问题呢?


1、多线程并发


2、有共享数据


3、共享数据有修改行为


960fec03e368fd6408405a3ce60183cb.png


上图是经典的银行取钱问题,当张三和张三媳妇同时去银行取钱,张三拿着银行卡去柜台取钱,张三媳妇在不知情的情况下拿着存折去银行取钱,他们同时都要取1000元,这时候多线程的情况下就出现了数据不一致的情况,他们共从银行拿走了2000元,而账户余额只有1000元。


那如何解决出现的安全问题?为了更好的讲述线程安全以及解决线程安全的问题,下面我从一个售票员卖票的例子讲起


临界资源问题


什么是临界资源?


多个线程同时争夺资源


实战演练:


class TicketCenter {
    //剩余票数
    static int restCount = 100;
}


package com.example;
import java.util.concurrent.locks.ReentrantLock;
public class CriticalResource {
    public static void main(String[] args) {
        Runnable r = () -> {
            while (TicketCenter.restCount > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余" + --TicketCenter.restCount +
                        "张");
            }
        };
        Thread t1 = new Thread(r, "t1--");
        Thread t2 = new Thread(r, "t2--");
        Thread t3 = new Thread(r, "t3--");
        Thread t4 = new Thread(r, "t4--");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
}


我们来看看控制台输出结果:


4e8e1222fffe5a090da46ea7257c2522.png


从输出结果我们可以看出来当多个线程并发的时候产生了数据不一致的情况,t4线程执行完毕之后剩余票数为91张,当t3线程开始执行的时候剩余票数却为97……


此时票数100就是那个临界资源,四个线程都在访问临界资源,导致数据不一致。那如何解决这个问题呢?我们可以通过三个办法来解决:同步代码段、同步方法、显示锁


解决方案有哪些?


方法一、同步代码段


添加synchronized锁,当剩余票数大于0的时候就给临界资源加上锁,此时其他线程在无法访问或修改临界资源,处于就绪状态排队等待,等待的线程需要等到当前线程释放了cpu才能进行访问


注意:当多个并发线程访问同一个同步代码段时,一段时间内只能有一个线程被执行


package com.example;
import java.util.concurrent.locks.ReentrantLock;
public class CriticalResource {
    public static void main(String[] args) {
        Runnable r = () -> {
            while (TicketCenter.restCount > 0) {
                //需要在锁当中加一层判断,否则会出现票数剩余负三的情况。
                //原因:剩余票数大于0,线程进入循环,获得了抢占cpu的资格,看到资源加锁了进入排队等待;
                //如果三个线程都进入了排队状态,那么当这三个线程拿到cpu资源之后能够继续执行减减操作,导致票数出现了负三的情况
                synchronized ("") {
                    //方法一:同步代码段
                    //当票数剩余0的时候退出当前循环,表示票已经卖完
                    if (TicketCenter.restCount <= 0) {
                        return;
                    }
                    System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余" + --TicketCenter.restCount +
                    "张");
                }
            }
        };
        Thread t1 = new Thread(r, "t1--");
        Thread t2 = new Thread(r, "t2--");
        Thread t3 = new Thread(r, "t3--");
        Thread t4 = new Thread(r, "t4--");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}
class TicketCenter {
    //剩余票数
    static int restCount = 100;
}


方法二、同步方法


package com.example;
import java.util.concurrent.locks.ReentrantLock;
public class CriticalResource {
    public static void main(String[] args) {
        Runnable r = () -> {
            while (TicketCenter.restCount > 0) {
                synchronized ("") {
                    //方法二:同步方法,将要加锁的方法抽成方法,不影响实际效果
                    soldTicket ();
                }
            }
        };
        Thread t1 = new Thread(r, "t1--");
        Thread t2 = new Thread(r, "t2--");
        Thread t3 = new Thread(r, "t3--");
        Thread t4 = new Thread(r, "t4--");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
    //方法二:同步方法
    //需要在锁当中加一层判断,否则会出现票数剩余负三的情况。
    //原因:剩余票数大于0,线程进入循环,获得了抢占cpu的资格,看到资源加锁了进入排队等待;
    //如果三个线程都进入了排队状态,那么当这三个线程拿到cpu资源之后能够继续执行减减操作,导致票数出现了负三的情况
    private synchronized static void soldTicket() {
        if (TicketCenter.restCount <= 0) {
            return;
        }
        System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余" + --TicketCenter.restCount + "张");
    }
}
class TicketCenter {
    //剩余票数
    static int restCount = 100;
}


方法三、显示锁


多个线程看到的锁是同一个


package com.example;
import java.util.concurrent.locks.ReentrantLock;
public class CriticalResource {
    public static void main(String[] args) {
        //方法三:显示锁
        Runnable r1 = () -> {
            while (TicketCenter.restCount > 0) {
                lock.lock();
                if (TicketCenter.restCount <= 0) {
                    return;
                }
                System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余" + --TicketCenter.restCount + "张");
                lock.unlock();
            }
        };
        Thread t1 = new Thread(r, "t1--");
        Thread t2 = new Thread(r, "t2--");
        Thread t3 = new Thread(r, "t3--");
        Thread t4 = new Thread(r, "t4--");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}
class TicketCenter {
    //剩余票数
    static int restCount = 100;
}


如果有想要交流的内容欢迎在评论区进行留言,如果这篇文档受到了您的喜欢那就留下你点赞+收藏+评论脚印支持一下博主~

相关文章
|
5月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
168 4
|
5月前
|
缓存 安全 Java
如何理解Java中的并发?
Java并发指多任务交替执行,提升资源利用率与响应速度。通过线程实现,涉及线程安全、可见性、原子性等问题,需用synchronized、volatile、线程池及并发工具类解决,是高并发系统开发的关键基础。(238字)
326 5
|
5月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
277 1
|
5月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
296 1
|
5月前
|
机器学习/深度学习 人工智能 监控
Java与AI模型部署:构建企业级模型服务与生命周期管理平台
随着企业AI模型数量的快速增长,模型部署与生命周期管理成为确保AI应用稳定运行的关键。本文深入探讨如何使用Java生态构建一个企业级的模型服务平台,实现模型的版本控制、A/B测试、灰度发布、监控与回滚。通过集成Spring Boot、Kubernetes、MLflow和监控工具,我们将展示如何构建一个高可用、可扩展的模型服务架构,为大规模AI应用提供坚实的运维基础。
414 0
|
6月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
Java 数据库 Spring
263 0
|
6月前
|
Java API 开发工具
【Azure Developer】Java代码实现获取Azure 资源的指标数据却报错 "invalid time interval input"
在使用 Java 调用虚拟机 API 获取指标数据时,因本地时区设置非 UTC,导致时间格式解析错误。解决方法是在代码中手动指定时区为 UTC,使用 `ZoneOffset.ofHours(0)` 并结合 `withOffsetSameInstant` 方法进行时区转换,从而避免因时区差异引发的时间格式问题。
321 4
|
6月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
449 16
|
7月前
|
缓存 并行计算 安全
关于Java多线程详解
本文深入讲解Java多线程编程,涵盖基础概念、线程创建与管理、同步机制、并发工具类、线程池、线程安全集合、实战案例及常见问题解决方案,助你掌握高性能并发编程技巧,应对多线程开发中的挑战。