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
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
44 17
|
16天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
1天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
18天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
18天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
18天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
42 3
|
18天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
48 1
|
3月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
66 1
|
3月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
47 3
|
3月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
29 2