并发概览
如何同步多个线程对共享资源的访问是多线程编程中最基本的问题之一。
当多个线程并发访问共享数据时会出现数据处于计算中间状态或者不一致的问题,从而影响到程序的正确运行。我们通常把这种情况叫做竞争条件(race condition),把并发访问共享数据的代码叫做关键区域(critical section)。
同步就是使得多个线程顺序进入关键区域从而避免竞争条件的发生。
编写线程安全的代码的核心是要对状态访问操作进行管理,尤其是对共享的和可变的状态访问。
线程安全性的定义:当多个线程访问某个类时,这个类始终能表现出正确的行为,那么就称这个类是线程安全的。
无状态对象一定是线程安全的。
(1)具有原子性的操作被称为原子操作
在Java中,对除了long和double之外的基本类型的简单操作都具有原子性。简单操作就是赋值或者return。比如”a = 1;“和 “return a;”这样的操作都具有原子性。
在某些JVM中”a += b”可能要经过这样三个步骤:
1) 读取:取出a和b
2) 修改:计算a+b
3) 写入:将计算结果写入内存
非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。concurrent包下提供了一些原子类,比如:AtomicInteger、AtomicLong、AtomicReference等。
(2)竞争条件,指的是在并发编程中,由于不恰当的执行时序而出现不正确的结果的情况
当某个计算结果的正确性取决于多个线程的交替执行时序时就会发生竞态条件。
常见的两种竞争条件是:“先检查后执行”和 “读取-修改-写入”。
(3)内置锁和可重入锁
Java提供了一种内置锁机制来支持原子性:同步代码块。同步代码块包括两个部分:一个是作为锁的对象引用,一个是作为由这个锁保护的代码块。每个Java对象都可以用作一个实现同步的锁,这些锁被称为内置锁。
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会被阻塞,然而内置锁是可以重入的。因此如果某个线程试图获得一个已经有它自己持有的锁,那么这个请求就会成功。
注意,“锁”的持有者是实例对象,而不是类!
(1)线程安全的集合类
java.util.Vector
java.util.Stack
java.util.HashTable
java.util.concurrent.ConcurrentHashMap
java.util.concurrent.CopyOnWriteArrayList
java.util.concurrent.CopyOnWriteArraySet
java.util.concurrent.ConcurrentLinkedQueue
(2)非线程安全集合类
java.util.BitSet
java.util.HashSet (LinkedHashSet)
java.util.TreeSet
java.util.HashMap (WeekHashMap, TreeMap, LinkedHashMap, IdentityHashMap)
java.util.ArrayList (LinkedList)
java.util.PriorityQueue
这些非线程安全的集合可以通过java.util.Collections.SynchronizedList、SynchronizedMap、SynchronizedSet等方法包装成线程安全的集合。包装器类简单地给被包装集合的各项操作加上了synchronized保护。值得注意的是在使用游标遍历这些包装器集合的时候必须加上额外的synchronized保护,否则会出现问题。
List list = Collections.synchronizedList(new ArrayList()); ... synchronized(list) { Iterator i = list.iterator(); // Must be in synchronized block while (i.hasNext()) foo(i.next()); }
(3)线程通知集合类
java.util.concurrent.ArrayBlockingQueue
java.util.concurrent.LinkedBlockingQueue
java.util.concurrent.SynchronousQueue
java.util.concurrent.PriorityBlockingQueue
java.util.concurrent.DelayQueue
这些集合类都实现了BlockingQueue接口。阻塞队列的特点是当从队列中取出元素时如果队列为空,线程会被阻塞直到队列中有元素被插入。当从队列中插入元素时如果队列已满,线程会被阻塞直到队列中有元素被取出出现空闲空间。阻塞队列可以用来实现生产者消费者模式(Producer/Consumer Pattern) 。
频繁地创建和销毁线程会降低程序的性能。
应用程序可以创建线程的数量是受机器物理条件制约的,过多的线程会耗尽机器的资源,在设计程序的时候需要限制并发线程的数量。
线程池在启动的时候一次性初始化若干个线程(也可以根据负载按需启动,也有闲置一定时间的线程会被销毁的策略),然后程序把任务交给线程池去执行而不是直接交给某个线程执行,由线程池给这些任务分配线程。
当某个线程执行完一个任务后,线程池会把它设成空闲状态以备下一个任务重用而不是销毁它。
线程池在初始化的时候需要指定线程数量上限,当并发任务数量超过线程数量的时候,
线程池不会再创建新的线程而是让新任务等待,这样我们就不在需要担心线程数量过多耗尽系统资源了。JDK1.5开始为我们提供了标准的线程池。
(1)Executor接口
Java的线程池实现了以下Executor接口:
Java的线程池实现了以下Executor接口: public interface Executor { void execute(Runnable command); }
在多线程编程中,执行器是一种常用的设计模式,它的好处在于提供了一种简单有效的编程模型,我们只需把需要并发处理的工作拆分成独立的任务,然后交给执行器去执行即可而不必关心线程的创建,分配和调度。
JDK主要提供了两种功能的执行器:ThreadPoolExecutor和ScheduledThreadPoolExecutor。ThreadPoolExecutor是基本的线程池实现,ScheduledThreadPoolExecutor在前者基础上增加了任务调度的功能,在把任务交给它时我们可以指定任务的执行时间,而不是立刻执行。
(2)Executors创建线程池
java.util.concurrent.Executors是用来创建线程池的工厂类,
通过它提供的工厂方法,我们可以方便地创建不同特性的线程池,包括缓存线程池、各种优先级线程池等。
(3)Future接口
Executor接口并没有看起来那么理想,有时候我们执行一个任务是要得到计算的结果,有时候我们需要对任务有更多控制,例如知道它是否完成,或者中途终止它。返回void的execute方法并不能满足我们这些需求。当然我们可以在传入的Runnable类上下功夫来提供类似的功能,但是这样做繁琐且容易出错。实际上线程池实现了一个更为丰富的ExecutorService接口,它定义了执行任务并返回代表该任务的Future对象的submit方法。
通过Future接口,我们可以查看已经被提交给线程池执行的任务是否完成,获取执行的结果或者终止任务。
(4) Runnable 和Callable 接口
实现了Runnable或Callable接口的类都可以作为任务提交给线程池执行,这两个接口的主要区别在于Callable的call方法有结果返回并且可以抛出异常而Runnable的run方法返回void且不允许有可检查的异常抛出(只能抛runtime exception)。因此如果我们的任务执行后有结果返回,应该使用Callable接口。
协调共享对象访问的机制:JDK5之前是synchronized和volatile(),JDK5增加了ReentrantLock,现在可以用Lock显式的lock()和unlock(),并且有定时锁,读写锁等。
(1)ReentrantLock
ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。
public interface Lock { void lock(); //如果当前线程未被中断,则获取锁定。 void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
使用ReentrantLock来保护对象状态。
Lock lock = new ReentrantLock(); lock.lock(); try { //相关操作 } finally { lock.unlock(); //一定要释放锁 }
(2)轮询锁和定时锁
可定时的与可轮询的锁获取模式是由tryLock方法实现的,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。
(3)可中断的锁
(4)ReadWriteLock 读-写锁
ReadWriteLock 维护了一对相关的锁定,一个用于只读操作,另一个用于写入操作。
//ReadWriteLock 接口
//ReadWriteLock 接口 public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
(1)CountDownLatch,计数器或者闭锁
CountDownLatch是一个同步辅助类,java.util.concurrent.CountDownLatch,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。
CountDownLatch即一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行
(2)CyclicBarrier,栅栏
通过闭锁(CountDownLatch)来同时启动一组相关线程,或等待一组相关线程的结束。可是闭锁是一次性对象,一旦进入终止状态,就不能被重置。栅栏类似于闭锁,它能够阻塞一组线程直到某个事件发生。
CyclicBarrier在并行迭代算法中是非常有用。
计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。
有时候我们有多个相同的共享资源可以同时被多个线程使用。我们希望在锁的基础上加上一个计数器,根据资源的个数来初始化这个计数器,每次成功的lock操作都会使计数器的值减去1,只要计数器的值不为零就表示还有资源可以使用,lock操作就能成功。每次unlock操作都会给这个计数器加1。只有当计数器的值为0的时候lock操作才会阻塞当前线程。这就是Java中的信号量Semaphore。
Semaphore类提供的方法和Lock接口非常类似,当把信号量的资源个数设置成1时,信号量就退化为普通的锁。
(1)是变量不是线程
如果每个线程都有自己私有的成员变量,那么我们也不需要同步。ThreadLocal就是线程的私有变量,每个使用ThreadLocal变量的线程都会有自己独立的ThreadLocal对象,因此就不存在多个线程访问同一个变量的问题。
它并不是一个Thread,而是threadlocalvariable(线程局部变量)。
(2)ThreadLocal 的实现原理
每个Thread对象有自己用来存储私有ThreadLocal对象的容器ThreadLocalMap,当某个线程调用ThreadLocal对象的get()方法来 取值的时候,
get方法首先会取得当前线程对象,然后取出该线程的ThreadLocalMap,然后检查自己是否已经在map中,如果自己已经存在,直接返回map中的value。
如果不存在,把自己作key并初始化一个value加入到当前线程的map中。
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); }
整理自 《Java并发编程实战》
Java Threads 多线程10分钟参考手册
Java并发编程学习笔记