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;
}


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

相关文章
|
3天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
36 14
|
6天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
34 13
|
6天前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
1月前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
109 17
|
2月前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
1月前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
2月前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
9月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。
|
6月前
|
存储 安全 Java
解锁Java并发编程奥秘:深入剖析Synchronized关键字的同步机制与实现原理,让多线程安全如磐石般稳固!
【8月更文挑战第4天】Java并发编程中,Synchronized关键字是确保多线程环境下数据一致性与线程安全的基础机制。它可通过修饰实例方法、静态方法或代码块来控制对共享资源的独占访问。Synchronized基于Java对象头中的监视器锁实现,通过MonitorEnter/MonitorExit指令管理锁的获取与释放。示例展示了如何使用Synchronized修饰方法以实现线程间的同步,避免数据竞争。掌握其原理对编写高效安全的多线程程序极为关键。
86 1
|
7月前
|
安全 Java 开发者
Java并发编程中的线程安全问题及解决方案探讨
在Java编程中,特别是在并发编程领域,线程安全问题是开发过程中常见且关键的挑战。本文将深入探讨Java中的线程安全性,分析常见的线程安全问题,并介绍相应的解决方案,帮助开发者更好地理解和应对并发环境下的挑战。【7月更文挑战第3天】
125 0

热门文章

最新文章