并发编程001 --- 初识并发

Posted sniffs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程001 --- 初识并发相关的知识,希望对你有一定的参考价值。

什么是并发编程

简单的说,所谓的并发编程指的是同一台处理器“同时”处理多个任务。

并发的三种场景

1、分工

      合理的拆解不同的任务,并能分配到线程,使多个任务更高效的执行。

2、同步

      线程的执行依赖其他线程的执行结果。

3、互斥

      多个线程需要抢占共享资源。

并发问题的源头

多线程的出现虽然可以提高应用程序的执行效率,但是不可避免的,也会引入一些问题,这些问题的源头如下:

1、缓存带来的可见性问题

     由于CPU的读写速度远远大于内存的读写速度,故CPU利用缓存来缓和CPU和内存读写速度差异带来的问题;

     对于多核处理器,每个核都有独立的缓存,这样CPU在计算完数值后,将数值存入缓存,但是写到内存的时机是不确定的,因此会发生缓存可见性问题

     技术图片

 

             示例:如下程序,预期结果为20000,但实际执行结果为10000~20000之间

public class Add 
    private static long count = 0;
    
    public static long testAdd() throws InterruptedException 
        Thread thread1 = new Thread(new Runnable() 
            @Override
            public void run() 
                for (int i = 0; i < 10000; i++) 
                    count ++;
                
            
        );

        Thread thread2 = new Thread(new Runnable() 
            @Override
            public void run() 
                for (int i = 0; i < 10000; i++) 
                    count ++;
                
            
        );

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        return count;
    

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

      操作系统支持的最大线程数要远远大于操作系统核数,这是为了缓解CPU的IO速度差异,采用了分时复用机制。

      原子性问题的原因是:多线程操作共享变量时,一个线程还未对该变量操作完成,由于分时复用策略,另外一个线程获取了执行权,这个线程获取到的值有可能是错误的。

      常见的面试题:long型的变量在32位系统的高并发应用程序中,为什么会有线程安全问题?

      原因:long型变量是64位的,32位操作系统对long型变量赋值的操作步骤为:先对高32位赋值,再对低32位赋值;这样,如果中间发生了线程切换,就可能获取到错误的值

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

      有序性问题是有编译器会对我们的指令进行优化重排序,这样不会影响最终的执行结果;但是有时还是会发生一些意想不到的问题;

      举例:单例模式下双重检查

public class Singleton 
    private static Singleton instance = null;
    
    public static Singleton getInstance() 
        if (null == instance) 
            synchronized (Singleton.class) 
                if (null == instance) 
                    instance = new Singleton();
                    return instance;
                
            
        
        
        return instance;
    

       第一层判空是为了避免加锁导致的性能问题;第二层判空是为了避免创建多个实例;这看起来并没有什么问题,但是由于编译器指令重排,可能会出现问题。

            正常创建实例的指令顺序为:分配内存--->内存初始化---->变量指向内存地址

            编译优化后指令顺序可能为:分配内存--->变量指向内存地址--->内存初始化

            如果线程执行到第二步的时候被剥夺执行权,另一个线程判空的结果为非空,从而直接返回了instance;由于此时instance未初始化,可能会导致空指针异常

     并发带来的三个问题

       1、安全性问题

             安全性问题的本质就是数据的正确性,为了保证线程安全,应该避免同一时刻不同线程操作共享数据。

       2、活跃性问题

            饥饿:由于线程优先级低等原因,可能会导致线程一直不能被执行

            死锁:线程竞争共享资源,并且互相持有对方的锁,造成多个线程一直等待,造成死锁。

            活锁:和死锁相反,活锁是由于“过于谦让”导致的问题;线程访问共享资源,发现另一个线程也需要访问共享资源,于是退出,等待重试;

                       另外的线程也是如此,因此出现活锁问题。

       3、性能问题 

             锁的过度使用,导致程序串行执行的范围过大,这样就违背了并发编程的优势;

             在实际应用中,应尽量减少不必要锁的使用,尽量减少串行

以上是关于并发编程001 --- 初识并发的主要内容,如果未能解决你的问题,请参考以下文章

C#并发编程之初识并行编程

初识网络并发编程

Python并发编程之初识异步IO框架:asyncio 上篇

并发编程初识

初识并发

Java并发编程初识-线程池