您的位置 首页 java

Java 17 多线程的同步和异步synchronized关键字

java 17 多线程的同步和异步

先简单的说说这两个名词的概念。

同步 Synchronize

同步是指在程序的运行中,要等待着处理结束后才能继续执行。比方说主函数中包含 a() 、 b()、 c() 三个方法。

a 调用了 b,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 Interrupted Exception  {
        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 的执行默认是同步的, 所以以下的调用方式和上面的结果也是一样的。

异步 Asynchronous

异步是指不需要等待着程序执行结束,还可以继续运行。也就是执行 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();  

完整代码如下:

查看效果:

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

上面的main 方法中最后加入两句代码。

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

重新执行。

你发现 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");  

完整代码如下:

查看运行效果:

为了方便测试出现脏读的情况,这里在 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) {
}  

执行添加方法的时候, 直接发起40个线程进行调用。运行查看效果:

发现应该是余额 4000, 但是却只有 3900,这个肯定接受不了。这么辛苦挣的钱, 还吞了我 100 元。

需要说明的是,以上的演示纯粹为了演示而演示。

synchronized 关键字

这个时候 synchronized 关键字登场了。 同步语句代表着执行线程的时候,获取互斥锁,然后执行块内容,执行完成释放锁。如果执行线程已经加锁,其他的线程就没有办法再获取锁了。

synchronized 语句块

改造我们的程序,调整线程里面的 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();
}  

为了方便查看,加入了线程名称。

完整代码如下图所示:

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

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

这里也可以把 synchronized 也可以放到方法上。 因为这里方法里面使用了线程,所以如果写到方法上,还是无法保证线程安全。所以无法使用该关键字写入到方法的使用方式, 只能使用 synchronized 代码块。

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

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

找到 add 方法。

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

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);
    }
}  

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

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

可以看到常量池中的代码

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

从思想来说,想要实现同步,一定要给虚拟机说我要同步了,虚拟机说好,我给你当个门卫,你走的时候, 要给我说一声,我再放行下一个。你不出来,门卫就在那一直等你出来。

同步和异步的简单使用,以及异步中如何保证数据的唯一性。线程的知识还有很多很多。今天就先到这了。

感谢您的阅读。欢迎关注,收藏,点赞。更多内容持续更新中。

文章来源:智云一二三科技

文章标题:Java 17 多线程的同步和异步synchronized关键字

文章地址:https://www.zhihuclub.com/186335.shtml

关于作者: 智云科技

热门文章

网站地图