本章探讨线程安全的java平台本身的机制,免于基于同步(内部锁)或显式锁的实现,可以简化开发,避免锁造成的各种问题和开销。
- 无状态对象
- 不可变对象
- ThreadLoacl线程特有对象
- 线程安全并发集合
无状态对象
无状态对象,就是没有实例变量的对象.不能保存数据,是线程安全的。
比如以下方法中的变量都是方法内部的变量
public class AdderImpl implements AdderImplRemote { public int add(int a,int b){ return a+b; } }
不可变对象Immutable Object
在创建状态后无法更改其状态的对象称为不可变对象。一个对象不可变的类称为不可变类。
不变的对象可以由程序的不同区域共享而不用担心其状态改变。不可变对象本质上是线程安全的。
以下不可变类创建对象后,只能读,不可写
public class IntWrapper { private final int value; public IntWrapper(int value) { this.value = value; } public int getValue() { return value; } }
自定义不可变类遵守如下原则:
- 1、使用private和final修饰符来修饰该类的属性(非必须)。
- 2、提供带参数的构造器,用于根据传入的参数来初始化属性。
- 3、仅为该类属性提供getter方法,不要提供setter方法。通过执行深度复制的构造函数初始化所有字段。执行getter方法中对象的克隆以返回副本,而不是返回实际的对象引用。
- 4、如果有必要,重写hashCode和equals方法,同时应保证两个用equals方法判断为相等的对象,其hashCode也应相等。
- 5、最好不允许类被继承(非必须)
一些类已经是不可变类,比如String
public class Testimmutablestring { public static void main(String args[]){ String s="Abc"; s.concat(" Def");//concat() method appends the string at the end System.out.println(s);//will print Abc because strings are immutable objects //实际t和s是不同的对象,地址指向不同的堆空间 String t = s.concat(" Def"); System.out.println(t); } }
以下例子是浅复制造成的地址一致,testMap容易被意外改变。
import java.util.HashMap; public class FinalClassExample { private int id; private double dou; private String name; private HashMaptestMap; public int getId() { return id; } public double getDou() { return dou; } public String getName() { return name; } public HashMap getTestMap() { return testMap; } /** * 浅复制测试 * @param i * @param n * @param hm */ public FinalClassExample(int i, double d, String n, HashMap hm){ System.out.println("对象初始化时浅复制"); this.id=i; this.dou=d; this.name=n; this.testMap=hm; } public static void main(String[] args) { HashMap h1 = new HashMap (); h1.put("1", "first"); h1.put("2", "second"); String s = "original"; int i=10; double d = 100D; FinalClassExample ce = new FinalClassExample(i,d,s,h1); //Lets see whether its copy by field or reference System.out.println(s==ce.getName()); System.out.println(h1 == ce.getTestMap()); //print the ce values System.out.println("ce id:"+ce.getId()); System.out.println("ce name:"+ce.getName()); System.out.println("ce testMap:"+ce.getTestMap()); //change the local variable values i=20; s="modified"; d = 200D; h1.put("3", "third"); //print the values again System.out.println("i after local variable change:"+i); System.out.println("d after local variable change:"+d); System.out.println("s after local variable change:"+s); System.out.println("s'hashCode after local variable change:"+s.hashCode()); System.out.println(s==ce.getName()); System.out.println(h1 == ce.getTestMap()); System.out.println("ce id after local variable change:"+ce.getId()); System.out.println("ce dou after local variable change:"+ce.getDou()); System.out.println("ce name after local variable change:"+ce.getName()); System.out.println("ce name'hashCode after local variable change:"+ce.getName().hashCode()); System.out.println("ce testMap after local variable change:"+ce.getTestMap()); HashMap hmTest = ce.getTestMap(); hmTest.put("4", "new"); System.out.println("ce testMap after changing variable from accessor methods:"+ce.getTestMap()); } } 对象初始化时浅复制 true true ce id:10 ce name:original ce testMap:{1=first, 2=second} i after local variable change:20 d after local variable change:200.0 s after local variable change:modified s'hashCode after local variable change:-615513399 false true ce id after local variable change:10 ce dou after local variable change:100.0 ce name after local variable change:original ce name'hashCode after local variable change:1379043793 ce testMap after local variable change:{1=first, 2=second, 3=third} ce testMap after changing variable from accessor methods:{1=first, 2=second, 3=third, 4=new}
string和int不可变,是因为外部变量和类内部变量的地址指向本身就是不同。
下面用深复制,保证了对象不可变:
import java.util.HashMap; import java.util.Iterator; public final class FinalClassExample { private final int id; private final String name; private final HashMaptestMap; public int getId() { return id; } public String getName() { return name; } /** * Accessor function for mutable objects */ public HashMap getTestMap() { //return testMap; return (HashMap ) testMap.clone(); } /** * Constructor performing Deep Copy * @param i * @param n * @param hm */ public FinalClassExample(int i, String n, HashMap hm){ System.out.println("Performing Deep Copy for Object initialization"); this.id=i; this.name=n; HashMap tempMap=new HashMap (); String key; Iterator it = hm.keySet().iterator(); while(it.hasNext()){ key=it.next(); tempMap.put(key, hm.get(key)); } this.testMap=tempMap; } /** * To test the consequences of Shallow Copy and how to avoid it with Deep Copy for creating immutable classes * @param args */ public static void main(String[] args) { HashMap h1 = new HashMap (); h1.put("1", "first"); h1.put("2", "second"); String s = "original"; int i=10; FinalClassExample ce = new FinalClassExample(i,s,h1); //Lets see whether its copy by field or reference System.out.println(s==ce.getName()); System.out.println(h1 == ce.getTestMap()); //print the ce values System.out.println("ce id:"+ce.getId()); System.out.println("ce name:"+ce.getName()); System.out.println("ce testMap:"+ce.getTestMap()); //change the local variable values i=20; s="modified"; h1.put("3", "third"); //print the values again System.out.println("ce id after local variable change:"+ce.getId()); System.out.println("ce name after local variable change:"+ce.getName()); System.out.println("ce testMap after local variable change:"+ce.getTestMap()); HashMap hmTest = ce.getTestMap(); hmTest.put("4", "new"); System.out.println("ce testMap after changing variable from accessor methods:"+ce.getTestMap()); } }
ThreadLoacl线程特有对象
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储。用于创建只能由同一线程读写的线程局部变量。例如,如果两个线程正在访问引用同一个threadLocal变量的代码,则每个线程都不会看到其他线程对threadLocal变量所做的任何修改。
使用ThreadLocal可以避免锁的争用。
例子
public class Context { private final String userName; Context(String userName) { this.userName = userName; } @Override public String toString() { return "Context{" + "userNameSecret='" + userName + '\'' + '}'; } } public class ThreadLocalWithUserContext implements Runnable { private static final ThreadLocaluserContext = new ThreadLocal<>(); private final Integer userId; private UserRepository userRepository = new UserRepository(); ThreadLocalWithUserContext(Integer userId) { this.userId = userId; } @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContext.set(new Context(userName)); System.out.println("thread context for given userId: " + userId + " is: " + userContext.get()); } } public class NoThreadLocalWithUserContext implements Runnable { private static Context userContext = new Context(null); private final Integer userId; private UserRepository userRepository = new UserRepository(); NoThreadLocalWithUserContext(Integer userId) { this.userId = userId; } @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContext = new Context(userName); System.out.println("thread context for given userId: " + userId + " is: " + userContext.toString()); } } public class UserRepository { String getUserNameForUserId(Integer userId) { return UUID.randomUUID().toString(); } } public class ThreadLocalTest{ public static void main(String []args){ ThreadLocalWithUserContext firstUser = new ThreadLocalWithUserContext(1); ThreadLocalWithUserContext secondUser = new ThreadLocalWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start(); NoThreadLocalWithUserContext thirdUser = new NoThreadLocalWithUserContext(1); NoThreadLocalWithUserContext fourthUser = new NoThreadLocalWithUserContext(2); new Thread(thirdUser).start(); new Thread(fourthUser).start(); } }
并发集合
ConcurrentHashMap
ConcurrentHashMap是一个线程安全的映射实现,除了HashTable或显式同步化或加锁HashMap之外,它还提供了在多线程环境中使用的另一种方法。ConcurrentHashMap是java.util.concurrent包的一部分
HashTable或HashMap上的显式同步,同步单个锁上的所有方法,并且所有方法都是同步的,即使方法用于检索元素。这使得运行效率非常缓慢。因为所有方法都是同步的,所以读取操作也很慢。ConcurrentHashMap试图解决这些问题。
ConcurrentHashMap内部实现不使用锁,而是CAS操作。
例子
import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class MapSynchro implements Runnable{ private MaptestMap; public MapSynchro(Map testMap){ this.testMap = testMap; } public static void main(String[] args) { //Map testMap = new HashMap Map(); testMap = new ConcurrentHashMap (); /// 4 threads Thread t1 = new Thread(new MapSynchro(testMap)); Thread t2 = new Thread(new MapSynchro(testMap)); Thread t3 = new Thread(new MapSynchro(testMap)); Thread t4 = new Thread(new MapSynchro(testMap)); t1.start(); t2.start(); t3.start(); t4.start(); try { t1.join(); t2.join(); t3.join(); t4.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Size of Map is " + testMap.size()); } @Override public void run() { System.out.println("in run method" + Thread.currentThread().getName()); String str = Thread.currentThread().getName(); for(int i = 0; i < 100; i++){ // adding thread name to make element unique testMap.put(str+i, str+i); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } }
CopyOnWriteArrayList
CopyOnWriteArrayList实现了List接口,与其他著名的ArrayList一样,它也是Java.util.concurrent包的一部分。CopyOnWriteArrayList与ArrayList的区别在于它是ArrayList的线程安全变体。当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
适用于遍历多于修改操作更频繁的情况。向CopyOnWriteArrayList中add方法的实现(向CopyOnWriteArrayList里添加元素),可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
所以添加操作频繁时,效率不高。CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。
- 内存占用问题
因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
- 数据一致性问题
CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
以下例子会出现异常,如果ArrayList在迭代过程中被修改,那么将抛出ConcurrentModificationException
package io.github.viscent.mtia.ext; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class CopyList { public static void main(String[] args) { //creating CopyOnWriteArrayList ListcarList = new ArrayList (); carList.add("Audi"); carList.add("Jaguar"); carList.add("Mini Cooper"); carList.add("BMW"); Thread t1 = new Thread(new ItrClass(carList));//读 Thread t2 = new Thread(new ModClass(carList));//写 t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("List elements in Main- " + carList); } } //Thread class for iteration class ItrClass implements Runnable{ List carList; public ItrClass(List carList){ this.carList = carList; } @Override public void run() { Iterator i = carList.iterator(); while (i.hasNext()){ System.out.println(i.next()); try { Thread.sleep(500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } //Thread class for modifying list class ModClass implements Runnable{ List carList; public ModClass(List carList){ this.carList = carList; } @Override public void run() { System.out.println("Adding new value to the list"); carList.add("Mercedes"); } } Adding new value to the list Audi Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901) at java.util.ArrayList$Itr.next(ArrayList.java:851) at io.github.viscent.mtia.ext.ItrClass.run(CopyList.java:41) at java.lang.Thread.run(Thread.java:745) List elements in Main- [Audi, Jaguar, Mini Cooper, BMW, Mercedes]
换成这样就没问题了:
ListcarList = new CopyOnWriteArrayList ();
CopyOnWriteArraySet
CopyOnWriteArraySet是一个Set接口实现,因此不允许重复元素。
CopyOnWriteArraySet是线程安全的。
由于CopyOnWriteArraySet在内部使用CopyOnWriteArrayList,所以就像在CopyOnWriteArrayList中一样,所有的变异操作(添加、设置等)都会创建基础数组的单独副本,这样就不会有线程干扰。
返回的迭代器是故障安全的,这意味着迭代器保证不会抛出ConcurrentModificationException,即使在迭代器创建后的任何时候对集合进行了结构修改。
迭代器的元素更改操作(如add、remove)不受支持,并引发UnsupportedOperationException。
注意点:
CopyOnWriteArraySet最适合于集较小、只读操作多于变更操作的应用程序,并且需要防止遍历期间线程之间的干扰。
由于增加了创建底层数组副本的任务,因此变更操作(添加、设置、删除等)成本高昂。
CopyOnWriteArraySet保证不会抛出ConcurrentModificationException,即使在迭代期间对集合进行了并发修改。同时,迭代器的元素更改操作(如remove)不受支持。
ConcurrentSkipListMap
ConcurrentSkipListMap是线程安全的有序的哈希表,适用于高并发的场景。
ConcurrentSkipListMap和TreeMap,它们虽然都是有序的哈希表。但是,第一,它们的线程安全机制不同,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。第二,ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的。
关于跳表(Skip List),它是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的插入和删除的工作是比较简单的。
public class SkipMapDemo { public static void main(String[] args) { // Creating ConcurrentSkipListMap ConcurrentNavigableMapnumberMap = new ConcurrentSkipListMap (); // Storing elements numberMap.put(1, "ONE"); numberMap.put(2, "TWO"); numberMap.put(5, "FIVE"); numberMap.put(8, "EIGHT" ); numberMap.put(10, "TEN"); numberMap.put(16, "SIXTEEN"); System.out.println("** reverse order view of the map **"); //Returns a reverse order view of the mappings ConcurrentNavigableMap reverseNumberMap = numberMap.descendingMap(); Set > numSet = reverseNumberMap.entrySet(); numSet.forEach((m)->System.out.println("key " + m.getKey() + " value " + m.getValue())); System.out.println("** First entry in the the map **"); //Returns a key-value mapping associated with the least key in this map Map.Entry mapEntry = numberMap.firstEntry(); System.out.println("key " + mapEntry.getKey() + " value " + mapEntry.getValue()); System.out.println("** Floor entry Example **"); //Returns a key-value mapping associated with the greatest key less than or equal to the given key mapEntry = numberMap.floorEntry(7); System.out.println("key " + mapEntry.getKey() + " value " + mapEntry.getValue()); System.out.println("** Ceiling entry Example **"); //Returns a key-value mapping associated with the least key greater than or equal to the given key mapEntry = numberMap.ceilingEntry(7); System.out.println("key " + mapEntry.getKey() + " value " + mapEntry.getValue()); } } ** reverse order view of the map ** key 16 value SIXTEEN key 10 value TEN key 8 value EIGHT key 5 value FIVE key 2 value TWO key 1 value ONE ** First entry in the the map ** key 1 value ONE ** Floor entry Example ** key 5 value FIVE ** Ceiling entry Example ** key 8 value EIGHT
ConcurrentLinkedQueue
ConcurrentLinkedQueue是一个线程安全的无边界队列。它将其元素存储为链接节点,其中每个节点存储对下一个节点的引用。
ConcurrentLinkedQueue与ArrayBlockingQueue、PriorityBlockingQueue等BlockingQueue实现的区别在于,ConcurrentLinkedQueue是非阻塞的,因此此队列中的操作不会阻塞。由于ConcurrentLinkedQueue是非阻塞的,因此没有put()或take()方法可以在需要时阻塞。
按照 FIFO(先进先出)原则对元素进行排序。队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。
新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许使用 null 元素。
import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class ConcurrentLQ { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(4); QueueconQueue = new ConcurrentLinkedQueue<>(); // One Producer thread executor.execute(new ConProducer(conQueue)); // Two Consumer thread executor.execute(new ConConsumer(conQueue)); executor.execute(new ConConsumer(conQueue)); executor.shutdown(); } } //Producer class ConProducer implements Runnable{ Queue conQueue; ConProducer(Queue conQueue){ this.conQueue = conQueue; } @Override public void run() { for(int i = 0; i < 6; i++){ System.out.println("Adding to queue-" + i); conQueue.add(i); } } } //Consumer class ConConsumer implements Runnable{ Queue conQueue; ConConsumer(Queue conQueue){ this.conQueue = conQueue; } @Override public void run() { for(int i = 0; i < 4; i++){ try { TimeUnit.MILLISECONDS.sleep(50); System.out.println("Thread Name -" + Thread.currentThread().getName() + " Consumer retrieved- " + conQueue.poll()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } Adding to queue-0 Adding to queue-1 Adding to queue-2 Adding to queue-3 Adding to queue-4 Adding to queue-5 Thread Name -pool-1-thread-3 Consumer retrieved- 1 Thread Name -pool-1-thread-2 Consumer retrieved- 0 Thread Name -pool-1-thread-3 Consumer retrieved- 2 Thread Name -pool-1-thread-2 Consumer retrieved- 3 Thread Name -pool-1-thread-2 Consumer retrieved- 4 Thread Name -pool-1-thread-3 Consumer retrieved- 5 Thread Name -pool-1-thread-3 Consumer retrieved- null Thread Name -pool-1-thread-2 Consumer retrieved- null
注意,最后的一次poll造成了取null,但不会抛出异常,是非阻塞的。
LinkedTransferQueue
LinkedTransferQueue是一个聪明的队列,他是 ConcurrentLinkedQueue,SynchronousQueue(in “fair” mode公平模式 ), and unbounded LinkedBlockingQueue 的超集。
LinkedTransferQueue 实现了一个重要的接口 TransferQueue, 该接口含有下面几个重要方法:
1. transfer(E e)
若当前存在一个正在等待获取的消费者线程,即立刻移交之;否则,会插入当前元素 e 到队列尾部,并且等待进入阻塞状态,到有消费者线程取走该元素。
2. tryTransfer(E e)
若当前存在一个正在等待获取的消费者线程(使用 take() 或者 poll() 函数),使用该方法会即刻转移 / 传输对象元素 e ;若不存在,则返回 false ,并且不进入队列。这是一个不阻塞的操作。
3. tryTransfer(E e, long timeout, TimeUnit unit)
若当前存在一个正在等待获取的消费者线程,会立即传输给它 ; 否则将插入元素 e 到队列尾部,并且等待被消费者线程获取消费掉 , 若在指定的时间内元素 e 无法被消费者线程获取,则返回 false ,同时该元素被移除。
4. hasWaitingConsumer()
判断是否存在消费者线程
5. getWaitingConsumerCount()
获取所有等待获取元素的消费线程数量
6. size()
因为队列的异步特性,检测当前队列的元素个数需要逐一迭代,可能会得到一个不太准确的结果,尤其是在遍历时有可能队列发生更改。
7. 批量操作
类似于addAll,removeAll,retainAll, containsAll, equals, toArray 等方法,API不能保证一定会立刻执行。
- LinkedTransferQueue是链接节点上的无限队列。
- 此队列针对任何给定的生产者排序元素FIFO(先进先出)。
- 元素被插入到尾部,并从队列的头部检索。
- 它提供阻塞插入和检索操作。
- 它不允许空对象。
- LinkedTransferQueue是线程安全的。
- 由于异步特性,size()方法不是一个常量时间操作,因此如果在遍历期间修改此集合,则可能会报告不准确的结果。
- 批量操作addAll、removeAll、retainAll、containsAll、equals和toArray不能保证以原子方式执行。例如,与addAll操作并发操作的迭代器可能只查看一些添加的元素。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TransferQueue; public class LinkedTQ { public static void main(String[] args) { TransferQueuetQueue = new LinkedTransferQueue<>(); ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute(new LinkedProducer(tQueue)); executor.execute(new LinkedConsumer(tQueue)); executor.shutdown(); } } //Producer class LinkedProducer implements Runnable{ TransferQueue tQueue; LinkedProducer(TransferQueue tQueue){ this.tQueue = tQueue; } @Override public void run() { for(int i = 0; i < 5; i++){ try { System.out.println("Adding to queue-" + i); tQueue.transfer(i); TimeUnit.MILLISECONDS.sleep(50); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } //Consumer class LinkedConsumer implements Runnable{ TransferQueue tQueue; LinkedConsumer(TransferQueue tQueue){ this.tQueue = tQueue; } @Override public void run() { for(int i = 0; i < 5; i++){ try { System.out.println("Consumer retrieved- " + tQueue.take()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } Adding to queue-0 Consumer retrieved- 0 Adding to queue-1 Consumer retrieved- 1 Adding to queue-2 Consumer retrieved- 2 Adding to queue-3 Consumer retrieved- 3 Adding to queue-4 Consumer retrieved- 4
LinkedTransferQueue和ConcurrentLinkedQueue和之前的集合类不同,这两个是用来做生产-消费同步队列的,LinkedTransferQueue用于阻塞异步队列,生产者可以等待消费者。ConcurrentLinkedQueue是非阻塞队列。
而前面的那些集合比如ConcurrentHashMap是线程安全的并发容器。