当前位置: 主页 > JAVA语言

java多线程数据同步-java线程之间数据

发布时间:2023-02-08 11:07   浏览次数:次   作者:佚名

同步和异步 Java 17 多线程

下面简单说一下这两个名词的概念。

同步同步

同步是指程序在运行过程中,必须等待处理结束,才能继续执行。 例如,main 函数包含三个方法:a()、b() 和 c()。

a调用bjava多线程数据同步,b调用c。 同步的概念是方法a执行完后,方法b执行,方法b执行完后,方法c执行。

对于每种方法,分别休眠 1 秒、2 秒和 3 秒。 在同步的情况下,执行时间为6秒。

演示代码如下:

import java.util.concurrent.TimeUnit;
public class Synchronize01 {
    public static void main(String[] args) throws InterruptedException {
        long beginTime = System.currentTimeMillis();
        new Synchronize01().a();
        long endTime = System.currentTimeMillis();
        System.out.println("执行的总时间为:" + (endTime - beginTime) + " 毫秒");
    }
    public void a() throws InterruptedException{
        TimeUnit.SECONDS.sleep(2);
        b();
        System.out.println("执行 a() 方法");
    }
    public void b() throws InterruptedException{
        TimeUnit.SECONDS.sleep(1);
        c();
        System.out.println("执行 b() 方法");
    }
    public void c() throws InterruptedException{
        TimeUnit.SECONDS.sleep(3);
        System.out.println("执行 c() 方法");
    }
}

完整代码及效果:

java多线程数据同步_java线程之间数据_java线程池设置线程超时时间

因为Java默认是同步执行的,下面的调用方法和上面的结果是一样的。

java多线程数据同步_java线程之间数据_java线程池设置线程超时时间

异步异步

异步意味着你不需要等待程序执行结束继续运行。 也就是执行了a()方法。 可以继续执行b()方法,也可以继续执行c()方法,前面两个方法没有执行完。

对于Java的异步的实现,需要封装成一个线程,让线程去执行。

同样是上面的a,b,c方法。

代码如下:

Thread ta = new Thread() {
    @Override
    public void run() {
        try {
            a();
            long endTime = System.currentTimeMillis();
            System.out.println("a() 执行的总时间为:" + (endTime - beginTime) + " 毫秒");
        } catch (Exception e) {
        }
    }
}.start();

完整代码如下:

java线程池设置线程超时时间_java线程之间数据_java多线程数据同步

查看效果:

java线程之间数据_java线程池设置线程超时时间_java多线程数据同步

线程执行的结束时间作为结束时间,所以用的最多的就是总时间。 这样,程序的执行时间取决于线程中执行时间最长的方法。

在上面main方法的末尾添加两行代码。

long endTime = System.currentTimeMillis();
System.out.println("main() 执行的总时间为:" + (endTime - beginTime) + " 毫秒");

重新运行。

java线程池设置线程超时时间_java线程之间数据_java多线程数据同步

您发现 main 方法在 1 毫秒内执行完毕。

以上是异步和同步的简单概念。

在线程同步的程序中没有歧义。 不能同时操作的问题。

当异步方法执行时,可以发现其执行的规律性和随机性。 而且杂乱无章。

这时候你会遇到一个新的概念,就是多个线程同时访问一个变量。 导致并发时,数据与实际运行数据不一致。 通常称为“脏读”。 读取数据在读取过程中被修改。 之所以会出现这种情况,是因为多线程条件下读取实例变量存在线程安全问题。 每个人都在抢资源,不管是不是已经在别人手里了。 无法保证多线程实例变量或方法无法保证唯一性和原子性。

举一个存款和取款的例子。

首先创建两个线程来增加和减少数量。

代码如下:

public void add(Asynchronous02 asynchronous02) {
    new Thread(() -> {
        asynchronous02.money = asynchronous02.money.add(new BigDecimal("100"));
        System.out.println("已存入,余额:" + asynchronous02.money);
    }).start();
}
public void minus(Asynchronous02 asynchronous02) {
    new Thread(() -> {
        asynchronous02.money = asynchronous02.money.add(new BigDecimal("100").negate());
        System.out.println("已取出,余额:" + asynchronous02.money);
    }).start();
}

定义了一个变量:

private BigDecimal money = new BigDecimal("0");

完整代码如下:

java线程池设置线程超时时间_java线程之间数据_java多线程数据同步

查看运行效果:

java多线程数据同步_java线程之间数据_java线程池设置线程超时时间

为了方便测试脏读,将main方法中的代码修改如下:

try (Scanner scanner = new Scanner(System.in);) {
    System.out.println("菜单");
    System.out.println("1. 存入金额 100");
    System.out.println("2. 取出金额 100");
    System.out.println("3. 查询当前余额");
    System.out.println("q. 退出");
    Asynchronous02 asynchronous02 = new Asynchronous02();
    String line;
    while (scanner.hasNextLine() && (line = scanner.nextLine()) != null) {
        System.out.println("=================");
        switch (line) {
            case "1":
                for (int i = 0; i < 40; i++) {
                    asynchronous02.add(asynchronous02);
                }
                break;
            case "2":
                asynchronous02.minus(asynchronous02);
                break;
            case "3":
                System.out.println("当前余额:" + asynchronous02.money);
                break;
            default:
                System.out.println("退出系统成功");
                System.exit(0);
        }
    }
} catch (Exception e) {
}

add方法执行时,直接启动40个线程进行调用。 运行看看效果:

java多线程数据同步_java线程池设置线程超时时间_java线程之间数据

应该发现余额应该是4000,但是才3900,这肯定是不能接受的。 这样的血汗钱竟然吞了我100块钱。

需要注意的是,以上演示纯粹是为了演示目的。

同步关键字

这时候synchronized关键字就登场了。 同步语句的意思是当线程执行时,获取互斥量,然后执行block的内容,执行完成后释放锁。 如果执行线程已经加锁,其他线程就不能再获取锁。

同步块

修改我们的程序java多线程数据同步,调整线程中run方法的内容如下:

public void add(asynchronous03 asynchronous03) {
    new Thread(() -> {
        synchronized(this){
            asynchronous03.money = asynchronous03.money.add(new BigDecimal("100"));
            System.out.println(Thread.currentThread().getName() + "已存入,余额:" + asynchronous03.money);
        }
    }).start();
}

为了便于查看,添加了线程名称。

完整代码如下图所示:

java线程之间数据_java线程池设置线程超时时间_java多线程数据同步

这样,重新运行代码。 运行后查看效果:

java线程之间数据_java多线程数据同步_java线程池设置线程超时时间

你可以看到结果。 虽然线程乱了,但是每次访问都会等待上一个线程释放线程锁。 所以加量是有规律的。 这样就达到了我们的目的。 保证数据的原子性和一致性。 确保业务的正确运行。

您也可以在此处对方法进行同步。 因为这个方法中使用了线程,如果写到方法中,线程安全就得不到保证。 所以不能用this关键字来写方法的用法,只能用synchronized代码块。

语句块如何保证其锁定状态。 写一段简单的代码。 为了方便我们查看字节码的内容。

java多线程数据同步_java线程之间数据_java线程池设置线程超时时间

使用javap -c -v Asynchronous04.class 查看字节码内容。

找到添加方法。

java多线程数据同步_java线程池设置线程超时时间_java线程之间数据

可以看到monitorenter和monitorexit命令。 在Java 17虚拟机中,该方法类用于控制同步的进入和退出。 它称为同步构造或监视器。 这只是语句块的方式。 如果是method,则不使用这种方式,而是使用运行时常量池中的ACC_SYNCHRONIZED标志来区分。

同步方法修饰

修改上面的代码。

import java.math.BigDecimal;
public class Asynchronous05 {
    private BigDecimal money = new BigDecimal("0");
    public static void main(String[] args) throws InterruptedException {
        Asynchronous05 asynchronous05 = new Asynchronous05();
        asynchronous05.add();
    }
    public synchronized void add() {
        money = money.add(new BigDecimal("100"));
        System.out.println(Thread.currentThread().getName() + " 已存入,余额:" + money);
    }
}

java多线程数据同步_java线程池设置线程超时时间_java线程之间数据

再次使用javap -c -v Asynchronous05.class 查看字节码内容的变化。

在字节码中找到add方法。 查看字节码常量池的flags内容。

java多线程数据同步_java线程之间数据_java线程池设置线程超时时间

可以看到常量池中的代码

public synchronized void add();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED

从心智上来说,如果要实现同步,就必须告诉虚拟机我要同步。 虚拟机说好,我来当你的看守,等你走的时候告诉我,我就放过下一个。 你不出来,守卫就在那里等着你出来。

同步和异步的简单使用,异步中如何保证数据的唯一性。 关于线程的知识还是很多的。 今天就到此为止。

谢谢阅读。 欢迎关注、收藏、点赞。 更多内容会持续更新。