Java Threads 多线程10分钟参考手册

1         同步

如何同步多个线程对共享资源的访问是多线程编程中最基本的问题之一。当多个线程并发访问共享数据时会出现数据处于计算中间状态或者不一致的问题,从而影响到程序的正确运行。我们通常把这种情况叫做竞争条件(race condition),把并发访问共享数据的代码叫做关键区域(critical section)。同步就是使得多个线程顺序进入关键区域从而避免竞争条件的发生。

1.1       Synchronized关键字

Synchronized是Java多线程编程中最常用的关键字。所有的Java 对象都有自己唯一的隐式同步锁。该锁只能同时被一个线程获得,其他试图获得该锁的线程都会被阻塞在对象的等待队列中直到获得该锁的线程释放锁才能继续工作。Synchronized关键字通常有两种用法。当Synchronized关键字用于类方法定义中时,表示所有调用该方法的线程都必须获得当前对象的锁。这种方式比较简单,但是同步的粒度比较大,当一个线程要执行某个对象的同步方法的时候,必须同时没有任何其他线程在执行该对象的任一同步方法。此外,同步方法中的所有代码均在同步块中,获得锁的线程必须在执行完所有的代码离开该方法后才会释放锁,这些代码中可能只有一部分涉及到对共享资源(例如成员变量)的访问需要同步,其余则不需要,那么这样粗粒度的同步显然增加了其他线程的等待时间。Synchronized的另一种 用法允许作用在某个对象上,并且只同步一段代码而不是整个方法。

synchronized (object)  {

// 需要同步的代码

}

这里synchronized所作用的对象可以是类的某个成员变量,也可以是这个类对象(用this表示)。这种用法使得程序员可以根据需要同步不同的成员变量,而不总是当前类对象,提高了灵活性。

值得一提的是,并不是只有对象才有锁,类本身也有自己的锁,这使得static方法同样可以用synchronized来修饰。访问同步static方法的线程需要获得类的同步锁才能继续执行。

1.2       Volatile关键字

在Java内存模型中每个线程拥有自己的本地存储(例如寄存器),并且允许线程拥有变量值的拷贝。这使得本来不需要同步的一些原子操作,例如boolean成员变量存储和读取也变得不安全。设想我们有个叫做done的boolean成员变量和一个当done为true时才会停止的循环,该循环由后台线程执行,另一个UI线程等待用户输入,用户按下某个按钮以后会把done设成true从而终止循环。由于UI线程自己本地拥有done的拷贝,用户在按下按钮时只是把自己本地的done设成了true而没有及时更新主内存中的done,所以后台线程由于看不到done的改变而不会终止。即使主内存中的done变化了,后台线程也会因为自己本地的变量值没有及时更新而没有察觉到done的变化。解决这一问题的方法之一是为done提供synchronized的setter和getter方法,这是因为获得同步锁会迫使所有变量的值从临时存储(寄存器)写会主内存。除此之外,Java提供了一个解决这个问题更为优雅的方法:Volatile关键字。每次使用volatile变量,JVM都会保证从主内存中读取它的值;同样每次修改volatile变量,JVM都会把值写回到主内存中。

Volatile适用的场景比较严格,必须很清楚地看到volatile只是告诉JVM对于该变量的读写必须每次都在主内存中进行而禁止使用临时的拷贝来优化,它只是出于JVM特殊的内存模型的需要,并没有同步的功能。因此只有对volatile变量进行的原子操作(读取和赋值)才是线程安全的,像自增++自减--这样包含多个命令的操作仍然需要其它的同步措施。

另一个需要注意的的地方是当用volatile修饰数组的时候,它只是说数组的引用是volatile的,而数组中的元素还是和普通变量一样,可能被JVM优化,我们无法为数组中的元素加上volatile修饰。解决上述问题的方法是使用Atomic变量。作为使用volatile修饰数组的一个例子,可以参考java.util.concurrent.CopyOnWriteArrayList。它的add操作是通过复制原来的数组并把新元素添加到新数组末尾然后再把内部数组引用变量指向新数组来实现的,因此数组变量经常会被修改,需要使用volatile。

1.3       显式锁Lock

尽管synchronized关键字可以解决大多数同步问题,J2SE5.0还是引入了Lock接口。相比使用synchronized关键字获取对象隐式的同步锁,我们称Lock为显式锁。使用显式锁的一个显而易见的好处是它不再属于某个对象,从而可以在多个对象之间共享它。Lock接口有lock()和unlock()两个方法,使用它们和使用synchronized关键字类似,在进入需要同步的代码之前调用lock,在离开同步代码块时调用unlock。通常unlock会被放在finally中以保证即使同步代码块中有异常发生,锁仍然可以被释放。

和使用synchronized关键字和lock()方法总是把未能获得锁的线程阻塞不同,Lock接口还提供了非阻塞的tryLock()方法。调用tryLock方法的线程如果未能获得锁会立刻返回false,线程可以继续执行其他代码而避免等待,这为程序员提供了更多自由。

Lock接口还提供了一个newCondition () 方法,它返回一个Condition对象。Condition对象的作用和Object用于线程通知的wait-notify机制相同。

1.4       信号量Semaphore

有时候我们有多个相同的共享资源可以同时被多个线程使用。我们希望在锁的基础上加上一个计数器,根据资源的个数来初始化这个计数器,每次成功的lock操作都会使计数器的值减去1,只要计数器的值不为零就表示还有资源可以使用,lock操作就能成功。每次unlock操作都会给这个计数器加1。只有当计数器的值为0的时候lock操作才会阻塞当前线程。这就是Java中的信号量Semaphore。

Semaphore类提供的方法和Lock接口非常类似,当把信号量的资源个数设置成1时,信号量就退化为普通的锁。

1.5       读写锁ReadWriteLock

对共享资源的访问通常可以分为读取和写入。在有些应用场景中读取可能需要花费较长时间,我们需要使用互斥锁来阻止并发的写入操作以保证数据的一致性。但是对于并发的读取线程其实并不需要使用同步。事实上只有使数据发生变化的操作才需要同步,我们希望有一种方法可以把读取和写入区分开来,读取和写入的操作之间是互斥的,但是多个读取操作可以同时进行,这样可以有效提高读取密集型程序的性能。J2SE5.0提供了ReadWriteLock接口并提供了实现该接口的ReentrantReadWriteLock类:

[java] view plaincopy

  1. public interface ReadWriteLock {
  2. Lock readLock();
  3. Lock writeLock();
  4. }

从接口方法中不难看出读写锁中包含读锁和写锁。实现类ReentrantReadWriteLock为我们提供了更多便捷的方法来使用读写锁,例如isWriteLocked可以用来检测是否被写锁定。

2         线程通知

除了同步锁,Java Object还有两个可用于线程间通知的同步方法wait和notify。wait和notify必须和synchronized联合使用,一个获得对象锁的线程通过调用对象的wait方法可以暂时放弃锁,把自己阻塞在对象的等待队列中,好让其它等待同一把锁的线程有机会执行。当另外那个获得了锁的线程完成工作后可以调用notify方法来唤醒它继续执行。每次notify调用只能唤醒一个在等待队列中的线程,notifyAll方法可以唤醒所有在该对象等待队列中的线程。

3         最小化同步

线程同步通过让线程顺序进入同步代码块解决了多个线程竞争同一资源而引起的不确定性,但是牺牲了效率,因此为了取得更好地性能,我们需要尽可能少地使用同步。事实上并不是所有的竞争条件都是需要避免的,只有当竞争条件出现在非线程安全的代码段时才会引起问题。

3.1       Atomic 变量

如果一个操作是原子操作,例如给一个boolean 变量赋值,我们就不需要同步。Java提供了一些Atomic类,使得一些本来不是原子操作(例如自增操作 ++,它包含了取值、加1、赋值三个原子操作)也能够原子执行,从而不需要使用同步。

Java提供了4个基本的原子类,AtomicInteger, AtomicLong, AtomicBoolean和AtomicReference分别提供针对int,long,boolean,object的原子操作。有意思的是如果你打开JDK的源代码想看看这些原子操作是如何实现的,你会失望地发现代码里面没有使用任何同步或其它技术。如果你在自己的程序中写下同样地代码,那么它们并不是原子的。

3.2       Thread Local 变量

如果每个线程都有自己私有的成员变量,那么我们也不需要同步。ThreadLocal就是线程的私有变量,每个使用ThreadLocal变量的线程都会有自己独立的ThreadLocal对象,因此就不存在多个线程访问同一个变量的问题。当然由于ThreadLocal变量为线程私有,它也就不可以用于在多个线程间共享状态。

ThreadLocal类并不神秘,它的实现原理比较简单:每个Thread对象有自己用来存储私有ThreadLocal对象的容器ThreadLocalMap,当某个线程调用ThreadLocal对象的get()方法来 取值的时候,get方法首先会取得当前线程对象,然后取出该线程的ThreadLocalMap,然后检查自己是否已经在map中,如果自己已经存在,直接返回map中的value。如果不存在,把自己作key并初始化一个value加入到当前线程的map中。

[java] view plaincopy

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this);
  6. if (e != null)
  7. return (T)e.value;
  8. }
  9. return setInitialValue();
  10. }

4         线程池Thread Pool

线程虽然不像进程需要那么多资源,但是它的创建也是有一定开销的,频繁地创建和销毁线程会降低程序的性能;此外应用程序可以创建线程的数量是受机器物理条件制约的,过多的线程会耗尽机器的资源,因此我们在设计程序的时候需要限制并发线程的数量。解决这两个问题的通常做法是使用线程池。线程池在启动的时候一次性初始化若干个线程(也可以根据负载按需启动,也有闲置一定时间的线程会被销毁的策略),然后程序把任务交给线程池去执行而不是直接交给某个线程执行,由线程池给这些任务分配线程。当某个线程执行完一个任务后,线程池会把它设成空闲状态以备下一个任务重用而不是销毁它。线程池在初始化的时候需要指定线程数量上限,当并发任务数量超过线程数量的时候,线程池不会再创建新的线程而是让新任务等待,这样我们就不在需要担心线程数量过多耗尽系统资源了。JDK1.5开始为我们提供了标准的线程池。

4.1       执行器Executor

Java的线程池实现了以下Executor接口:         

[java] view plaincopy

  1. public interface Executor {
  2. void execute(Runnable command);
  3. }

在多线程编程中,执行器是一种常用的设计模式,它的好处在于提供了一种简单有效的编程模型,我们只需把需要并发处理的工作拆分成独立的任务,然后交给执行器去执行即可而不必关心线程的创建,分配和调度。J2SE5.0主要提供了两种功能的执行器:ThreadPoolExecutor和ScheduledThreadPoolExecutor。ThreadPoolExecutor是基本的线程池实现,ScheduledThreadPoolExecutor在前者基础上增加了任务调度的功能,在把任务交给它时我们可以指定任务的执行时间,而不是立刻执行。

java.util.concurrent.Executors是用来创建线程池的工厂类,通过它提供的工厂方法,我们可以方便地创建不同特性的线程池。

4.2       Future接口

Executor接口并没有看起来那么理想,有时候我们执行一个任务是要得到计算的结果,有时候我们需要对任务有更多控制,例如知道它是否完成,或者中途终止它。返回void的execute方法并不能满足我们这些需求。当然我们可以在传入的Runnable类上下功夫来提供类似的功能,但是这样做繁琐且容易出错。既然J2SE为我们提供了线程池的标准实现把我们从多线程编程中解放出来,这些常见的需求当然也会很好地满足。事实上线程池实现了一个更为丰富的ExecutorService接口,它定义了执行任务并返回代表该任务的Future对象的submit方法。

通过Future接口,我们可以查看已经被提交给线程池执行的任务是否完成,获取执行的结果或者终止任务。

4.3       Runnable 和Callable 接口

实现了Runnable或Callable接口的类都可以作为任务提交给线程池执行,这两个接口的主要区别在于Callable的call方法有结果返回并且可以抛出异常而Runnable的run方法返回void且不允许有可检查的异常抛出(只能抛runtime exception)。因此如果我们的任务执行后有结果返回,应该使用Callable接口。

5         线程和集合类

5.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

5.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保护,否则会出现问题。

[java] view plaincopy

  1. List list = Collections.synchronizedList(new ArrayList());
  2. ...
  3. synchronized(list) {
  4. Iterator i = list.iterator(); // Must be in synchronized block
  5. while (i.hasNext())
  6. foo(i.next());
  7. }

5.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) 。

时间: 2024-11-03 04:52:29

Java Threads 多线程10分钟参考手册的相关文章

2. Java中的垃圾收集 - GC参考手册

标记-清除(Mark and Sweep)是最经典的垃圾收集算法.将理论用于生产实践时, 会有很多需要优化调整的地点, 以适应具体环境.下面通过一个简单的例子, 让我们一步步记录下来, 看看如何才能保证JVM能安全持续地分配对象. 您应该已经阅读了前一章: 1. 垃圾收集简介 - GC参考手册 碎片整理(Fragmenting and Compacting) 每次执行清除(sweeping), JVM 都必须保证不可达对象占用的内存能被回收重用.但这(最终)有可能会产生内存碎片(类似于磁盘碎片)

GC 算法(实现篇) - GC参考手册

您应该已经阅读了前面的章节: 垃圾收集简介 - GC参考手册 Java中的垃圾收集 - GC参考手册 GC 算法(基础篇) - GC参考手册 学习了GC算法的相关概念之后, 我们将介绍在JVM中这些算法的具体实现.首先要记住的是, 大多数JVM都需要使用两种不同的GC算法 -- 一种用来清理年轻代, 另一种用来清理老年代. 我们可以选择JVM内置的各种算法.如果不通过参数明确指定垃圾收集算法, 则会使用宿主平台的默认实现.本章会详细介绍各种算法的实现原理. 下面是关于Java 8中各种组合的垃圾

[转帖]1. 垃圾收集简介 - GC参考手册

1. 垃圾收集简介 - GC参考手册 https://blog.csdn.net/renfufei/article/details/53432995 翻译铁锚 发布于2016-12-02 11:29:01 阅读数 10357  收藏 展开 说明: 在本文中, Garbage Collection 翻译为 “垃圾收集”, garbage collector 翻译为 “垃圾收集器”; 一般认为, 垃圾回收 和 垃圾收集 是同义词. Minor GC 翻译为: 小型GC; 而不是 次要GC Major

GC 调优(实战篇) - GC参考手册

本章介绍导致GC性能问题的典型情况.相关示例都来源于生产环境, 为演示需要做了一定长度的精简. 说明: Allocation Rate, 翻译为分配速率, 而不是分配率; 因为不是百分比,而是单位时间内分配的量; 同理, Promotion Rate 翻译为 提升速率; 您应该已经阅读了前面的章节: 垃圾收集简介 - GC参考手册 Java中的垃圾收集 - GC参考手册 GC 算法(基础篇) - GC参考手册 GC 算法(实现篇) - GC参考手册 GC 调优(基础篇) - GC参考手册 GC

java使用线程请求访问每次间隔10分钟连续5次,之后停止请求

java使用线程请求访问每次间隔10分钟连续5次,收到相应的时候停止请求 package com.qlwb.business.util; /** * * * @类编号: * @类名称:RequestTask * @内容摘要: 若开发者发出了回调通知,却没有收到兑吧ok响应时,开发者需重试5次,每次间隔10分钟. * @author:鹿伟伟 * @创建日期:2016年4月15日 下午4:25:20 * @修改人: * @修改日期: * @修改描述:简单描述修改的内容 * @version 1.0.

MiniGUI文档参考手册 基于v1.6.10版

MiniGUI的函数及各个预定义宏均分布于各个头文件内,特别不方便查找,也不利于新手的学习. 有一天发现了doxygen,于是用该工具生成了minigui的文档参考手册 ,基于v1.6.10版. 地址:http://download.csdn.net/detail/u013148209/8137895 演示如下: 1.搜索函数,可见具有自动提示补充功能: 2.搜索结果如下: 3.搜索消息:

dubbu 官方参考手册~备案(防止哪天阿里一生气把dubbo给删除了)

首页  ||  下载  ||  用户指南  ||  开发者指南  ||  管理员指南  ||  培训文档  ||  常见问题解答  ||  发布记录  ||  发展路线  ||  社区 English | 中文 用户指南 入门 背景 需求 架构 用法 快速启动 服务提供者 服务消费者 依赖 必需依赖 缺省依赖 可选依赖 成熟度 功能成熟度 策略成熟度 配置 Xml配置 属性配置 注解配置 API配置 示例 启动时检查 集群容错 负载均衡 线程模型 直连提供者 只订阅 只注册 静态服务 多协议 多

Dubbo -- 系统学习 笔记 -- 配置参考手册

配置参考手册 <dubbo:service/> <dubbo:reference/> <dubbo:protocol/> <dubbo:registry/> <dubbo:monitor/> <dubbo:application/> <dubbo:module/> <dubbo:provider/> <dubbo:consumer/> <dubbo:method/> <dubbo:

JSTL标签参考手册

前言 ========================================================================= JSTL标签库,是日常开发经常使用的,也是众多标签中性能最好的.把常用的内容,放在这里备份一份,随用随查.尽量做到不用查,就可以随手就可以写出来.这算是Java程序员的基本功吧,一定要扎实. JSTL全名为JavaServer Pages Standard Tag Library,目前最新的版本为1.1版.JSTL是由JCP(Java Comm