• 并发编程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、性能问题 

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

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

           并发问题解决

          1、volatile修饰的变量能够直接写入内存,并且线程读取时,也是直接从内存读取;并且会禁止指令重排;因此解决了可见性和有序性问题,但是不能解决原子性问题

          2、原子性问题的解决,依赖多线程提供的锁机制,但是锁的过度使用会引起性能问题

          3、解决性能问题方案如下:

                 使用无锁结构:TLS、Copy-On-Write、乐观锁、java原子类、Diaruptor无锁队列

                 减少锁的持有时间,让锁细粒度:ConcurrentHashMap、读写锁(读无锁写有锁)

  • 相关阅读:
    Linux下gdb attach的使用(调试已在运行的进程)
    Linux ps 命令
    SemaphoreFullException when checking user role via ASP.NET membership
    c程序内存分布
    正则表达式
    事务实战感悟
    oracle免客户端安装 plsql连接
    关于tomcat的热部署
    json工具包比较 fastjson jackson gson
    图片 滚动 放大缩小 旋转
  • 原文地址:https://www.cnblogs.com/sniffs/p/11622728.html
Copyright © 2020-2023  润新知