【Java多线程】通过等待唤醒机制、局部变量、原子变量实现线程同步

目录

1、等待唤醒机制

1.1、生产者和消费者(有示例实现等待唤醒机制)

1.2、阻塞队列((有示例实现等待唤醒机制)

2、使用特殊域变量(volatile)实现线程同步

3、使用局部变量实现线程同步

1、等待唤醒机制

等待唤醒机制概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

比如:线程A(生产者)用来生成面条的,线程B(消费者)用来吃面条的,做面条和吃面条都在桌子上,面条可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。 

为什么要处理线程间通信:

        多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:

        多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

1.1、生产者和消费者(有示例实现等待唤醒机制)

        “生产者-消费者”模型是一个典型的线程协作通信的例子。在这一模型中有两类角色,即若干个生产者线程和若干个消费者线程。生产者线程负责提交用户请求,消费者线程负责处理生产者提交的请求。很多情况下,生产者与消费者不能够达到一定的平衡,即有时候生产者生产的速度过快,消费者来不及消费;而有时候可能是消费者过于旺盛,生产者来不及生产。在此情况下就需要一个生产者与消费者共享的内存缓存区来平衡二者的协作。生产者与消费者之间通过共享内存缓存区进行通信,从而平衡生产者与消费者线程,并将生产者和消费者解耦。如上面吃面条的例子所示

生产者和消费者(常见方法)

方法名称

说明

void wait()

当前线程等待,直到被其他线程唤醒

void notify()

随机唤醒单个线程

void notifyAll()

唤醒所有线程

示例:

公共资源部分代码实现

public class Desk {
    /*
    作用:控制生产者和消费者的执行
     */
    //桌子上是否有面条  0:没有面条 1:有面条
    public static int foodFlag = 0;
    //消费者能吃面条的总个数
    public static int count = 10;
    //锁对象
    public static Object lock = new Object();
}

消费者代码实现

public class Foodie extends Thread{
    @Override
    public void run() {
        /*写线程的步骤
        1.循环
        2.同步代码块
        3.判断共享数据是否到了末尾(到了末尾)
        4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
         */
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    //先判断桌子上是否有面条
                    if (Desk.foodFlag == 0) {
                        //如果没有,就让消费者等待
                        try {
                            Desk.lock.wait();//让当前线程跟锁进行绑定
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        //如果有,就开吃,并把能吃的总数-1
                        Desk.count--;
                        System.out.println("消费者在吃面条,还能吃" +Desk.count+ "碗面条!!!");
                        //吃完之后,唤醒厨师继续做,一般都是唤醒全部线程
                        Desk.lock.notifyAll();
                        //修改桌子的状态,0为桌子上没有面条了
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}

生产者代码实现

public class Cook extends Thread{
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                //如果消费者能吃的面条数已经到0了,生产者就不需要再做面条了,就break跳出循环
                if (Desk.count == 0) {
                    break;
                }
                //判断桌子上是否有食物
                if (Desk.foodFlag == 1) {
                    //如果有,就让生产者等待
                    try {
                        Desk.lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    //如果没有,生产者就制作食物
                    //System.out.println("生产者做了一碗面条!!!");
                    System.out.print("生产者做了一碗面条!!!");
                    //修改桌子的状态,1表示桌子上有面条
                    Desk.foodFlag = 1;
                    //唤醒等待的消费者来吃
                    Desk.lock.notifyAll();
                }
            }
        }
    }
}

 测试类代码实现

public class ThreadDemo {
    /*
    需求:完成生产者和消费者(等待唤醒机制)的代码
    实现线程轮流交替执行的效果
     */
    public static void main(String[] args) {
        //创建生产者和消费者线程对象
        Cook c = new Cook();
        Foodie f = new Foodie();
        //给线程设置名字
        c.setName("生产者");
        f.setName("消费者");
        //开启线程
        c.start();
        f.start();
    }
}

 运行结果

1.2、阻塞队列((有示例实现等待唤醒机制)

        阻塞队列:阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

        简单来说,拿上面的吃面条举例,阻塞队列就好比是连接生产者和消费者的管道,生产者做好面条后就可以把面条放到这个管道当中,而消费者就可以在这个管道中去拿面条来吃,只不过这个例子里的管道最多只能放一碗面,只能做一碗吃一碗,中间的这个管道就是阻塞队列

阻塞队列的继承结构:

接口:lterable——>Collection——>Queue——>BlockingQueue

 BlockingQueue继承关系源代码:

        因为上面的四个都是接口,不能创建它们的对象,只能创建它们的实现类对象,比如下面的两个实现类

阻塞队列实现类:

        ArrayBlockingQueue:底层是数组,数组大小有界限。

        LinkedBlockingQueue:底层是链表,数组大小无界限,但不是真正的无界,最大为 int 的最大值,但数组大小是到不了int的最大值的,所以才说数组大小无界限。

实现代码:

生产者Cook类实现代码

public class Cook extends Thread{
    //定义阻塞队列变量
    ArrayBlockingQueue<String> queue;
    //创建给阻塞队列变量queue初始化的构造方法
    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true) {
            //不断的把面条放到阻塞队列中去
            try {
                //阻塞队列的put()方法中已经写好锁的操作了,所以这里不需要写锁
                queue.put("面条");
                System.out.println("生产者放了一碗面条!!! ");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

消费者Foodie类实现代码

public class Foodie extends Thread{
    //定义阻塞队列变量
    ArrayBlockingQueue<String> queue;
    //创建给阻塞队列变量queue初始化的构造方法
    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true) {
            //不断的从阻塞队列中去获取面条
            try {
                //阻塞队列的put()方法中已经写好锁的操作了,所以这里不需要写锁
                String food = queue.take();
                System.out.println(food);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

测试类实现代码

public class ThreadDemo {
    public static void main(String[] args) {
        //创建阻塞队列实现类ArrayBlockingQueue的对象,形参capacity表示这个队列放几个数据
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
        //利用Cook和Foodie两个类中提供的带参构造方法来创建它们的对象,并把参数:阻塞队列queue 传过去
        Cook c = new Cook(queue);
        Foodie f = new Foodie(queue);
        c.start();
        f.start();
    }
}

运行结果:

最后看结果发现,好像和我们想的不一样,这是因为打印语句不在锁的里面,但是阻塞队列里的数据是正确的,因为数据在锁里面,所以这并不会对数据造成影响

2、使用特殊域变量(volatile)实现线程同步

        volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见。相较于 synchronized 是一种较为轻量级的同步策略。

       

        实现方法就是在要进行共享操作的数据前加上volatile 关键字

示例:

//创建线程类。并且在要进行共享操作的数据前加上volatile 关键字
public class MyThread extends Thread{
    //volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见。相较于 synchronized 是一种较为轻量级的同步策略。
    volatile int ticket = 0;//0 ~ 99
    @Override
    public void run() {
        while (true) {
            if (ticket < 50) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket++;
                System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
            } else {
                break;
            }
        }
    }
}


//创建线程示例,然后启动线程
public class ThreadDemo {
    public static void main(String[] args) {
        //创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        //给线程命名
        t1.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");
        //开启线程
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:可以看出这三个线程是同步执行的

3、使用局部变量实现线程同步

        java.lang.ThreadLocal<T>(直接已知子类),在Java的多线程并发执行过程中,为了保证多个线程对变量的安全访问,可以将变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时被另一个线程修改的现象。

ThreadLocal 类的常用方法:

ThreadLocal()

创建一个线程本地变量

get()

返回此线程局部变量的当前线程副本中的值

initialValue()

返回此线程局部变量的当前线程的"初始值"

set(value)

将此线程局部变量的当前线程副本中的值设置为value

示例:

//创建线程类
public class MyThread extends Thread{
    ThreadLocal<Integer> ticket = new ThreadLocal<>(){
        @Override

        protected Integer initialValue(){
            return 0;
        }
    };//0 ~ 99
    @Override
    public void run() {
        while (true) {
            if (ticket.get() < 50) {//在这里获取值
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket.set(ticket.get()+1);//在这里修改值
                System.out.println(getName() + "正在卖第" + ticket + "张票!!!");
            } else {
                break;
            }
        }
    }
}

//创建线程实例并启动线程
public class ThreadDemo {
    public static void main(String[] args) {
        //创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

        //给线程命名
        t1.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");
        //开启线程
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:可以看出这三个线程是同步执行的

感谢浏览!

推荐: 

【java多线程】线程同步问题:用同步代码块、同步方法和重入锁实现线程同步-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_65277261/article/details/137022053?spm=1001.2014.3001.5501

【Java多线程】多线程的三种实现方式和多线程常用方法-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_65277261/article/details/136961604?spm=1001.2014.3001.5501

【java多线程】线程基础知识笔记-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_65277261/article/details/136905234?spm=1001.2014.3001.5501

  • 27
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值