并发编程三大特性之原子性

发布于:2024-04-07 ⋅ 阅读:(133) ⋅ 点赞:(0)

一、并发编程3大特性是什么?

       并发编程三大特性分别是原子性、可见性和有序性。java内存模型JMM就是围绕着原子性、

       可见性、原子性来处理java线程间通信的。

二、原子性

       1、什么是原子性?

                   原子性是指一个操作是不可分割的,不可终端的;一个线程执行时,另一个线程

             不会影响到它 ,即多线程操作临界资源,预期结果要与最终结果一致。

             如:赋值操作 a=1 是原子操作,但注意 i++/i--、++j/--j 不是原子操作,

                   以下边代码举例说明:

public class Test01 {

    private static int count = 0;

    public static void increment() {
        count++;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for(int i=0;i<100;i++){
                increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for(int i=0;i<100;i++){
                increment();
            }
        });

        t1.start();
        t2.start();

        //主线程(这里是main线程)阻塞,等待t1、t2执行完成后主线程(main线程)才继续往下执行
        t1.join();
        t2.join();
        //若count++是原子的,那么最后count一定输出200,否则说明count++不是原子性的
        System.out.println(count);
    }
}

                   当前程序多线程操作时,最终结果与预期结果不一致

           2、通过javap 反编译java字节码文件来观察i++操作的执行过程

                 1)在当前文件下执行命令 javac  java文件,将java文件编译成字节码文件,

                        即.class 文件,如Test01.class

                 2)执行 javap -v Test01.class 命令,反编译字节码文件,i++ 的执行过程如下:

                       

                       可以看到i++的操作分了3个步骤,即如上图所示分别是:从主内存中取变量、

                      在线程私有本地内存中计算、最后把计算结果刷新到主内存

         3、如何保证原子性?

              3.1)通过synchronized关键字 保证原子性

                       可以在方法上添加关键字或在方法中使用synchronized 同步代码块来保证原子性。

                       synchronized 可以避免多个线程同时操作临界资源,同一时间点只有一个线程在

                       操作临界资源。

                       以使用同步代码块为例,如下所示:

public  static int count = 0;

 
    public void increment(){
        synchronized (this){
            count++;
        }

    }

                        

                     java文件编译后,monitorenter指令插入到代码块开始的位置,表示获取了锁;

                       而 monitorexit 指令插入到代码块结束或异常的位置,表示释放锁。

                       jvm保证每个 monitorenter指令 都有一个 monitorexit 指令 与其对应

              3.2)CAS 保证原子性

                       (1)什么是CAS

                                 compare  and swap(简称CAS)也就是比较和交换,是一条CPU层面

                                 上的并发源语,在cpu层面上执行。它在替换内存中某个位置的值时,首先

                                 会查看该位置的值是否与预期的值一致,若一致则交换;这个操作是原子的

                                  java中基于Unsafe的类提供了对CAS的操作方法,jvm会帮助我们将方法实现

                                  编译成CAS汇编指令。

                                  注意:CAS只是比较和交换,但从内存中取变量需要自己实现;

                                             CAS只能保证一个变量的原子性,不能保证多行代码的原子性

                        (2)使用CAS保证原子性,示例代码如下:

                                  

public static AtomicInteger count = new AtomicInteger(0);

    public static void increment(){
        //自增
        count.incrementAndGet();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

                 3.3)使用Lock 锁来保证原子性,示例代码如下:

                          

                           

                 3.4)使用ThreadLocal保证原子性

                           ThreadLocal保证原子性的方式是不让多线程去操作临界资源,让每个线程操作

                           自己私有本地内存中的资源(即操作自己的数据,不操作共享数据)

                           示例代码如下:

                                    

                       ThreadLocal 实现原理:

                               1)每个Thread中都存储着一个成员变量ThreadLocalMap,用与存放与其关联

                                     的ThreadLocal,ThreadLocal绑定的是当前线程

                                     ThreadLocal的set/get方法如下图:

                                           

                                           

                               2)ThreadLocal 本身不存储数据,像是一个工具类,基于ThreadLocal去操作

                                     ThreadLocalMap

                               3)ThreadLocalMap本身就是基于Entry[] 实现的,因为一个线程可以绑定多个

                                     ThreadLocal,这样一来可能需要存储多个数据,所以采用Entry[] 形式实现

                               4)每个Thread 都有自己独立的 ThreadLocalMap ,再基于ThreadLocal 对象

                                     本身作为key,对value进行存取

                                5)ThreadLocalMap的key是一个弱引用,若引用的特点是:对象即使存在弱

                                      引用,在GC时,也必须被回收。这里弱引用是为了解决在ThreadLocal对

                                      象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法

                                      回收的问题,即弱引用是为了解决ke内存泄漏的问题。

                               ThreadLocal 存储结构图如下:

                                           

                         ThreadLocal 内存泄漏的问题:

                                   1)什么是  ThreadLocal 内存泄漏?

                                         如果 ThreadLocal 引用丢失,key因为弱引用会被GC回收掉,如果同时

                                         线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同

                                          时也无法被获取到。

                                    2) 如何解决 ThreadLocal 内存泄漏?

                                           只需要在使用完  ThreadLocal 对象之后,及时调用 ThreadLocal 的

                                           remove() 方法把 ThreadLocal 的值从 Entry 删除。

                                           ThreadLocal的 remove方法如下:

                                                  

                                      


网站公告

今日签到

点亮在社区的每一天
去签到