非原子性的64位操作: 非volatile类型的64位数值变量double、 long,JVM允许将64位的读操作或写操作分解为两个32位的操作,当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。
加锁的含义: 不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步
正确使用volatile变量的方式: 1. 当变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值; 2. 该变量不会与其他状态变量一起纳入不变性条件中; 3. 在访问变量时不需要加锁。
加锁机制和volatile变量的区别: 加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
概念:不共享数据 **分类:**Ad-hoc线程封闭、栈封闭、ThreadLocal类 栈封闭:指的是局部变量封闭在执行线程中 ThreadLocal类:这个类能使线程中的某个值与保存值的对象关联起来,通常用于防止对可变的单实例变量或全局变量进行共享。
概念:不可变对象一定是线程安全的 对象不可变必须满足以下条件: - 对象创建以后其状态就不能修改 - 对象的所有域都是final类型 - 对象时正确创建的(在对象的创建期间,this引用没有逸出)
final域 final域能够确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无需同步
安全发布的常用模式: - 在静态初始化函数中初始化一个对象引用:例如 public static Holder holder = new Holder(42); - 将对象的引用保存到volatile类型的域或者AtomicReference对象中 - 将对象的引用保存到某个正确构造对象的final类型域中 - 将对象的引用保存到一个由锁保护的域中
对象的发布需求取决于它的可变性: - 不可变对象可以通过任意机制来发布 - 事实不可变对象必须通过安全方式来发布 - 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。 **举例:**Java平台类库提供了很多线程封闭的实例,就是讲非线程安全的类转化为线程安全的类,比如sychronizedList和concurrentHashMap。
Java监视器模式:将对象所有的可变状态封装起来,并由自己的内置锁来保护。比如Vector和HashTable。
@ThreadSafe public final class Counter { @GuardedBy("this") private long value = 0; public synchronized long getValue() { return value; } public synchronized long increament() { if (value == Long.MAX_VALUE) { throw new IllegalStateException("counter overflow"); } return ++value; } }如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。
客户端加锁机制:对于使用某个对象X的客户端代码,使用X本身用户保护其状态的锁来保护这段客户代码。
vector的问题: 比如getLats()、deleteLast()法交替执行时会抛出ArrayIndexOutOfBoundsException异常
迭代器的问题: 容器在迭代过程中被修改时,就会抛出ConcurrentModificationException异常
概念:它们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,此外还提供了一些方法对状态进行操作,以及另一些方法用于高效地等待同步工具类进入到预期状态。
闭锁:闭锁可以用来确保某些活动直到其他活动都完成后才继续执行 信号量:用来控制同时访问某个特定资源的操作数量,或者执行某个指定操作的数量。 栅栏:栅栏与闭锁的关键区别在于,所有线程必须同时的到达栅栏位置,才能继续执行。闭锁用于等待时间,而栅栏用于等待线程。
示例:基于Executor的Web服务器
class TaskExecutionWebServer { private static final int NTHREADS = 100; private static final Executor exec = Executors.newFixedThreadPool(NTHREADS); public static void main (String[] args) throws IOException { ServerSocket socket = new ServerSocket(80); while(true) { final Socket connection = socket.accept(); Runnable task = new Runnable() { public void run() { handleRequest(connection); } }; exec.execute(task); } } }线程池: - newFixedThreadPool:返回通用的ThreadPool-Executor实例 - newCachedThreadPool:返回通用的ThreadPool-Executor实例 - newSingleThreadExecutor - newScheduledThreadPool
ExecutorService的生命周期: - 运行 - 关闭 - 已终止
延迟任务与周期任务:使用ScheduledThreadPoolExecutor来代替Timer。 原因: 1. Timer在执行所有定时任务时只会创建一个线程 2. 不会捕获异常,并会终止定时任务
携带结果的任务Callable与Future - 与Runable的区别:可以返回一个值或者一个受检查的异常 - Future表示一个任务的生命周期
示例:利用Future和CompletionService实现页面渲染器
为任务设置时限: 如果某个任务无法在指定时间内完成,那么将不再需要它的结果,此时可以放弃这个任务
示例:旅行预订门户网站
中断:
//Thread中的中断方法 public class Thread { //中断目标线程 public void interrupt(){} //返回目标线程的中断状态 public boolean isInterrupt(){} //清楚当前线程的中断状态,返回它之前的值 public static boolean interrupted(){} }对中断操作的正确理解:它并不会真正地中断一个正在运行的线程,而是发出中断请求,然后由线程在下一个合适的时刻中断自己。
响应中断: 处理InterruptedException: - 传递异常(可能在执行某个特定于任务的清除操作之后),从而使你的方法也成为可中断的阻塞方法 - 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理
通过Future来实现取消:
public static void timedRun(Runnable r, long timeout, TimeUnit unit) throws InterruptedException { Future<?> task = taskExec.submit(r); try { task.get(timeout, unit); } catch (TimeoutException e) { //接下来任务将被取消 } catch (ExecutionException e) { //如果在任务中抛出了异常,那么重新抛出该异常 throws launderThrowable(e.getCause()); } finally { //如果任务已经结束,那么执行取消操作也不会带来任何影响 task.cancel; //如果任务正在运行,那么将被中断 } }毒丸对象:是指一个放在队列上的对象,其含义是当得到这个对象时,立即停止。在FIFO队列中,毒丸对象将确保消费者在关闭之前首先完成队列中的所有工作,在提交毒丸对象之前的提交的所有工作都会被处理,而生产者在提交了毒丸对象之后,将不会再提交任何工作。
public class IndexingService { private static final File POISON = new File(""); private final IndexingThread consumer = new IndexerThread(); private final CrawlerThread producer = new CrawlerThread(); private final BlockingQueue<File> queue; private final FileFilter fileFilter; private final File root; class CrawlerThread extends Thread {} class IndexerThread extends Thread{} public void start() { producer.start(); consumer.start(); } public void stop() { producer.interrupt(); } public void awaitTermination() throws InterruptException { consumer.join(); } }未捕获异常: 当一个线程由于未捕获异常而退出时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器。如果没有提供任何异常处理器,那么默认的行为是将栈追踪信息输出到System.err。
关闭钩子: 在正常关闭中,JVM首先调用所有已注册的关闭钩子,指的是通过Runtime.addShutdownHook注册的但尚未开始的线程。
注意:对所有服务使用同一个关闭钩子(而不是每个服务使用一个不同的关闭钩子),并且在该关闭钩子中执行一系列的关闭操作。这确保了关闭操作在单个线程中串行执行,从而避免了在关闭操作之间出现竞态条件或死锁等问题。
守护线程:执行一些辅助性工作,但又不阻碍JVM的关闭的线程。 与普通线程的区别:仅在于退出时发生的操作,当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作。当JVM停止时,所有仍然存在的守护线程都将被抛弃——既不会执行finally代码块,也不会执行回卷栈,而JVM只是直接退出。
注意:守护线程最好用于执行“内部”任务,例如周期性地从内存的缓存中移除逾期的数据。
通用构造函数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {}管理队列任务: - 有界队列 - 无界队列 - 同步移交
饱和策略:ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改。 - AbortPolicy - CallerRunsPolicy - DiscardPolicy - DiscardOldestPolicy
ThreadFactory:每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的,即newThread()方法。
锁顺序死锁:两个线程试图以不同的顺序来获得同样的锁 动态锁顺序死锁:如果两个线程同时调用thransferMoney,其中一个线程从X向Y转账,另一个线程从Y向X转账,那么就会发生死锁 在协作对象之间发生的死锁 开放调用:调用某个方法时不需要持有锁
饥饿:当线程由于无法访问它所需要的资源而不能继续执行时。 糟糕的响应性 活锁:线程不断重复执行相同的操作,而且总会失败 例子:活锁通常发生在处理事务消息的应用程序中,如果不能成功地处理某个消息,那么消息处理机制将会回滚整个事务,并将它重新放回队列的开头。 解决方法:在重试机制中引入随机性
定律:在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程序中并行组件与串行组件所占的比重
上下文切换:如果可运行的线程大于cpu的数量,那么操作系统最终会将某个正在运行的线程调度出来,从而使其他线程能够使用cpu,这将导致一次上下文切换,在这个过程中将保存当前运行线程的执行上下文,并将重新调度进来的线程的执行上下文设置为当前上下文。
内存同步: 阻塞:可以通过自旋等待或者被挂起
在Java5.0中,ReentrantLock能提供更高的吞吐量,但在Java6.0中,二者的吞吐量非常接近。
大多数情况下,非公平锁的性能要高于公平锁的性能。
ReentrantReadWriteLock: - 可以选择非公平还是公平锁 - 如果这个锁由读线程持有,而另一个线程请求写入锁,那么其他线程都不能获得读取锁 - 写线程降级为读线程是可以的,但从读线程升级为写线程是不可以的,防止死锁
性能: 分别用ReentrantLock和ReadWriteLock来封装ArrayList的吞吐量,在线程数量大于4的时候,后者的吞吐量大约是前者的3倍左右。
条件谓词:put方法的条件谓词是“缓存不满”,take方法的条件谓词是“缓存不为空” 过早唤醒:wait方法的返回并不意味着线程正在等待的条件谓词已经变成真的了 当使用条件等待时(例如Object.wait或Condition.await): - 通常都有一个条件谓词——包括一些对象状态的测试,线程在执行前必须首先通过这些测试 - 在调用wait之前测试条件谓词,并且从wait中返回时再次进行测试 - 在一个循环中调用wait - 确保使用与条件队列相关的锁来保护构成条件谓词的各个状态变量 - 当调用wait、notify或notifyAll等方法时,一定要持有与队列相关的锁 - 在检查条件谓词之后以及开始执行相应的操作之前,不要释放锁
通知:优先选择notifyAll 同时满足以下两个条件,才能用单一的notify而是notifyAll: - 所有的等待线程的类型都相同 - 单进单出
使用显式条件变量的有界缓存:
@ThreadSafe public class ConditionBoundedBuffer<T> { protected final Lock lock = new ReentrantLock(); //条件谓词:notFull (count < items.length) private final Condition notFull = lock.newCondition(); //条件谓词:notEmpty (count > 0) private final Condition notEmpty = lock.newCondition(); @GuardedBy("lock") private final T[] items = (T[]) new Object[BUFFER_SIZE]; @GuardedBy("lock") private int tail, head, count; //阻塞并直到: notFull public void put(T x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[tail] = x; if (++tail == items.length) tail = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } //阻塞并直到: notEmpty public T take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); T x = items[head]; items[head] = null; if (++head == items.length) tail = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }概念:是许多同步类的基类,例如ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock、SynchronousQueue和FutureTask
性能比较:在中低程度的竞争下,原子变量能提供更高的可伸缩性,而在高强度的竞争下,锁能更有效地避免竞争。
概念:如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。 实现的技巧:将执行原子修改的范围缩小到单个变量上。
**简介:**Java内存模型是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作,JMM为程序中所有的操作定义了一个偏序关系,称之为Happen-Before。
不安全的发布:当缺少Happen-Before关系时,就可能出现重排序问题,导致在没有充分同步的情况下发布一个对象会导致另一个线程看一个只被部分构造的对象。
线程安全的延迟初始化:
@ThreadSafe public class SafeLazyInitialization { private static Resource resource; public synchronized static Resource getInstance() { if (resource == null) resource = new Resource(); return resource; } }双重检查加锁:不要这么做!!!
@NotThreadSafe public class DoubleCheckLocking { private static Resource resource; public static Resource getInstance() { if (resource == null) { synchronized (DoubleCheckLocking.class) { if (resource == null) { resource = new Resource(); } } } return resource; } }