(八)多线程:线程相关类

 Java还为线程安全提供了一些工具类,如ThreadLocal类,它代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从而避免并发访问线程安全问题。除此之外,Java5还新增了大量的线程安全类。

1.ThreadLocal类

 早在JDK1.2推出之时,Java就为多线程编程提供了一个ThreadLocal类;从Java5.0以后,Java引入了泛型支持,Java为该ThreadLocal类增加了泛型支持,即ThreadLocal。通过使用ThreadLocal类可以简化多线程编程时的并发访问,使用这个工具类可以简捷地隔离多线程程序的竞争资源。功用其实非常简单,就是为每个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都可以完全拥有该变量一样。
ThreadLocal类的用法非常简单,它只提供了如下三个public方法

  • T get():返回此线程局部变量中当前线程副本中的值。
  • void remove():删除此线程局部变量中当前线程的值。
  • void set(T value):设置此线程局部变量中当前线程副本中的值。
    示例代码:
public class ThreadLocalTest {
    public static void main(String[] args) {
        Acct acct = new Acct("init_name");
        /**
         * 虽然两个线程共享同一个账户,即只有一个账户名
         * 但是由于账户名是ThreadLocal类型的,所以每个线程都完全拥有各自的账户副本,
         * 因此在i==6之后,将看到两个线程访问同一个账户时出现不同的账户名
         */
        new MyTest("t_A", acct).start();
        new MyTest("t_B", acct).start();
    }
}

class MyTest extends Thread {

    private Acct acct;

    public MyTest(String t_name, Acct acct) {
        super(t_name);
        this.acct = acct;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i == 6) {
                acct.setName(getName());
            }
            System.out.println(acct.getName() + " i:" + i);
        }
    }
}

class Acct {

    private ThreadLocal<String> name = new ThreadLocal<>();

    public Acct(String name) {
        this.name.set(name);
        System.out.println("---" + this.name.get());
    }

    public String getName() {
        return this.name.get();
    }

    public void setName(String name) {
        this.name.set(name);
    }
}

运行结果:

---init_name
null i:0
null i:0
null i:1
null i:1
null i:2
null i:3
null i:2
null i:4
null i:3
null i:5
null i:4
t_A i:6
null i:5
t_A i:7
t_B i:6
t_A i:8
t_B i:7
t_A i:9
t_B i:8
t_B i:9

 ThreadLocal和其他所有的同步机制一样,都是为了解决多线程中对同一变量的访问冲突,在普通的同步机制中,是通过对象加锁来实现多个线程对同一变量的安全访问的。该变量是多个线程共享的,所以需要使用这种同步机制,需要很细致地分析在什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放该对象的锁等。在这种情况下,系统并没有将者份资源复制多份,只是采用了安全机制来控制对这份资源的访问而已。
 ThreadLocal从另一个角度来解决多线程的并发访问,ThreadLocal将需要并发访问的资源复制多份,每个线程拥有一份资源,每个线程都拥有自己的资源副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把线程不安全的整个变量封装进ThreadLocal,或者把该对象与线程关联的状态使用ThreadLocal。
 ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源(变量)的竞争,也就不需要对多个线程进行同步了。

2.包装线程不安全的集合

 Java集合中ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap等都是线程不安全的,也就是说,当多个线程并发向这些集合中存,取元素时,就可能会破坏这些集合数据完整性。
 如果程序中有多个线程可能访问以上这些集合,就可以使用Collections提供的类方法把这些集合包装成线程安全的集合。Collections提供了如下几个静态方法。

3.线程安全的集合类

 实际上从Java5开始,在java.util.concurrent包下提供了大量支持高并发并访问的集合接口和实现类:

这些线程安全的集合类可分为如下两类

  • 以Concurrent开头的集合类,如ConcurrentHashMap,ConcurrentSkipListMap,ConcurrentSkipListSet,ConcurrentLinkedQueue和ConcurrentLinkedDeque。
  • 以CopyOnWrite开头的集合类,如CopyOnWriteArrayList,CopyOnWriteArraySet。

 其中以Concurrent开头的集合代表支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但是读取操作不必锁定。以Concurrent开头的集合类采用了更负载的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。
 当多个线程共享访问一个公共集合时,ConcurrentLinkedQueue是一个恰当的选择。ConcurrentLinkedQueue不允许使用null元素。ConcurrentLinkedQueue实现了多线程的高效访问,多线程访问ConcurrentLinkedQueue集合时无须等待。
 在默认情况下,ConcurrentHashMap 支持16个线程并发写入,当有超过16个线程并发向该Map中写入数据时,可能有一些线程需要等待。实际上,程序通过设置concurrentLevel构造参数(默认值为16)来支持更多的并发写入线程。
 与前面介绍的HashMap和普通集合不同的时,因为ConcurrentLinkedQueue和ConcurrentHashMap支持多线程并发访问,所以当使用迭代器来遍历元素集合时,该迭代器可能不能反映出迭代器之后所做的修改。但程序不会抛出任何异常。
 Java8扩展了ConcurrentHashMap的功能,Java8为该类新增了30多个新方法,这些方法可借助于Stream和Lambda表达式支持执行聚集操作。ConcurrentHashMap新增的方法大致可分如下三类。

  • forEach 系列
  • search 系列
  • reduce 系列

 由于CopyOnWriteArraySet 的底层封装了CopyOnWriteArrayList,因此它的实现机制完全类似于CopyOnWriteArrayList集合。
 对于CopyOnWriteArrayList集合,正如它的名字所暗示,它采用复制底层数据的方式来实现写操作。
 当线程对CopyOnWriteArrayList集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对CopyOnWriteArrayList集合执行写入操作时(包括add(),remove(),set()等方法),该集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对CopyOnWriteArrayList集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。需要指出的是,由于CopyOnWriteArrayList执行写入操作需要频繁地复制数组,性能比较差,但是由于读操作与写操作不是操作同一个数组,而且读操作也不需要加锁,因此读操作就很快,很安全。由此可见,CopyOnWriteArrayList适合用在读取操作远远大于写操作的场景中,例如缓存等。

文章内容均取自《疯狂Java讲义-李刚》一书中多线程章节。截取重要知识点作为笔记记录,方便自己回顾。

原文地址:https://www.cnblogs.com/everyingo/p/12827130.html