《Java7并发编程实战手册》学习笔记(七)——并发集合

472 阅读21分钟

此篇博客为个人学习笔记,如有错误欢迎大家指正

本次内容

  1. 使用非阻塞式线程安全列表
  2. 使用阻塞式线程安全列表
  3. 使用按优先级排序的阻塞式线程安全列表
  4. 使用带有延迟元素的线程安全列表
  5. 使用线程安全可遍历映射
  6. 生成并发随机数
  7. 使用原子变量
  8. 使用原子数组

Java为我们提供了集合框架,它实现了大量在程序中常用的数据结构。但是大多数集合类并不能直接用于并发程序中,因为它们没有对数据的并发访问进行控制,中直接使用这些类会有极大的可能出现错误。当然,还有一些集合类它们是线程安全的,可以直接在并发程序中应用,这些集合类大致可以被分为如下两类:

  • 阻塞式集合(Blocking Collection):当此类集合内部容量为空或已满时,被调用的移除或添加方法就无法被立即执行,调用此方法的线程会被阻塞直到方法被成功执行。因此这类集合被称为阻塞式集合
  • 非阻塞式集合(Non-Blocking Collection):这类集合在容量为空或已满时,调用移除或添加方法的线程并不会被阻塞,因为调用的方法在这种情况下会立刻返回null或抛出异常。因此这类集合被称为非阻塞式集合

各种列表与其在Java中对应的实现类(包括但不限于以下类型):

  • 非阻塞式列表:ConcurrentLinkedDeque
  • 阻塞式列表:LinkedBlockingDeque
  • 按优先级排列表元素的阻塞式列表:PriorityBlockingQueue
  • 带有延迟列表元素的阻塞式列表:DelayQueue
  • 非阻塞式可遍历映射:ConcurrentSkipListMap
  • 随机数字:ThreadLocalRandom
  • 原子变量:AtomicLongAtomicIntegerArray

1.使用非阻塞式线程安全列表

非阻塞式列表在某些操作不能执行时(例如从空表中取数据)不会阻塞线程,而是直接返回null或抛出异常。

范例实现

关于ConcurrentLinkedDeque类的部分方法:

  • add(E e):向队列尾部插入指定元素,因ConcurrentLinkedDeque类是一个无界双端队列,因此一般不会出现容量已满的情况,此方法也就可以正常执行
  • pollFirst()pollLast():检索并删除队头、尾元素。如果队列为空,返回null
  • getFirst()getLast():检索但不删除队头、尾元素。如果队列为空,方法将抛出NoSuchElementException异常
  • peekFirst()peekLast():检索但不删除队头、尾元素。如果队列为空,方法返回null
  • removeFirst()removeLast():检索并删除队头、尾元素。如果队列为空,方法抛出NoSuchElementException异常

在这个范例中,我们将向启动多个线程向ConcurrentLinkedDeque对象中增加、删除大量数据,代码如下:

AddTask(增添数据类):

package day07.code_1;

import java.util.concurrent.ConcurrentLinkedDeque;

public class AddTask implements Runnable {

    //列表
    private ConcurrentLinkedDeque<String> list;

    public AddTask(ConcurrentLinkedDeque<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        //设置名字为当前线程的名字
        String name = Thread.currentThread().getName();
        //向容器中加入数据
        for (int i = 0; i < 10000; i++) {
            list.add(name + " : Element " + i);
        }
    }
}

PollTask(移除数据类):

package day07.code_1;

import java.util.concurrent.ConcurrentLinkedDeque;

public class PollTask implements Runnable {

    //列表
    private ConcurrentLinkedDeque<String> list;

    public PollTask(ConcurrentLinkedDeque<String> list) {
        this.list = list;
    }

    @Override
    public void run() {
        //循环5000次每次取出两个数据
        for (int i = 0; i < 5000; i++) {
            list.pollFirst();
            list.pollLast();
        }
    }
}

main方法:

package day07.code_1;

import java.util.concurrent.ConcurrentLinkedDeque;

public class Main {

    public static void main(String[] args) {
        //创建一个列表
        ConcurrentLinkedDeque<String> list = new ConcurrentLinkedDeque<>();
        //创建一个线程数组
        Thread[] threads = new Thread[100];
        //创建100个加入数据的线程并启动
        for (int i = 0; i < threads.length; i++) {
            AddTask task = new AddTask(list);
            threads[i] = new Thread(task);
            threads[i].start();
        }
        //打印线程启动提示信息
        System.out.printf("Main: %d AddTask threads have been launched\n",
                threads.length);
        //等待加入数据的线程运行结束
        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //打印列表中当前的数据量
        System.out.printf("Main: Size of the List: %d\n", list.size());
        //创建100个取出数据的线程并启动
        for (int i = 0; i < threads.length; i++) {
            PollTask task = new PollTask(list);
            threads[i] = new Thread(task);
            threads[i].start();
        }
        //打印线程启动提示信息
        System.out.printf("Main: %d PollTask threads have been launched\n",
                threads.length);
        //等待取出数据的线程运行结束
        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //打印列表中当前的数据量
        System.out.printf("Main: Size of the List: %d\n", list.size());
    }

}

2.使用阻塞式线程安全列表

阻塞式列表在队列已满或为空时,调用增添、移除方法的线程会被阻塞,直到方法可以执行

范例实现

我们在创建LinkedBlockingDeque类对象时,可以通过有参构造方法指定列表的最大容量,调用无参构造方法的话列表的最大容量默认为Integer.MAX_VALUE,也就是2^31-1=2147483647。
LinkedBlockingDeque类的常用方法:

  • putFirst(E e)putLast(E e)put(E e):向队列头部和尾部添加元素,如果队列容量已满则线程阻塞直到队列中有可用的空间
  • take()takeFirst()takeLast():返回并移除队头和队尾元素。如果队列为空则阻塞线程直到队列不为空

LinkedBlockingDeque类也实现了一些不会阻塞线程的方法如下:

  • getFirst()getLast():返回但不删除对头和队尾的元素。如果队列为空则抛出NoSuchElementException异常
  • peek()peekFirst()peekLast():返回但不删除对头和队尾的元素。如果队列为空返回null
  • poll()pollFirst()pollLast():返回并移除队头和队尾元素。如果队列为空则返回null
  • add(E e)addFirst(E e)addLast(E e):将元素添加至队头或队尾,如果队列已满则抛出IllegalStateException异常

在这个范例中,我们将向启动多个客户线程向LinkedBlockingDeque对象中增加大量请求,代码如下:
Client(客户类):

package day07.code_2;

import java.util.Date;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;

public class Client implements Runnable {

    //列表
    private LinkedBlockingDeque<String> requestList;

    public Client(LinkedBlockingDeque<String> requestList) {
        this.requestList = requestList;
    }

    @Override
    public void run() {
        //每2秒向列表中添加5条请求数据,循环3次
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 5; j++) {
                StringBuilder request = new StringBuilder();
                request.append(i);
                request.append(":");
                request.append(j);
                try {
                    //如果列表未满,向尾部添加数据
                    requestList.put(request.toString());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //打印请求数据
                System.out.printf("Client: %s at %s\n",
                        request, new Date());
            }
            //休眠2秒
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //打印结束提示信息
        System.out.println("Client: End");
    }
}

main方法:

package day07.code_2;

import java.util.Date;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;

public class Main {

    public static void main(String[] args) {
        //创建一个列表
        LinkedBlockingDeque<String> list = new LinkedBlockingDeque<>(1);
        //创建客户端对象
        Client client = new Client(list);
        //创建线程并开启
        Thread thread = new Thread(client);
        thread.start();
        //每300毫秒从列表中取出5条数据,循环三次
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 5; j++) {
                String request = null;
                try {
                    //返回并删除列表第一个元素
                    request = list.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //打印相关信息
                System.out.printf("Main: Request: %s at %s. Size: %d\n",
                        request, new Date(), list.size());
            }
            //休眠300毫秒
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //打印程序结束提示语
        System.out.println("Main: End of the program");
    }
}

3.使用按优先级排序的阻塞式线程安全列表

PriorityBlockingQueue是一个线程安全的、无界的、可按照传入对象的某些属性进行自定义排序的列表。要装入此列表的元素必须实现Comparable接口并重写compareTo(Event o)方法,传入的参数是一个同类型的对象。可以在此方法中用当前对象的属性和传入对象的进行比较,并给出对应的返回值。插入元素时,PriorityBlockingQueue使用compareTo()方法来决定插入元素的位置,compareTo(Event o)方法返回小于0的数表示此对象的位置相对于传入对象靠前;大于0则表示靠后;等于0顺序不确定。这里我们需要注意一个问题,通过查看源码发现,PriorityBlockingQueue底层是使用数组维护的最小堆来装载元素的。因此元素在集合中的位置并不一定等于在底层数组中的位置,我们在发现这个问题时千万不要误以为重写的compareTo()方法没有生效。PriorityBlockingQueue同时也是一个阻塞式队列,它具有上面我们提到过的阻塞式集合的特点。
以下为PriorityBlockingQueue类的常用方法:

  • add(E e)put(E e):将指定的元素插入到此优先级队列中
  • poll():检索并删除此队列的头部,如果此队列为空,则返回 null
  • clear():移除队列中的所有元素
  • take():返回并删除队列中第一个元素,如果队列为空则阻塞线程直到有可返回的元素
  • peek():返回但不删除队列中的第一个元素,如果队列为空则返回null

范例实现

在这个范例中,我们将实现了Comparable接口并重写了compareTo()方法的类装入PriorityBlockingQueue集合中,并判断顺序是否正确
Event(事件类):

package day07.code_3;

public class Event implements Comparable<Event> {

    //线程的编号
    private int thread;

    //优先级
    private int priority;

    public Event(int thread, int priority) {
        this.thread = thread;
        this.priority = priority;
    }

    public int getThread() {
        return thread;
    }

    public int getPriority() {
        return priority;
    }

    @Override
    public int compareTo(Event o) {
        //如果比传入的事件优先级高
        if (this.priority > o.getPriority()) {
            //返回<0的数表示排在传入事件的前面
            return -1;
        } else if (this.priority < o.getPriority()) {
            //返回>0的数表示排在传入事件的后面
            return 1;
        } else {
            //返回0表示和传入事件的顺序不确定
            return 0;
        }
    }
}

Task(任务类):

package day07.code_3;

import java.util.concurrent.PriorityBlockingQueue;

public class Task implements Runnable {

    //任务编号
    private int id;

    //列表
    private PriorityBlockingQueue<Event> queue;

    public Task(int id, PriorityBlockingQueue<Event> queue) {
        this.id = id;
        this.queue = queue;
    }

    @Override
    public void run() {
        //将1000个事件加入列表
        for (int i = 0; i < 1000; i++) {
            Event event = new Event(id, i);
            queue.add(event);
        }
    }
}

main方法

package day07.code_3;

import java.util.concurrent.PriorityBlockingQueue;

public class Main {

    public static void main(String[] args) {
        //创建列表
        PriorityBlockingQueue<Event> queue = new PriorityBlockingQueue<>();
        //创建线程数组
        Thread[] threads = new Thread[5];
        //初始化数组并启动线程
        for (int i = 0; i < threads.length; i++) {
            Task task = new Task(i, queue);
            threads[i] = new Thread(task);
            threads[i].start();
        }
        //等待所有线程执行完毕
        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //打印列表的容量
        System.out.printf("Main: Queue Size: %d\n", queue.size());
        //取出所有事件
        for (int i = 0; i < threads.length * 1000; i++) {
            Event event = queue.poll();
            //打印取出事件的线程编号和优先级
            System.out.printf("Thread %s: Priority %d\n",
                    event.getThread(), event.getPriority());
        }
        //打印列表容量
        System.out.printf("Main: Queue Size: %d\n", queue.size());
        //打印程序运行结束提示语
        System.out.println("Main: End of the program");
    }

}

4.使用带有延迟元素的线程安全列表

DelayQueue类是一个容量无限制的延迟队列。这个集合允许我们向其中添加带有激活日期的元素前提是这些元素必须实现了Delayed接口,只有当元素到达激活时间后才可以被取出。因为Delayed接口继承了Comparable接口,所以实现了Delay接口的类需要重写以下两个方法:

  • compareTo(Event o):这里的compareTo()方法要求我们对元素的延迟值进行比较。如果当前对象的延迟值大于传入对象的就返回大于0的值,小于传入对象则返回小于0的值;如果一样则返回0。通过查询DelayQueue类的源码我们可以发现,底层使用的是PriorityBlockingQueue对象。看来此方法会在元素装入PriorityBlockingQueue集合时被调用
  • getDelay(TimeUnit unit):这个方法传入参数为一个时间类型,它要求我们返回激活时间到当前时间的时间差,并且需要以传入参数为时间单位。可以看到DelayQueue类底层调用此方法时传入的时间单位为纳秒

DelayQueue类中的一些常用方法:

  • add(E e)offer(E e):向延迟队列中添加一个元素
  • poll():如果队列不为空且存在已经被激活的元素,返回队头元素。否则返回null
  • size():返回队列中激活、未激活元素的数量,此数量并不是实时的
  • clear():移除队列中所有元素
  • peek():返回队列中的第一个元素,但不删除,注意并不是第一个已激活元素。如果队列为空则返回null
  • take():如果队列不为空且存在已经被激活的元素,返回并删除队头元素。否则将阻塞线程直到方法可以完成

范例实现

在这个范例中我们将使用DelayQueue集合进行存取操作
Event(事件类):

package day07.code_4;

import java.util.Date;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class Event implements Delayed {

    //预定的开始时间
    private Date startDate;

    public Event(Date startDate) {
        this.startDate = startDate;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        //当前事件
        Date now = new Date();
        //当前时间和预定时间的差
        long diff = startDate.getTime() - now.getTime();
        //转为指定的时间格式
        return unit.convert(diff, TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        //比较当前事件和传入事件的时间差
        long result = this.getDelay(TimeUnit.NANOSECONDS)
                - o.getDelay(TimeUnit.NANOSECONDS);
        //当前事件的时间差较小
        if (result < 0) {
            //返回<0的数表示排在传入事件前面
            return -1;
        } else if (result > 0) {
            //返回>0的数表示排在传入事件后面
            return 1;
        }
        //不做排序
        return 0;
    }
}

Task(任务类):

package day07.code_4;

import java.util.Date;
import java.util.concurrent.DelayQueue;

public class Task implements Runnable {

    //任务编号
    private int id;

    //列表
    private DelayQueue<Event> queue;

    public Task(int id, DelayQueue<Event> queue) {
        this.id = id;
        this.queue = queue;
    }

    @Override
    public void run() {
        //当前时间
        Date now = new Date();
        //推迟的时间
        Date delay = new Date();
        //设置推迟时间
        delay.setTime(now.getTime() + id * 1000);
        //打印线程信息和推迟的时间
        System.out.printf("Thread %s: %s\n", id, delay);
        //将事件装入列表中
        for (int i = 0; i < 100; i++) {
            Event event = new Event(delay);
            queue.add(event);
        }
    }
}

main方法:

package day07.code_4;

import java.util.Date;
import java.util.concurrent.DelayQueue;

public class Main {

    public static void main(String[] args) {
        //创建列表
        DelayQueue<Event> queue = new DelayQueue<>();
        //创建线程并启动
        Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; i++) {
            Task task = new Task(i + 1, queue);
            threads[i] = new Thread(task);
            threads[i].start();
        }
        //等待线程运行结束
        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //读取事件直到列表为空
        do {
            //初始化计数器
            int counter = 0;
            Event event;
            //从列表中不断取出事件直到没有可用事件
            do {
                event = queue.poll();
                //如果事件不为null,计数器增加
                if (event != null) {
                    counter++;
                }
            } while (event != null);
            //打印已读取的事件个数
            System.out.printf("At %s you have read %d events\n",
                    new Date(), counter);
            //休眠500毫秒后继续读取
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } while (queue.size() > 0);
    }
}

5.使用线程安全可遍历映射

Java提供了一个可返回子映射、非阻塞、支持并发排序的映射类——ConcurrentSkipListMap类。它的底层使用了Skip List来存放数据,这种数据结构效率与二叉树相近,相比于在有序列表中添加、搜索、删除元素消耗的时间更少。另外,它会根据传入元素的键进行排序,如果我们以自定义的类作为键,需要实现Comparable接口并重写compareTo(Event o)方法。
ConcurrentSkipListMap类的常用方法:

  • firstEntry():返回map中的第一个映射,如果map为空则返回null
  • lastEntry():返回map中的最后一个映射,如果map为空则返回null
  • subMap(K fromKey, K toKey):返回map中键在参数之间的子映射,包括formKey但不包括toKey
  • pollFirstEntry():返回并删除map中第一个映射,如果map为空则返回null
  • pollLastEntry():返回并删除map中最后一个映射,如果map为空则返回null
  • headMap(K toKey):返回map中所有键小于toKey的子映射
  • tailMap(K fromKey):返回map中所有键大于fromKey的子映射
  • putIfAbsent(K key, V value):如果map中不存在键key,那么将key和value保存包map中
  • replace(K key, V value):如果map中已存在键Key,那么用value替换原有的值

范例实现

在这个范例中我们将对一个ConcurrentSkipListMap对象进行各种形式的存取操作
Contact(联系方式类):

package day07.code_5;

public class Contact {

    //姓名
    private String name;

    //电话号码
    private String phone;

    public Contact(String name, String phone) {
        this.name = name;
        this.phone = phone;
    }

    public String getName() {
        return name;
    }

    public String getPhone() {
        return phone;
    }
}

Task(任务类):

package day07.code_5;

import java.util.concurrent.ConcurrentSkipListMap;

public class Task implements Runnable {

    //图
    private ConcurrentSkipListMap<String,Contact> map;

    //任务id
    private String id;

    public Task(ConcurrentSkipListMap<String, Contact> map, String id) {
        this.map = map;
        this.id = id;
    }

    @Override
    public void run() {
        //向图中装填1000个联系方式
        for (int i = 0; i < 1000; i++) {
            //联系方式中的name为id,号码为循环次数+1000
            Contact contact = new Contact(id, String.valueOf(i + 1000));
            //图的键为任务id+电话号码,值为联系方式对象
            map.put(id+contact.getPhone(),contact);
        }
    }
}

main方法:

package day07.code_5;

import java.util.Map;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;

public class Main {

    public static void main(String[] args) {
        //创建一个图
        ConcurrentSkipListMap<String, Contact> map =
                new ConcurrentSkipListMap<>();
        //创建数组
        Thread[] threads = new Thread[26];
        int counter = 0;
        //循环26次
        for (char i = 'A'; i <= 'Z'; i++) {
            //创建任务,id为此次循环中的英文字母
            Task task = new Task(map, String.valueOf(i));
            //填充数组
            threads[counter] = new Thread(task);
            //开启线程
            threads[counter++].start();
        }
        //等待线程运行完毕
        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //打印图中键值对的数量
        System.out.printf("Main: Size of the map: %d\n",
                map.size());
        //声明变量
        Map.Entry<String, Contact> element;
        Contact contact;

        //获取第一个键值对并打印联系方式中的姓名和号码
        element = map.firstEntry();
        contact = element.getValue();
        System.out.printf("Main: First Entry: %s: %s\n",
                contact.getName(), contact.getPhone());

        //获取最后一个键值对并打印联系方式中的姓名和号码
        element = map.lastEntry();
        contact = element.getValue();
        System.out.printf("Main: Last Entry: %s: %s\n",
                contact.getName(), contact.getPhone());

        //获取键在A1996和B1002之间的所有键值对
        System.out.printf("Main: Submap from A1996 to B1002: \n");
        ConcurrentNavigableMap<String, Contact> subMap = map.subMap("A1996", "B1002");
        //遍历获取到的键值对并打印相关信息
        do {
            element = subMap.pollFirstEntry();
            if (element != null) {
                contact = element.getValue();
                System.out.printf("%s: %s\n", contact.getName(), contact.getPhone());
            }
        } while (element != null);
    }
}

6.生成并发随机数

在单线程程序中,使用Random类可以正确的生成伪随机数。但是在并发程序中如果我们通过共享Random对象来为线程生成伪随机数,那么会出现一些性能上的问题。首先要简单了解一下Random类生成伪随机数的原理。在我们创建Random对象时,可以通过构造方法传入一个值作为Random对象的旧种子值,如果不传入的话此类会根据当前时间自动生成一个。在每次生成伪随机数前,Random对象都会根据旧的种子生成一个新的种子,并尝试通过CAS(Compare And Set)去更新种子的值,这样一来每次只能有一个线程成功修改种子的值并生成伪随机数,其他修改种子值失败的线程只能不断自旋尝试,降低了性能。Java为我们提供了ThreadLocalRandom类来解决这个问题。ThreadLocalRandom类中的种子值并不是原子变量,它是一个线程级别的变量,因此并不需要担心产生竞争问题,每个线程会维护属于自己的那份种子值。
Random类有所不同,ThreadLocalRandom类并没有提供公开的构造方法,我们需要通过ThreadLocalRandom类的静态方法current()来获得属于当前线程的类实例,经过调试我们可以发现不同线程获得的ThreadLocalRandom引用指向了同一个对象。但这并不会影响线程安全,因为种子值并没有保存在ThreadLocalRandom对象中

范例实现

TaskLocalRandom(获取线程随机数的类):

package day07.code_6;

import java.util.concurrent.ThreadLocalRandom;

public class TaskLocalRandom implements Runnable {

    //并发随机数生成器
    private ThreadLocalRandom random;

    @Override
    public void run() {
        //得到当前线程的名字
        String name = Thread.currentThread().getName();
        //为随机数生成器赋值
        random = ThreadLocalRandom.current();
        for (int i = 0; i < 10; i++) {
            //打印线程信息和生成的随机数
            System.out.printf("%s: %d\n", name, this.random.nextInt(10));
        }
    }
}

main方法:

package day07.code_6;


public class Main {

    public static void main(String[] args) {
        //创建线程数组
        Thread[] threads = new Thread[3];
        //创建并开启三个线程
        for (int i = 0; i < threads.length; i++) {
            TaskLocalRandom task = new TaskLocalRandom();
            threads[i] = new Thread(task);
            threads[i].start();
        }
    }
}

7.使用原子变量

当多个线程共享某个变量时,我们通常需要对操作这一变量的代码进行同步。当然,我们也可以使用原子变量来解决多个线程共享一个变量时可能出现的数据不一致错误。原子变量的实现类提供了一套机制来检查对此变量的操作是否合法。一般情况下使用CAS原子操作:需要修改原子变量时,通常先将变量拷贝一份到本地,按照相应的需求在本地进行变量值的修改并尝试使用新的变量值去替换原有的变量值。是否能替换成功要取决于原有值是否已经被修改,我们用本地的值与原有值进行比对,如果不一致则证明原有值已被修改,不能完成替换操作;如果一致则可以替换。Java为我们提供了许多原子类,例如:AtomicBooleanAtomicLongAtomicIntegerAtomicReference等。这里需要注意的是,AtomicReference类在进行CAS操作时,比较的是对象的地址是否相同而不是其内部属性,就算我们重写了equals()hashCode()方法比较的也仅仅是地址。各原子类中的方法就不在此赘述了,API文档里写的已经比较清楚了

范例实现

在这个范例中,我们创建了一个账号类,其中使用一个原子变量来保存账号中的金额。在main方法中创建公司、银行线程分别负责增添、减少账户金额。以下是范例中使用到的AtomicLong类方法:

  • set(Long x):设置当前值
  • get():获取当前值
  • getAndAdd(long delta):返回旧值后将传入参数的值以原子方式添加到旧值

Account类(账号类):

package day07.code_7;

import java.util.concurrent.atomic.AtomicLong;

public class Account {

    //原子变量
    private AtomicLong balance;

    public Account() {
        balance = new AtomicLong();
    }

    //返回原子变量的值
    public long getBalance() {
        return balance.get();
    }

    //设置原子变量的值
    public void setBalance(long balance) {
        this.balance.set(balance);
    }

    //增加金额
    public void addAmount(long amount) {
        this.balance.getAndAdd(amount);
    }

    //减少金额
    public void substractAmount(long amount) {
        this.balance.getAndAdd(-amount);
    }

}

Bank(银行类):

package day07.code_7;

public class Bank implements Runnable {

    //账号
    private Account account;

    public Bank(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        //每次从账户中减少1000元,循环十次
        for (int i = 0; i < 10; i++) {
            this.account.substractAmount(1000);
        }
    }
}

Company(公司类):

package day07.code_7;

public class Company implements Runnable {

    //账号
    private Account account;

    public Company(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        //每次向a账户中增加1000元,循环十次
        for (int i = 0; i < 10; i++) {
            account.addAmount(1000);
        }
    }
}

main方法:

package day07.code_7;

public class Main {

    public static void main(String[] args) {
        //创建一个账号,设置初始金额为1000元
        Account account = new Account();
        account.setBalance(1000);
        //创建一个公司类和线程,负责增加金额
        Company company = new Company(account);
        Thread companyThread = new Thread(company);
        //创建一个银行类和线程,负责减少金额
        Bank bank = new Bank(account);
        Thread bankThread = new Thread(bank);
        //打印账号初始金额
        System.out.printf("Account : Initial Balance: %d\n",
                account.getBalance());
        //开启线程
        companyThread.start();
        bankThread.start();
        try {
            //等待线程结束运行
            companyThread.join();
            bankThread.join();
            //打印账号最终金额
            System.out.printf("Account : Final Balance: %d\n",
                    account.getBalance());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

8.使用原子数组

Java也提供了原子数组,因此我们可以对Integer和long类型的数组进行原子操作

范例实现

在这个范例中,我们将对AtomicIntegerArray对象进行一系列的操作,以下是AtomicIntegerArray类的部分方法:

  • AtomicIntegerArray(int length)AtomicIntegerArray(int[] array):两个构造方法,第一个构造方法可以指定数组的大小。第二个构造方法传入一个一维数组,此数组会被拷贝。
  • getAndDecrement(int i):以原子方式将数组中指定位置的元素减少1
  • getAndIncrement(int i):以原子方式将数组中指定位置的元素增加1
  • get(int i):获取数组中指定位置的值
  • set(int i, int value):将数组中指定位置的值设定为value

Incrementer(增加器类):

package day07.code_8;

import java.util.concurrent.atomic.AtomicIntegerArray;

public class Incrementer implements Runnable {

    //原子数组
    private AtomicIntegerArray vector;

    public Incrementer(AtomicIntegerArray vector) {
        this.vector = vector;
    }

    @Override
    public void run() {
        for (int i = 0; i < vector.length(); i++) {
            //将数组中指定位置的元素增加1
            vector.getAndIncrement(i);
        }
    }
}

Decrementer(减少器类):

package day07.code_8;

import java.util.concurrent.atomic.AtomicIntegerArray;

public class Decrementer implements Runnable {

    //原子数组
    private AtomicIntegerArray vector;

    public Decrementer(AtomicIntegerArray vector) {
        this.vector = vector;
    }

    @Override
    public void run() {
        for (int i = 0; i < vector.length(); i++) {
            //将数组中指定位置的元素减少1
            vector.getAndDecrement(i);
        }
    }
}

main方法:

package day07.code_8;

import java.util.concurrent.atomic.AtomicIntegerArray;

public class Main {

    public static void main(String[] args) {
        //常量值为100
        final int THREADS = 100;
        //创建一个容量为1000的原子数组
        AtomicIntegerArray vector = new AtomicIntegerArray(1000);
        //创建增、降两个对象
        Incrementer incrementer = new Incrementer(vector);
        Decrementer decrementer = new Decrementer(vector);
        //创建两个数组
        Thread[] incrementerThreads = new Thread[THREADS];
        Thread[] decrementerThreads = new Thread[THREADS];
        //填充数组并开启线程
        for (int i = 0; i < THREADS; i++) {
            incrementerThreads[i] = new Thread(incrementer);
            decrementerThreads[i] = new Thread(decrementer);
            incrementerThreads[i].start();
            decrementerThreads[i].start();
        }
        //等待所有线程执行结束
        for (int i = 0; i < THREADS; i++) {
            try {
                incrementerThreads[i].join();
                decrementerThreads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //检查错误的结果并打印
        for (int i = 0; i < vector.length(); i++) {
            if (vector.get(i) != 0) {
                System.out.printf("Vector[%d] : %d", i, vector.get(i));
            }
        }
        //程序结束提示语
        System.out.println("Main: End of the example");
    }
}