JAVA并发编程

一、什么是并发编程?

近些年,我们的CPU、内存、I/O设备都在不断迭代,计算机技术进步迅速,但是由于CPU的发展速度远快于内存和I/O设备,导致了CPU常常处于等待状态,不能充分利用计算机的性能。而并发编程正是一种解决这个问题的方法,它能够让计算机多个任务同时执行,充分利用CPU的计算资源,提高程序的执行效率和响应速度。并且在处理大量并发请求的业务方面也具有重要的作用,能够提高系统的吞吐量和性能。

学习并发编程还可以帮助我们更好地理解多线程编程的本质和原理,提高我们的编程技能和代码质量。另外,并发编程也是当今软件开发行业的一个重要趋势,是每一位程序员必须要了解和掌握的技能之一。

二、并发编程要解决什么问题?

1、缓存导致的可见性问题

一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。

在单核时代,所有的线程都是在一颗CPU上执行,多个线程同时操作一个cpu中的变量,一个线程对缓存的写,对另外一个线程来说一定是可见的。

在多核时代,多个线程在不同的CPU上执行时,这些线程操作的是不同的CPU缓存。线程A在CPU-A上执行,线程B在CPU-B上执行,这时候线程A操作变量S时,对于线程B就不具备可见性了。

public class DemoOne {

    private int x = 0;

    public static void main(String[] args) throws InterruptedException {
        DemoOne demoOne = new DemoOne();
      
        Thread threadOne = new Thread(() -> {
            demoOne.addx();
        });
        Thread threadTwo = new Thread(() -> {
            demoOne.addx();
        });

        threadOne.start();
        threadTwo.start();

        threadOne.join();
        threadTwo.join();
        demoOne.soutX();
    }

    private void addx(){
        int idx = 0;
        while(idx++ < 10000) {
            x += 1;
        }
    }

    private void soutX(){
        System.out.println(this.x);
    }
}

print:14973

直观来看,两个线程都调用addx方法,最后返回结果x应该是20000,其实不然,假设两个线程同时执行,将x=0的读入各自的CPU中,同时加1,再将结果写回内存,我们会发现内存中x是1,而不是我们期望的2,这就是缓存导致的可见性问题。

2、线程切换带来的原子性问题

众所周知,我们的计算机是可以同时执行多个任务的,你可以一边听歌一边写代码,是因为计算机操作系统用来实现多任务调度的时间片轮转调度技术,操作系统给多个程序分配一个时间片,时间片通常为几十毫秒或几百毫秒,每个时间片执行完后,操作系统会将当前程序挂起,同时调度另外一个程序运行,由于时间片很短,因此用户感受不到多个程序之间的切换。这同时也给我们带来了原子性问题。

public class DemoOne {

    private volatile int x = 0;

    public static void main(String[] args) throws InterruptedException {
        DemoOne demoOne = new DemoOne();
      
        Thread threadOne = new Thread(() -> {
            demoOne.addx();
        });
        Thread threadTwo = new Thread(() -> {
            demoOne.addx();
        });

        threadOne.start();
        threadTwo.start();

        threadOne.join();
        threadTwo.join();
        demoOne.soutX();
    }

    private void addx(){
        int idx = 0;
        while(idx++ < 10000) {
            x += 1;
        }
    }

    private void soutX(){
        System.out.println(this.x);
    }
}

print:18944

上述代码和缓存导致的可见性问题的示例代码基本一致,对x变量使用volatile关键字修饰,禁用cpu缓存,执行会发现结果仍不是我们期望的20000,就是因为当一个线程在执行一个操作时,如果在操作还没有完成时,该线程被切换到其他线程,而其他线程对于同一个操作也进行了修改,则会导致原子性问题。

3、编译优化带来的有序性问题

有序性。顾名思义,有序性指的是程序按照代码的先后顺序执行。

编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:“a=1;b=2;”编译器优化后可能变成“b=2;a=1;”,有时候编译器及解释器的优化可能导致意想不到的Bug。

public class DemoThree {
    
    private static boolean flag = false;
    private static int value = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread one = new Thread(() -> {
            value = 1;
            flag = true;
        });

        Thread two = new Thread(() -> {
            while (!flag) {
                // 如果flag为false,线程一可能还没启动完毕,所以让出CPU时间片
                Thread.yield();
            }

            // 由于volatile可以保证可见性,所以这里flag肯定是true,value肯定为1;但编译器可能会对代码进行指令重排序,从而导致出现异常结果
            if (value == 0) {
                System.out.println("发生了乘0异常");
            }
        });

        one.start();
        two.start();

        one.join();
        two.join();
    }
}

在主线程中,创建了两个线程,线程one先执行,将value变量赋值为1,然后将flag标志位设为true,线程two执行的时候,不断循环直到flag变量为true,在循环结束后判断value变量是否为0。在这个示例中,如果编译器对代码进行了优化,可能会将执行flag变量改为true和执行value变量赋值的这两步操作互换顺序,这样在线程two中判断value的值时可能仍然为0,从而造成一个未预期的异常结果。

总结

以上总结了并发编程及其要解决的问题,提到缓存、线程、编译优化,其实它们的目的和我们写并发程序的目的是相同的,都是提高程序性能。但是技术在解决一个问题的同时,必然会带来另外一个问题,所以在采用一项技术的同时,一定要清楚它带来的问题是什么,以及如何规避

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值