Java多线程同步锁、Lock锁和等待唤醒机制及代码演示

简介: Java多线程同步锁、Lock锁和等待唤醒机制及代码演示

线程的生命周期

问:sleep方法会让线程睡眠,睡眠时间到了之后,立马就会执行下面的代码吗?

解:sleep方法时间到了之后,线程就变成了就绪状态,他会先去抢CPU的执行权,抢到了,才会去执行下面的代码,所以他是有一个抢的过程的。

线程的安全问题

线程会帮我们提高程序的效率,但是提高效率的同时,也会有个弊端,就是不安全。

举个小栗子:

需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

代码如下:

package com.heima.thread001;
public class MyThread extends Thread{
    int ticket = 0;
    @Override
    public void run() {
        while (true){
            if (ticket < 100){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket ++;
                System.out.println(getName() + "正在卖第" + ticket + "张票 ...");
            }else{
                break;
            }
        }
    }
}
package com.heima.thread001;
import java.util.concurrent.ExecutionException;
public class TestDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建线程对象
        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();
    }
}

运行结果:

发现有重复卖票的情况,那如何解决呢?请继续看下面的章节哦

同步代码块

  • 方式:把操作共享数据的代码锁起来
  • 格式:
synchronized (锁对象){
  操作共享数据的代码
}
  • 特点1:锁默认打开,有一个线程进去了,锁自动关闭
  • 特点2:里面的代码全部执行完毕,线程出来,锁自动打开

使用同步代码块来解决,上面重复卖票的问题,代码如下:

package com.heima.thread001;
public class MyThread extends Thread{
    static int ticket = 0;
    //锁对象,一定要是唯一的
    static Object obj = new Object();
    @Override
    public void run() {
        while (true){
            synchronized (obj){
            if (ticket < 100){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket ++;
                System.out.println(getName() + "正在卖第" + ticket + "张票 ...");
            }else{
                break;
            }
            }
        }
    }
}
package com.heima.thread001;
import java.util.concurrent.ExecutionException;
public class TestDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建线程对象
        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();
    }
}

运行结果:

窗口1正在卖第1张票 ...
窗口1正在卖第2张票 ...
窗口2正在卖第3张票 ...
窗口2正在卖第4张票 ...
窗口2正在卖第5张票 ...
窗口2正在卖第6张票 ...
....此处省略....
窗口1正在卖第95张票 ...
窗口1正在卖第96张票 ...
窗口1正在卖第97张票 ...
窗口1正在卖第98张票 ...
窗口1正在卖第99张票 ...
窗口1正在卖第100张票 ...
Process finished with exit code 0

注意点:

  • 锁对象,一定要保持唯一,所以使用static关键字来修饰,确保当前线程类无论创建多少个对象,都共享一个锁对象哦
  • 成员变量ticket,也要保持唯一性,与锁对象一样的思路,确保所以得线程对象共享一个变量
  • synchronized中的锁对象,不能写成this哦,因为this代表当前线程对象,不同的线程对象,那么对于的锁对象也不一样,这样加锁就没有意义了。因为锁对象不唯一。可以使用以下写法来解决:
//一个类的字节码文件,肯定是唯一的,所以这样写也没有问题
synchronized (MyThread.class){...}

同步方法

  • 方式:就是把 synchronized 关键字加的方法上
  • 格式:修饰符 synchronized 返回值类型 方法名 (方法参数) { … }
  • 特点1:同步方法是锁住方法里面所有的代码
  • 特点2:锁对象不能自己指定,是java已经规定好的。如果当前方法是非静态的,那么锁对象为 this,即当前方法的调用者;如果当前方法是静态的,那么锁对象为 当前类的字节码文件对象,比如MyThread.class

接下来,我们使用 同步方法来完成,窗口售票功能,代码入;

package com.heima.thread001;
public class MyRun implements Runnable {
  //此处没有使用static修饰,是因为MyRun只创建了一次对象,所以不需要设置成共享变量哦
    int ticket = 0;
    @Override
    public void run() {
        while (true){
            if (sale()) break;
        }
    }
    private synchronized boolean sale() {
        if (ticket == 100) {
            return true;
        }else {
            ticket ++;
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票 ...");
        }
        return false;
    }
}
package com.heima.thread001;
import java.util.concurrent.ExecutionException;
public class TestDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyRun myRun = new MyRun();
        //创建线程对象
        Thread t1 = new Thread(myRun);
        Thread t2 = new Thread(myRun);
        Thread t3 = new Thread(myRun);
        //起名字
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        //开启线程
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果

Lock锁

我们知道 synchronized 给代码加锁或解锁时,我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

  • Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
  • Lock中提供了获得锁和释放锁的方法:
    void lock();//获得锁
    void unlock();//释放锁
  • lock是接口不能直接实例化,这里采用他的实现类ReentrantLock来实例化
    ReentrantLock的构造方法
    ReentrantLock():创建一个ReentrantLock的实例

接下来使用lock锁,来处理卖票的问题

package com.heima.thread001;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyRun implements Runnable {
    int ticket = 0;
    Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            lock.lock();
            try {
                if (ticket == 100) {
                    break;
                }else {
                    ticket ++;
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票 ...");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}
package com.heima.thread001;
import java.util.concurrent.ExecutionException;
public class TestDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyRun myRun = new MyRun();
        //创建线程对象
        Thread t1 = new Thread(myRun);
        Thread t2 = new Thread(myRun);
        Thread t3 = new Thread(myRun);
        //起名字
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        //开启线程
        t1.start();
        t2.start();
        t3.start();
    }
}

生产者和消费者(等待唤醒机制)

生产者和消费者是一个十分经典的多线程协作模式

举个小栗子来说明一下消费者和生产者的等待唤醒过程:

常见方法:

  • void wait() 当前线程等待,直到被其他线程唤醒
  • void notify() 随机唤醒单个线程
  • void notifyAll() 唤醒所有线程

接下来,使用代码来演示生产者和消费者的等待唤醒过程

消费者代码:

package com.heima.thread001;
public class FoodThread extends Thread {
    @Override
    public void run() {
        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();
                        //修改桌子的状态
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}

生产者代码

package com.heima.thread001;
public class CookThread extends Thread {
    @Override
    public void run() {
        while (true){
            synchronized (Desk.lock){
                //判断桌子上有没有面条
                if (Desk.count == 0){
                    break;
                }else {
                    //判断桌子上是否有实物
                    if(Desk.foodFlag == 1){
                        //如果有,就等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else {
                        //如果没有,就制作面条
                        System.out.println("厨师做了一碗面条");
                        //修改桌子上的食物状态
                        Desk.foodFlag = 1;
                        //叫醒等待的消费者开吃
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

控制生产者和消费者的执行类

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

测试类

package com.heima.thread001;
public class TestDemo {
    public static void main(String[] args){
        CookThread cookThread = new CookThread();
        FoodThread foodThread = new FoodThread();
        cookThread.setName("厨师");
        foodThread.setName("吃货");
        cookThread.start();
        foodThread.start();
    }
}

运行结果

Connected to the target VM, address: '127.0.0.1:52025', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:52025', transport: 'socket'
厨师做了一碗面条
吃货在吃面条,还能再吃9碗
厨师做了一碗面条
吃货在吃面条,还能再吃8碗
厨师做了一碗面条
吃货在吃面条,还能再吃7碗
厨师做了一碗面条
吃货在吃面条,还能再吃6碗
厨师做了一碗面条
吃货在吃面条,还能再吃5碗
厨师做了一碗面条
吃货在吃面条,还能再吃4碗
厨师做了一碗面条
吃货在吃面条,还能再吃3碗
厨师做了一碗面条
吃货在吃面条,还能再吃2碗
厨师做了一碗面条
吃货在吃面条,还能再吃1碗
厨师做了一碗面条
吃货在吃面条,还能再吃0碗
Process finished with exit code 0


相关文章
|
1天前
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
|
2天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
3天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
19 4
|
3天前
|
消息中间件 供应链 Java
掌握Java多线程编程的艺术
【10月更文挑战第29天】 在当今软件开发领域,多线程编程已成为提升应用性能和响应速度的关键手段之一。本文旨在深入探讨Java多线程编程的核心技术、常见问题以及最佳实践,通过实际案例分析,帮助读者理解并掌握如何在Java应用中高效地使用多线程。不同于常规的技术总结,本文将结合作者多年的实践经验,以故事化的方式讲述多线程编程的魅力与挑战,旨在为读者提供一种全新的学习视角。
24 3
|
3天前
|
安全 IDE Java
Java反射Reflect机制详解
Java反射(Reflection)机制是Java语言的重要特性之一,允许程序在运行时动态地获取类的信息,并对类进行操作,如创建实例、调用方法、访问字段等。反射机制极大地提高了Java程序的灵活性和动态性,但也带来了性能和安全方面的挑战。本文将详细介绍Java反射机制的基本概念、常用操作、应用场景以及其优缺点。 ## 基本概念 ### 什么是反射 反射是一种在程序运行时动态获取类的信息,并对类进行操作的机制。通过反射,程序可以在运行时获得类的字段、方法、构造函数等信息,并可以动态调用方法、创建实例和访问字段。 ### 反射的核心类 Java反射机制主要由以下几个类和接口组成,这些类
11 2
|
4天前
|
安全 Java 调度
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
17 1
|
4天前
|
Java 开发者
深入理解Java异常处理机制
【10月更文挑战第29天】在Java的世界中,异常处理如同生活的调味品,不可或缺。它确保了程序在遇到错误时不会崩溃,而是优雅地继续运行或者给出提示。本文将带你领略异常处理的奥秘,从基础的try-catch语句到高级的自定义异常,让你在面对程序中的各种“意外”时,能够从容应对。
|
6月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。
|
3月前
|
存储 安全 Java
解锁Java并发编程奥秘:深入剖析Synchronized关键字的同步机制与实现原理,让多线程安全如磐石般稳固!
【8月更文挑战第4天】Java并发编程中,Synchronized关键字是确保多线程环境下数据一致性与线程安全的基础机制。它可通过修饰实例方法、静态方法或代码块来控制对共享资源的独占访问。Synchronized基于Java对象头中的监视器锁实现,通过MonitorEnter/MonitorExit指令管理锁的获取与释放。示例展示了如何使用Synchronized修饰方法以实现线程间的同步,避免数据竞争。掌握其原理对编写高效安全的多线程程序极为关键。
62 1
|
4月前
|
安全 Java 开发者
Java并发编程中的线程安全问题及解决方案探讨
在Java编程中,特别是在并发编程领域,线程安全问题是开发过程中常见且关键的挑战。本文将深入探讨Java中的线程安全性,分析常见的线程安全问题,并介绍相应的解决方案,帮助开发者更好地理解和应对并发环境下的挑战。【7月更文挑战第3天】
88 0