1、Java多线程并发处理方式
synchronized关键字主要解决多线程共享数据同步问题。
ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题。
ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别:
synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。
1、java中synchronized用法
使用了synchronized关键字可以轻松地解决多线程共享数据同步问题。
synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
synchronized取得的锁都是对象;每个对象只有一个锁(lock)与之相关联;实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
synchronized的4种用法 :
1.方法声明时使用,线程获得的是成员锁.
2.对某一代码块使用,synchronized后跟括号,括号里是变量,线程获得的是成员锁.
3.synchronized后面括号里是一对象,此时,线程获得的是对象锁.
4.synchronized后面括号里是类,此时,线程获得的是对象锁.
2、java.lang.ThreadLocal()的用法
一、概述
ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
二、API说明
ThreadLocal()
创建一个线程本地变量。
T get()
返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
protected T initialValue()
返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。
若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。
void remove()
移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。
void set(T value)
将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。
在程序中一般都重写initialValue方法,以给定一个特定的初始值。
三、典型实例
四、总结
ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题。
ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。
五、ThreadLocal使用的一般步骤
1、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
2、在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
3、在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。
我们知道Spring通过各种DAO模板类降低了开发者使用各种数据持久技术的难度。这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突。
我们使用模板类访问底层数据,根据持久化技术的不同,模板类需要绑定数据连接或会话的资源。但这些资源本身是非线程安全的,也就是说它们不能在同一时刻被多个线程共享。
虽然模板类通过资源池获取数据连接或会话,但资源池本身解决的是数据连接或会话的缓存问题,并非数据连接或会话的线程安全问题。
按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步。但Spring的DAO模板类并未采用线程同步机制,因为线程同步限制了并发访问,会带来很大的性能损失。
此外,通过代码同步解决性能安全问题挑战性很大,可能会增强好几倍的实现难度。那模板类究竟仰丈何种魔法神功,可以在无需同步的情况下就化解线程安全的难题呢?答案就是ThreadLocal!
ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它们的身影,起着举足轻重的作用。要想了解Spring事务管理的底层技术,ThreadLocal是必须攻克的山头堡垒。
2、Java.多线程编程详解
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。
由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。
1. synchronized 方法:通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:
- public synchronized void accessVal(int newVal);
synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。
在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。
synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。
2. synchronized 块:通过 synchronized关键字来声明synchronized 块。语法如下:
synchronized(syncObject) {
//允许访问控制的代码 }
synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。
六:线程的阻塞
为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察多个线程对共享资源的访问,显然同步机制已经不够了,因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。为了解决这种情况下的访问控制问题,Java 引入了对阻塞机制的支持。
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面让我们逐一分析。
1. sleep() 方法:sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。
典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。
2. suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
3. yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
4. wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。
初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。上述的核心区别导致了一系列的细节上的区别。
首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用 任意对象的notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。
wait() 和 notify() 方法的上述特性决定了它们经常和synchronized 方法或块一起使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于 block 和wakeup 原语(这一对方法均声明为 synchronized)。它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。
关于 wait() 和 notify() 方法最后再说明两点:
第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 wait() 方法的调用都可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。
以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了 wait() 和 notify() 方法,因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效率较低,较容易出错。实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目的。
七:守护线程
守护线程是一类特殊的线程,它和普通线程的区别在于它并不是应用程序的核心部分,当一个应用程序的所有非守护线程终止运行时,即使仍然有守护线程在运行,应用程序也将终止,反之,只要有一个非守护线程在运行,应用程序就不会终止。守护线程一般被用于在后台为其它线程提供服务。
可以通过调用方法 isDaemon() 来判断一个线程是否是守护线程,也可以调用方法 setDaemon() 来将一个线程设为守护线程。
八:线程组
线程组是一个 Java 特有的概念,在 Java 中,线程组是类ThreadGroup 的对象,每个线程都隶属于唯一一个线程组,这个线程组在线程创建时指定并在线程的整个生命期内都不能更改。你可以通过调用包含 ThreadGroup 类型参数的 Thread 类构造函数来指定线程属的线程组,若没有指定,则线程缺省地隶属于名为 system 的系统线程组。
在 Java 中,除了预建的系统线程组外,所有线程组都必须显式创建。在 Java 中,除系统线程组外的每个线程组又隶属于另一个线程组,你可以在创建线程组时指定其所隶属的线程组,若没有指定,则缺省地隶属于系统线程组。这样,所有线程组组成了一棵以系统线程组为根的树。
Java 允许我们对一个线程组中的所有线程同时进行操作,比如我们可以通过调用线程组的相应方法来设置其中所有线程的优先级,也可以启动或阻塞其中的所有线程。
Java 的线程组机制的另一个重要作用是线程安全。线程组机制允许我们通过分组来区分有不同安全特性的线程,对不同组的线程进行不同的处理,还可以通过线程组的分层结构来支持不对等安全措施的采用。Java 的 ThreadGroup 类提供了大量的方法来方便我们对线程组树中的每一个线程组以及线程组中的每一个线程进行操作。
九:总结
在这一讲中,我们一起学习了 Java 多线程编程的方方面面,包括创建线程,以及对多个线程进行调度、管理。我们深刻认识到了多线程编程的复杂性,以及线程切换开销带来的多线程程序的低效性,这也促使我们认真地思考一个问题:我们是否需要多线程?何时需要多线程?
多线程的核心在于多个代码块并发执行,本质特点在于各代码块之间的代码是乱序执行的。我们的程序是否需要多线程,就是要看这是否也是它的内在特点。
假如我们的程序根本不要求多个代码块并发执行,那自然不需要使用多线程;假如我们的程序虽然要求多个代码块并发执行,但是却不要求乱序,则我们完全可以用一个循环来简单高效地实现,也不需要使用多线程;只有当它完全符合多线程的特点时,多线程机制对线程间通信和线程管理的强大支持才能有用武之地,这时使用多线程才是值得的。
线程的(同步)控制
一个Java程序的多线程之间可以共享数据。当线程以异步方式访问共享数据时,有时候是不安全的或者不和逻辑的。比如,同一时刻一个线程在读取数据,另外一个线程在处理数据,当处理数据的线程没有等到读取数据的线程读取完毕就去处理数据,必然得到错误的处理结果。这和我们前面提到的读取数据和处理数据并行多任务并不矛盾,这儿指的是处理数据的线程不能处理当前还没有读取结束的数据,但是可以处理其它的数据。
如果我们采用多线程同步控制机制,等到第一个线程读取完数据,第二个线程才能处理该数据,就会避免错误。可见,线程同步是多线程编程的一个相当重要的技术。
在讲线程的同步控制前我们需要交代如下概念:
1 用Java关键字synchonized同步对共享数据操作的方法
在一个对象中,用synchonized声明的方法为同步方法。Java中有一个同步模型-监视器,负责管理线程对对象中的同步方法的访问,它的原理是:赋予该对象唯一一把'钥匙',当多个线程进入对象,只有取得该对象钥匙的线程才可以访问同步方法,其它线程在该对象中等待,直到该线程用wait()方法放弃这把钥匙,其它等待的线程抢占该钥匙,抢占到钥匙的线程后才可得以执行,而没有取得钥匙的线程仍被阻塞在该对象中等待。
file://声明同步的一种方式:将方法声明同步
- class store
- { public synchonized void store_in()
- { …. }
- public synchonized void store_out(){ ….}
- }
2 利用wait()、notify()及notifyAll()方法发送消息实现线程间的相互联系
Java程序中多个线程通过消息来实现互动联系的,这几种方法实现了线程间的消息发送。例如定义一个对象的synchonized 方法,同一时刻只能够有一个线程访问该对象中的同步方法,其它线程被阻塞。通常可以用notify()或notifyAll()方法唤醒其它一个或所有线程。而使用wait()方法来使该线程处于阻塞状态,等待其它的线程用notify()唤醒。
一个实际的例子就是生产和销售,生产单元将产品生产出来放在仓库中,销售单元则从仓库中提走产品,在这个过程中,销售单元必须在仓库中有产品时才能提货;如果仓库中没有产品,则销售单元必须等待。
程序中,假如我们定义一个仓库类store,该类的实例对象就相当于仓库,在store类中定义两个成员方法:store_in(),用来模拟产品制造者往仓库中添加产品;strore_out()方法则用来模拟销售者从仓库中取走产品。然后定义两个线程类:customer类,其中的run()方法通过调用仓库类中的store_out()从仓库中取走产品,模拟销售者;另外一个线程类producer中的run()方法通过调用仓库类中的store_in()方法向仓库添加产品,模拟产品制造者。在主类中创建并启动线程,实现向仓库中添加产品或取走产品。
如果仓库类中的store_in() 和store_out()方法不声明同步,这就是个一般的多线程,我们知道,一个程序中的多线程是交替执行的,运行也是无序的,这样,就可能存在这样的问题:
仓库中没有产品了,销售者还在不断光顾,而且还不停的在'取'产品,这在现实中是不可思义的,在程序中就表现为负值;如果将仓库类中的stroe_in()和store_out()方法声明同步,如上例所示:就控制了同一时刻只能有一个线程访问仓库对象中的同步方法;即一个生产类线程访问被声明为同步的store_in()方法时,其它线程将不能够访问对象中的store_out()同步方法,当然也不能访问store_in()方法。必须等到该线程调用wait()方法放弃钥匙,其它线程才有机会访问同步方法。
这个原理实际中也很好理解,当生产者(producer)取得仓库唯一的钥匙,就向仓库中添放产品,此时其它的销售者(customer,可以是一个或多个)不可能取得钥匙,只有当生产者添放产品结束,交还钥匙并且通知销售者,不同的销售者根据取得钥匙的先后与否决定是否可以进入仓库中提走产品。
轻松使用线程: 不共享有时是最好的
利用 ThreadLocal 提高可伸缩性
ThreadLocal 类是悄悄地出现在 Java 平台版本 1.2 中的。虽然支持线程局部变量早就是许多线程工具(例如 Posix pthreads 工具)的一部分,但 Java Threads API 的最初设计却没有这项有用的功能。而且,最初的实现也相当低效。由于这些原因,ThreadLocal 极少受到关注,但对简化线程安全并发程序的开发来说,它却是很方便的。在轻松使用线程的第 3 部分,Java 软件顾问 Brian Goetz 研究了 ThreadLocal 并提供了一些使用技巧。
参加 Brian 的多线程 Java 编程讨论论坛以获得您工程中的线程和并发问题的帮助。
编写线程安全类是困难的。它不但要求仔细分析在什么条件可以对变量进行读写,而且要求仔细分析其它类能如何使用某个类。有时,要在不影响类的功能、易用性或性能的情况下使类成为线程安全的是很困难的。有些类保留从一个方法调用到下一个方法调用的状态信息,要在实践中使这样的类成为线程安全的是困难的。
管理非线程安全类的使用比试图使类成为线程安全的要更容易些。非线程安全类通常可以安全地在多线程程序中使用,只要您能确保一个线程所用的类的实例不被其它线程使用。例如,JDBC Connection 类是非线程安全的 ― 两个线程不能在小粒度级上安全地共享一个 Connection ― 但如果每个线程都有它自己的 Connection,那么多个线程就可以同时安全地进行数据库操作。
不使用 ThreadLocal 为每个线程维护一个单独的 JDBC 连接(或任何其它对象)当然是可能的;Thread API 给了我们把对象和线程联系起来所需的所有工具。而 ThreadLocal 则使我们能更容易地把线程和它的每线程(per-thread)数据成功地联系起来。
什么是线程局部变量(thread-local variable)?
线程局部变量高效地为每个使用它的线程提供单独的线程局部变量值的副本。每个线程只能看到与自己相联系的值,而不知道别的线程可能正在使用或修改它们自己的副本。一些编译器(例如 Microsoft Visual C++ 编译器或 IBM XL FORTRAN 编译器)用存储类别修饰符(像 static 或 volatile)把对线程局部变量的支持集成到了其语言中。Java 编译器对线程局部变量不提供特别的语言支持;相反地,它用 ThreadLocal 类实现这些支持,核心 Thread 类中有这个类的特别支持。
因为线程局部变量是通过一个类来实现的,而不是作为 Java 语言本身的一部分,所以 Java 语言线程局部变量的使用语法比内建线程局部变量语言的使用语法要笨拙一些。要创建一个线程局部变量,请实例化类 ThreadLocal 的一个对象。 ThreadLocal 类的行为与 java.lang.ref 中的各种 Reference 类的行为很相似;ThreadLocal 类充当存储或检索一个值时的间接句柄。清单 1 显示了 ThreadLocal 接口。
清单 1. ThreadLocal 接口
- public class ThreadLocal {
- public Object get();
- public void set(Object newValue);
- public Object initialValue();
- }
get() 访问器检索变量的当前线程的值;set() 访问器修改当前线程的值。initialValue() 方法是可选的,如果线程未使用过某个变量,那么您可以用这个方法来设置这个变量的初始值;它允许延迟初始化。用一个示例实现来说明 ThreadLocal 的工作方式是最好的方法。清单 2 显示了 ThreadLocal 的一个实现方式。它不是一个特别好的实现(虽然它与最初实现非常相似),所以很可能性能不佳,但它清楚地说明了 ThreadLocal 的工作方式。
清单 2. ThreadLocal 的糟糕实现
- public class ThreadLocal {
- private Map values = Collections.synchronizedMap(new HashMap());
- public Object get() {
- Thread curThread = Thread.currentThread();
- Object o = values.get(curThread);
- if (o == null && !values.containsKey(curThread)) {
- o = initialValue();
- values.put(curThread, o);
- }
- return o;
- }
- public void set(Object newValue) {
- values.put(Thread.currentThread(), newValue);
- }
- public Object initialValue() {
- return null;
- }
- }
这个实现的性能不会很好,因为每个 get() 和 set() 操作都需要 values 映射表上的同步,而且如果多个线程同时访问同一个 ThreadLocal,那么将发生争用。此外,这个实现也是不切实际的,因为用 Thread 对象做 values 映射表中的关键字将导致无法在线程退出后对 Thread 进行垃圾回收,而且也无法对死线程的 ThreadLocal 的特定于线程的值进行垃圾回收。
用 ThreadLocal 实现每线程 Singleton
线程局部变量常被用来描绘有状态“单子”(Singleton) 或线程安全的共享对象,或者是通过把不安全的整个变量封装进 ThreadLocal,或者是通过把对象的特定于线程的状态封装进 ThreadLocal。例如,在与数据库有紧密联系的应用程序中,程序的很多方法可能都需要访问数据库。在系统的每个方法中都包含一个 Connection 作为参数是不方便的 ― 用“单子”来访问连接可能是一个虽然更粗糙,但却方便得多的技术。然而,多个线程不能安全地共享一个 JDBC Connection。如清单 3 所示,通过使用“单子”中的 ThreadLocal,我们就能让我们的程序中的任何类容易地获取每线程 Connection 的一个引用。这样,我们可以认为 ThreadLocal 允许我们创建每线程单子。
清单 3. 把一个 JDBC 连接存储到一个每线程 Singleton 中
- public class ConnectionDispenser {
- private static class ThreadLocalConnection extends ThreadLocal {
- public Object initialValue() {
- return DriverManager.getConnection(ConfigurationSingleton.getDbUrl());
- }
- }
- private ThreadLocalConnection conn = new ThreadLocalConnection();
- public static Connection getConnection() {
- return (Connection) conn.get();
- }
- }
任何创建的花费比使用的花费相对昂贵些的有状态或非线程安全的对象,例如 JDBC Connection 或正则表达式匹配器,都是可以使用每线程单子(singleton)技术的好地方。当然,在类似这样的地方,您可以使用其它技术,例如用池,来安全地管理共享访问。然而,从可伸缩性角度看,即使是用池也存在一些潜在缺陷。因为池实现必须使用同步,以维护池数据结构的完整性,如果所有线程使用同一个池,那么在有很多线程频繁地对池进行访问的系统中,程序性能将因争用而降低。
用 ThreadLocal 简化调试日志纪录
其它适合使用 ThreadLocal 但用池却不能成为很好的替代技术的应用程序包括存储或累积每线程上下文信息以备稍后检索之用这样的应用程序。例如,假设您想创建一个用于管理多线程应用程序调试信息的工具。您可以用如清单 4 所示的 DebugLogger 类作为线程局部容器来累积调试信息。在一个工作单元的开头,您清空容器,而当一个错误出现时,您查询该容器以检索这个工作单元迄今为止生成的所有调试信息。
清单 4. 用 ThreadLocal 管理每线程调试日志
- public class DebugLogger {
- private static class ThreadLocalList extends ThreadLocal {
- public Object initialValue() {
- return new ArrayList();
- }
- public List getList() {
- return (List) super.get();
- }
- }
- private ThreadLocalList list = new ThreadLocalList();
- private static String[] stringArray = new String[0];
- public void clear() {
- list.getList().clear();
- }
- public void put(String text) {
- list.getList().add(text);
- }
- public String[] get() {
- return list.getList().toArray(stringArray);
- }
- }
在您的代码中,您可以调用 DebugLogger.put() 来保存您的程序正在做什么的信息,而且,稍后如果有必要(例如发生了一个错误),您能够容易地检索与某个特定线程相关的调试信息。 与简单地把所有信息转储到一个日志文件,然后努力找出哪个日志记录来自哪个线程(还要担心线程争用日志纪录对象)相比,这种技术简便得多,也有效得多。
ThreadLocal 在基于 servlet 的应用程序或工作单元是一个整体请求的任何多线程应用程序服务器中也是很有用的,因为在处理请求的整个过程中将要用到单个线程。您可以通过前面讲述的每线程单子技术用 ThreadLocal 变量来存储各种每请求(per-request)上下文信息。
ThreadLocal 的线程安全性稍差的堂兄弟,InheritableThreadLocal
ThreadLocal 类有一个亲戚,InheritableThreadLocal,它以相似的方式工作,但适用于种类完全不同的应用程序。创建一个线程时如果保存了所有 InheritableThreadLocal 对象的值,那么这些值也将自动传递给子线程。如果一个子线程调用 InheritableThreadLocal 的 get(),那么它将与它的父线程看到同一个对象。为保护线程安全性,您应该只对不可变对象(一旦创建,其状态就永远不会被改变的对象)使用 InheritableThreadLocal,因为对象被多个线程共享。InheritableThreadLocal 很合适用于把数据从父线程传到子线程,例如用户标识(user id)或事务标识(transaction id),但不能是有状态对象,例如 JDBC Connection。
ThreadLocal 的性能
虽然线程局部变量早已赫赫有名并被包括 Posix pthreads 规范在内的很多线程框架支持,但最初的 Java 线程设计中却省略了它,只是在 Java 平台的版本 1.2 中才添加上去。在很多方面,ThreadLocal 仍在发展之中;在版本 1.3 中它被重写,版本 1.4 中又重写了一次,两次都专门是为了性能问题。
在 JDK 1.2 中,ThreadLocal 的实现方式与清单 2 中的方式非常相似,除了用同步 WeakHashMap 代替 HashMap 来存储 values 之外。(以一些额外的性能开销为代价,使用 WeakHashMap 解决了无法对 Thread 对象进行垃圾回收的问题。)不用说,ThreadLocal 的性能是相当差的。
Java 平台版本 1.3 提供的 ThreadLocal 版本已经尽量更好了;它不使用任何同步,从而不存在可伸缩性问题,而且它也不使用弱引用。相反地,人们通过给 Thread 添加一个实例变量(该变量用于保存当前线程的从线程局部变量到它的值的映射的 HashMap)来修改 Thread 类以支持 ThreadLocal。因为检索或设置一个线程局部变量的过程不涉及对可能被另一个线程读写的数据的读写操作,所以您可以不用任何同步就实现 ThreadLocal.get() 和 set()。而且,因为每线程值的引用被存储在自已的 Thread 对象中,所以当对 Thread 进行垃圾回收时,也能对该 Thread 的每线程值进行垃圾回收。
不幸的是,即使有了这些改进,Java 1.3 中的 ThreadLocal 的性能仍然出奇地慢。据我的粗略测量,在双处理器 Linux 系统上的 Sun 1.3 JDK 中进行 ThreadLocal.get() 操作,所耗费的时间大约是无争用同步的两倍。性能这么差的原因是 Thread.currentThread() 方法的花费非常大,占了 ThreadLocal.get() 运行时间的三分之二还多。虽然有这些缺点,JDK 1.3 ThreadLocal.get() 仍然比争用同步快得多,所以如果在任何存在严重争用的地方(可能是有非常多的线程,或者同步块被频繁地执行,或者同步块很大),ThreadLocal 可能仍然要高效得多。
在 Java 平台的最新版本,即版本 1.4b2 中,ThreadLocal 和 Thread.currentThread() 的性能都有了很大提高。有了这些提高,ThreadLocal 应该比其它技术,如用池,更快。由于它比其它技术更简单,也更不易出错,人们最终将发现它是避免线程间出现不希望的交互的有效途径。
ThreadLocal 的好处
ThreadLocal 能带来很多好处。它常常是把有状态类描绘成线程安全的,或者封装非线程安全类以使它们能够在多线程环境中安全地使用的最容易的方式。使用 ThreadLocal 使我们可以绕过为实现线程安全而对何时需要同步进行判断的复杂过程,而且因为它不需要任何同步,所以也改善了可伸缩性。除简单之外,用 ThreadLocal 存储每线程单子或每线程上下文信息在归档方面还有一个颇有价值好处 ― 通过使用 ThreadLocal,存储在 ThreadLocal 中的对象都是不被线程共享的是清晰的,从而简化了判断一个类是否线程安全的工作。
我希望您从这个系列中得到了乐趣,也学到了知识,我也鼓励您到我的讨论论坛中来深入研究多线程问题。
Java多线程学习笔记
一、线程类
Java是通过Java.lang.Thread类来实现多线程的,第个Thread对象描述了一个单独的线程。要产生一个线程,有两种方法:
1、需要从Java.lang.Thread类继承一个新的线程类,重载它的run()方法;
2、通过Runnalbe接口实现一个从非线程类继承来类的多线程,重载Runnalbe接口的run()方法。运行一个新的线程,只需要调用它的start()方法即可。如:
- /**=====================================================================
- * 文件:ThreadDemo_01.java
- * 描述:产生一个新的线程
- * ======================================================================
- */
- class ThreadDemo extends Thread{
- Threads()
- {
- }
- Threads(String szName)
- {
- super(szName);
- }
- // 重载run函数
- public void run()
- {
- for (int count = 1,row = 1; row < 20; row++,count++)
- {
- for (int i = 0; i < count; i++)
- {
- System.out.print('*');
- }
- System.out.println();
- }
- }
- }
- class ThreadMain{
- public static void main(String argv[]){
- ThreadDemo th = new ThreadDemo();
- // 调用start()方法执行一个新的线程
- th.start();
- }
- }
线程类的一些常用方法:
sleep(): 强迫一个线程睡眠N毫秒。
isAlive(): 判断一个线程是否存活。
join(): 等待线程终止。
activeCount(): 程序中活跃的线程数。
enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
setName(): 为线程设置一个名称。
wait(): 强迫一个线程等待。
notify(): 通知一个线程继续运行。
setPriority(): 设置一个线程的优先级。
二、等待一个线程的结束
有些时候我们需要等待一个线程终止后再运行我们的另一个线程,这时我们应该怎么办呢?请看下面的例子:
- /**=====================================================================
- * 文件:ThreadDemo_02.java
- * 描述:等待一个线程的结束
- * ======================================================================
- */
- class ThreadDemo extends Thread{
- Threads()
- {
- }
- Threads(String szName)
- {
- super(szName);
- }
- // 重载run函数
- public void run()
- {
- for (int count = 1,row = 1; row < 20; row++,count++)
- {
- for (int i = 0; i < count; i++)
- {
- System.out.print('*');
- }
- System.out.println();
- }
- }
- }
- class ThreadMain{
- public static void main(String argv[]){
- //产生两个同样的线程
- ThreadDemo th1 = new ThreadDemo();
- ThreadDemo th2 = new ThreadDemo();
- // 我们的目的是先运行第一个线程,再运行第二个线程
- th1.start();
- th2.start();
- }
- }
这里我们的目标是要先运行第一个线程,等第一个线程终止后再运行第二个线程,而实际运行的结果是如何的呢?实际上我们运行的结果并不是两个我们想要的直角三角形,而是一些乱七八糟的*号行,有的长,有的短。为什么会这样呢?因为线程并没有按照我们的调用顺序来执行,而是产生了线程赛跑现象。实际上Java并不能按我们的调用顺序来执行线程,这也说明了线程是并行执行的单独代码。如果要想得到我们预期的结果,这里我们就需要判断第一个线程是否已经终止,如果已经终止,再来调用第二个线程。代码如下:
- /**=====================================================================
- * 文件:ThreadDemo_03.java
- * 描述:等待一个线程的结束的两种方法
- * ======================================================================
- */
- class ThreadDemo extends Thread{
- Threads()
- {
- }
- Threads(String szName)
- {
- super(szName);
- }
- // 重载run函数
- public void run()
- {
- for (int count = 1,row = 1; row < 20; row++,count++)
- {
- for (int i = 0; i < count; i++)
- {
- System.out.print('*');
- }
- System.out.println();
- }
- }
- }
- class ThreadMain{
- public static void main(String argv[]){
- ThreadMain test = new ThreadMain();
- test.Method1();
- // test.Method2();
- }
- // 第一种方法:不断查询第一个线程是否已经终止,如果没有,则让主线程睡眠一直到它终止为止
- // 即:while/isAlive/sleep
- public void Method1(){
- ThreadDemo th1 = new ThreadDemo();
- ThreadDemo th2 = new ThreadDemo();
- // 执行第一个线程
- th1.start();
- // 不断查询第一个线程的状态
- while(th1.isAlive()){
- try{
- Thread.sleep(100);
- }catch(InterruptedException e){
- }
- }
- //第一个线程终止,运行第二个线程
- th2.start();
- }
- // 第二种方法:join()
- public void Method2(){
- ThreadDemo th1 = new ThreadDemo();
- ThreadDemo th2 = new ThreadDemo();
- // 执行第一个线程
- th1.start();
- try{
- th1.join();
- }catch(InterruptedException e){
- }
- // 执行第二个线程
- th2.start();
- }
三、线程的同步问题
有些时候,我们需要很多个线程共享一段代码,比如一个私有成员或一个类中的静态成员,但是由于线程赛跑的问题,所以我们得到的常常不是正确的输出结果,而相反常常是张冠李戴,与我们预期的结果大不一样。看下面的例子:
- /**=============================================================================
- * 文件:ThreadDemo_04.java
- * 描述:多线程不同步的原因
- * =============================================================================
- */
- // 共享一个静态数据对象
- class ShareData{
- public static String szData = "";
- }
- class ThreadDemo extends Thread{
- private ShareData oShare;
- ThreadDemo(){
- }
- ThreadDemo(String szName,ShareData oShare){
- super(szName);
- this.oShare = oShare;
- }
- public void run(){
- // 为了更清楚地看到不正确的结果,这里放一个大的循环
- for (int i = 0; i < 50; i++){
- if (this.getName().equals("Thread1")){
- oShare.szData = "这是第 1 个线程";
- // 为了演示产生的问题,这里设置一次睡眠
- try{
- Thread.sleep((int)Math.random() * 100);
- catch(InterruptedException e){
- }
- // 输出结果
- System.out.println(this.getName() + ":" + oShare.szData);
- }else if (this.getName().equals("Thread2")){
- oShare.szData = "这是第 1 个线程";
- // 为了演示产生的问题,这里设置一次睡眠
- try{
- Thread.sleep((int)Math.random() * 100);
- catch(InterruptedException e){
- }
- // 输出结果
- System.out.println(this.getName() + ":" + oShare.szData);
- }
- }
- }
- class ThreadMain{
- public static void main(String argv[]){
- ShareData oShare = new ShareData();
- ThreadDemo th1 = new ThreadDemo("Thread1",oShare);
- ThreadDemo th2 = new ThreadDemo("Thread2",oShare);
- th1.start();
- th2.start();
- }
- }
由于线程的赛跑问题,所以输出的结果往往是Thread1对应“这是第 2 个线程”,这样与我们要输出的结果是不同的。为了解决这种问题(错误),Java为我们提供了“锁”的机制来实现线程的同步。锁的机制要求每个线程在进入共享代码之前都要取得锁,否则不能进入,而退出共享代码之前则释放该锁,这样就防止了几个或多个线程竞争共享代码的情况,从而解决了线程的不同步的问题。可以这样说,在运行共享代码时则是最多只有一个线程进入,也就是和我们说的垄断。锁机制的实现方法,则是在共享代码之前加入synchronized段,把共享代码包含在synchronized段中。上述问题的解决方法为:
- /**=============================================================================
- * 文件:ThreadDemo_05.java
- * 描述:多线程不同步的解决方法--锁
- * =============================================================================
- */
- // 共享一个静态数据对象
- class ShareData{
- public static String szData = "";
- }
- class ThreadDemo extends Thread{
- private ShareData oShare;
- ThreadDemo(){
- }
- ThreadDemo(String szName,ShareData oShare){
- super(szName);
- this.oShare = oShare;
- }
- public void run(){
- // 为了更清楚地看到不正确的结果,这里放一个大的循环
- for (int i = 0; i < 50; i++){
- if (this.getName().equals("Thread1")){
- // 锁定oShare共享对象
- synchronized (oShare){
- oShare.szData = "这是第 1 个线程";
- // 为了演示产生的问题,这里设置一次睡眠
- try{
- Thread.sleep((int)Math.random() * 100);
- catch(InterruptedException e){
- }
- // 输出结果
- System.out.println(this.getName() + ":" + oShare.szData);
- }
- }else if (this.getName().equals("Thread2")){
- // 锁定共享对象
- synchronized (oShare){
- oShare.szData = "这是第 1 个线程";
- // 为了演示产生的问题,这里设置一次睡眠
- try{
- Thread.sleep((int)Math.random() * 100);
- catch(InterruptedException e){
- }
- // 输出结果
- System.out.println(this.getName() + ":" + oShare.szData);
- }
- }
- }
- }
- class ThreadMain{
- public static void main(String argv[]){
- ShareData oShare = new ShareData();
- ThreadDemo th1 = new ThreadDemo("Thread1",oShare);
- ThreadDemo th2 = new ThreadDemo("Thread2",oShare);
- th1.start();
- th2.start();
- }
- }
由于过多的synchronized段将会影响程序的运行效率,因此引入了同步方法,同步方法的实现则是将共享代码单独写在一个方法里,在方法前加上synchronized关键字即可。
在线程同步时的两个需要注意的问题:
1、无同步问题:即由于两个或多个线程在进入共享代码前,得到了不同的锁而都进入共享代码而造成。
2、死锁问题:即由于两个或多个线程都无法得到相应的锁而造成的两个线程都等待的现象。这种现象主要是因为相互嵌套的synchronized代码段而造成,因此,在程序中尽可能少用嵌套的synchronized代码段是防止线程死锁的好方法。
在写上面的代码遇到的一个没能解决的问题,在这里拿出来,希望大家讨论是什么原因。
- /**=============================================================================
- * 文件:ThreadDemo_06.java
- * 描述:为什么造成线程的不同步。
- * =============================================================================
- */
- class ThreadDemo extends Thread{
- //共享一个静态数据成员
- private static String szShareData = "";
- ThreadDemo(){
- }
- ThreadDemo(String szName){
- super(szName);
- }
- public void run(){
- // 为了更清楚地看到不正确的结果,这里放一个大的循环
- for (int i = 0; i < 50; i++){
- if (this.getName().equals("Thread1")){
- synchronized(szShareData){
- szShareData = "这是第 1 个线程";
- // 为了演示产生的问题,这里设置一次睡眠
- try{
- Thread.sleep((int)Math.random() * 100);
- catch(InterruptedException e){
- }
- // 输出结果
- System.out.println(this.getName() + ":" + szShareData);
- }
- }else if (this.getName().equals("Thread2")){
- synchronized(szShareData){
- szShareData = "这是第 1 个线程";
- // 为了演示产生的问题,这里设置一次睡眠
- try{
- Thread.sleep((int)Math.random() * 100);
- catch(InterruptedException e){
- }
- // 输出结果
- System.out.println(this.getName() + ":" + szShareData);
- }
- }
- }
- }
- class ThreadMain{
- public static void main(String argv[]){
- ThreadDemo th1 = new ThreadDemo("Thread1");
- ThreadDemo th2 = new ThreadDemo("Thread2");
- th1.start();
- th2.start();
- }
- }
这段代码的共享成员是一个类中的静态成员,按理说,这里进入共享代码时得到的锁应该是同样的锁,而实际上以上程序的输入却是不同步的,为什么呢??
Java多线程学习笔记(二)
四、Java的等待通知机制
在有些时候,我们需要在几个或多个线程中按照一定的秩序来共享一定的资源。例如生产者--消费者的关系,在这一对关系中实际情况总是先有生产者生产了产品后,消费者才有可能消费;又如在父--子关系中,总是先有父亲,然后才能有儿子。然而在没有引入等待通知机制前,我们得到的情况却常常是错误的。这里我引入《用线程获得强大的功能》一文中的生产者--消费者的例子:
- /* ==================================================================================
- * 文件:ThreadDemo07.java
- * 描述:生产者--消费者
- * 注:其中的一些注释是我根据自己的理解加注的
- * ==================================================================================
- */
- // 共享的数据对象
- class ShareData{
- private char c;
- public void setShareChar(char c){
- this.c = c;
- }
- public char getShareChar(){
- return this.c;
- }
- }
- // 生产者线程
- class Producer extends Thread{
- private ShareData s;
- Producer(ShareData s){
- this.s = s;
- }
- public void run(){
- for (char ch = 'A'; ch <= 'Z'; ch++){
- try{
- Thread.sleep((int)Math.random() * 4000);
- }catch(InterruptedException e){}
- // 生产
- s.setShareChar(ch);
- System.out.println(ch + " producer by producer.");
- }
- }
- }
- // 消费者线程
- class Consumer extends Thread{
- private ShareData s;
- Consumer(ShareData s){
- this.s = s;
- }
- public void run(){
- char ch;
- do{
- try{
- Thread.sleep((int)Math.random() * 4000);
- }catch(InterruptedException e){}
- // 消费
- ch = s.getShareChar();
- System.out.println(ch + " consumer by consumer.");
- }while(ch != 'Z');
- }
- }
- class Test{
- public static void main(String argv[]){
- ShareData s = new ShareData();
- new Consumer(s).start();
- new Producer(s).start();
- }
- }
在以上的程序中,模拟了生产者和消费者的关系,生产者在一个循环中不断生产了从A-Z的共享数据,而消费者则不断地消费生产者生产的A-Z的共享数据。我们开始已经说过,在这一对关系中,必须先有生产者生产,才能有消费者消费。但如果运行我们上面这个程序,结果却出现了在生产者没有生产之前,消费都就已经开始消费了或者是生产者生产了却未能被消费者消费这种反常现象。为了解决这一问题,引入了等待通知(wait/notify)机制如下:
1、在生产者没有生产之前,通知消费者等待;在生产者生产之后,马上通知消费者消费。
2、在消费者消费了之后,通知生产者已经消费完,需要生产。
下面修改以上的例子(源自《用线程获得强大的功能》一文):
- /* ==================================================================================
- * 文件:ThreadDemo08.java
- * 描述:生产者--消费者
- * 注:其中的一些注释是我根据自己的理解加注的
- * ==================================================================================
- */
- class ShareData{
- private char c;
- // 通知变量
- private boolean writeable = true;
- // -------------------------------------------------------------------------
- // 需要注意的是:在调用wait()方法时,需要把它放到一个同步段里,否则将会出现
- // "java.lang.IllegalMonitorStateException: current thread not owner"的异常。
- // -------------------------------------------------------------------------
- public synchronized void setShareChar(char c){
- if (!writeable){
- try{
- // 未消费等待
- wait();
- }catch(InterruptedException e){}
- }
- this.c = c;
- // 标记已经生产
- writeable = false;
- // 通知消费者已经生产,可以消费
- notify();
- }
- public synchronized char getShareChar(){
- if (writeable){
- try{
- // 未生产等待
- wait();
- }catch(InterruptedException e){}
- }
- // 标记已经消费
- writeable = true;
- // 通知需要生产
- notify();
- return this.c;
- }
- }
- // 生产者线程
- class Producer extends Thread{
- private ShareData s;
- Producer(ShareData s){
- this.s = s;
- }
- public void run(){
- for (char ch = 'A'; ch <= 'Z'; ch++){
- try{
- Thread.sleep((int)Math.random() * 400);
- }catch(InterruptedException e){}
- s.setShareChar(ch);
- System.out.println(ch + " producer by producer.");
- }
- }
- }
- // 消费者线程
- class Consumer extends Thread{
- private ShareData s;
- Consumer(ShareData s){
- this.s = s;
- }
- public void run(){
- char ch;
- do{
- try{
- Thread.sleep((int)Math.random() * 400);
- }catch(InterruptedException e){}
- ch = s.getShareChar();
- System.out.println(ch + " consumer by consumer.**");
- }while (ch != 'Z');
- }
- }
- class Test{
- public static void main(String argv[]){
- ShareData s = new ShareData();
- new Consumer(s).start();
- new Producer(s).start();
- }
- }
在以上程序中,设置了一个通知变量,每次在生产者生产和消费者消费之前,都测试通知变量,检查是否可以生产或消费。最开始设置通知变量为true,表示还未生产,在这时候,消费者需要消费,于时修改了通知变量,调用notify()发出通知。这时由于生产者得到通知,生产出第一个产品,修改通知变量,向消费者发出通知。这时如果生产者想要继续生产,但因为检测到通知变量为false,得知消费者还没有生产,所以调用wait()进入等待状态。因此,最后的结果,是生产者每生产一个,就通知消费者消费一个;消费者每消费一个,就通知生产者生产一个,所以不会出现未生产就消费或生产过剩的情况。
五、线程的中断
在很多时候,我们需要在一个线程中调控另一个线程,这时我们就要用到线程的中断。用最简单的话也许可以说它就相当于播放机中的暂停一样,当第一次按下暂停时,播放器停止播放,再一次按下暂停时,继续从刚才暂停的地方开始重新播放。而在Java中,这个暂停按钮就是Interrupt()方法。在第一次调用interrupt()方法时,线程中断;当再一次调用interrupt()方法时,线程继续运行直到终止。这里依然引用《用线程获得强大功能》一文中的程序片断,但为了更方便看到中断的过程,我在原程序的基础上作了些改进,程序如下:
- /* ===================================================================================
- * 文件:ThreadDemo09.java
- * 描述:线程的中断
- * ===================================================================================
- */
- class ThreadA extends Thread{
- private Thread thdOther;
- ThreadA(Thread thdOther){
- this.thdOther = thdOther;
- }
- public void run(){
- System.out.println(getName() + " 运行...");
- int sleepTime = (int)(Math.random() * 10000);
- System.out.println(getName() + " 睡眠 " + sleepTime
- + " 毫秒。");
- try{
- Thread.sleep(sleepTime);
- }catch(InterruptedException e){}
- System.out.println(getName() + " 觉醒,即将中断线程 B。");
- // 中断线程B,线程B暂停运行
- thdOther.interrupt();
- }
- }
- class ThreadB extends Thread{
- int count = 0;
- public void run(){
- System.out.println(getName() + " 运行...");
- while (!this.isInterrupted()){
- System.out.println(getName() + " 运行中 " + count++);
- try{
- Thread.sleep(10);
- }catch(InterruptedException e){
- int sleepTime = (int)(Math.random() * 10000);
- System.out.println(getName() + " 睡眠" + sleepTime
- + " 毫秒。觉醒后立即运行直到终止。");
- try{
- Thread.sleep(sleepTime);
- }catch(InterruptedException m){}
- System.out.println(getName() + " 已经觉醒,运行终止...");
- // 重新设置标记,继续运行
- this.interrupt();
- }
- }
- System.out.println(getName() + " 终止。");
- }
- }
- class Test{
- public static void main(String argv[]){
- ThreadB thdb = new ThreadB();
- thdb.setName("ThreadB");
- ThreadA thda = new ThreadA(thdb);
- thda.setName("ThreadA");
- thdb.start();
- thda.start();
- }
- }
- 运行以上程序,你可以清楚地看到中断的过程。首先线程B开始运行,接着运行线程A,在线程A睡眠一段时间觉醒后,调用interrupt()方法中断线程B,此是可能B正在睡眠,觉醒后掏出一个InterruptedException异常,执行其中的语句,为了更清楚地看到线程的中断恢复,我在InterruptedException异常后增加了一次睡眠,当睡眠结束后,线程B调用自身的interrupt()方法恢复中断,这时测试isInterrupt()返回true,线程退出。
- 线程和进程(Threads and Processes)
- 第一个关键的系统级概念,究竟什么是线程或者说究竟什么是进程?她们其实就是操作系统内部的一种数据结构。
- 进程数据结构掌握着所有与内存相关的东西:全局地址空间、文件句柄等等诸如此类的东西。当一个进程放弃执行(准确的说是放弃占有CPU),而被操作系统交换到硬盘上,使别的进程有机会运行的时候,在那个进程里的所有数据也将被写到硬盘上,甚至包括整个系统的核心(core memory)。可以这么说,当你想到进程(process),就应该想到内存(memory) (进程 == 内存)。如上所述,切换进程的代价非常大,总有那么一大堆的内存要移来移去。你必须用秒这个单位来计量进程切换(上下文切换),对于用户来说秒意味着明显的等待和硬盘灯的狂闪(对于作者的我,就意味着IBM龙腾3代的烂掉,5555555)。言归正传,对于Java而言,JVM就几乎相当于一个进程(process),因为只有进程才能拥有堆内存(heap,也就是我们平时用new操作符,分出来的内存空间)。
- 那么线程是什么呢?你可以把它看成“一段代码的执行”---- 也就是一系列由JVM执行的二进制指令。这里面没有对象(Object)甚至没有方法(Method)的概念。指令执行的序列可以重叠,并且并行的执行。后面,我会更加详细的论述这个问题。但是请记住,线程是有序的指令,而不是方法(method)。
- 线程的数据结构,与进程相反,仅仅只包括执行这些指令的信息。它包含当前的运行上下文(context):如寄存器(register)的内容、当前指令的在运行引擎的指令流中的位置、保存方法(methods)本地参数和变量的运行时堆栈。如果发生线程切换,OS只需把寄存器的值压进栈,然后把线程包含的数据结构放到某个类是列表(LIST)的地方;把另一个线程的数据从列表中取出,并且用栈里的值重新设置寄存器。切换线程更加有效率,时间单位是毫秒。对于Java而言,一个线程可以看作是JVM的一个状态。
- 运行时堆栈(也就是前面说的存储本地变量和参数的地方)是线程数据结构一部分。这是因为多个线程,每一个都有自己的运行时堆栈,也就是说存储在这里面的数据是绝对线程安全(后面将会详细解释这个概念)的。因为可以肯定一个线程是无法修改另一个线程的系统级的数据结构的。也可以这么说一个不访问堆内存的(只读写堆栈内存)方法,是线程安全的(Thread Safe)。
- 线程安全和同步
- 线程安全,是指一个方法(method)可以在多线程的环境下安全的有效的访问进程级的数据(这些数据是与其他线程共享的)。事实上,线程安全是个很难达到的目标。
- 线程安全的核心概念就是同步,它保证多个线程:
- 同时开始执行,并行运行
- 不同时访问相同的对象实例
- 不同时执行同一段代码
- 我将会在后面的章节,一一细诉这些问题。但现在还是让我们来看看同步的一种经典的
- 实现方法——信号量。信号量是任何可以让两个线程为了同步它们的操作而相互通信的对象。Java也是通过信号量来实现线程间通信的。
- 不要被微软的文档所暗示的信号量仅仅是Dijksta提出的计数型信号量所迷惑。信号量其实包含任何可以用来同步的对象。
- 如果没有synchronized关键字,就无法用JAVA实现信号量,但是仅仅只依靠它也不足够。我将会在后面为大家演示一种用Java实现的信号量。
- 同步的代价很高哟!
- 同步(或者说信号量,随你喜欢啦)的一个很让人头痛的问题就是代价。考虑一下,下面的代码:
- Listing 1.2:
- import java.util.*;
- import java.text.NumberFormat;
- class Synch
- {
- private static long[ ] locking_time = new long[100];
- private static long[ ] not_locking_time = new long[100];
- private static final long ITERATIONS = 10000000;
- synchronized long locking (long a, long b){return a + b;}
- long not_locking (long a, long b){return a + b;}
- private void test( int id )
- {
- long start = System.currentTimeMillis();
- for(long i = ITERATIONS; --i >= 0 ;)
- { locking(i,i); }
- locking_time[id] = System.currentTimeMillis() - start;
- start = System.currentTimeMillis();
- for(long i = ITERATIONS; --i >= 0 ;)
- { not_locking(i,i); }
- not_locking_time[id] = System.currentTimeMillis() - start;
- }
- static void print_results( int id )
- { NumberFormat compositor = NumberFormat.getInstance();
- compositor.setMaximumFractionDigits( 2 );
- double time_in_synchronization = locking_time[id] - not_locking_time[id];
- System.out.println( "Pass " + id + ": Time lost: "
- + compositor.format( time_in_synchronization )
- + " ms. "
- + compositor.format( ((double)locking_time[id]/not_locking_time[id])*100.0 )
- + "% increase" );
- }
- static public void main(String[ ] args) throws InterruptedException
- {
- final Synch tester = new Synch();
- tester.test(0); print_results(0);
- tester.test(1); print_results(1);
- tester.test(2); print_results(2);
- tester.test(3); print_results(3);
- tester.test(4); print_results(4);
- tester.test(5); print_results(5);
- tester.test(6); print_results(6);
- final Object start_gate = new Object();
- Thread t1 = new Thread()
- { public void run()
- { try{ synchronized(start_gate) { start_gate.wait(); } }
- catch( InterruptedException e ){}
- tester.test(7);
- }
- };
- Thread t2 = new Thread()
- { public void run()
- { try{ synchronized(start_gate) { start_gate.wait(); } }
- catch( InterruptedException e ){}
- tester.test(8);
- }
- };
- Thread.currentThread().setPriority( Thread.MIN_PRIORITY );
- t1.start();
- t2.start();
- synchronized(start_gate){ start_gate.notifyAll(); }
- t1.join();
- t2.join();
- print_results( 7 );
- print_results( 8 );
- }
- }
这是一个简单的基准测试程序,她清楚的向大家揭示了同步的代价是多么的大。test(…)方法调用2个方法1,000,000,0次。其中一个是同步的,另一个则否。下面是在我的机器上输出的结果(CPU: P4 2.4G(B); Memory: 1GB; OS: windows 2000 server(sp3); JDK: Ver1.4.01 and HotSpot 1.4.01-b01):
- C:\>java -verbose:gc Synch
- Pass 0: Time lost: 251 ms. 727.5% increase
- Pass 1: Time lost: 250 ms. 725% increase
- Pass 2: Time lost: 251 ms. 602% increase
- Pass 3: Time lost: 250 ms. 725% increase
- Pass 4: Time lost: 261 ms. 752.5% increase
- Pass 5: Time lost: 260 ms. 750% increase
- Pass 6: Time lost: 261 ms. 752.5% increase
- Pass 7: Time lost: 1,953 ms. 1,248.82% increase
- Pass 8: Time lost: 3,475 ms. 8,787.5% increase
这里为了使HotSpot JVM充分的发挥其威力,test( )方法被多次反复调用。一旦这段程序被彻底优化以后,也就是大约在Pass 6时,同步的代价达到最大。Pass 7 和Pass 8与前面的区别在于,我new了两个新的线程来并行执行test方法,两个线程竞争执行(后面是适当的地方,我会解释什么是“竞争”,如果你已经等不及了,买本大学的操作系统课本看看吧! J),这使结果更加接近真实。同步的代价是如此之高的,应该尽量避免无谓的同步代价。
现在是时候我们更深入的讨论一下同步的代价了。HotSpot JVM一般会使用一到两个方法来实现同步,这主要取决于是否存在线程的竞争。当没有竞争的时候,计算机的汇编指令顺序的执行,这些指令的执行是不被打断。指令试图测试一个比特(bit),然后设置各种二进制位来表示测试的结果,如果这个bit没有被设置,指令就设置它。这可以说是非常原始的信号量,因为当两个线程同步的企图设置一个bit的值时,只有一个线程可以成功,两个线程都会检查结果,看看是不是自己设成功了。
如果bit已经被设置(这里说的是有线程竞争的情况下),失败的JVM(线程)不得不离开操作系统的核心进程等待这个bit位被清零。这样来回的在系统核心中切换是非常耗时的。在NT系统下,需要600次机械指令循环来进入一次系统内核,这还仅仅是进入所耗费的时间还不包括做操作的时间。
是不是觉得很无聊了,呵呵!今天似乎都是些不顶用的东西。但这是必须的,为了使你能够读懂后面的内容。下一篇,我将会谈到一些更有趣的话题,例如如何避免同步,如果大家不反对,我还想讲一些设计模式的东西。下回见!
避免同步
大部分显示的同步都可以避免。一般不操作对象状态信息(例如数据成员)的方法都不需要同步,例如:一些方法只访问本地变量(也就是说在方法内部声明的变量),而不操作类级别的数据成员,并且这些方法不会通过传入的引用参数来修改外部的对象。符合这些条件的方法都不需要使用synchronization这种重量级的操作。除此之外,还可以使用一些设计模式(Design Pattern)来避免同步(我将会在后面提到)。
你甚至可以通过适当的组织你的代码来避免同步。相对于同步的一个重要的概念就是原子性。一个原子性的操作事不能被其他线程中断的,通常的原子性操作是不需要同步的。
Java定义一些原子性的操作。一般的给变量付值的操作是原子的,除了long和double。看下面的代码:
- class Unreliable
- {
- private long x;
- public long get_x( ) {return x;}
- public void set_x(long value) { x = value; }
- }
线程1调用:
obj.set_x( 0 );
线程2调用:
obj.set_x( 0x123456789abcdef )
问题在于下面这行代码:
x = value;
JVM为了效率的问题,并没有把x当作一个64位的长整型数来使用,而是把它分为两个32-bit,分别付值:
- x.hgh_word = value.high_word;
- x.low_word = value.low_word;
因此,存在一个线程设置了高位之后被另一个线程切换出去,而改变了其高位或低位的值。所以,x的值最终可能为0x0123456789abcdef、0x01234567000000、0x00000000abcdef和0x00000000000000。你根本无法确定它的值,唯一的解决方法是,为set_x( )和get_x()方法加上synchronized这个关键字或者把这个付值操作封装在一个确保原子性的代码段里。
所以,在操作的long型数据的时候,千万不要想当然。强迫自己记住吧:只有直接付值操作是原子的(除了上面的例子)。其它,任何表达式,象x = ++y、x += y都是不安全的,不管x或y的数据类型是否是小于64位的。很可能在付值之前,自增之后,被其它线程抢先了(preempted)。
竞争条件
在术语中,对于前面我提到的多线程问题——两个线程同步操作同一个对象,使这个对象的最终状态不明——叫做竞争条件。竞争条件可以在任何应该由程序员保证原子操作的,而又忘记使用synchronized的地方。在这个意义上,可以把synchronized看作一种保证复杂的、顺序一定的操作具有原子性的工具,例如给一个boolean值变量付值,就是一个隐式的同步操作。
不变性
一种有效的语言级的避免同步的方法就是不变性(immutability)。一个自从产生那一刻起就无法再改变的对象就是不变性对象,例如一个String对象。但是要注意类似这样的表达式:string1 += string2;本质上等同于string1 = string1 + string2;其实第三个包含string1和string2的string对象被隐式的产生,最后,把string1的引用指向第三个string。这样的操作,并不是原子的。
由于不变对象的值无法发生改变,所以可以为多个线程安全的同步操作,不需要synchronized。
把一个类的所有数据成员都声明为final就可以创建一个不变类型了。那些被声明为final的数据成员并不是必须在声明的时候就写死,但必须在类的构造函数中,全部明确的初始化。例如:
- Class I_am_immutable
- {
- private final int MAX_VALUE = 10;
- priate final int blank_final;
- public I_am_immutable( int_initial_value )
- {
- blank_final = initial_value;
- }
- }
一个由构造函数进行初始化的final型变量叫做blank final。一般的,如果你频繁的只读访问一个对象,把它声明成一个不变对象是个保证同步的好办法,而且可以提高JVM的效率,因为HotSpot会把它放到堆栈里以供使用。
同步封装器(Synchronization Wrappers)
同步还是不同步,是问题的所在。让我们跳出这样的思维模式吧,世事无绝对。有什么办法可以使你的类灵活的在同步与不同步之间切换呢? 有一个非常好的现成例子,就是新近引入JAVA的Collection框架,它是用来取代原本散乱的、繁重的Vector等类型。Vector的任何方法
这里我们做一个完整的例子来说明线程产生的方式不同而生成的线程的区别:
package debug; import java.io.*; import java.lang.Thread; class MyThread extends Thread{ public int x = 0; public void run(){ System.out.println(++x); } } class R implements Runnable{ private int x = 0; public void run(){ System.out.println(++x); } } public class Test { public static void main(String[] args) throws Exception{ for(int i=0;i<10;i++){ Thread t = new MyThread(); t.start(); } Thread.sleep(10000);//让上面的线程运行完成 R r = new R(); for(int i=0;i<10;i++){ Thread t = new Thread(r); t.start(); } } }
上面10个线程对象产生的10个线程运行时打印了10次1。下面10个线程对象产生的10个线程运行时打印了1到10。我们把下面的10个线程称为同一实例(Runnable实例)的多个线程。
下节我们将研究线程对象方法,还是那句话,一般文档中可以读到的内容我不会介绍太多
请大家自己了解。
线程对象的几个重要的方法
尽管线程对象的常用方法可以通过API文档来了解,但是有很多方法仅仅从API说明是无法详细了解的。
本来打算用一节的篇幅来把线程方法中一些重要的知识说完,但这样下来估计要很常的篇幅,可能要用好几节才能说把和线程方法相关的一些重要的知识说完。
首先我们接基础篇(二)来说明start()方法。
一个线程对象生成后,如果要产生一个执行的线程,就一定要调用它的start()方法.在介绍这个方法时不得不同时说明run方法.其实线程对 象的run方法完全是一个接口回调方法,它是你这个线程对象要完成的具体逻辑.简单说你要做什么就你在run中完成,而如何做,什么时候做就不需要你控制 了,你只要调用start()方法,JVM就会管理这个线程对象让它产生一个线程并注册到线程处理系统中。
从表面上看,start()方法调用了run()方法,事实上,start()方法并没有直接调用run方法.在JDK1.5以前 start()方法是本地方法,它如何最终调用run方法已经不是JAVA程序员所能了解的.而在JDK1.5中,原来的那个本地start()方法被 start0()代替,另个一个纯JAVA的start()中调用本地方法start0(),而在start()方法中做了一个验证,就是对一个全局变量 (对象变量)started做检验,如果为true,则start()抛出异常,不会调用本地方法start0(),否则,先将该变量设有true,然后 调用start0()。
从中我们可以看到这个为了控制一个线程对象只能运行成功一次start()方法.这是因为线程的运行要获取当前环境,包括安全,父线程的权限, 优先级等条件,如果一个线程对象可以运行多次,那么定义一个static 的线程在一个环境中获取相应权限和优先级,运行完成后它在另一个环境中利用原来的权限和优先级等属性在当前环境中运行,这样就造成无法预知的结果.简单说 来,让一个线程对象只能成功运行一次,是基于对线程管理的需要。
start()方法最本质的功能是从CPU中申请另一个线程空间来执行 run()方法中的代码,它和当前的线程是两条线,在相对独立的线程空间运行,也就是说,如果你直接调用线程对象的run()方法,当然也会执行,但那是 在当前线程中执行,run()方法执行完成后继续执行下面的代码.而调用start()方法后,run()方法的代码会和当前线程并发(单CPU)或并行 (多CPU)执行。
所以请记住一句话[调用线程对象的run方法不会产生一个新的线程],虽然可以达到相同的执行结果,但执行过程和执行效率不同。
[线程的interrupt()方法,interrupted()和isInterrupted()]
这三个方法是关系非常密切而且又比较复杂的,虽然它们各自的功能很清楚,但它们之间的关系有大多数人不是真正的了解。
先说interrupt()方法,它是实例方法,而它也是最奇怪的方法,在java语言中,线程最初被设计为"隐晦难懂"的东西,直到现在它的 语义不没有象它的名字那样准确。大多数人以为,一个线程象调用了interrupt()方法,那它对应的线程就应该被中断而抛出异常,事实中,当一个线程 对象调用interrupt()方法,它对应的线程并没有被中断,只是改变了它的中断状态。
使当前线程的状态变以中断状态,如果没有其它影响,线程还会自己继续执行。
只有当线程执行到sleep,wait,join等方法时,或者自己检查中断状态而抛出异常的情况下,线程才会抛出异常。
如果线程对象调用interrupt()后它对应的线程就立即中断,那么interrupted()方法就不可能执行。
因为interrupted()方法是一个static方法,就是说只能在当前线程上调用,而如果一个线程interrupt()后它已经中断了,那它又如何让自己interrupted()?
正因为一个线程调用interrupt()后只是改变了中断状态,它可以继续执行下去,在没有调用sleep,wait,join等法或自己抛 出异常之前,它就可以调用interrupted()来清除中断状态(还会原状)interrupted()方法会检查当前线程的中断状态,如果为 "被中断状态"则改变当前线程为"非中断状态"并返回true,如果为"非中断状态"则返回false,它不仅检查当前线程是否为中断状态,而且在保证当 前线程回来非中断状态,所以它叫"interrupted",是说中断的状态已经结束(到非中断状态了)isInterrupted()方法则仅仅检查线 程对象对应的线程是否是中断状态,并不改变它的状态。
目前大家只能先记住这三个方法的功能,只有真正深入到多线程编程实践中,才会体会到它们为什么是对象方法,为什么是类方法。
线程到底什么时候才被中断抛出InterruptedException异常,我们将在提高篇中详细讨论。
[sleep(),join(),yield()方法]
在现在的环节中,我只能先说明这些方法的作用和调用原则,至于为什么,在基础篇中无法深入,只能在提高篇中详细说明。
sleep()方法中是类方法,也就是对当前线程而言的,程序员不能指定某个线程去sleep,只能是当前线程执行到sleep()方法时,睡 眠指定的时间(让其它线程运行).事实上也只能是类方法,在当前线程上调用.试想如果你调用一个线程对象的sleep()方法,那么这个对象对应的线程如 果不是正在运行,它如何sleep()?所以只有当前线程,因为它正在执行,你才能保证它可以调用sleep()方法。
原则:[在同步方法中尽量不要调用线程的sleep()方法],或者简单说,对于一般水平的程序员你基本不应该调用sleep()方法。
join()方法,正如第一节所言,在一个线程对象上调用join方法,是当前线程等待这个线程对象对应的线程结束,比如有两个工作,工作A要耗时10秒钟,工作B要耗时10秒或更多。我们在程序中先生成一个线程去做工作B,然后做工作A。
new?B().start();//做工作B
A();//做工作A
工作A完成后,下面要等待工作B的结果来进行处理.如果工作B还没有完成我就不能进行下面的工作C,所以
B?b?=?new?B();
b.start();//做工作B
A();//做工作A
b.join();//等工作B完成。
C();//继续工作C。
原则:[join是测试其它工作状态的唯一正确方法],我见过很多人,甚至有的是博士生,在处理一项工作时如果另一项工作没有完成,说让当前工 作线程sleep(x),我问他,你这个x是如何指定的,你怎么知道是100毫秒而不是99毫秒或是101毫秒?其实这就是OnXXX事件的实质,我们不 是要等多长时间才去做什么事,而是当等待的工作正好完成的时候去做。
yield()方法也是类方法,只在当前线程上调用,理由同上,它主是让当前线程放弃本次分配到的时间片原则:[不是非常必要的情况下,没有理 由调用它].调用这个方法不会提高任何效率,只是降低了CPU的总周期上面介绍的线程一些方法,基于(基础篇)而言只能简单提及.以后具体应用中我会结合 实例详细论述。
线程本身的其它方法请参看API文档.下一节介绍非线程的方法,但和线程密切相关的两[三]个对象方法:
[wait(),notify()/notifyAll()]
这是在多线程中非常重要的方法。
关于这两个方法,有很多的内容需要说明.在下面的说明中可能会有很多地方不能一下子明白,但在看完本节后,即使不能完全明白,你也一定要回过头来记住下面的两句话:
[wait(),notify()/notityAll()方法是普通对象的方法(Object超类中实现),而不是线程对象的方法]
[wait(),notify()/notityAll()方法只能在同步方法中调用]
[线程的互斥控制]
多个线程同时操作某一对象时,一个线程对该对象的操作可能会改变其状态,而该状态会影响另一线程对该对象的真正结果.
这个例子我们在太多的文档中可以看到,就象两个操售票员同时售出同一张票一样.
线程A | 线程B |
---|---|
1.线程A在数据库中查询存票,发现票C可以卖出 | |
class="left"2.线程A接受用户订票请求,准备出票. | |
3.这时切换到了线程B执行 | |
4.线程B在数据库中查询存票,发现票C可以卖出 | |
5.线程B将票卖了出去 | |
6.切换到线程A执行,线程A卖了一张已经卖出的票 |
所以需要一种机制来管理这类问题的发生,当某个线程正在执行一个不可分割的部分时,其它线程不能不能同时执行这一部分.
象这种控制某一时刻只能有一个线程执行某个执行单元的机制就叫互斥控制或共享互斥(mutual exclusion)
在JAVA中,用synchornized关键字来实现互斥控制(暂时这样认为,JDK1.5已经发展了新的机制)
[synchornized关键字]
把一个单元声明为synchornized,就可以让在同一时间只有一个线程操作该方法.
有人说synchornized就是一把锁,事实上它确实存在锁,但是是谁的锁,锁谁,这是一个非常复杂的问题.
每个对象只有一把监视锁(monitor lock),一次只能被一个线程获取.当一个线程获取了这一个锁后,其它线程就只能等待这个线程释放锁才能再获取.
那么synchornized关键字到底锁什么?得到了谁的锁?
对于同步块,synchornized获取的是参数中的对象锁:
synchornized(obj){ //............... }
线程执行到这里时,首先要获取obj这个实例的锁,如果没有获取到线程只能等待.如果多个线程执行到这里,只能有一个线程获取obj的锁,然后执行{}中的语句,所以,obj对象的作用范围不同,控制程序不同.
假如:
public void test(){ Object o = new Object(); synchornized(obj){ //............... } }
这段程序控制不了任何,多个线程之间执行到Object o = new Object();时会各自产生一个对象然后获取这个对象有监视锁,各自皆大欢喜地执行.
而如果是类的属性:
class Test{ Object o = new Object(); public void test(){ synchornized(o){ //............... } } }
所有执行到Test实例的synchornized(o)的线程,只有一个线程可以获取到监视锁.
有时我们会这样:
public void test(){ synchornized(this){ //............... } }
那么所有执行Test实例的线程只能有一个线程执行.而synchornized(o)和synchornized(this)的范围是不同 的,因为执行到Test实例的synchornized(o)的线程等待时,其它线程可以执行Test实例的synchornized(o1)部分,但多 个线程同时只有一个可以执行Test实例的synchornized(this).]
而对于
synchornized(Test.class){ //............... }
这样的同步块而言,所有调用Test多个实例的线程赐教只能有一个线程可以执行.
[synchornized方法]
如果一个方法声明为synchornized的,则等同于把在为个方法上调用synchornized(this).
如果一个静态方法被声明为synchornized,则等同于把在为个方法上调用synchornized(类.class).
现在进入wait方法和notify/notifyAll方法.这两个(或叫三个)方法都是Object对象的方法,而不是线程对象的方法.如同锁一样,它们是在线程中调用某一对象上执行的.
class Test{ public synchornized void test(){ //获取条件,int x 要求大于100; if(x < 100) wait(); } }
这里为了说明方法没有加在try{}catch(){}中,如果没有明确在哪个对象上调用wait()方法,则为this.wait();
假如:
Test t = new Test();
现在有两个线程都执行到t.test();方法.其中线程A获取了t的对象锁,进入test()方法内.
这时x小于100,所以线程A进入等待.
当一个线程调用了wait方法后,这个线程就进入了这个对象的休息室(waitset),这是一个虚拟的对象,但JVM中一定存在这样的一个数据结构用来记录当前对象中有哪些程线程在等待.
当一个线程进入等待时,它就会释放锁,让其它线程来获取这个锁.
所以线程B有机会获得了线程A释放的锁,进入test()方法,如果这时x还是小于100,线程B也进入了t的休息室.
这两个线程只能等待其它线程调用notity[All]来唤醒.
但是如果调用的是有参数的wait(time)方法,则线程A,B都会在休息室中等待这个时间后自动唤醒.
[为什么真正的应用都是用while(条件)而不用if(条件)]
在实际的编程中我们看到大量的例子都是用?
while(x < 100)
wait();go();而不是用if,为什么呢?
在多个线程同时执行时,if(x <100)是不安全的.因为如果线程A和线程B都在t的休息室中等待,这时另一个线程使x==100了,并调用notifyAll方法,线程A继续 执行下面的go().而它执行完成后,x有可能又小于100,比如下面的程序中调用了--x,这时切换到线程B,线程B没有继续判断,直接执行go(); 就产生一个错误的条件,只有while才能保证线程B又继续检查一次.
[notify/notifyAll方法]
这两个方法都是把某个对象上休息区内的线程唤醒,notify只能唤醒一个,但究竟是哪一个不能确定,而notifyAll则唤醒这个对象上的休息室中所有的线程.
一般有为了安全性,我们在绝对多数时候应该使用notifiAll(),除非你明确知道只唤醒其中的一个线程.
那么是否是只要调用一个对象的wait()方法,当前线程就进入了这个对象的休息室呢?事实中,要调用一个对象的wait()方法,只有当前线程获取了这个对象的锁,换句话说一定要在这个对象的同步方法或以这个对象为参数的同步块中.
class MyThread extends Thread{ Test t = new Test(); public void run(){ t.test(); System.out.println("Thread say:Hello,World!"); } } public class Test { int x = 0; public void test(){ if(x==0) try{ wait(); }catch(Exception e){} } public static void main(String[] args) throws Exception{ new MyThread().start(); } }
这个线程就不会进入t的wait方法而直接打印出Thread say:Hello,World!.
而如果改成:
public class Test { int x = 0; public synchornized void test(){ if(x==0) try{ wait(); }catch(Exception e){} } public static void main(String[] args) throws Exception{ new MyThread().start(); } }
我们就可以看到线程一直等待,注意这个线程进入等待后没有其它线程唤醒,除非强行退出JVM环境,否则它一直等待.
所以请记住:
[线程要想调用一个对象的wait()方法就要先获得该对象的监视锁,而一旦调用wait()后又立即释放该锁]
Java多线程下载
- package com.jwzhangjie;
- /**
- * 说明:
- * 每一个线程下载的位置计算方式:
- * 开始位置:
- * (线程id - 1)*每一块大小
- * 结束位置:
- * (线程id*每一块大小) - 1
- * ---注意有时候不一定能够整除,所以最后一个线程的结束位置应该是文件的末尾
- *
- * 步骤:
- * 1.本地创建一个大小跟服务器文件相同的临时文件
- * 2.计算分配几个线程去下载服务器上的资源,知道每个线程下载文件的位置
- * 3.开启三个线程,每一个线程下载对应位置的文件
- * 4.如果所有的线程,都把自己的数据下载完毕后,服务器上的资源都被下载到本地了
- */
- import java.io.InputStream;
- import java.io.RandomAccessFile;
- import java.net.HttpURLConnection;
- import java.net.URL;
- public class Demo {
- public static String path = "<a target="_blank" href="http://softdownload.hao123.com/hao123-soft-online-bcs/soft/Y/2013-07-18_YoudaoDict_baidu.alading.exe">http://softdownload.hao123.com/hao123-soft-online-bcs/soft/Y/2013-07-18_YoudaoDict_baidu.alading.exe</a>";
- public static int threadCount = 3;
- public static void main(String[] args) throws Exception{
- //1.连接服务器,获取一个文件,获取文件的长度,在本地创建一个跟服务器一样大小的临时文件
- URL url = new URL(path);
- HttpURLConnection conn = (HttpURLConnection)url.openConnection();
- conn.setConnectTimeout(5000);
- conn.setRequestMethod("GET");
- int code = conn.getResponseCode();
- if (code == 200) {
- //服务器端返回的数据的长度,实际上就是文件的长度
- int length = conn.getContentLength();
- System.out.println("文件总长度:"+length);
- //在客户端本地创建出来一个大小跟服务器端一样大小的临时文件
- RandomAccessFile raf = new RandomAccessFile("setup.exe", "rwd");
- //指定创建的这个文件的长度
- raf.setLength(length);
- raf.close();
- //假设是3个线程去下载资源。
- //平均每一个线程下载的文件大小.
- int blockSize = length / threadCount;
- for (int threadId = 1; threadId <= threadCount; threadId++) {
- //第一个线程下载的开始位置
- int startIndex = (threadId - 1) * blockSize;
- int endIndex = threadId * blockSize - 1;
- if (threadId == threadCount) {//最后一个线程下载的长度要稍微长一点
- endIndex = length;
- }
- System.out.println("线程:"+threadId+"下载:---"+startIndex+"--->"+endIndex);
- new DownLoadThread(path, threadId, startIndex, endIndex).start();
- }
- }else {
- System.out.printf("服务器错误!");
- }
- }
- /**
- * 下载文件的子线程 每一个线程下载对应位置的文件
- * @author jie
- *
- */
- public static class DownLoadThread extends Thread{
- private int threadId;
- private int startIndex;
- private int endIndex;
- /**
- * @param path 下载文件在服务器上的路径
- * @param threadId 线程Id
- * @param startIndex 线程下载的开始位置
- * @param endIndex 线程下载的结束位置
- */
- public DownLoadThread(String path, int threadId, int startIndex, int endIndex) {
- super();
- this.threadId = threadId;
- this.startIndex = startIndex;
- this.endIndex = endIndex;
- }
- @Override
- public void run() {
- try {
- URL url = new URL(path);
- HttpURLConnection conn = (HttpURLConnection)url.openConnection();
- conn.setConnectTimeout(5000);
- conn.setRequestMethod("GET");
- //重要:请求服务器下载部分文件 指定文件的位置
- conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);
- //从服务器请求全部资源返回200 ok如果从服务器请求部分资源 返回 206 ok
- int code = conn.getResponseCode();
- System.out.println("code:"+code);
- InputStream is = conn.getInputStream();//已经设置了请求的位置,返回的是当前位置对应的文件的输入流
- RandomAccessFile raf = new RandomAccessFile("setup.exe", "rwd");
- //随机写文件的时候从哪个位置开始写
- raf.seek(startIndex);//定位文件
- int len = 0;
- byte[] buffer = new byte[1024];
- while ((len = is.read(buffer)) != -1) {
- raf.write(buffer, 0, len);
- }
- is.close();
- raf.close();
- System.out.println("线程:"+threadId+"下载完毕");
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- }
将源码添加到一个java工程里面就可以了,为了真实下载,我将自己搭建的服务器替换为网络有效的链接,进行测试,打印信息如下:
- 文件总长度:5577744
- 线程:1下载:---0--->1859247
- 线程:2下载:---1859248--->3718495
- 线程:3下载:---3718496--->5577744
- code:206
- code:206
- code:206
- 线程:2下载完毕
- 线程:1下载完毕
- 线程:3下载完毕
之后我会封装一个界面,其实迅雷等下载软件下载原理也是这样的,每一个服务器对下载的请求都有带宽限制的,所以多个线程下载从整体上是增加了下载的带宽,不过启动的线程也不是没有限制的,这个受限于你的真实的物理带宽。
Java多线程和并发编程
一、多线程
1、操作系统有两个容易混淆的概念,进程和线程。
进程:一个计算机程序的运行实例,包含了需要执行的指令;有自己的独立地址空间,包含程序内容和数据;不同进程的地址空间是互相隔离的;进程拥有各种资源和状态信息,包括打开的文件、子进程和信号处理。
线程:表示程序的执行流程,是CPU调度执行的基本单位;线程有自己的程序计数器、寄存器、堆栈和帧。同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源。
2、Java标准库提供了进程和线程相关的API,进程主要包括表示进程的java.lang.Process类和创建进程的java.lang.ProcessBuilder类;
表示线程的是java.lang.Thread类,在虚拟机启动之后,通常只有Java类的main方法这个普通线程运行,运行时可以创建和启动新的线程;还有一类守护线程(damon thread),守护线程在后台运行,提供程序运行时所需的服务。当虚拟机中运行的所有线程都是守护线程时,虚拟机终止运行。
3、线程间的可见性:一个线程对进程中共享的数据的修改,是否对另一个线程可见
可见性问题:
a、CPU采用时间片轮转等不同算法来对线程进行调度
- public class IdGenerator{
- private int value = 0;
- public int getNext(){
- return value++;
- }
- }
以上代码getNext()的指令序列:CPU切换可能发生在7条指令之间,多个getNext的指令交织在一起。
- aload_0
- dup
- getfield #12
- dup_x1
- iconst_1
- iadd
- putfield #12
目前CPU一般采用层次结构的多级缓存的架构,有的CPU提供了L1、L2和L3三级缓存。当CPU需要读取主存中某个位置的数据时,会一次检查各级缓存中是否存在对应的数据。如果有,直接从缓存中读取,这比从主存中读取速度快很多。当CPU需要写入时,数据先被写入缓存中,之后再某个时间点写回主存。所以某些时间点上,缓存中的数据与主存中的数据可能是不一致。
c、指令顺序重排
出行性能考虑,编译器在编译时可能会对字节代码的指令顺序进行重新排列,以优化指令的执行顺序,在单线程中不会有问题,但在多线程可能产生与可见性相关的问题。
二、Java内存模型(Java Memory Model)
屏蔽了CPU缓存等细节,只关注主存中的共享变量;关注对象的实例域、静态域和数组元素;关注线程间的动作。
1、volatile关键词:用来对共享变量的访问进行同步,上一次写入操作的结果对下一次读取操作是肯定可见的。(在写入volatile变量值之后,CPU缓存中的内容会被写回内存;在读取volatile变量时,CPU缓存中的对应内容会被置为失效,重新从主存中进行读取),volatile不使用锁,性能优于synchronized关键词。
用来确保对一个变量的修改被正确地传播到其他线程中。
例子:A线程是Worker,一直跑循环,B线程调用setDone(true),A线程即停止任务
- public class Worker{
- private volatile boolean done;
- public void setDone(boolean done){
- this.done = done;
- }
- public void work(){
- while(!done){
- //执行任务;
- }
- }
- }
- public class Counter {
- public volatile static int count = 0;
- public static void inc() {
- //这里延迟1毫秒,使得结果明显
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- }
- count++;
- }
- public static void main(String[] args) {
- //同时启动1000个线程,去进行i++计算,看看实际结果
- for (int i = 0; i < 1000; i++) {
- new Thread(new Runnable() {
- @Override
- public void run() {
- Counter.inc();
- }
- }).start();
- }
- //这里每次运行的值都有可能不同,可能不为1000
- System.out.println("运行结果:Counter.count=" + Counter.count);
- }
- }
final关键词声明的域的值只能被初始化一次,一般在构造方法中初始化。。(在多线程开发中,final域通常用来实现不可变对象)
当对象中的共享变量的值不可能发生变化时,在多线程中也就不需要同步机制来进行处理,故在多线程开发中应尽可能使用不可变对象。
另外,在代码执行时,final域的值可以被保存在寄存器中,而不用从主存中频繁重新读取。
3、java基本类型的原子操作
1)基本类型,引用类型的复制引用是原子操作;(即一条指令完成)
2)long与double的赋值,引用是可以分割的,非原子操作;
3)要在线程间共享long或double的字段时,必须在synchronized中操作,或是声明成volatile
三、Java提供的线程同步方式
1、synchronized关键字
方法或代码块的互斥性来完成实际上的一个原子操作。(方法或代码块在被一个线程调用时,其他线程处于等待状态)
所有的Java对象都有一个与synchronzied关联的监视器对象(monitor),允许线程在该监视器对象上进行加锁和解锁操作。
a、静态方法:Java类对应的Class类的对象所关联的监视器对象。
b、实例方法:当前对象实例所关联的监视器对象。
c、代码块:代码块声明中的对象所关联的监视器对象。
注:当锁被释放,对共享变量的修改会写入主存;当活得锁,CPU缓存中的内容被置为无效。编译器在处理synchronized方法或代码块,不会把其中包含的代码移动到synchronized方法或代码块之外,从而避免了由于代码重排而造成的问题。
例:以下方法getNext()和getNextV2() 都获得了当前实例所关联的监视器对象
- public class SynchronizedIdGenerator{
- private int value = 0;
- public synchronized int getNext(){
- return value++;
- }
- public int getNextV2(){
- synchronized(this){
- return value++;
- }
- }
- }
2、Object类的wait、notify和notifyAll方法
生产者和消费者模式,判断缓冲区是否满来消费,缓冲区是否空来生产的逻辑。如果用while 和 volatile也可以做,不过本质上会让线程处于忙等待,占用CPU时间,对性能造成影响。
wait: 将当前线程放入,该对象的等待池中,线程A调用了B对象的wait()方法,线程A进入B对象的等待池,并且释放B的锁。(这里,线程A必须持有B的锁,所以调用的代码必须在synchronized修饰下,否则直接抛出java.lang.IllegalMonitorStateException异常)。
notify:将该对象中等待池中的线程,随机选取一个放入对象的锁池,当当前线程结束后释放掉锁, 锁池中的线程即可竞争对象的锁来获得执行机会。
notifyAll:将对象中等待池中的线程,全部放入锁池。
(notify锁唤醒的线程选择由虚拟机实现来决定,不能保证一个对象锁关联的等待集合中的线程按照所期望的顺序被唤醒,很可能一个线程被唤醒之后,发现他所要求的条件并没有满足,而重新进入等待池。因为当等待池中包含多个线程时,一般使用notifyAll方法,不过该方法会导致线程在没有必要的情况下被唤醒,之后又马上进入等待池,对性能有影响,不过能保证程序的正确性)
工作流程:a、Consumer线程A 来 看产品,发现产品为空,调用产品对象的wait(),线程A进入产品对象的等待池并释放产品的锁。
b、Producer线程B获得产品的锁,执行产品的notifyAll(),Consumer线程A从产品的等待池进入锁池,Producer线程B生产产品,然后退出释放锁。
c、Consumer线程A获得产品锁,进入执行,发现有产品,消费产品,然后退出。
例子:
- public synchronized String pop(){
- this.notifyAll();// 唤醒对象等待池中的所有线程,可能唤醒的就是 生产者(当生产者发现产品满,就会进入对象的等待池,这里代码省略,基本略同)
- while(index == -1){//如果发现没产品,就释放锁,进入对象等待池
- this.wait();
- }//当生产者生产完后,消费者从this.wait()方法再开始执行,第一次还会执行循环,万一产品还是为空,则再等待,所以这里必须用while循环,不能用if
- String good = buffer[index];
- buffer[index] = null;
- index--;
- return good;// 消费完产品,退出。
- }
注:wait()方法有超时和不超时之分,超时的在经过一段时间,线程还在对象的等待池中,那么线程也会推出等待状态。
3、线程状态转换:
已经废弃的方法:stop、suspend、resume、destroy,这些方法在实现上时不安全的。
线程的状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING(有超时的等待)、TERMINATED。
a、方法sleep()进入的阻塞状态,不会释放对象的锁(即大家一起睡,谁也别想执行代码),所以不要让sleep方法处在synchronized方法或代码块中,否则造成其他等待获取锁的线程长时间处于等待。
b、方法join()则是主线程等待子线程完成,再往下执行。例如main方法新建两个线程A和B
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(new ThreadTesterA());
- Thread t2 = new Thread(new ThreadTesterB());
- t1.start();
- t1.join(); // 等t1执行完再往下执行
- t2.start();
- t2.join(); // 在虚拟机执行中,这句可能被忽略
- }
c、方法interrupt(),向被调用的对象线程发起中断请求。如线程A通过调用线程B的d的interrupt方法来发出中断请求,线程B来处理这个请求,当然也可以忽略,这不是必须的。Object类的wait()、Thread类的join()和sleep方法都会抛出受检异常java.lang.InterruptedException,通过interrupt方法中断该线程会导致线程离开等待状态。对于wait()调用来说,线程需要重新获取监视器对象上的锁之后才能抛出InterruptedException异常,并致以异常的处理逻辑。
可以通过Thread类的isInterrupted方法来判断是否有中断请求发生,通常可以利用这个方法来判断是否退出线程(类似上面的volatitle修饰符的例子);
Thread类还有个方法Interrupted(),该方法不但可以判断当前线程是否被中断,还会清楚线程内部的中断标记,如果返回true,即曾被请求中断,同时调用完后,清除中断标记。
如果一个线程在某个对象的等待池,那么notify和interrupt 都可以使该线程从等待池中被移除。如果同时发生,那么看实际发生顺序。如果是notify先,那照常唤醒,没影响。如果是interrupt先,并且虚拟机选择让该线程中断,那么即使nofity,也会忽略该线程,而唤醒等待池中的另一个线程。
e、yield(),尝试让出所占有的CPU资源,让其他线程获取运行机会,对操作系统上的调度器来说是一个信号,不一定立即切换线程。(在实际开发中,测试阶段频繁调用yeid方法使线程切换更频繁,从而让一些多线程相关的错误更容易暴露出来)。
四、非阻塞方式
线程之间同步机制的核心是监视对象上的锁,竞争锁来获得执行代码的机会。当一个对象获取对象的锁,然后其他尝试获取锁的对象会处于等待状态,这种锁机制的实现方式很大程度限制了多线程程序的吞吐量和性能(线程阻塞),且会带来死锁(线程A有a对象锁,等着获取b对象锁,线程B有b对象锁,等待获取a对象锁)和优先级倒置(优先级低的线程获得锁,优先级高的只能等待对方释放锁)等问题。
如果能不阻塞线程,又能保证多线程程序的正确性,就能有更好的性能。
在程序中,对共享变量的使用一般遵循一定的模式,即读取、修改和写入三步组成。之前碰到的问题是,这三步执行中可能线程执行切换,造成非原子操作。锁机制是把这三步变成一个原子操作。
目前CPU本身实现 将这三步 合起来 形成一个原子操作,无需线程锁机制干预,常见的指令是“比较和替换”(compare and swap,CAS),这个指令会先比较某个内存地址的当前值是不是指定的旧指,如果是,就用新值替换,否则什么也不做,指令返回的结果是内存地址的当前值。通过CAS指令可以实现不依赖锁机制的非阻塞算法。一般做法是把CAS指令的调用放在一个无限循环中,不断尝试,知道CAS指令成功完成修改。
java.util.concurrent.atomic包中提供了CAS指令。(不是所有CPU都支持CAS,在某些平台,java.util.concurrent.atomic的实现仍然是锁机制)
atomic包中提供的Java类分成三类:
1、支持以原子操作来进行更新的数据类型的Java类(AtomicBoolean、AtomicInteger、AtomicReference),在内存模型相关的语义上,这四个类的对象类似于volatile变量。
类中的常用方法:
a、compareAndSet:接受两个参数,一个是期望的旧值,一个是替换的新值。
b、weakCompareAndSet:效果同compareAndSet(JSR中表示weak原子方式读取和有条件地写入变量但不创建任何 happen-before 排序,但在源代码中和compareAndSet完全一样,所以并没有按JSR实现)
c、get和set:分别用来直接获取和设置变量的值。
d、lazySet:与set类似,但允许编译器把lazySet方法的调用与后面的指令进行重排,因此对值得设置操作有可能被推迟。
例:
- public class AtomicIdGenerator{
- private final AtomicInter counter = new AtomicInteger(0);
- public int getNext(){
- return counter.getAndIncrement();
- }
- }
- // getAndIncrement方法的内部实现方式,这也是CAS方法的一般模式,CAS方法不一定成功,所以包装在一个无限循环中,直到成功
- public final int getAndIncrement(){
- for(;;){
- int current = get();
- int next = current +1;
- if(compareAndSet(current,next))
- return current;
- }
- }
3、通过反射的方式对任何对象中包含的volatitle变量使用CAS方法,AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater。他们提供了一种方式把CAS的功能扩展到了任何Java类中声明为volatitle的域上。(灵活,但语义较弱,因为对象的volatitle可能被非atomic的其他方式被修改)
- public class TreeNode{
- private volatile TreeNode parent;
- // 静态工厂方法
- private static final AtomicReferenceFieldUpdater<TreeNode, TreeNode> parentUpdater = AtomicReferenceFieldUpdater.newUpdater(TreeNode.class,TreeNode.class,"parent");
- public boolean compareAndSetParent(TreeNode expect, TreeNode update){
- return parentUpdater.compareAndSet(this, expect, update);
- }
- }
比较多的用AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference。在实现线程安全的计数器时,AtomicInteger和AtomicLong类时最佳的选择。
五、高级同步机制(比synchronized更灵活的加锁机制)
synchronized和volatile,以及wait、notify等方法抽象层次低,在程序开发中使用比较繁琐,易出错。
而多线程之间的交互来说,存在某些固定的模式,如生产者-消费者和读者-写者模式,把这些模式抽象成高层API,使用起来会非常方便。
java.util.concurrent包为多线程提供了高层的API,满足日常开发中的常见需求。
常用接口
1、Lock接口,表示一个锁方法:
a、lock(),获取所,如果无法获取所锁,会处于等待状态
b、unlock(),释放锁。(一般放在finally代码块中)
c、lockInterruptibly(),与lock()类似,但允许当前线程在等待获取锁的过程中被中断。(所以要处理InterruptedException)
d、tryLock(),以非阻塞方式获取锁,如果无法获取锁,则返回false。(tryLock()的另一个重载可以指定超时,如果指定超时,当无法获取锁,会等待而阻塞,同时线程可以被中断)
2、ReadWriteLock接口,表示两个锁,读取的共享锁和写入的排他锁。(适合常见的读者--写者场景)
ReadWriteLock接口的readLock和writeLock方法来获取对应的锁的Lock接口的实现。
在多数线程读取,少数线程写入的情况下,可以提高多线程的性能,提高使用该数据结构的吞吐量。
如果是相反的情况,较多的线程写入,则接口会降低性能。
3、ReentrantLock类和ReentrantReadWriteLock,分别为上面两个接口的实现类。
他们具有重入性:即允许一个线程多次获取同一个锁(他们会记住上次获取锁并且未释放的线程对象,和加锁的次数,getHoldCount())
同一个线程每次获取锁,加锁数+1,每次释放锁,加锁数-1,到0,则该锁被释放,可以被其他线程获取。
- public class LockIdGenrator{
- //new ReentrantLock(true)是重载,使用更加公平的加锁机制,在锁被释放后,会优先给等待时间最长的线程,避免一些线程长期无法获得锁
- private int ReentrantLock lock = ReentrantLock();
- privafte int value = 0;
- public int getNext(){
- lock.lock(); //进来就加锁,没有锁会等待
- try{
- return value++;//实际操作
- }finally{
- lock.unlock();//释放锁
- }
- }
- }
4、Condition接口,Lock接口代替了synchronized,Condition接口替代了object的wait、nofity。
a、await(),使当前线程进入等待状态,知道被唤醒或中断。重载形式可以指定超时时间。
b、awaitNanos(),以纳秒为单位等待。
c、awaitUntil(),指定超时发生的时间点,而不是经过的时间,参数为java.util.Date。
d、awaitUninterruptibly(),前面几种会响应其他线程发出的中断请求,他会无视,直到被唤醒。
注:与Object类的wait()相同,await()会释放其所持有的锁。
e、signal()和signalAll, 相当于 notify和notifyAll
- Lock lock = new ReentrantLock();
- Condition condition = lock.newCondition();
- lock.lock();
- try{
- while(/*逻辑条件不满足*/){
- condition.await();
- }
- }finally{
- lock.unlock();
- }
六、底层同步器
多线程程序中,线程之间存在多种不同的同步方式。除了Java标准库提供的同步方式之外,程序中特有的同步方式需要由开发人员自己来实现。
常见的一种需求是 对有限个共享资源的访问,比如多台个人电脑,2台打印机,当多个线程在等待同一个资源时,从公平角度出发,会用FIFO队列。
如果程序中的同步方式可以抽象成对有限个资源的访问,那么可以使用java.util.concurrent.locks包中的AbstractQueuedSynchronizer类和AbstractQueuedLongSynchronizer类作为实现的基础,前者用int类型的变量来维护内部状态,而后者用long类型。(可以将这个变量理解为共享资源个数)
通过getState、setState、和compareAndSetState3个方法更新内部变量的值。
AbstractQueuedSynchronizer类是abstract的,需要覆盖其中包含的部分方法,通常做法是把其作为一个Java类的内部类,外部类提供具体的同步方式,内部类则作为实现的基础。有两种模式,排他模式和共享模式,分别对应方法 tryAcquire()、tryRelease 和 tryAcquireShared、tryReleaseShared,在这些方法中,使用getState、setState、compareAndSetState3个方法来修改内部变量的值,以此来反应资源的状态。
- public class SimpleResourceManager{
- private final InnerSynchronizer synchronizer;
- private static class InnerSynchronizer extends AbstractQueuedSynchronizer{
- InnerSynchronizer(int numOfResources){
- setState(numOfResources);
- }
- protected int tryAcquireShared(int acquires){
- for(;;){
- int available = getState();
- int remain = available - acquires;
- if(remain <0 || comapreAndSetState(available, remain){
- return remain;
- }
- }
- }
- protected boolean try ReleaseShared(int releases){
- for(;;){
- int available = getState();
- int next = available + releases;
- if(compareAndSetState(available,next){
- return true;
- }
- }
- }
- }
- public SimpleResourceManager(int numOfResources){
- synchronizer = new InnerSynchronizer(numOfResources);
- }
- public void acquire() throws InterruptedException{
- synchronizer.acquireSharedInterruptibly(1);
- }
- pubic void release(){
- synchronizer.releaseShared(1);
- }
- }
七、高级同步对象(提高开发效率)
atomic和locks包提供的Java类可以满足基本的互斥和同步访问的需求,但这些Java类的抽象层次较低,使用比较复杂。
更简单的做法是使用java.util.concurrent包中的高级同步对象。
1、信号量。
信号量一般用来数量有限的资源,每类资源有一个对象的信号量,信号量的值表示资源的可用数量。
在使用资源时,需要从该信号量上获取许可,成功获取许可,资源的可用数-1;完成对资源的使用,释放许可,资源可用数+1; 当资源数为0时,需要获取资源的线程以阻塞的方式来等待资源,或过段时间之后再来检查资源是否可用。(上面的SimpleResourceManager类实际上时信号量的一个简单实现)
java.util.concurrent.Semaphore类,在创建Semaphore类的对象时指定资源的可用数
a、acquire(),以阻塞方式获取许可
b、tryAcquire(),以非阻塞方式获取许可
c、release(),释放许可。
d、accquireUninterruptibly(),accquire()方法获取许可以的过程可以被中断,如果不希望被中断,使用此方法。
- public class PrinterManager{
- private final Semphore semaphore;
- private final List<Printer> printers = new ArrayList<>():
- public PrinterManager(Collection<? extends Printer> printers){
- this.printers.addAll(printers);
- //这里重载方法,第二个参数为true,以公平竞争模式,防止线程饥饿
- this.semaphore = new Semaphore(this.printers.size(),true);
- }
- public Printer acquirePrinter() throws InterruptedException{
- semaphore.acquire();
- return getAvailablePrinter();
- }
- public void releasePrinter(Printer printer){
- putBackPrinter(pinter);
- semaphore.release();
- }
- private synchronized Printer getAvailablePrinter(){
- printer result = printers.get(0);
- printers.remove(0);
- return result;
- }
- private synchronized void putBackPrinter(Printer printer){
- printers.add(printer);
- }
- }
多线程协作时,一个线程等待另外的线程完成任务才能继续进行。
java.util.concurrent.CountDownLatch类,创建该类时,指定等待完成的任务数;当一个任务完成,调用countDonw(),任务数-1。等待任务完成的线程通过await(),进入阻塞状态,直到任务数量为0。CountDownLatch类为一次性,一旦任务数为0,再调用await()不再阻塞当前线程,直接返回。
例:
- public class PageSizeSorter{
- // 并发性能远远优于HashTable的 Map实现,hashTable做任何操作都需要获得锁,同一时间只有有个线程能使用,而ConcurrentHashMap是分段加锁,不同线程访问不同的数据段,完全不受影响,忘记HashTable吧。
- private static final ConcurrentHashMap<String , Interger> sizeMap = new ConcurrentHashMap<>();
- private static class GetSizeWorker implements Runnable{
- private final String urlString;
- public GetSizeWorker(String urlString , CountDownLatch signal){
- this.urlString = urlStirng;
- this.signal = signal;
- }
- public void run(){
- try{
- InputStream is = new URL(urlString).openStream();
- int size = IOUtils.toByteArray(is).length;
- sizeMap.put(urlString, size);
- }catch(IOException e){
- sizeMap.put(urlString, -1);
- }finally{
- signal.countDown()://完成一个任务 , 任务数-1
- }
- }
- }
- private void sort(){
- List<Entry<String, Integer> list = new ArrayList<sizeMap.entrySet());
- Collections.slort(list, new Comparator<Entry<String,Integer>>(){
- public int compare (Entry<String, Integer> o1, Entry<Sting , Integer> o2){
- return Integer.compare(o2.getValue(),o1.getValue());
- };
- System.out.println(Arrays.deepToString(list.toArray()));
- }
- public void sortPageSize(Collection<String> urls) throws InterruptedException{
- CountDownLatch sortSignal = new CountDownLatch(urls.size());
- for(String url: urls){
- new Thread(new GetSizeWorker(url, sortSignal)).start();
- }
- sortSignal.await()://主线程在这里等待,任务数归0,则继续执行
- sort();
- }
- }
3、循环屏障
循环屏障在作用上类似倒数闸门,不过他不像倒数闸门是一次性的,可以循环使用。另外,线程之间是互相平等的,彼此都需要等待对方完成,当一个线程完成自己的任务之后,等待其他线程完成。当所有线程都完成任务之后,所有线程才可以继续运行。
当线程之间需要再次进行互相等待时,可以复用同一个循环屏障。
类java.uti.concurrent.CyclicBarrier用来表示循环屏障,创建时指定使用该对象的线程数目,还可以指定一个Runnable接口的对象作为每次循环后执行的动作。(当最后一个线程完成任务之后,所有线程继续执行之前,被执行。如果线程之间需要更新一些共享的内部状态,可以利用这个Runnalbe接口的对象来处理)。
每个线程任务完成之后,通过调用await方法进行等待,当所有线程都调用await方法之后,处于等待状态的线程都可以继续执行。在所有线程中,只要有一个在等待中被中断,超时或是其他错误,整个循环屏障会失败,所有等待中的其他线程抛出java.uti.concurrent.BrokenBarrierException。
例:每个线程负责找一个数字区间的质数,当所有线程完成后,如果质数数目不够,继续扩大范围查找
- public class PrimeNumber{
- private static final int TOTAL_COUTN = 5000;
- private static final int RANGE_LENGTH= 200;
- private static final int WORKER_NUMBER = 5;
- private static volatitle boolean done = false;
- private static int rangeCount = 0;
- private static final List<Long> results = new ArrayList<Long>():
- private static final CyclicBarrier barrier = new CyclicBarrier(WORKER_NUMBER, new Runnable(){
- public void run(){
- if(results.size() >= TOTAL_COUNT){
- done = true;
- }
- }
- });
- private static class PrimeFinder implements Runnable{
- public void run(){
- while(!done){// 整个过程在一个 while循环下,await()等待,下次循环开始,会再次判断 执行条件
- int range = getNextRange();
- long start = rang * RANGE_LENGTH;
- long end = (range + 1) * RANGE_LENGTH;
- for(long i = start; i<end;i++){
- if(isPrime(i)){
- updateResult(i);
- }
- }
- try{
- barrier.await();
- }catch (InterruptedException | BokenBarrierException e){
- done = true;
- }
- }
- }
- }
- private synchronized static void updateResult(long value){
- results.add(value);
- }
- private synchronized static int getNextRange(){
- return rangeCount++;
- }
- private static boolean isPrime(long number){
- //找质数的代码
- }
- public void calculate(){
- for(int i=0;i<WORKER_NUMBER;i++){
- new Thread(new PrimeFinder()).start();
- }
- while(!done){
- }
- //计算完成
- }
- }
适合于两个线程需要进行数据交换的场景。(一个线程完成后,把结果交给另一个线程继续处理)
java.util.concurrent.Exchanger类,提供了这种对象交换能力,两个线程共享一个Exchanger类的对象,一个线程完成对数据的处理之后,调用Exchanger类的exchange()方法把处理之后的数据作为参数发送给另外一个线程。而exchange方法的返回结果是另外一个线程锁提供的相同类型的对象。如果另外一个线程未完成对数据的处理,那么exchange()会使当前线程进入等待状态,直到另外一个线程也调用了exchange方法来进行数据交换。
例:
- public class SendAndReceiver{
- private final Exchanger<StringBuilder> exchanger = new Exchanger<StringBuilder>();
- private class Sender implements Runnable{
- public void run(){
- try{
- StringBuilder content = new StringBuilder("Hello");
- content = exchanger.exchange(content);
- }catch(InterruptedException e){
- Thread.currentThread().interrupt();
- }
- }
- }
- private class Receiver implements Runnable{
- public void run(){
- try{
- StringBuilder content = new StringBuilder("World");
- content = exchanger.exchange(content);
- }catch(InterruptedException e){
- Thread.currentThread().interrupt();
- }
- }
- }
- public void exchange(){
- new Thread(new Sender()).start();
- new Thread(new Receiver()).start();
- }
- }
java.util.concurrent包中提供了一些适合多线程程序使用的高性能数据结构,包括队列和集合类对象等。
1、队列
a、BlockingQueue接口:线程安全的阻塞式队列;当队列已满时,想队列添加会阻塞;当队列空时,取数据会阻塞。(非常适合消费者-生产者模式)
阻塞方式:put()、take()。
非阻塞方式:offer()、poll()。
实现类:基于数组的固定元素个数的ArrayBolockingQueue和基于链表结构的不固定元素个数的LinkedBlockQueue类。
b、BlockingDeque接口: 与BlockingQueue相似,但可以对头尾进行添加和删除操作的双向队列;方法分为两类,分别在队首和对尾进行操作。
实现类:标准库值提供了一个基于链表的实现,LinkedBlockgingDeque。
2、集合类
在多线程程序中,如果共享变量时集合类的对象,则不适合直接使用java.util包中的集合类。这些类要么不是线程安全,要么在多线程下性能比较差。
应该使用java.util.concurrent包中的集合类。
a、ConcurrentMap接口: 继承自java.util.Map接口
putIfAbsent():只有在散列表不包含给定键时,才会把给定的值放入。
remove():删除条目。
replace(key,value):把value 替换到给定的key上。
replace(key, oldvalue, newvalue):CAS的实现。
实现类:ConcurrentHashMap:
创建时,如果可以预估可能包含的条目个数,可以优化性能。(因为动态调整所能包含的数目操作比较耗时,这个HashMap也一样,只是多线程下更耗时)。
创建时,预估进行更新操作的线程数,这样实现中会根据这个数把内部空间划分为对应数量的部分。(默认是16,如果只有一个线程进行写操作,其他都是读取,那么把值设为1 可以提高性能)。
注:当从集合中创建出迭代器遍历Map元素时,不一定能看到正在添加的数据,只能和集合保证弱一致性。(当然使用迭代器不会因为查看正在改变的Map,而抛出java.util.ConcurrentModifycationException)
b、CopyOnWriteArrayList接口:继承自java.util.List接口。
顾名思义,在CopyOnWriteArrayList的实现类,所有对列表的更新操作都会新创建一个底层数组的副本,并使用副本来存储数据;对列表更新操作加锁,读取操作不加锁。
适合多读取少修改的场景,如果更新操作多,那么不适合用,同样迭代器只能表示创建时列表的状态,更新后使用了新的底层数组,迭代器还是引用旧的底层数组。
九、多线程任务的执行
过去线程的执行,是先创建Thread类的想,再调用start方法启动,这种做法要求开发人员对线程进行维护,在线程较多时,一般创建一个线程池同一管理,同时降低重复创建线程的开销
在J2SE5.0中,java.util.concurrent包提供了丰富的用来管理线程和执行任务的实现。
1、基本接口(描述任务)
a、Callable接口:
Runnable接口受限于run方法的类型签名,而Callable只有一个方法call(),可以有返回值,可以抛出受检异常。
b、Future接口:
过去,需要异步线程的任务执行结果,要求主线程和任务执行线程之间进行同步和数据传递。
Future简化了任务的异步执行,作为异步操作的一个抽象。调用get()方法可以获取异步的执行结果,如果任务没有执行完,会等待,直到任务完成或被取消,cancel()可以取消。
c、Delayed接口:
延迟执行任务,getDelay()返回当前剩余的延迟时间,如果不大于0,说明延迟时间已经过去,应该调度并执行该任务。
2、组合接口(描述任务)
a、RunnableFuture接口:继承自Runnable接口和Future接口。
当来自Runnalbe接口中的run方法成功执行之后,相当于Future接口表示的异步任务已经完成,可以通过get()获取运行结果。
b、ScheduledFuture接口:继承Future接口和Delayed接口,表示一个可以调用的异步操作。
c、RunnableScheduledFuture接口:继承自Runnable、Delayed和Future,接口中包含isPeriodic,表明该异步操作是否可以被重复执行。
3、Executor接口、ExcutorServer接口、ScheduleExecutorService接口和CompletionService接口(描述任务执行)
a、executor接口,execute()用来执行一个Runnable接口的实现对象,不同的Executor实现采取不同执行策略,但提供的任务执行功能比较弱。
b、excutorServer接口,继承自executor;
提供了对任务的管理:submit(),可以吧Callable和Runnable作为任务提交,得到一个Future作为返回,可以获取任务结果或取消任务。
提供批量执行:invokeAll()和invokeAny(),同时提交多个Callable;invokeAll(),会等待所有任务都执行完成,返回一个包含每个任务对应Future的列表;invokeAny(),任何一个任务成功完成,即返回该任务结果。
提供任务关闭:shutdown()、shutdownNow()来关闭服务,前者不允许新的任务提交,后者试图终止正在运行和等待的任务,并返回已经提交单没有被运行的任务列表。(两个方法都不会等待服务真正关闭,只是发出关闭请求。)。shutdownDow,通常做法是向线程发出中断请求,所以确保提交的任务实现了正确的中断处理逻辑。
c、ScheduleExecutorService接口,继承自excutorServer接口:支持任务的延迟执行和定期执行,可以执行Callable或Runnable。
schedule(),调度一个任务在延迟若干时间之后执行;
scheduleAtFixedRate():在初始延迟后,每隔一段时间循环执行;在下一次执行开始时,上一次执行可能还未结束。(同一时间,可能有多个)
scheduleWithFixedDelay:同上,只是在上一次任务执行完后,经过给定的间隔时间再开始下一次执行。(同一时间,只有一个)
以上三个方法都返回ScheduledFuture接口的实现对象。
d、CompletionService接口,共享任务执行结果。
通常在使用ExecutorService接口,通过submit提交任务,并得到一个Future接口来获取任务结果,如果任务提交者和执行结果的使用者是程序的不同部分,那就要把Future在不同部分进行传递;而CompletionService就是解决这个问题,程序不同部分可以共享CompletionService,任务提交后,执行结果可以通过take(阻塞),poll(非阻塞)来获取。
标准库提供的实现是 ExecutorCompletionService,在创建时,需要提供一个Executor接口的实现作为参数,用来实际执行任务。
例:多线程方式下载文件
- public class FileDownloader{
- // 线程池
- private final ExecutorService executor = Executors.newFixedThreadPool(10);
- public boolean download(final URL url, final Path path){
- Future<Path> future = executor.submit(new Callable<Path>(){ //submit提交任务
- public Path call(){
- //这里就省略IOException的处理了
- InputStream is = url.openStream();
- Files.copy(is, path, StandardCopyOption.REPLACE_EXISTING);
- return path;
- });
- try{
- return future.get() !=null ? true : false;
- }<span style="font-family: Arial, Helvetica, sans-serif;">catch(InterruptedException | ExecutionException e){</span>
- return false;
- }
- }
- public void close(){//当不再使用FileDownloader类的对象时,应该使用close方法关闭其中包含的ExecutorService接口的实现对象,否则虚拟机不会退出,占用内存不释放
- executor.shutdown();// 发出关闭请求,此时不会再接受新任务
- try{
- if(!executor.awaitTermination(3, TimeUnit.MINUTES)){// awaitTermination 来等待一段时间,使正在执行的任务或等待的任务有机会完成
- executor.shutdownNow();// 如果等待时间过后还有任务没完成,则强制结束
- executor.awaitTermination(1, TimeUnit.MINUTES);// 再等待一段时间,使被强制结束的任务完成必要的清理工作
- }
- }catch(InterruptedException e){
- executor.shutdownNow();
- Thread.currentThread().interrupt();
- }
- }
- }
十、Java SE 7 新特性
对java.util.concurrent包进行更新,增加了新的轻量级任务执行框架fork/join和多阶段线程同步工具。
1、轻量级任务执行框架fork/join
这个框架的目的主要是更好地利用底层平台上的多核和多处理器来进行并行处理。
通过分治算法或map/reduce算法来解决问题。
fork/join 类比于 map/reduce。
fork操作是把一个大的问题划分为若干个较小的问题,划分过程一般为递归,直到可以直接进行计算的粒度适合的子问题;子问题在结算后,可以得到整个问题的部分解
join操作收集子结果,合并,得到完整解,也可能是 递归进行的。
相对一般的线程池实现,F/J框架的优势在任务的处理方式上。在一般线程池中,一个线程由于某些原因无法运行,会等待;而在F/J,某个子问题由于等待另外一个子问题的完成而无法继续运行,那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行。这种方式减少了等待时间,提高了性能。
为了F/J能高效,在每个子问题视线中应避免使用synchronized或其他方式进行同步,也不应使用阻塞式IO或过多访问共享变量。在理想情况下,每个子问题都应值进行CPU计算,只使用每个问题的内部对象,唯一的同步应只发生在子问题和创建它的父问题之间。(这完全就是Hadoop的MapReduce嘛)
a、ForkJoinTask类:表示一个由F/J框架执行的任务,该类实现了Future接口,可以按照Future接口的方式来使用。(表示任务)
fork(),异步方式启动任务的执行。
join(),等待任务完成并返回执行结果。
在创建自己的任务时,最好不要直接继承自ForkJoinTask,而是继承其子类,RecuriveTask或RecursiveAction,前者可以返回结果,后者不行。
b、ForkJoinPool类:表示任务执行,实现了ExecutorService接口,除了可以执行ForkJoinTask,也可以执行Callable和Runnable。(任务执行)
执行任务的两大类:
第一类:execute、invoke或submit方法:直接提交任务。
第二类:fork():运行ForkJoinTask在执行过程中的子任务。
一般作法是表示整个问题的ForkJoinTask用第一类提交,执行过程中产生的子任务不需要处理,ForkJoinPool会负责子任务执行。
例:查找数组中的最大值
- private static class MaxValueTask extends RecursiveTask<Long>{
- private final long[] array;
- private final int start;
- private final int end;
- MaxValueTask(long[] array, int start, int end){
- this.array = array;
- this.start = start;
- this.end = end;
- }
- //compute是RecursiveTask的主方法
- protected long compute(){
- long max = Long.MIN_VALUE;
- if(end - start < RANG_LENGTH){//寻找最大值
- for(int i = start; i<end;i++{
- if(array[i] > max){
- max = array[i];
- }
- }
- }else{// 二分任务
- int mid = (start + end) /2;
- MaxValueTask lowTask = new MaxValueTask(array, start , mid);
- MaxValueTask highTask = new MaxValueTask(array, mid, end);
- lowTask.fork();// 异步启动任务
- highTask.fork();
- max = Math.max(max, lowTask.join());//等待执行结果
- max = Math.max(max, highTask.join();
- }
- return max;
- }
- public Long calculate(long[] array){
- MaxValueTask task = new MaxValueTask(array, 0 , array.length);
- Long result = forkJoinPool.invoke(task);
- return result;
- }
- }
在实际中,F/J框架发挥作用的场合很多,比如在一个目录包含的所有文本中搜索某个关键字,可以每个文件创建一个子任务。
如果相关的功能可以用递归和分治来解决,就适合F/J。
2、多阶段线程同步工具
Phaser类是Java SE 7中新增的一个使用同步工具,功能和灵活性比倒数闸门和循环屏障要强很多。
在F/J框架中的子任务之间要进行同步时,应优先考虑Phaser。
Phaser把多个线程写作执行的任务划分成多个阶段(phase),编程时要明确各个阶段的任务,每个阶段都可以有任意个参与者,线程可以随时注册并参与到某个阶段,当一个阶段中所有线程都成功完成之后,Phaser的onAdvance()被调用,可以通过覆盖添加自定义处理逻辑(类似循环屏障的使用的Runnable接口),然后Phaser类会自动进入下个阶段。如此循环,知道Phaser不再包含任何参与者。
Phaser创建后,初始阶段编号为0,构造函数中指定初始参与个数。
register(),bulkRegister(),动态添加一个或多个参与者。
arrive(),某个参与者完成任务后调用
arriveAndDeregister(),任务完成,取消自己的注册。
arriveAndAwaitAdvance(),自己完成等待其他参与者完成。,进入阻塞,直到Phaser成功进入下个阶段。
awaitAdvance()、awaitAdvanceInterruptibly(),等待phaser进入下个阶段,参数为当前阶段的编号,后者可以设置超时和处理中断请求。
另外,Phaser的一个重要特征是多个Phaser可以组成树形结构,Phaser提供了构造方法来指定当前对象的父对象;当一个子对象参与者>0,会自动注册到父对象中;当=0,自动解除注册。
例:从指定网址,下载img标签的照片
阶段1、处理网址对应的html文本,和抽取img的链接;2、创建图片下载子线程,主线程等待;3、子线程下载图片,主线程等待;4、任务完成退出
- public class WebPageImageDownloader{
- private final Phaser phaser = new Phaser(1);//初始参与数1,代表主线程。
- public void download(URL url, final Path path) throws IOException{
- String content = getContent(url);//获得HTML文本,省略。
- List<URL> imageUrls = extractImageUrls(content);//获得图片链接,省略。
- for(final URL imageUrl : imageUrls){
- phaser.register();//子线程注册
- new Thread(){
- public void run(){
- phaser.arriveAndAwaitAdvance();//第二阶段的等待,等待进入第三阶段
- try{
- InputStream is = imageUrl.openStream();
- File.copy(is, getSavePath(path, imageUrl), StandardCopyOption.REPLACE_EXISTING);
- }catch(IOException e){
- e.printStackTrace():
- }finally{
- phaser.arriveAndDeregister();//子线程完成任务,退出。
- }
- }
- }.start();
- }
- phaser.arriveAndAwaitAdvance();//第二阶段等待,子线程在注册
- phaser.arriveAndAwaitAdvance();//第三阶段等待,子线程在下载
- phaser.arriveAndDeregister();//所有线程退出。
- }
- }
java.lang.ThreadLocal,线程局部变量,把一个共享变量变为一个线程的私有对象。不同线程访问一个ThreadLocal类的对象时,锁访问和修改的事每个线程变量各自独立的对象。通过ThreadLocal可以快速把一个非线程安全的对象转换成线程安全的对象。(同时也就不能达到数据传递的作用了)。
a、get()和set()分别用来获取和设置当前线程中包含的对象的值。
b、remove(),删除。
c、initialValue(),初始化值。如果没有通过set方法设置值,第一个调用get,会通过initValue来获取对象的初始值。
ThreadLoacl的一般用法,创建一个ThreadLocal的匿名子类并覆盖initalValue(),把ThreadLoacl的使用封装在另一个类中
- public class ThreadLocalIdGenerator{
- private static final ThreadLocal<IdGenerator> idGenerator = new ThreadLocal<IdGenerator>(){
- protected IdGenerator initalValue(){
- return new IdGenerator();//IdGenerator 是个初始int value =0,然后getNext(){ return value++}
- }
- };
- public static int getNext(){
- return idGenerator.get().getNext();
- }
- }
如:在多线程中,生成随机数
java.util.Random会带来竞争问题,java.util.concurrent.ThreadLocalRandom类提供多线程下的随机数声场,底层是ThreadLoacl。
总结:多线程开发中应该优先使用高层API,如果无法满足,使用java.util.concurrent.atomic和java.util.concurrent.locks包提供的中层API,而synchronized和volatile,以及wait,notify和notifyAll等低层API 应该最后考虑。