Java中多线程同步问题、生产者与消费者、守护线程和volatile关键字(附带相关面试题)

简介: 1.多线程同步问题(关键字Synchronized),2. Object线程的等待与唤醒方法,3.模拟生产者与消费者,4.守护线程,5.volatile关键字

 

1.多线程同步问题(关键字Synchronized)

问题:多线程访问同一个资源时候可能就会出现资源完整性的问题

所以引入关键字synchronized(同步)

    • synchronized关键字的作用机制是给对象加锁,并为每个线程提供了一个计数器,初始值为0。当第一个线程获得锁时,计数器变为1,其他线程被阻塞。当第一个线程执行完代码并释放锁时,计数器归零,意味着资源可用,所有被阻塞的线程将恢复执行。
    • 一个通俗的比喻是厕所的使用情况。假设只有一个厕所位置但有很多人需要使用。当第一个人进入厕所并锁上门时,其他人不得不在外面等待。当第一个人使用完毕并打开门锁时,表示厕所空闲可用,所有等待的人可以继续使用。

    关于synchronized有两种用法

    1.设置同步代码块

    public void method() {
        synchronized (obj) {
            // 同步代码块
        }
    }

    image.gif

    意味着只有同步代码块内部的代码需要同步,其他操作无需同步

    2.设置同步代码方法

    public synchronized void method() {
        // 同步代码块
    }

    image.gif

    这个方法就是整个方法内的代码都是同步的

    注意:在生产案例中不要随意使用同步方法,因为一旦同步,整个程序的运行效率就会非常低,比如10个学生想要去学校上厕所,那么最好的操作就是让10个学生一起先到学校再同步操作上厕所,而不是10个学生其中某一个去学校上完厕所,其他学生才去学校上厕所


    关于同步案例:(多个售票员售卖固定数量的票)

    在上一篇文章的代码中就实现到这一步

    package Example1401;
    class MyThread extends Thread{
    //    设置只有100张票
        private int ticket = 100;
        @Override
        public void run() {
            while (ticket>0){
                try {
    //                内部数字单位为毫秒 1000毫秒就是1秒
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"卖出第"+(ticket--)+"张票");
            }
        }
    }
    public class javaDemo {
        public static void main(String[] args) {
            MyThread m1 = new MyThread();
            MyThread m2 = new MyThread();
            MyThread m3 = new MyThread();
            m1.setName("售票员1");
            m2.setName("售票员2");
            m3.setName("售票员3");
            m1.start();
            m2.start();
            m3.start();
        }
    }

    image.gif

    这一步实现了售票员之间的售卖间隔,就不会一下子就把所有票卖光。

    现在通过同步代码块方法实现同步:

    package ExampleThread;
    import java.util.Random;
    class Mythread implements Runnable {
        private int ticket = 100;
        @Override
        public void run() {
            while (true) {
    //           当没买完票则开始卖票
    //                实现同步
                synchronized (this) {
    //                判断是否有票
                    if (ticket > 0) {
                        try {
    //                        设置随机售卖出去的时间间隔
                            Thread.sleep(1000);
    //                        输出售卖信息
                            System.out.println(Thread.currentThread().getName() + "卖第" + ticket-- + "张票");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                if (ticket <= 0) {
                    System.out.println("票已经全部卖光了");
                    break;
                }
    //            线程礼让,让其他线程也有机会售出票
                Thread.yield();
            }
        }
    }
    public class test {
        public static void main(String[] args) {
            Mythread task = new Mythread();
            new Thread(task, "售票员A").start();
            new Thread(task, "售票员B").start();
            new Thread(task, "售票员C").start();
        }
    }

    image.gif

    由于票太多所以为了方便显示就改成5了

    image.gif编辑

    面试题: Synchronized 用过吗,其原理是什么?

    (1)可重入性

    synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁;

       可重入的好处:

       可以避免死锁;

       可以让我们更好的封装代码;

    synchronized是可重入锁,每部锁对象会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。

    (2)不可中断性

       一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断;

       synchronized 属于不可被中断;

       Lock lock方法是不可中断的;

       Lock tryLock方法是可中断的;

    面试题:为什么说 Synchronized 是非公平锁?

    当锁被释放后,任何一个线程都有机会竞争得到锁,这样做的目的是提高效率,但缺点是可能产生线程饥饿现象。

    面试题:为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?

    Synchronized的并发策略是悲观的,不管是否产生竞争,任何数据的操作都必须加锁。

    乐观锁的核心是CAS,CAS包括内存值、预期值、新值,只有当内存值等于预期值时,才会将内存值修改为新值。


    2. Object线程的等待与唤醒方法

    注意:

    这些方法必须在同步块或同步方法中使用,因为它们会改变对象的内部锁状态。调用wait()方法将释放当前线程持有的对象锁,并使线程进入等待状态。而调用notify()notifyAll()方法会唤醒等待在该对象上的线程,并将其重新放入可运行状态。

    案例要求:设置一个图书类,一个图书管理员可以放图书的书名和作者,一个读者可以看图书的书名和作者

    package Example;
    class Book{
    //    封装属性
        private String author;
        private String bookName;
    //    设置获取属性
        public void setAuthor(String author) {
            this.author = author;
        }
        public void setBookName(String bookName) {
            this.bookName = bookName;
        }
        public String getAuthor() {
            return author;
        }
        public String getBookName() {
            return bookName;
        }
    }
    //图书管理员
    class Bookmaneger implements  Runnable{
        Book book;
        Bookmaneger(Book book){
            this.book = book;
        }
        @Override
        public void run() {
            synchronized (this){
                for (int i =0;i<1000;i++){
                    try {
    //                    假设网络延迟
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
    //                奇偶数时候设置不同图书
                    if (i%2==0){
                        book.setAuthor("西游记");
                        book.setBookName("吴承恩");
                    }
                    else {
                        book.setAuthor("天龙八部");
                        book.setBookName("金庸");
                    }
                }
            }
        }
    }
    //读者线程
    class Reader implements Runnable{
        Book book;
        Reader(Book book){
            this.book = book;
        }
        @Override
        public void run() {
            while (true){
                synchronized (this){
    //                读取图书信息
                    try {
    //                    假设网络延迟
                        Thread.sleep(1000);
                        System.out.println(book.getBookName()+"--->"+book.getAuthor());
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public class Library {
        public static void main(String[] args) {
    //        公用同一图书对象book
            Book book = new Book();
            Bookmaneger manegerTask = new Bookmaneger(book);
            Reader readerTask = new Reader(book);
    //        创建并运行读者和管理员的线程
            new Thread(manegerTask).start();
            new Thread(readerTask).start();
        }
    }

    image.gif

    image.gif编辑

    在网络延迟的情况下可能会出现问题:

    1.数据不匹配(解决需要-》结果能够一一对应)

    2.重复取同一个数据(解决需要--》每次都只取一次,只有更改后再取)

    解决方法:

    1.数据错乱,根本原因在于多线程下,图书管理员线程在设置图书信息到一半的时候,读者就读取图书信息造成图书信息错乱,解决方法很简单,只需要在book下将所有get和set方法设置为同步代码方法就可以解决数据错乱了

    2.其原理也很简单就是因为同步代码块,所以在完成Manger在执行完set前不会执行get。Reader在执行完get前也不会执行set

    package Example;
    class Book{
    //    封装属性
        private String author;
        private String bookName;
    //    简化设置和获取属性
        public synchronized void  set(String author,String bookName) {
            this.bookName = bookName;
            this.author = author;
        }
        public String get() {
            return author +"----->"+ bookName;
        }
    }
    //图书管理员
    class Bookmaneger implements  Runnable{
        Book book;
        Bookmaneger(Book book){
            this.book = book;
        }
        @Override
        public void run() {
            synchronized (this){
                for (int i =0;i<1000;i++){
                    try {
    //                    假设网络延迟
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
    //                奇偶数时候设置不同图书
                    if (i%2==0){
                        book.set("西游记","吴承恩");
                    }
                    else {
                        book.set("天龙八部","金庸");
                    }
                }
            }
        }
    }
    //读者线程
    class Reader implements Runnable{
        Book book;
        Reader(Book book){
            this.book = book;
        }
        @Override
        public void run() {
            while (true){
                synchronized (this){
    //                读取图书信息
                    try {
    //                    假设网络延迟
                        Thread.sleep(1000);
                        System.out.println(book.get());
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public class Library {
        public static void main(String[] args) {
    //        公用同一图书对象book
            Book book = new Book();
            Bookmaneger manegerTask = new Bookmaneger(book);
            Reader readerTask = new Reader(book);
    //        创建并运行读者和管理员的线程
            new Thread(manegerTask).start();
            new Thread(readerTask).start();
        }
    }

    image.gif

    image.gif编辑

    虽然解决了数据错乱的问题但是这样的数据出现有重复,我们的目标是需要西游记后输出天龙八部的交替输出。这就是最基础的生产者消费者模型了。

    那么如何具体实现呢?就需要用到前面给的方法,等待唤醒机制,即wait();与notify();

    其工作原理是设置一个标志位(资源量)当一个Reader读取时候就设置为true,maneger就是false并且进入沉睡。当Reader执行完任务后又将标志位设置为false 并让执行notify()唤醒其他熟睡的线程

    案例实现代码:

    class  Book{
        private String author;
        private String bookName;
        boolean flag = false;
        int i = 0;
        public synchronized void BookManger(){
    //        判断标志位
            if (flag==true){
    //            此处如果存在书籍则管理员休息
                try {
                    super.wait();
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
    //            如果不存在工作管理员就需要进行给书加作者和名字
            }else {
                if (i%2==0){
                    this.author = "吴承恩";
                    this.bookName = "西游记";
                }else {
                    this.author = "金庸";
                    this.bookName = "天龙八部";
                }
    //            执行完后设置标志位表示已经放好一本新的书了,并且唤醒其他线程
                i++;
                flag = true;
                super.notify();
            }
        }
        public synchronized void  Reader(){
    //        判断如果没有书籍则休息
            if (flag==false){
                try {
                    super.wait();
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
    //            如果有书籍就输出书籍的信息
            }else {
                System.out.println(this.bookName+"----->"+this.author);
                flag = false;
                super.notify();
            }
        }
    }
    //读者线程
    class Reader implements Runnable{
    //    设置共同对象用以通信
        Book book;
        Reader(Book book){
            this.book = book;
        }
        @Override
        public void run() {
            while (true){
                book.Reader();
            }
        }
    }
    //管理员线程
    class BookManger implements Runnable{
    //    设置同一对象用以通信
        Book book;
        BookManger(Book book){
            this.book = book;
        }
    //  线程调用对应方法
        @Override
        public void run() {
            while (true){
                book.BookManger();
            }
        }
    }
    public class ExampleThreadtest {
        public static void main(String[] args) {
    //        创建共有对象book
            Book book = new Book();
            BookManger mangerTask = new BookManger(book);
            Reader readerTask = new Reader(book);
    //        线程启动
            new Thread(mangerTask).start();
            new Thread(readerTask).start();
        }
    }

    image.gif

    image.gif编辑

    面试题: Java 如何实现多线程之间的通讯和协作?

    1.可以通过中断 和 共享变量的方式实现线程间的通讯和协作

    比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。

    Java中线程通信协作的最常见的两种方式:

    1、syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()

    2、ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()

    线程间直接的数据交换:

    通过管道进行线程间通信:1)字节流;2)字符流


    3.模拟生产者与消费者

    模拟生产者与消费者

    通过上面一个案例应该就有对生产者消费者模型有初步了解,下面将举一个十分经典的消费者生产者模型让大家有更深层次的理解

    案例目标:设计一个生产计算机类与一个搬运计算机类,要求是生产者生产一台计算机就要搬走一台计算机,如果没有新的计算机那么搬运工就要等待新的计算机产出,如果生产出的计算机没有被搬走就要等待搬运者将计算机搬走,最后搬运统计搬运走的电脑个数

    案例代码:

    package Test1402;
    class Computer{
    //    设置标志位
        private int flag = 0;
        private  int id=1;
        private int count = 0;
        //模拟生产函数
        public void produce(){
            synchronized (this){
                while (true){
                    if (flag == 1){
                        try {
                            super.wait();
                            Thread.sleep(1000);
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                    System.out.println("生产电脑第"+id++);
                    count++;
                    flag = 1;
                    super.notify();
                }
            }
        }
    //    模拟搬运函数
        public  void  Carry(){
            synchronized (this){
                while (true){
                    if (flag ==0){
                        try {
                            super.wait();
                            Thread.sleep(1000);
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                    }
                    System.out.println("搬运电脑"+(id+-1));
                    flag = 0;
                    super.notify();
                }
            }
        }
    }
    class Producer implements Runnable{
        Computer computer;
        Producer(Computer computer){
            this.computer = computer;
        }
        @Override
        public void run() {
            computer.produce();
        }
    }
    class Carryer implements  Runnable{
        Computer computer;
        Carryer(Computer computer){
            this.computer =computer;
        }
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            computer.Carry();
        }
    }
    public class javaDemo {
        public static void main(String[] args) {
            Computer computer = new Computer();
            new Thread(new Producer(computer)).start();
            new Thread(new Carryer(computer)).start();
        }
    }

    image.gif

    image.gif编辑


    4.守护线程

    一个进程的运行往往可能需要十分多的子进程辅助运行,比如聊天软件,主线程是软件的使用。而所有的聊天对象都是子线程可以分别接收消息。当软件关闭主线程时候即关闭软件使用时候,此时子线程的存在就没有了意义。就会自动关闭。这样的子线程就叫做守护线程

    设置守护线程

    Thread 对象.setDeamon(true/false);true-》开启守护线程,false-关闭守护线程

    具体应用案例:

    package Example1413;
    public class javaDemo {
        public static void main(String[] args) {
    //        主线程
            new Thread( ()->{
    //            执行输出三次
            for (int i=0;i<3;i++){
                try {
                    Thread.sleep(300);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+i);
            }
            },"userThread").start();
    //        守护线程
            Thread daemon = new Thread(()->{
    //            正常情况下应该执行输出10000次
                for (int i=0;i<10000;i++){
                    try {
                        Thread.sleep(200);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+i);
                }
            },"daemonThread");
    //        设置守护模式
            daemon.setDaemon(true);
            daemon.start();
        }
    }

    image.gif

    image.gif编辑


    5.volatile关键字

    一般情况下比如之前的售票员售票其调用ticket时候是进行先复制其数据副本再通过加载-》使用-》赋值-》存储-》写入才对内存的数据ticket进行同步,就是线程操作的数据都只是原始数据的备份,在操作完成后再和原始数据进行替换。而volatile则不需要这些数据备份直接操作内存的原始数据

    好处:volatile关键字可以直接对内存进行操作,就不需要同步数据了,所以可以减少程序运行的时间

    使用案例代码:

    package Example1414;
    class sale implements Runnable{
        private volatile int ticket =100;
        @Override
        public void run() {
            synchronized (this){
                while (ticket>0){
                    try {
                        Thread.sleep(100);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"售卖出"+ticket--+"张票");
                }
            }
        }
    }
    public class javaDemo {
        public static void main(String[] args) {
            sale s = new sale();
            new Thread(s,"售票员A").start();
            new Thread(s,"售票员B").start();
            new Thread(s,"售票员C").start();
        }
    }

    image.gif

    *面试题:volatile 关键字的作用

    对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

    从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。

    volatile 常用于多线程环境下的单次操作(单次读或者单次写)。

    *面试题:既然 volatile 能够保证线程间的变量可见性,是不是就意味着基于 volatile 变量的运算就是并发安全的?

    volatile修饰的变量在各个线程的工作内存中不存在一致性的问题(在各个线程工作的内存中,volatile修饰的变量也会存在不一致的情况,但是由于每次使用之前都会先刷新主存中的数据到工作内存,执行引擎看不到不一致的情况,因此可以认为不存在不一致的问题),但是java的运算并非原子性的操作,导致volatile在并发下并非是线程安全的。

     *面试题:请谈谈 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?

    volatile只能作用于变量,保证了操作可见性和有序性,不保证原子性。

    在Java的内存模型中分为主内存和工作内存,Java内存模型规定所有的变量存储在主内存中,每条线程都有自己的工作内存。

    主内存和工作内存之间的交互分为8个原子操作:

       lock

       unlock

       read

       load

       assign

       use

       store

       write

    volatile修饰的变量,只有对volatile进行assign操作,才可以load,只有load才可以use,,这样就保证了在工作内存操作volatile变量,都会同步到主内存中。


    目录
    相关文章
    |
    监控 Kubernetes Java
    阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
    本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
    |
    数据采集 Java Linux
    面试大神教你:如何巧妙回答线程优先级这个经典考题?
    大家好,我是小米。本文通过故事讲解Java面试中常见的线程优先级问题。小明和小华的故事帮助理解线程优先级:高优先级线程更可能被调度执行,但并非越高越好。实际开发需权衡业务需求,合理设置优先级。掌握线程优先级不仅能写出高效代码,还能在面试中脱颖而出。最后,小张因深入分析成功拿下Offer。希望这篇文章能助你在面试中游刃有余!
    256 4
    面试大神教你:如何巧妙回答线程优先级这个经典考题?
    |
    Java 程序员 开发者
    Java社招面试题:一个线程运行时发生异常会怎样?
    大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
    774 14
    |
    算法 安全 Java
    Java线程调度揭秘:从算法到策略,让你面试稳赢!
    在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
    609 16
    |
    缓存 安全 Java
    面试中的难题:线程异步执行后如何共享数据?
    本文通过一个面试故事,详细讲解了Java中线程内部开启异步操作后如何安全地共享数据。介绍了异步操作的基本概念及常见实现方式(如CompletableFuture、ExecutorService),并重点探讨了volatile关键字、CountDownLatch和CompletableFuture等工具在线程间数据共享中的应用,帮助读者理解线程安全和内存可见性问题。通过这些方法,可以有效解决多线程环境下的数据共享挑战,提升编程效率和代码健壮性。
    424 6
    |
    安全 Java 程序员
    面试直击:并发编程三要素+线程安全全攻略!
    并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
    458 11
    |
    Java Linux 调度
    硬核揭秘:线程与进程的底层原理,面试高分必备!
    嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
    435 6
    |
    存储 Java
    【IO面试题 四】、介绍一下Java的序列化与反序列化
    Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
    |
    存储 算法 Java
    大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
    本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
    大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
    |
    存储 缓存 算法
    面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
    本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
    面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
    下一篇
    开通oss服务