Java线程同步及实现方法详解

 更新时间:2023年11月09日 09:39:12   作者:y_initiate  
这篇文章主要介绍了Java线程同步及实现方法详解,当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常,需要的朋友可以参考下

1. 什么是线程同步?

首先,引用一个非常经典的例子来说明为什么要进行线程同步

当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。 举个例子,动物园有三个窗口同时在售卖门票,假设还剩最后一张门票时,有两个窗口同时有人在买门票,此时两个窗口都观察到还有一张门票,于是两个窗口都选择了卖出,此时门票数变成了-1,出现错误。

还可能会出现其他情况的错误,比如剩余10张票时,两个窗口同时售卖出一张票后修改票数为9。

package test;
import java.io.*;
public class TicketThreadTest {
    public static void main(String[] args) throws IOException, InterruptedException {
        TicketThread ticket1 = new TicketThread();
        Thread thread1 = new Thread(ticket1, "窗口1");
        Thread thread2 = new Thread(ticket1, "窗口2");
        Thread thread3 = new Thread(ticket1, "窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
        thread1.join(); // 等待三个线程结束后打印卖出总数
        thread2.join();
        thread3.join();
        System.out.println("sellNum: " + TicketThread.sellNum);
    }
}
class TicketThread implements Runnable {
    private static int ticketNum = 20; // 总票数
    public static int sellNum = 0; // 统计卖出总票数
    @Override
    public void run() {
        while (true) {
            if (ticketNum > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余:" + --ticketNum);  // 卖出一张票
                sellNum++;  // 卖出总票数加1
            } else {
                break;
            }
            try {
                Thread.sleep(10);  // 每次sleep 10ms,提高出错可能
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

这是某次程序运行结果,很显然卖出29张票,发生了错误

窗口3卖出了一张票,剩余:19
窗口1卖出了一张票,剩余:18
窗口2卖出了一张票,剩余:17
窗口3卖出了一张票,剩余:16
窗口1卖出了一张票,剩余:16
窗口2卖出了一张票,剩余:16
窗口1卖出了一张票,剩余:15
窗口3卖出了一张票,剩余:14
窗口2卖出了一张票,剩余:15
窗口2卖出了一张票,剩余:13
窗口3卖出了一张票,剩余:12
窗口1卖出了一张票,剩余:13
窗口3卖出了一张票,剩余:9
窗口1卖出了一张票,剩余:11
窗口2卖出了一张票,剩余:10
窗口1卖出了一张票,剩余:7
窗口3卖出了一张票,剩余:8
窗口2卖出了一张票,剩余:8
窗口1卖出了一张票,剩余:5
窗口3卖出了一张票,剩余:6
窗口2卖出了一张票,剩余:6
窗口2卖出了一张票,剩余:4
窗口3卖出了一张票,剩余:3
窗口1卖出了一张票,剩余:4
窗口1卖出了一张票,剩余:2
窗口3卖出了一张票,剩余:2
窗口2卖出了一张票,剩余:1
窗口2卖出了一张票,剩余:-1
窗口1卖出了一张票,剩余:0
sellNum: 29

2. Java线程同步方法

Java线程同步有7种方法

  • 使用 synchronized关键字实现线程同步
  • 使用wait和notify实现线程同步
  • 使用特殊域变量(volatile)实现线程同步
  • 使用重入锁实现线程同步,在JavaSE5.0中新增了一个java.util.concurrent包来支持同步
  • 使用局部变量实现线程同步,如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
  • 使用阻塞队列实现线程同步
  • 使用原子变量实现线程同步

3 使用synchronized实现线程同步

synchronized的作用主要有三个:

  • 原子性:确保线程互斥地访问同步代码
  • 可见性:保证共享变量的修改能够及时可见

可见性是通过Java内存模型中的“对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值”来保证的

  • 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”

happen-before:If one action _happens-before _another, then the first is visible to and ordered before the second. 如果指令甲happens-before指令乙,那么指令甲必须排序在指令乙之前,并且指令甲的执行结果对指令乙可见。

3.1 同步代码块

同步代码块是通过锁定一个指定的对象,来对同步代码块中的代码进行同步。 一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该代码块的线程将被阻塞。 注意synchronized必须锁住的是指定的对象,不同对象间不会阻塞,如果需要锁住类对象,只需要使用synchronized(Class clazz)锁住类即可。

我们使用同步代码块来解决售票问题

package test;
import java.io.*;
public class TicketThreadTest {
    public static void main(String[] args) throws IOException, InterruptedException {
        TicketThread ticket1 = new TicketThread();
        Thread thread1 = new Thread(ticket1, "窗口1");
        Thread thread2 = new Thread(ticket1, "窗口2");
        Thread thread3 = new Thread(ticket1, "窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
        thread1.join(); // 等待三个线程结束后打印卖出总数
        thread2.join();
        thread3.join();
        System.out.println("sellNum: " + TicketThread.sellNum);
    }
}
class TicketThread implements Runnable {
    private static int ticketNum = 20; // 总票数
    public static int sellNum = 0; // 统计卖出总票数
    @Override
    public void run() {
        while (true) {
            synchronized (this) {	// 锁住对共享变量的访问
                if (ticketNum > 0) {
                    System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余:" + --ticketNum);  // 卖出一张票
                    sellNum++;  // 卖出总票数加1
                } else {
                    break;
                }
            }
            try {
                Thread.sleep(10);  // 每次sleep 10ms,提高出错可能
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

从运行结果可以看到synchronized修饰的代码块同一时间只能有一个线程访问

窗口1卖出了一张票,剩余:19
窗口2卖出了一张票,剩余:18
窗口3卖出了一张票,剩余:17
窗口3卖出了一张票,剩余:16
窗口2卖出了一张票,剩余:15
窗口1卖出了一张票,剩余:14
窗口1卖出了一张票,剩余:13
窗口2卖出了一张票,剩余:12
窗口3卖出了一张票,剩余:11
窗口2卖出了一张票,剩余:10
窗口3卖出了一张票,剩余:9
窗口1卖出了一张票,剩余:8
窗口1卖出了一张票,剩余:7
窗口2卖出了一张票,剩余:6
窗口3卖出了一张票,剩余:5
窗口2卖出了一张票,剩余:4
窗口3卖出了一张票,剩余:3
窗口1卖出了一张票,剩余:2
窗口3卖出了一张票,剩余:1
窗口1卖出了一张票,剩余:0
sellNum: 20

注意上述类中的ticketNum和sellNum都属于类对象,如果我们使用不同的实例对象,使用synchronized(this)锁住的不是同一个对象,会发现并没有实现线程同步,此时就需要锁住synchronized(this.getClass())。

使用不同实例对象,main方法中修改如下:

//使用不同实例对象,main方法中修改如下:
TicketThread ticket1 = new TicketThread();
TicketThread ticket2 = new TicketThread();
TicketThread ticket3 = new TicketThread();
Thread thread1 = new Thread(ticket1, “窗口1”);
Thread thread2 = new Thread(ticket2, “窗口2”);
Thread thread3 = new Thread(ticket3, “窗口3”);
//TicketThread类中修改为
synchronized(this.getClass())或synchronized(TicketThread.class)

3.2 同步方法

同步方法是对这个方法块里的代码进行同步,而这种情况下锁定的对象就是方法所属的对象自身。

相当于使用synchronized(this)锁住方法中的代码

如果这个方法是静态同步方法呢?那么线程锁定的就不是这个类的对象了,而是这个类对应的java.lang.Class类型的对象。

相当于使用synchronized(this.getClass())锁住方法中的代码

**注意:**当一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其他线程无法在此时访问这个对象的同步方法,也不能执行同步块,但可以访问非同步方法中的非同步代码块。

上述售票问题使用同步方法实现线程同步

package test;
import java.io.*;
public class TicketThreadTest {
    public static void main(String[] args) throws IOException, InterruptedException {
        TicketThread ticket1 = new TicketThread();
        Thread thread1 = new Thread(ticket1, "窗口1");
        Thread thread2 = new Thread(ticket1, "窗口2");
        Thread thread3 = new Thread(ticket1, "窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
        thread1.join(); // 等待三个线程结束后打印卖出总数
        thread2.join();
        thread3.join();
        System.out.println("sellNum: " + TicketThread.sellNum);
    }
}
class TicketThread implements Runnable {
    private static int ticketNum = 20; // 总票数
    public static int sellNum = 0; // 统计卖出总票数
    @Override
    public void run() {
        while (true) {
            if(!sellOneTicket()){
                break;
            }
            try {
                Thread.sleep(10);  // 每次sleep 10ms,提高出错可能
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    private synchronized boolean sellOneTicket(){
        if (ticketNum > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出了一张票,剩余:" + --ticketNum);  // 卖出一张票
            sellNum++;  // 卖出总票数加1
            return true;
        } else {
            return false;
        }
    }
}

到此这篇关于Java线程同步及实现方法详解的文章就介绍到这了,更多相关Java线程同步内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java 环境配置(2023年详细教程)

    java 环境配置(2023年详细教程)

    这篇文章首先为了完善我的知识体系,今后一些软件的安装教程也可能会用到想写一个更加详细的,因为这并不仅仅是写给 IT 行业的,其它行业可能也需要配置java环境
    2023-06-06
  • SpringCloud微服务之Config知识总结

    SpringCloud微服务之Config知识总结

    今天带大家学习SpringCloud微服务中的Config的相关知识,文中有非常详细的介绍,对正在学习SpringCloud微服务的小伙伴们有很好地帮助,需要的朋友可以参考下
    2021-05-05
  • Java中的RestTemplate使用详解

    Java中的RestTemplate使用详解

    这篇文章主要介绍了Java中的RestTemplate使用详解,Spring内置了RestTemplate作为Http请求的工具类,简化了很多操作,虽然Spring5推出了WebClient,但是整体感觉还是RestTemplate用起来更简单方便一些,需要的朋友可以参考下
    2023-10-10
  • spring 和 spring boot 中的属性配置方式

    spring 和 spring boot 中的属性配置方式

    这篇文章主要介绍了spring 和 spring boot 中的属性配置方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • 详解Java编程中protected修饰符与static修饰符的作用

    详解Java编程中protected修饰符与static修饰符的作用

    这篇文章主要介绍了Java编程中protected关键字与static关键字的作用,是Java入门学习中的基础知识,需要的朋友可以参考下
    2016-01-01
  • Java创建、读取和更新Excel文档的实现指南

    Java创建、读取和更新Excel文档的实现指南

    还在用Java代码逐行雕刻Excel文档吗,是时候升级你的生产力工具箱了,借助Spire.XLS for Java,无论是创建新报表、读取关键数据还是实时更新内容,现在你都能以简单的代码指令轻松实现,所以本文给大家介绍了如何使用Java创建、读取和更新Excel文档,需要的朋友可以参考下
    2025-10-10
  • Java8中的默认方法(面试者必看)

    Java8中的默认方法(面试者必看)

    这篇文章主要介绍了Java8中的默认方法(面试者必看),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • springboot捕获全局异常实现过程

    springboot捕获全局异常实现过程

    本文主要介绍了Java中的异常和错误,包括Exception和Error的区别、如何捕捉全局异常、自定义异常的实现等,通过实例代码和步骤,展示了如何在Spring Boot项目中实现全局异常处理,并自定义异常类来增强程序的健壮性
    2026-03-03
  • Java中final关键字的使用与注意总结

    Java中final关键字的使用与注意总结

    这篇文章主要给大家介绍了关于Java中final关键字的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者使用Java具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2020-08-08
  • WebSocket无法注入属性的问题及解决方案

    WebSocket无法注入属性的问题及解决方案

    这篇文章主要介绍了WebSocket无法注入属性的问题及解决方法,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-09-09

最新评论