Java AtomicInteger类使用

一个计数器

对于普通的变量,在涉及多线程操作时,会遇到经典的线程安全问题。考虑如下代码:

private static final int TEST_THREAD_COUNT = 100;
private static int counter = 0;

public static void main(String[] args) {
    final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);  
    Thread[] threads = new Thread[TEST_THREAD_COUNT];

    for (int i = 0; i < TEST_THREAD_COUNT; i++) {
        threads[i] = new Thread(new Runnable() {

            @Override
            public  void run() {
                ++counter;
                System.out.println("Thread " + Thread.currentThread().getId() + "  / Counter : " + counter);
                latch.countDown();
            }
        });
        threads[i].start();  

    }

    try {
        latch.await();
        System.out.println("Main Thread " + "  / Counter : " + counter);
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }
}

多次执行这段程序,我们会发现最后counter的值会出现98,99等值,而不是预想中的100

...
...
Thread 100  / Counter : 90
Thread 101  / Counter : 91
Thread 102  / Counter : 92
Thread 103  / Counter : 93
Thread 104  / Counter : 95
Thread 105  / Counter : 95
Thread 106  / Counter : 96
Thread 107  / Counter : 97
Thread 108  / Counter : 98
Thread 109  / Counter : 99
Main Thread   / Counter : 99

这个问题发生的原因是++counter不是一个原子性操作。当要对一个变量进行计算的时候,CPU需要先从内存中将该变量的值读取到高速缓存中,再去计算,计算完毕后再将变量同步到主内存中。这在多线程环境中就会遇到问题,试想一下,线程A从主内存中复制了一个变量a=3到工作内存,并且对变量a进行了加一操作,a变成了4,此时线程B也从主内存中复制该变量到它自己的工作内存,它得到的a的值还是3,a的值不一致了(这里工作内存就是高速缓存)。

2019-10-21 15-07-56屏幕截图.png

同步

java有个sychronized关键字,它能后保证同一个时刻只有一条线程能够执行被关键字修饰的代码,其他线程就会在队列中进行等待,等待这条线程执行完毕后,下一条线程才能对执行这段代码。
它的修饰对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

现在我们开始使用我们的新知识,调整以上代码,在run()上添加sychronized关键字。

private static final int TEST_THREAD_COUNT = 100;
private static int counter = 0;

public static void main(String[] args) {
    final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);  
    Thread[] threads = new Thread[TEST_THREAD_COUNT];

    for (int i = 0; i < TEST_THREAD_COUNT; i++) {
        threads[i] = new Thread(new Runnable() {

            @Override
            public synchronized void run() {
                ++counter;
                System.out.println("Thread " + Thread.currentThread().getId() + "  / Counter : " + counter);
                latch.countDown();
            }
        });
        threads[i].start();  

    }

    try {
        latch.await();
        System.out.println("Main Thread " + "  / Counter : " + counter);
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }
}

多次执行新代码,我们依旧发现结果不正确:

...
...
Thread 98  / Counter : 87
Thread 97  / Counter : 86
Thread 99  / Counter : 89
Thread 100  / Counter : 89
Thread 101  / Counter : 90
Thread 102  / Counter : 91
Thread 104  / Counter : 95
Thread 108  / Counter : 97
Thread 106  / Counter : 96
Thread 105  / Counter : 95
Thread 103  / Counter : 95
Thread 109  / Counter : 98
Thread 107  / Counter : 97
Main Thread   / Counter : 98

这里的原因在于synchronized是锁定当前实例对象的代码块。也就是当多条线程操作同一个实例对象的同步方法是时,只有一条线程可以访问,其他线程都需要等待。这里Runnable实例有多个,所以锁就不起作用。
我们继续修改代码,使得Runnable实例只有一个:

private static final int TEST_THREAD_COUNT = 100;
private static int counter = 0;
private final static CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);

static class MyRunnable implements Runnable {

    @Override
    public synchronized void run() {
        ++counter;
        System.out.println("Thread " + Thread.currentThread().getId() + "  / Counter : " + counter);
        latch.countDown();
    }
    
}

public static void main(String[] args) {
    Thread[] threads = new Thread[TEST_THREAD_COUNT];

    MyRunnable myRun = new MyRunnable();
    for (int i = 0; i < TEST_THREAD_COUNT; i++) {
        threads[i] = new Thread(myRun);
        threads[i].start();
    }

    try {
        latch.await();
        System.out.println("Main Thread " + "  / Counter : " + counter);
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }
}

现在我们发现多次执行代码后,最后结果都是100
我们可以给counter变量添加volatile关键字(这里它对于结果没有影响)。
当一个变量被定义为volatile之后,它对所有的线程就具有了可见性,也就是说当一个线程修改了该变量的值,所有的其它线程都可以立即知道。通过synchronizedLock也能够保证可见性,synchronizedLock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

sychronized的优缺点

synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。另外在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronized,另外可读性非常好,不管用没用过5.0多线程包的程序员都能理解。但是当同步竞争非常激烈的时候,synchronized的性能一下子会下降几十倍。还有一个最大的问题就是多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。这种情况下就会造成大量的竞争线程性能的下降。

Atomic

针对synchronized的一系列缺点,JDK5提供了Lock类,目的是为同步机制进行改善。Locksynchronized有一点非常大的不同,采用synchronized不需要用户手动的去释放锁,当synchronized方法或者代码块执行完毕之后,系统会自动的让线程释放对锁的占有,而Lock则必须要用户去手动释放锁,如果没有主动的释放锁,就会可能导致出现死锁的现象。不过这篇文章这里不讨论Lock类。
在Java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作
我们这里使用AtomicInteger

private static final int TEST_THREAD_COUNT = 100;
private static AtomicInteger at = new AtomicInteger(0);

public static void main(String[] args) {
    final CountDownLatch latch = new CountDownLatch(TEST_THREAD_COUNT);  
    Thread[] threads = new Thread[TEST_THREAD_COUNT];

    for (int i = 0; i < TEST_THREAD_COUNT; i++) {
        threads[i] = new Thread(new Runnable() {

            @Override
            public void run() {
                int value = at.incrementAndGet();
                System.out.println("Thread " + Thread.currentThread().getId() + "  / Counter : " + value);
                latch.countDown();
            }
        });
        threads[i].start();
    }
    
    try {
        latch.await();
        System.out.println("Main Thread " + "  / Counter : " + at.get());
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    }
}

lalala
每天都是lalala
6.7k 声望
407 粉丝
0 条评论
推荐阅读
Spring事务传播行为详解
Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。事务传播行为是Spring框架独有的事务增强特性,他不属于的事务实际提供方数据库行为。这是Spring为我们提供的强大的工具箱,使用事务传播行可...

JerryTse242阅读 122.7k评论 97

一文搞懂秒杀系统,欢迎参与开源,提交PR,提高竞争力。早日上岸,升职加薪。
前言秒杀和高并发是面试的高频考点,也是我们做电商项目必知必会的场景。欢迎大家参与我们的开源项目,提交PR,提高竞争力。早日上岸,升职加薪。知识点详解秒杀系统架构图秒杀流程图秒杀系统设计这篇文章一万多...

王中阳Go32阅读 2.4k评论 1

封面图
计算机网络连环炮40问
本文已经收录到Github仓库,该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点,欢迎star~

程序员大彬14阅读 1.7k

万字详解,吃透 MongoDB!
MongoDB 是一个基于 分布式文件存储 的开源 NoSQL 数据库系统,由 C++ 编写的。MongoDB 提供了 面向文档 的存储方式,操作起来比较简单和容易,支持“无模式”的数据建模,可以存储比较复杂的数据类型,是一款非常...

JavaGuide8阅读 1.6k

封面图
花了半个小时基于 ChatGPT 搭建了一个微信机器人
相信大家最近被 ChatGPT 刷屏了,其实在差不多一个月前就火过一次,不会那会好像只在程序员的圈子里面火起来了,并没有被大众认知到,不知道最近是因为什么又火起来了,而且这次搞的人尽皆知。

Java极客技术12阅读 3.1k评论 3

封面图
数据结构与算法:二分查找
一、常见数据结构简单数据结构(必须理解和掌握)有序数据结构:栈、队列、链表。有序数据结构省空间(储存空间小)无序数据结构:集合、字典、散列表,无序数据结构省时间(读取时间快)复杂数据结构树、 堆图二...

白鲸鱼9阅读 5.2k

PHP转Go实践:xjson解析神器「开源工具集」
我和劲仔都是PHP转Go,身边越来越多做PHP的朋友也逐渐在用Go进行重构,重构过程中,会发现php的json解析操作(系列化与反序列化)是真的香,弱类型语言的各种隐式类型转换,很大程度的减低了程序的复杂度。

王中阳Go11阅读 2.7k评论 4

封面图
6.7k 声望
407 粉丝
宣传栏