多线程编程基础

多线程编程基础

随着计算机硬件的发展,CPU从最初的单核、到现在的多核。软件为了适应硬件,也由最初的依赖单核计算(单任务)到多核计算(多任务)。计算能力有了大幅提升,同时也引入了多线程编程带来的数据一致性安全问题。

网上Java内存模型 这篇文章从计算机硬件、以及JVM内存模型相结合详细说明了数据一致性的原理。强烈推荐看看

主要内容:

  1. 计算机硬件多核CPU 到 高速缓存 再到主存之间对数据一致性的操作解说
  2. JVM基于计算机对内存一致性的原理定义的一种对内存操作的一种规范,即Java的抽象内存模型,该模型非常复杂,考虑到应用层在使用的时候尽可能简单、易用。java规范使用了一系列 synchronized,volatile,Lock/,inal,等语法来避免多线程在操作数据一致性安全的方面得到保证

基于以上:其实在应用层的程序员,只要在多线程编程部分遵循Java内存模型的规范、即原子性、内存可见性、有序性三大特性即可实现线程安全。

线程安全本质: 多个线程同一时刻对同一块内存数据或者多个不可分割的非原子操作进行安全的操作,达到程序运行结果的始终一致性即可。

注意: 在单线程的编程中,不会涉及到线程安全问题,所以以下三大特性仅需在多线程编程中遵循即可。

多线程安全遵循的三大特性

一、 原子性

jvm在执行代码语句的过程中,一个在执行过程中不会被其他线程中断(要么执行完成,要么不执行)的操作被定位为具有原子性。 在某些可能被多个线程同时访问的代码块中,对状态变量、或非原子性操作的指令集合需要特别注意。

提醒:对于非原子操作可以使用synchronized 方法、代码块、或者Lock来进行约束

常见非原子性情况:

  1. i++类似的非原子性语句操作。该操作属于非原子性,分为读取内存值、操作内存值、更新内存值三个步骤,在多线程操作过程中,尤其需要注意,可以使用jdk本身自带的原子类,如AtomicInteger 等线程安全的类

  2. long,double非原子性类型。jvm在加载这两种类型数据的时候会分为高低4位,这种也是非原子性操作,通过volatile修饰该变量,可以确保读写的原子性

  3. 集合类的复合操作。如下代码:

        //HashMap本身线程不安全
        private static Map<String, String> MAP = new HashMap<>();
        /**
         * 这段代码的语义:如果MAP中没有该key的值则赋值value 。
         * 但是当有两个以上线程同时访问的时候,可能会存在第二个线程或者之后的线程将第一个线程的的值覆盖掉
         * 风险发生在:同时执行了语句2,然后线程一先执行了语句3,接着线程2执行了语句3
         * **/
        public static void putIfAbsent(String key, String value) {
            String val = MAP.get(key);  //语句1
            if (val == null) {      //语句2
                MAP.put(key, value);    //语句3
            }
        }
    

    以上情况可以通过使用并发容器如ConcurrentHashMap来声明Map类型

  4. 懒加载带来的非原子性操作。如下代码

        /** 
         * 延迟加载 
         * 
         * **/  
        private static Singleton singleton;    
      
        public static Singleton getInstance(){  
            if(singleton ==null){  	//非线程安全
                singleton = new Singleton();  
                return singleton;              
            }  
            return singleton;  
        }  
    
    /****以下为线程安全模式***/
        /** 
         * 延迟加载 
         * 
         * **/  
        private static volatile Singleton singleton;    //volatile 多线程情况下保证内存可见性  
      
        public static Singleton getInstance(){  
            if(singleton ==null){  
                synchronized (Singleton.class){ //双重加锁,防止多线程同时访问  
                    if(singleton==null){  
                        singleton = new Singleton();  
                        return singleton;  
                    }  
                }  
            }  
            return singleton;  
        }  
    

二、内存可见性

内存可见性问题主要是由于每个工作线程都有自己的工作内存(线程之间不可见),当线程对某个数据进行操作,需要将主存中的数据副本拷贝到自己的工作内存中进行操作,操作完成之后再适时的更新到主存中。这样就造成多个线程同时对一块内存数据操作的时候,对数据可能造成脏读、脏写的问题。

提醒:解决内存可见性问题,主要是使用volatile关键字修饰变量。这样当某个线程要开始读、写某个变量数据时,会根据内存一致性原则,操作最新的数据。

三、有序性

为了提高程序的性能,java编译器和处理器会对指令进行重新排序。在单线程环境下,重排序不会对结果造成影响;但是在多线程环境下,重排序可能会对结果造成影响。

源代码--->编译器优化重排序--->指令级并行重排序--->内存系统重排序--->最终执行的指令序列

排序类型:

  1. java编译器优化的重排序。
  2. 处理器指令级并行的重排序、处理器内存系统的重排序。

多线程编程知识点

无状态对象(类中没有状态变量)、不可变对象(状态变量在初始化的过程已经确定)本身线程安全。

1. final 关键字

分为final 类(类不能被继承,如String)、final方法(方法不能被覆写,如模板方法设计模式)、final变量。这里主要聊变量

声明了final的变量,只有在定义的地方初始化,或者是在构造方法中初始化,本身是线程安全的。在修饰基本类型的时候,则变量的状态就不可变,在声明引用类型的时候,引用地址不可变(不会被其他对象覆盖其引用值),地址本身指向的数据仍然可变。

2.volatile 关键字

作用在变量上面的一个关键字。当被声明之后,如果该变量被更新会被通知到其他线程。它是一种比加锁更轻量级的同步 机制。加锁保证了原子性和内存可见性,但是该变量只是保证了内存可见性。

使用案例:
1. 一般在标记线程是否终止状态的时候,会使用该变量去修饰boolean类型,以确保该变量对多个线程的内存可见性。
2. 声明64位的基本类型long、double ,能确保JVM读取该变量具有原子性。如果不用volitale修饰long/double,则JVM对其读写本身不具备原子性。
  
使用条件: 当且仅当满足下面所有的条件,才可以使用该变量。
1. 对变量的写入操作不依赖变量的当前值,才应该使用volitale
2. 该变量不会与其他状态变量一起纳入不变形条件中
3. 在访问变量时不需要加锁 

3. synchronized关键字

重量级锁,能够保证原子性、内存可见性,并且有序性。分为类级别的锁、对象锁。

  1. 类级别的锁

    锁的范围比较大,这个类的所有对象都会别加锁,慎重使用

        public void classLock(){
            synchronized (Connection.class){
    
            }
        }
        public static synchronized void methodClassLock(){
        }
    
  2. 对象锁

    锁的作用范围只是当前对象。当某个对象的某个属性线程非安全,则相应的读写方法都要加锁

        public synchronized void objLock(){
            
        }
        public void methodObjLock(){
            synchronized (this){
                
            }
        }
    

4. 显式的可重入锁 ReentrantLock

5. 轻量级的可重入读写锁ReentrantReadWriteLock

并发工具

1.并发容器

并发容器则是采用了分段锁、或者是CopyOnWriteX的性质,提供多线程的并发访问,从而提高吞吐性。

常用的并发容器:ConcurrentHashMap 、 CopyOnWriteArrayList、BlockingQueue、
注意:
0. 容器类本身要具备高性能,不同于普通的业务代码,
1. 并发容器在多线程并发操作的时候,不会抛出并发修改错误。
2. 并发容器在对size()等方法采用了弱一致的支持,也就是并不能保证结果是一定正确,因为本身该容器是在实时的变化。
3. 并发容器提供了putIfAbsent()等常用的复合操作

2. 同步容器

Hashtable 、Vector 之类的容器。线程安全,但是同一时刻只能支持单线程访问,效率低

不足之处:

  1. 涉及到复合操作的时候, 需要调出者再次加锁控制
  2. 多线程操作的时候,会抛出并发修改异常。如:线程A在遍历集合,线程B在删除某一个元素。所以同步线程在此处所做的只是抛出异常,提醒调用者。
  3. 多线程环境下效率低。尤其是涉及到size()、 contains()等需要对比所有元素的方法。
原文地址:https://www.cnblogs.com/henuzyy/p/10896508.html