第三章 Java多线程--并发编程的三大特性--原子性

目录

一、原子性

1 什么是原子性

2 保证并发编程的原子性

2.1 synchronized(上锁)

 2.2 CAS

2.3 Lock锁

2.4 ThreadLocal


一、原子性

1 什么是原子性

JMM(Java Memory Model):不同的硬件和不同的操作系统在内存上的操作有一定差异的。Java为了解决相同代码在不同操作系统上出现的各种问题,用JMM屏蔽掉各种硬件和操作系统带来的差异。

JMM规定所有变量都会存储在主内存中,在操作的时候,需要从主内存中复制一份到线程内存(CPU内存),在线程内部做计算。然后再写回主内存中(不一定!)(虽然理论上每次修改都应该立即反映在主内存中,但实际上由于上述原因,修改后的值可能不会立即写回到主内存,而是会在某个适当的时机(如缓存满、同步点到达或其他JMM规则触发)才进行写回)。

原子性的定义:原子性指一个操作是不可分割的,不可中断的,不受其他线程影响。

由一个例子来阐述

private static int count;

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

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();
    t1.join();
    t2.join();
    System.out.println(count);
}

当前的代码:多线程操作共享变量,预期结果与实际结果不符。

原子性:多线程操作临界资源(共享变量),预期结果与最终结果一致。

2 保证并发编程的原子性

2.1 synchronized(上锁)

上面代码中++操作,编译后的代码,看看其是怎么处理的。

可以在方法上追加synchronized关键字或者采用同步代码块的形式来保证原子性

synchronized可以让避免多线程同时操作临界资源(在操作资源前后添加标识,进行锁定资源),同一时间点,只会有一个线程正在操作临界资源

 2.2 CAS

CAS:CAS是compare and swap也就是比较和交换简称,是一条CPU的并发原语。

CAS的操作是在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。

Unsafe的类提供了对CAS的操作的方法,JVM将方法实现CAS汇编指令。

下面是一个简单的例子:

private static AtomicInteger count = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            count.incrementAndGet();
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            count.incrementAndGet();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

在CAS的基础上实现了一些原子类,其中就包括上方用到的AtomicInteger ,还有很多其他原子类

CAS的缺点:只能保证一个变量的操作是原子性,无法实现对多行代码实现原子性。

CAS存在的问题:

  • ABA问题:可以从下图了解ABA问题,可以通过引入版本号的方式来解决。AtomicStampeReference原子类,就是通过该方法,解决ABA问题。

  • 下面是AtomicStampedReference的使用例子,在CAS时,不仅比较原值,还比较版本信息。
public static void main(String[] args) {
    AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA",1);

    String oldValue = reference.getReference();
    int oldVersion = reference.getStamp();

    boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
    System.out.println("修改1版本的:" + b);

    boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);
    System.out.println("修改2版本的:" + c);
}
  •  自旋时间过长问题:
    • 可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、自适应自旋锁)
    • 可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果。

2.3 Lock锁

 Lock锁初次出现于JDK1.5,其性能比同版本的时期的synchronized好得多,但是在JDK1.6对synchronized进行之后,性能已经相差不大,但是在并发较高的时候建议使用ReentrantLock,下面是Lock的一个使用例子:

private static int count;

private static ReentrantLock lock = new ReentrantLock();

public static void increment()  {
    lock.lock();
    try {
        count++;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    } finally {
        lock.unlock();
    }


}

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();
    t1.join();
    t2.join();
    System.out.println(count);
}

ReentrantLock与synchronized对比,在功能上来说,都是锁。但是ReentrantLock的使用场景比synchronized更加丰富。

ReentrantLock底层基于AQS实现,有一个基于CAS维护的state变量来实现锁操作。

2.4 ThreadLocal

Java中的四种引用类型

Java中的使用引用类型分别是强,软,弱,虚

强引用:在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它始终处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

软引用:对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,作为缓存使用。

弱引用:比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题。

虚引用:不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。不过在开发中,我们用的更多的还是强引用。

本地线程变量ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据。

例子:

static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();

public static void main(String[] args) {
    tl1.set("123");
    tl2.set("456");
    Thread t1 = new Thread(() -> {
        System.out.println("t1:" + tl1.get());
        System.out.println("t1:" + tl2.get());
    });
    t1.start();

    System.out.println("main:" + tl1.get());
    System.out.println("main:" + tl2.get());
}

ThreadLocal实现原理:

  • 每个Thread中都存储着一个成员变量,ThreadLocalMap
  • ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap
  • ThreadLocalMap本身就是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。
  • 每一个现有都自己独立的ThreadLocalMap,再基于ThreadLocal对象本身作为key,对value进行存取
  • ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回收

ThreadLocal内存泄漏问题:

  • 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到。
  • 只需要在使用完毕ThreadLocal对象之后,及时的调用remove方法,移除Entry即可、

  • 22
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值