Java多线程基础总结:
1.使用synchronized关键字
2.使用JDK 5中提供的java.util.concurrent.lock包中的Lock对象。
一.synchronized关键字
java线程同步的基本原理是采用了锁的机制,在jvm中,每个对象和类都分配一个锁和它关联,对象锁保护对象的实例变量,类锁其实也是通过对象锁来实现的,通过保护类的Class对象,实现锁对类的保护。
如果一个线程获取了某个对象的锁,其他线程就不能再获取该对象的锁了。在java程序中,使用synchronized块或者synchronized方法就可以标识一个同步区域,进入同步区域,需要得到同步区域指定的锁,进入synchronized方法需要的对象锁为当前对象,而进入synchronized块则需要得到synchronized关键字指定的对象的对象锁。一下为使用两者的例子:
public synchronized void run() {
for (int i = 1; i < 1000; i++) {
System.out.println("No." + threadNo + ":" + i);
}
public void run() {
synchronized(lock){
for (int i = 1; i < 1000; i++) {
System.out.println("No." + threadNo + ":" + i);
}
}
}
通过使用synchronized关键字的一个例子来得到的几个tip:
1. 如果可以保证该对象是单例对象,则可以在要同步的方法或者代码块上直接加上synchronized关键字或者synchronized(this)。
2. 可以通过对一个静态变量上锁,比如说生成一个String类的对象。String对象甚至可以不用是static变量,在上例中,只需写成String lock = “aa”就可以实现对该同步区域进行同步的目的,因为每个String对象lock都使用栈中同一个”aa”的string变量,而使用String lock = new String(“aa”)则不能达到目的。
3. 可以通过在方法中调用一个静态的加了synchronized关键字的同步方法,因为静态方法使用的锁是类锁,而类的Class对象只有一个。
以下是一个一个生产者消费者的例子:
package com.tang.thread;
public class PCThreadTest{
public static void main(String[] args) throws Exception {
SynData data = new SynData();
Comsumer c1 = new Comsumer(data);
Comsumer c2 = new Comsumer(data);
Productor p1 = new Productor(data);
Productor p2 = new Productor(data);
Thread r1 = new Thread(c1,"comsumer1");
Thread r2 = new Thread(c2,"comsumer2");
Thread r3 = new Thread(p1,"Productor1");
Thread r4 = new Thread(p2,"Productor2");
r1.start();
r2.start();
r3.start();
r4.start();
}
}
class Comsumer implements Runnable {
SynData data ;
public Comsumer(SynData data){
this.data = data;
}
@Override
public void run() {
while(true){
if(data.hasData()){
data.comsume();
}else{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
class Productor implements Runnable {
SynData data ;
public Productor(SynData data){
this.data = data;
}
@Override
public void run() {
while(true){
for(int i = 0;i<100;i++){
data.product();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class SynData{
int a;
public boolean hasData(){
return a > 0;
}
public synchronized void comsume(){
if(a > 0){
a--;
System.out.println(Thread.currentThread().getName()+"comsume --" + a);
}else{
System.out.println(Thread.currentThread().getName()+"no data!!");
}
}
public synchronized void product(){
a++;
System.out.println(Thread.currentThread().getName()+"product ++" + a);
}
}
输出信息:
Productor1product ++1
Productor1product ++2
Productor1product ++3
Productor1product ++4
Productor1product ++5
Productor1product ++6
Productor1product ++7
Productor1product ++8
Productor1product ++9
Productor1product ++10
Productor2product ++11
Productor2product ++12
Productor2product ++13
Productor2product ++14
Productor2product ++15
Productor2product ++16
Productor2product ++17
Productor2product ++18
Productor2product ++19
Productor2product ++20
comsumer2comsume --19
comsumer2comsume --18
comsumer2comsume --17
comsumer2comsume --16
comsumer2comsume --15
comsumer2comsume --14
comsumer2comsume --13
comsumer2comsume --12
comsumer2comsume --11
comsumer2comsume --10
comsumer2comsume --9
comsumer2comsume --8
comsumer2comsume --7
comsumer2comsume --6
comsumer2comsume --5
comsumer2comsume --4
comsumer2comsume --3
comsumer2comsume --2
comsumer2comsume --1
comsumer2comsume --0
comsumer1no data!!
Productor1product ++1
Productor1product ++2
Productor1product ++3
Productor1product ++4
Productor1product ++5
Productor1product ++6
Productor1product ++7
Productor1product ++8
Productor1product ++9
Productor1product ++10
Productor2product ++11
Productor2product ++12
Productor2product ++13
Productor2product ++14
Productor2product ++15
Productor2product ++16
Productor2product ++17
Productor2product ++18
Productor2product ++19
Productor2product ++20
comsumer1comsume --19
comsumer1comsume --18
comsumer1comsume --17
comsumer1comsume --16
comsumer1comsume --15
comsumer1comsume --14
comsumer1comsume --13
comsumer1comsume --12
comsumer1comsume --11
comsumer1comsume --10
comsumer1comsume --9
comsumer1comsume --8
comsumer1comsume --7
comsumer1comsume --6
comsumer2comsume --5
comsumer2comsume --4
comsumer2comsume --3
comsumer2comsume --2
comsumer1comsume --1
comsumer1comsume --0
comsumer2no data!!
Productor1product ++1
Productor1product ++2
SynData类中方法consume和product都是Synchronized方法,通过加锁,保证了两个生产者和两个消费者能够有秩序的生产和消费,也使得生产者和消费者之间竞争处理数据时能够保证数据的正确性。
二.ThreadLocal
处理多线程情况下数据正确性的方法,除了使用同步锁机制之外,java还提供了ThreadLocal类,以下是使用ThreadLocal处理多线程的例子。
package com.tang.thread1;
public class PCThreadTest1{
public static void main(String[] args) throws Exception {
SynData data = new SynData(50);
Comsumer c1 = new Comsumer(data);
Productor p2 = new Productor(data);
c1.start();
p2.start();
}
}
class Comsumer extends Thread {
SynData data;
public Comsumer(SynData data){
this.data = data;
}
public void run() {
data.setA(40);
while(true){
try {
data.comsume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Productor extends Thread{
SynData data;
public Productor(SynData data){
this.data = data;
}
public void run() {
data.setA(1);
while(true){
for(int i = 0;i<10;i++){
data.product();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class SynData{
int a;
static ThreadLocal<Integer> local = new ThreadLocal<Integer>();
public SynData(int a){
this.a = a;
System.out.println(Thread.currentThread().getName()+"SynData");
}
public boolean hasData(){
return local.get() > 0;
}
public void setA(int b){
System.out.println(Thread.currentThread().getName()+"set A to "+ b);
a = b ;
local.set(a);
}
public void comsume() throws InterruptedException{
local.get();
if(local.get() > 0){
local.set(local.get() - 1);
System.out.println(Thread.currentThread().getName()+"comsume --" + local.get());
}else{
System.out.println(Thread.currentThread().getName()+"no data!!");
Thread.sleep(1000);
}
}
public void product(){
local.set(local.get()+1);
System.out.println(Thread.currentThread().getName()+"product ++" + local.get());
}
}
输出结果:
mainSynData
Thread-0set A to 40
Thread-1set A to 1
Thread-0comsume --39
Thread-0comsume --38
Thread-0comsume --37
Thread-0comsume --36
Thread-0comsume --35
Thread-0comsume --34
Thread-0comsume --33
Thread-0comsume --32
Thread-0comsume --31
Thread-0comsume --30
Thread-0comsume --29
Thread-0comsume --28
Thread-0comsume --27
Thread-0comsume --26
Thread-0comsume --25
Thread-0comsume --24
Thread-0comsume --23
Thread-0comsume --22
Thread-0comsume --21
Thread-0comsume --20
Thread-0comsume --19
Thread-1product ++2
Thread-1product ++3
Thread-1product ++4
Thread-1product ++5
Thread-1product ++6
Thread-1product ++7
Thread-1product ++8
Thread-1product ++9
Thread-1product ++10
Thread-1product ++11
Thread-0comsume --18
Thread-0comsume --17
Thread-0comsume --16
Thread-0comsume --15
Thread-0comsume --14
Thread-0comsume --13
Thread-0comsume --12
Thread-0comsume --11
Thread-0comsume --10
Thread-0comsume --9
Thread-0comsume --8
Thread-0comsume --7
Thread-0comsume --6
Thread-0comsume --5
Thread-0comsume --4
Thread-0comsume --3
Thread-0comsume --2
Thread-0comsume --1
Thread-0comsume --0
Thread-0no data!!
Thread-1product ++12
Thread-1product ++13
Thread-1product ++14
Thread-1product ++15
Thread-1product ++16
Thread-0no data!!
以上结果说明的几个问题:
1. SynData的构造函数所在的线程是名称为main的线程,所以,在多线程情况下,在构造函数中初始化ThreadLocal变量的值是不正确的。想要达到初始化的效果可以采用两种方法,第一,重写ThreadLocal的initialValue方法,该方法会在线程第一次调用get方法时,返回在initialValue中指定的初始值,否则返回值为null。第二,取得ThreadLocal变量的值之后,增加判断,如果为null则赋以新值。
2. 调用consume方法的线程和调用product方法的线程,虽然没有使用同步锁,但是却能够保证各自数据的正确性,各自数据是隔离的。
Synchronized和ThreadLocal处理多线程问题的差异:Synchronized通过保证数据更改后线程获得数据的一致保证同步情况下程序的正确性,而ThreadLocal通过为每个线程维护一份数据,保证同步情况下数据的正确性。它们的不同之处在于,1.Synchronized方式是以时间换取空间,而ThreadLocal方式则是以空间换取时间。但是采用Synchronized方式可以通过多线程来提高程序性能的可能性在使用ThreadLocal方式时则消失了。简言之,(2.)Synchronized让线程之间共享数据,ThreadLocal则隔离线程之间的数据共享。
再提供一个Hibernate中使用ThreadLocal管理session的例子:
public class HibernateUtil {
private static Log log = LogFactory.getLog(HibernateUtil.class);
private static final SessionFactory sessionFactory;//定义SessionFactory
static {
try {
// 通过默认配置文件hibernate.cfg.xml创建SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
log.error("初始化SessionFactory失败!", ex);
throw new ExceptionInInitializerError(ex);
}
}
//创建线程局部变量session,用来保存Hibernate的Session
public static final ThreadLocal session = new ThreadLocal();
/**
* 获取当前线程中的Session
* @return Session
* @throws HibernateException
*/
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// 如果Session还没有打开,则新开一个Session
if (s == null) {
s = sessionFactory.openSession();
session.set(s); //将新开的Session保存到线程局部变量中
}
return s;
}
public static void closeSession() throws HibernateException {
//获取线程局部变量,并强制转换为Session类型
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
}
三.重入锁
java.util.concurrent.lock中的Lock采用面向对象的方式对锁进行了抽象,它提供了更精确的线程语义,也为锁的多种实现留下了空间以及据说更好的性能。以下引用JDK官方文档:
ReentrantLock是“一个可重入的互斥锁 Lock,它具有与使用 synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。 ”
简单来说,ReentrantLock有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义:如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。和synchronized相比,Lock在遇到以下几种情况下更有优势:
第一, 某个线程在等待一个锁的控制权的这段时候需要中断,第二,需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程,第三,具有公平锁功能。
先来了解下java线程的中断。在java中,线程的中断,也就是调用某个线程的interrupt方法时,并不表示中断该线程,只是改变了线程的中断标识。这里说的中断线程意思为停止线程,但是java线程中的stop方法是不建议使用的,原因是这个方法会当机立断得让当前线程直接停止,并抛出一个ThreadDeath异常,而我们希望的是当需要停止一个线程时,起码要对当前线程做一些善后的清理工作, interrupt方法可以部分情况下满足这样的需求。一个线程被调用了interrupt方法之后,如果线程正在执行的是正常逻辑,则不会产生影响,如果这个线程正在执行wait,sleep或者join方法,它则会抛出来一个InterruptedException。根据interrupt对线程的这些影响,就可以为线程编写合适的停止方法:
package com.tang.thread;
public class ThreadInterrupt extends Thread{
public static void main(String[] args) throws InterruptedException {
final Thread t1 = new ThreadInterrupt();
t1.start();
Thread.sleep(5000);
System.out.println("断了它");
t1.interrupt();
}
public void run(){
acccess();
}
public synchronized void acccess(){
String name = Thread.currentThread().getName();
System.out.println(name+" 1");
try {
while(!isInterrupted()){
long time = System.currentTimeMillis();
System.out.println(time);
while(System.currentTimeMillis() - time < 1000){
}
Thread.currentThread().sleep(20);
}
}catch (InterruptedException e) {
System.out.println("被断了");
}finally{
//清理工作
}
}
}
输出为:
Thread-0 1
1269596988230
1269596989246
1269596990261
1269596991277
1269596992293
断了它
被断了
上述例子通过对一个线程执行interrupt操作使得线程可以安全得结束。然而,对于某些线程阻塞操作,JVM并不会自动抛出InterruptedException异常。
1)java.io中的异步socket I/O
读写socket的时候,InputStream和OutputStream的read和write方法会阻塞等待,但不会响应java中断。不过,调用Socket的close方法后,被阻塞线程会抛出SocketException异常。
2)利用Selector实现的异步I/O
如果线程被阻塞于Selector.select(在java.nio.channels中),调用wakeup方法会引起ClosedSelectorException异常。
3)锁获取
如果线程在等待获取一个内部锁,我们将无法中断它。但是,利用Lock类的lockInterruptibly方法,我们可以在等待锁的同时,提供中断能力。考虑这样一种情况:这个线程在被interrupt之前是在等待另外一个需要执行很长时间的线程所占有的锁,因为无法忍受长时间的等待而要退出,而这个时候它接收不到中断信息,那么它也就只能一直等待,等到花儿也谢了。例子如下:
这个例子当中,writer和reader竞争buffer对象,但是writer先得到了这个对象,而它偏偏是一个寿比南山的线程,于是reader就只能在那里干等。 Main方法中,使用匿名类生成的线程在5秒钟之后对reader发出中断信息,可以结果reader线程依然无动于衷,默默等待writer。于是,Lock要发挥了。
修改buffer类如下:
Reader类也做相应的修改:
Buffer类的read方法中,lock调用了lockInterruptibly方法,这个方法可以接受中断信息并抛出异常,在Reader中截获该异常就可以做一些线程结束的清理工作。
ReentrantLock还提供一个构造函数接受一个布尔型的参数,实现公平锁和非公平锁机制。关于该参数,JDK官方文档的描述如下:
“此类的构造方法接受一个可选的公平参数。当设置为 true 时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock 方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。 ”
简单来讲:公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许讨价还价,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁.
采用非公平的锁时,当一个线程释放了第一个锁以后,由于线程的抢占,刚刚被释放的锁马上被下一个线程占有。采用公平锁时,由于公平锁倾向于将访问权授予等待时间最长的线程,所以,当第一个锁被第一个线程释放以后,第二个锁马上将访问权授予第一个线程,而第一个锁将访问权授予了第二个线程。这里,公平锁在平衡分配方面耗费了一定的时间,这使得第一个线程获得第二个锁的时间优先于第二个线程获得第一个锁。这样,采用不同的锁,就出现了两种不同的结果。当线程足够多的时候,由于公平锁需要维护锁分配的均衡性,所以公平锁程序的运行时间要明显长一些。
其实同步内置的监控器锁就是不公平的,而且永远都是不公平的。而JVM保证了所有线程最终都会得到它们所等候的锁。确保统计上的公平性,对多数情况来说,这就已经足够了,而这花费的成本则要比绝对的公平保证的低得多。
说了那么多,关于使用锁机制的问题上,我的理解是,如果对线程等待公平性不是要求的那么高,使用非公平锁就可以了,如果对Lock类提供的一些新特性需求不是特别明显,则使用synchronized关键字机制就可以了。
以上是java多线程基础总结by hanmu。
参考:
http://www.blogjava.net/zhangwei217245/archive/2010/03/15/315526.html
http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html
http://www.javaeye.com/topic/417539
http://heller.javaeye.com/blog/89736
http://www.blogjava.net/zhangwei217245/archive/2010/03/15/315526.html