1.线程
几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能有多个顺序执行流,每个顺序执行流就是一个线程。
1.1 线程与进程
当一个程序进入内存运行,即变成一个进程。进程是出于运行中的程序,并且具有一定独立功能,进程是系统进行资源分配和调度的一个独立单位。
进程特征:
- 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间
- 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合
- 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会影响。(并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果)
线程时进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不再拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程里的全部资源,因此编程更加方便;但必须更加小心,我们必须确保线程不会妨碍同一进程里的其他线程。
线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行时抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。线程的调度和管理由进程本身负责完成,而不是操作系统。
简而言之:一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。
1.2 多线程的优势
线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性:多个线程将共享同一个进程虚拟空间。线程共享的环境包括:进程代码段、进程的公有数据等。利用这些共享的数据,线程很容易实现相互之间的通信。
总结,使用多线程编程包含以下几个优点:
- 进程之间不能共享内存,但线程之间共享内存很容易
- 系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程效率高
- java语言内置多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了java的多线程编程
2. 线程的创建和启动
所有线程对象都必须是Thread类或其子类的实例。每条线程的作用是完成一定的任务,实际上就是执行一段程序流。Java用run()方法来封装这样一段程序流。在java中要想实现多线程,有两种手段,一种是继承Thread类,另外一种是实现Runable接口。
2.1 继承Thread类
//通过继承Thread类来创建线程类
public class FirstThread extends Thread
{
private int i ;
//重写run方法,run方法的方法体就是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
//当线程类继承Thread类时,可以直接调用getName()方法来返回当前线程的名。
//如果想获取当前线程,直接使用this即可
//Thread对象的getName返回当前该线程的名字
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
//调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
//创建、并启动第一条线程
new FirstThread().start();
//创建、并启动第二条线程
new FirstThread().start();
}
}
}
}
执行后查看输出结果,可以发现两条线程输出的i变量不连续,所以这两条线程不能共享数据。
-
注意:使用继承Thread类的方法来创建线程类,多条线程之间无法共享线程类的实例变量。
2.2 实现Runnable接口创建线程类
//通过实现Runnable接口来创建线程类
public class SecondThread implements Runnable
{
private int i ;
//run方法同样是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
//当线程类实现Runnable接口时,
//如果想获取当前线程,只能用Thread.currentThread()方法。
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
SecondThread st = new SecondThread();
//通过new Thread(target , name)方法创建新线程
new Thread(st , "新线程1").start();
new Thread(st , "新线程2").start();
}
}
}
}
实现Runnable接口的实例只是作为Thread的Target来创建Thread对象,该Thread对象才是真正的线程对象。
从执行结果可以看出,采用实现Runnable接口的方式来创建的多条线程可以共享线程类的实例属性。
- 这是因为在这种情况下,程序所创建的Runnable对象只是线程的target,而多条线程可以共享同一个target,所以多条线程可以共享一个线程类(实际上应该是线程的target类)的实例属性。
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
- 注意:启动线程使用start方法,而不是run方法!永远不要调用线程对象的run方法!调用start方法来启动线程,系统会把该run方法当成线程执行体来处理。但如果直接调用线程对象的run方法,则run方法立即就会被执行,而且在run方法返回之前,其他线程无法并发执行——也就是说系统把线程对象当成了一个普通对象,而run方法也是一个普通方法,而不是线程执行体。
3. 控制线程
3.1 join线程
Thread提供了让一个线程等待另一个线程完成的方法:join()方法。当在某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join方法加入的join线程完成为止。
public class JoinThread extends Thread
{
//提供一个有参数的构造器,用于设置该线程的名字
public JoinThread(String name)
{
super(name);
}
//重写run方法,定义线程执行体
public void run()
{
for (int i = 0; i < 10 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) throws Exception
{
//启动子线程
new JoinThread("新线程").start();
for (int i = 0; i < 10 ; i++ )
{
if (i == 5)
{
JoinThread jt = new JoinThread("被Join的线程");
jt.start();
//main线程调用了jt线程的join方法,main线程
//必须等jt执行结束才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
"被Join的线程"被join到主线程中,则当i==5时,主线程被阻塞,此时只有“新线程”和 "被Join的线程"是并发执行的了。
3.2 后台线程
有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又被称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
调用Thread对象setDaemon(true)方法可将指定线程设置成后台线程。
public class DaemonThread extends Thread
{
//定义后台线程的线程执行体与普通线程没有任何区别
public void run()
{
for (int i = 0; i < 1000 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
DaemonThread t = new DaemonThread();
//将此线程设置成后台线程
t.setDaemon(true);
//启动后台线程
t.start();
for (int i = 0 ; i < 10 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
//------程序执行到此处,前台线程(main线程)结束------
//后台线程也应该随之结束
}
}
注意:前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
前台线程死亡后,JVM会通知后台线程死亡,但从它接受指令,到它做出响应,需要一定时间。而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说setDaemon(true)必须在start()方法之前调用。
3.3 线程睡眠:sleep
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep方法。
当当前线程调用sleep方法进入阻塞状态后,在其sleep时间段内,该线程不会获得执行的机会,即使系统中没有其他可以运行的线程,处于sleep中的线程也不会运行,因此sleep方法常用来暂停程序的执行。
public class TestSleep
{
public static void main(String[] args) throws Exception
{
for (int i = 0; i < 10 ; i++ )
{
System.out.println("当前时间: " + new Date());
//调用sleep方法让当前线程暂停1s。
Thread.sleep(1000);
}
}
}
上面的代码使得主线程每停止1s后再执行。
3.4 线程让步:yield
yield方法也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当前某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
实际上:当某个线程调用了yield方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程才会获得执行的机会。
public class TestYield extends Thread
{
public TestYield()
{
}
public TestYield(String name)
{
super(name);
}
//定义run方法作为线程执行体
public void run()
{
for (int i = 0; i < 50 ; i++ )
{
System.out.println(getName() + " " + i);
//当i等于20时,使用yield方法让当前线程让步
if (i == 20)
{
Thread.yield();
}
}
}
public static void main(String[] args) throws Exception
{
//启动两条并发线程
TestYield ty1 = new TestYield("高级");
//将ty1线程设置成最高优先级
//ty1.setPriority(Thread.MAX_PRIORITY);
ty1.start();
TestYield ty2 = new TestYield("低级");
//将ty1线程设置成最低优先级
//ty2.setPriority(Thread.MIN_PRIORITY);
ty2.start();
}
}
通常不要依靠yield方法来控制并发线程的执行。
3.5 改变线程优先级
每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。
public class PriorityTest extends Thread
{
public PriorityTest(){}
//定义一个有参数的构造器,用于创建线程时指定name
public PriorityTest(String name)
{
super(name);
}
public void run()
{
for (int i = 0 ; i < 50 ; i++ )
{
System.out.println(getName() + ",其优先级是:"
+ getPriority() + ",循环变量的值为:" + i);
}
}
public static void main(String[] args)
{
//改变主线程的优先级
Thread.currentThread().setPriority(6);
for (int i = 0 ; i < 30 ; i++ )
{
if (i == 10)
{
PriorityTest low = new PriorityTest("低级");
low.start();
System.out.println("创建之初的优先级:" + low.getPriority());
//设置该线程为最低优先级
low.setPriority(Thread.MIN_PRIORITY);
}
if (i == 20)
{
PriorityTest high = new PriorityTest("高级");
high.start();
System.out.println("创建之初的优先级:" + high.getPriority());
//设置该线程为最高优先级
high.setPriority(Thread.MAX_PRIORITY);
}
}
}
}
注:虽然java提供了10个级别的优先级(1-10),但这些优先级级别需要操作系统的支持。不幸的是,不同的操作系统优先级并不相同。所以我们应该尽量避免直接为线程指定优先级,而应该使用MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY三个静态常量来设置优先级,这样才可以保证程序具有最好的可移植性。
4. 线程同步
当使用多个线程来访问同一个数据时,非常容易出现线程安全问题,这是由于系统的线程调度具有一定的随机性。
4.1 同步代码块
之所以出现线程安全问题,是因为run方法的方法体不具有同步安全性。为了解决这个问题,java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块语法如下:
synchronized(obj){
...
//此处的代码就是同步代码块
}
上面语法格式中的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须获得对同步监视器的锁定。任何时刻只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监视器的锁定。
- 虽然java程序允许使用任何对象来作为同步监视器,但想一下同步监视器的目的:阻止两条线程对同一个共享资源进行并发访问。因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
以取款为例:
实体Account:
public class Account
{
//封装账户编号、账户余额两个属性
private String accountNo;
private double balance;
public Account(){}
//构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
public void setBalance(double balance)
{
this.balance = balance;
}
public double getBalance()
{
return this.balance;
}
//下面两个方法根据accountNo来计算Account的hashCode和判断equals
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if (obj != null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
取款线程类DrawThread:
public class DrawThread extends Thread
{
//模拟用户账户
private Account account;
//当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name , Account account ,
double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//当多条线程修改同一个共享数据时,将涉及到数据安全问题。
public void run()
{
//使用account作为同步监视器,任何线程进入下面同步代码块之前,
//必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
//这种做法符合:加锁-->修改完成-->释放锁 逻辑
synchronized (account)
{
//账户余额大于取钱数目
if (account.getBalance() >= drawAmount)
{
//吐出钞票
System.out.println(getName() +
"取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
}
else
{
System.out.println(getName() + "取钱失败!余额不足!");
}
}
}
}
测试类TestDraw:
public class TestDraw
{
public static void main(String[] args)
{
//创建一个账户
Account acct = new Account("1234567" , 1000);
//模拟两个线程对同一个账户取钱
new DrawThread("甲" , acct , 800).start();
new DrawThread("乙" , acct , 800).start();
}
}
对上面的程序,我们就考虑使用用户账户(Account)作为同步监视器。
4.2 同步方法
与同步代码块对应的,java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于同步方法而言,无须显示指定同步监视器,同步方法的同步监视器是this,也就是该对象本身。
通过使用同步方法可以非常方便地将某类变成线程安全的类,线程安全的类的特征如下:
- 该类的对象可以被多个线程安全的访问
- 每个线程调用该类的任意方法之后都将得到正确的结果
- 每个线程调用该类的任意方法之后,该对象状态依然保持合理状态
上面取钱的例子中Account就是一个线程不安全的类,当两个线程同时修改Account的balance属性时,程序就出现了异常,现在将Account类变成一个线程安全的类,只需要把修改balance的方法修饰成同步方法即可。
Account:
public class Account
{
private String accountNo;
private double balance;
public Account(){}
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
public double getBalance()
{
return this.balance;
}
public synchronized void draw(double drawAmount)
{
//账户余额大于取钱数目
if (balance >= drawAmount)
{
//吐出钞票
System.out.println(Thread.currentThread().getName() +
"取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
//修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName() +
"取钱失败!余额不足!");
}
}
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if (obj != null && obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
上面程序将代表取钱的方法draw()使用了synchronized修改为同步方法了,同步方法的同步监视器是this,就当前对象(Account的实例),因此对于同一个Account而言,任意时刻只能有一条线程获得对Account对象的锁定,然后进入draw方法进行取钱,这样既可保证多条线程并发取钱时的线程安全了。此时,取钱的线程类DrawThread,只需要调用Account对象的draw方法既可。
DrawThread:
public class DrawThread extends Thread
{
//模拟用户账户
private Account account;
//当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name , Account account ,
double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//当多条线程修改同一个共享数据时,将涉及到数据安全问题。
public void run()
{
account.draw(drawAmount);
}
}
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,可以采用如下策略:
- 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(共享资源)的方法进行同步。例如Account的balance属性
- 如果可变类有两种运行环境:单线程、多线程环境,则应该为该可变类提供两种版本:线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。
4.3 同步锁
java提供了另外一种线程同步的机制:它通过显示定义同步锁对象来实现同步,在这种机制下,同步锁应该使用Lock对象充当。
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。不过,某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁)。在实现线程安全的控制中,通常喜欢使用ReentrantLock(可重入锁)。
通过Lock对象我们可以将Account修改为如下形式,它依然是线程安全的:
Account:
public class Account
{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
private String accountNo;
private double balance;
public Account(){}
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
public double getBalance()
{
return this.balance;
}
public void draw(double drawAmount)
{
lock.lock();
try
{
//账户余额大于取钱数目
if (balance >= drawAmount)
{
//吐出钞票
System.out.println(Thread.currentThread().getName() +
"取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
//修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName() +
"取钱失败!余额不足!");
}
}
finally
{
lock.unlock();
}
}
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if (obj != null && obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
- ReentrantLock锁具有重入性,也就是说线程可以对它已经加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计算器来追踪lock方法的嵌套调用,线程在每次调用lock()加锁后,必须显示调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
4.4 死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,JVM没有检测,也没用采用措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁的出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给任何提示,只是所有线程处于阻塞状态,无法继续。死锁是很容易发生的,尤其在系统中出现多个同步监视器的情况下。
class A
{
public synchronized void foo( B b )
{
System.out.println("当前线程名: " +
Thread.currentThread().getName() + " 进入了A实例的foo方法" );
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " +
Thread.currentThread().getName() + " 企图调用B实例的last方法");
b.last();
}
public synchronized void last()
{
System.out.println("进入了A类的last方法内部");
}
}
class B
{
public synchronized void bar( A a )
{
System.out.println("当前线程名: "
+ Thread.currentThread().getName() + " 进入了B实例的bar方法" );
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: "
+ Thread.currentThread().getName() + " 企图调用A实例的last方法");
a.last();
}
public synchronized void last()
{
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable
{
A a = new A();
B b = new B();
public void init()
{
Thread.currentThread().setName("主线程");
//调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run()
{
Thread.currentThread().setName("副线程");
//调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args)
{
DeadLock dl = new DeadLock();
//以dl为target启动新线程
new Thread(dl).start();
//执行init方法作为新线程
dl.init();
}
}
上述程序中,主线程调用A对象的同步方法foo()时对A进行加锁,sleep过后,副线程得到执行调用B对象的同步方法bar(),sleep过后,主线程继续执行。此时,主线程需要访问B对象的last()方法,由于此时B对象被副线程加锁了,无法访问,而副线程由于同样的原因也无法访问A对象的last()方法。这样,两个线程都在等待对方先释放锁,程序就“僵住”了,无法执行下去,也不会报任何异常。
5 线程通信
5.1 线程的协调运行
可以借助Object类提供的wait()、notify()、notifyAll()三个方法。这三个方法必须由同步监视器对象来调用,可以分为两种情况:
- 对于同步方法,因为同步监视器就是this,所以可以在同步方法中直接调用这三个方法
- 对于同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法
关于这三个方法的解释:
- wait():调用wait()方法的当前线程会释放对该同步监视器的锁定,直到其他线程调用该同步监视器的notify()或notifyAll()方法来唤醒该线程
- notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会随机选择唤醒其中一个线程。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程
- notifyAll():唤醒在此同步监视器上等待的所有线程
以一个取款和存款的线程为例,存款后立即取款,不允许多次存款或多次取款。
Account:
public class Account
{
private String accountNo;
private double balance;
//标识账户中是否已有存款的旗标
private boolean flag = false;
public Account(){}
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
public double getBalance()
{
return this.balance;
}
public synchronized void draw(double drawAmount)
{
try
{
//如果flag为假,表明账户中还没有人存钱进去,则取钱方法阻塞
if (!flag)
{
wait();
}
else
{
//执行取钱
System.out.println(Thread.currentThread().getName() +
" 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
//将标识账户是否已有存款的旗标设为false。
flag = false;
//唤醒其他线程
notifyAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
}
public synchronized void deposit(double depositAmount)
{
try
{
//如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
if (flag)
{
wait();
}
else
{
//执行存款
System.out.println(Thread.currentThread().getName() +
" 存款:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
//将表示账户是否已有存款的旗标设为true
flag = true;
//唤醒其他线程
notifyAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
}
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if (obj != null && obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
存款线程DepositThread:
public class DepositThread extends Thread
{
//模拟用户账户
private Account account;
//当前取钱线程所希望存款的钱数
private double depositAmount;
public DepositThread(String name , Account account ,
double depositAmount)
{
super(name);
this.account = account;
this.depositAmount = depositAmount;
}
//重复100次执行存款操作
public void run()
{
for (int i = 0 ; i < 100 ; i++ )
{
account.deposit(depositAmount);
}
}
}
取款线程DrawThread:
public class DrawThread extends Thread
{
//模拟用户账户
private Account account;
//当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name , Account account ,
double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//重复100次执行取钱操作
public void run()
{
for (int i = 0 ; i < 100 ; i++ )
{
account.draw(drawAmount);
}
}
}
测试类TestDraw:
public class TestDraw
{
public static void main(String[] args)
{
//创建一个账户
Account acct = new Account("1234567" , 0);
new DrawThread("取钱者" , acct , 800).start();
new DepositThread("存款者甲" , acct , 800).start();
new DepositThread("存款者乙" , acct , 800).start();
new DepositThread("存款者丙" , acct , 800).start();
}
}
程序最后会被阻塞,这是由于一共有三个存款者而只有一个取款者,300次存款操作只有100次取款操作,存款线程需要取款线程释放同步监视器锁,故而存款操作无法继续下去,所以最后被阻塞。注意,阻塞并不是死锁。
5.2 使用条件变量控制协调
如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器对象,也就不能使用wait、notify、notifyAll方法来协调进程的运行。
当使用Lock对象来保证同步时,java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。
Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。实例实质上被绑定到一个Lock对象上,要获得特定Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。Condition类提供了三个方法:
- await():同wait()
- signal:同notify()
- signalAll:同notifyAll()
Account:
public class Account
{
//显示定义Lock对象
private final Lock lock = new ReentrantLock();
//获得指定Lock对象对应的条件变量
private final Condition cond = lock.newCondition();
private String accountNo;
private double balance;
//标识账户中是否已经存款的旗标
private boolean flag = false;
public Account(){}
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
public double getBalance()
{
return this.balance;
}
public void draw(double drawAmount)
{
//加锁
lock.lock();
try
{
//如果账户中还没有存入存款,该线程等待
if (!flag)
{
cond.await();
}
else
{
//执行取钱操作
System.out.println(Thread.currentThread().getName() +
" 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
//将标识是否成功存入存款的旗标设为false
flag = false;
//唤醒该Lock对象对应的其他线程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
//使用finally块来确保释放锁
finally
{
lock.unlock();
}
}
public void deposit(double depositAmount)
{
lock.lock();
try
{
//如果账户中已经存入了存款,该线程等待
if(flag)
{
cond.await();
}
else
{
//执行存款操作
System.out.println(Thread.currentThread().getName() +
" 存款:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
//将标识是否成功存入存款的旗标设为true
flag = true;
//唤醒该Lock对象对应的其他线程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
//使用finally块来确保释放锁
finally
{
lock.unlock();
}
}
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if (obj != null && obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
执行效果和前一个方法一样。
5.3 使用管道流
管道流有3种形式:PipedInputStream和PipedOutputStream,PipedReader和PipedWriter以及Pipe.SinkChannel和Pipe.SourceChannel,它们分别是管道字节流、管道字符流和新IO的管道Channel。
class ReaderThread extends Thread
{
private PipedReader pr;
//用于包装管道流的BufferReader对象
private BufferedReader br;
public ReaderThread(){}
public ReaderThread(PipedReader pr)
{
this.pr = pr;
this.br = new BufferedReader(pr);
}
public void run()
{
String buf = null;
try
{
//逐行读取管道输入流中的内容
while ((buf = br.readLine()) != null)
{
System.out.println(buf);
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
//使用finally块来关闭输入流
finally
{
try
{
if (br != null)
{
br.close();
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
}
class WriterThread extends Thread
{
String[] books = new String[]
{
"Struts2权威指南",
"ROR敏捷开发指南",
"基于J2EE的Ajax宝典",
"轻量级J2EE企业应用指南"
};
private PipedWriter pw;
public WriterThread(){}
public WriterThread(PipedWriter pw)
{
this.pw = pw;
}
public void run()
{
try
{
//循环100次,向管道输出流中写入100个字符串
for (int i = 0; i < 100 ; i++)
{
pw.write(books[i % 4] + "\n");
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
//使用finally块来关闭管道输出流
finally
{
try
{
if (pw != null)
{
pw.close();
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
}
public class PipedCommunicationTest
{
public static void main(String[] args)
{
PipedWriter pw = null;
PipedReader pr = null;
try
{
//分别创建两个独立的管道输出流、输入流
pw = new PipedWriter();
pr = new PipedReader();
//连接管道输出流、出入流
pw.connect(pr);
//将连接好的管道流分别传入2个线程,
//就可以让两个线程通过管道流进行通信
new WriterThread(pw).start();
new ReaderThread(pr).start();
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
通常没有必要使用管道流来控制两个线程之间的通信,因为两个线程属于同一个进程,它们可以非常方便的共享数据,这种方式才是线程之间进行信息交换的最好方式,而不是使用管道流。
6 ThreadLocal类
线程局部变量(ThreadLocal)的功能非常简单,就是为每一个使用该变量的线都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。
class Account
{
/*定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量
每个线程都会保留该变量的一个副本*/
private ThreadLocal<String> name =
new ThreadLocal<String>();
//定义一个初始化name属性的构造器
public Account(String name)
{
this.name.set(name);
//下面代码看到输出“初始名”
System.out.println("------" + this.name.get());
}
//定义了name属性的setter和getter方法
public String getName()
{
return name.get();
}
public void setName(String str)
{
this.name.set(str);
}
}
class MyTest extends Thread
{
//定义一个Account属性
private Account account;
public MyTest(Account account, String name)
{
super(name);
this.account = account;
}
public void run()
{
//循环10次
for (int i = 0 ; i < 10 ; i++)
{
//当i == 6时输出将账户名替换成当前线程名
if (i == 6)
{
account.setName(getName());
}
//输出同一个账户的账户名和循环变量
System.out.println(account.getName()
+ " 账户的i值:" + i);
}
}
}
public class ThreadLocalTest
{
public static void main(String[] args)
{
//启动两条线程,两条线程共享同一个Account
Account at = new Account("初始名");
/*
虽然两条线程共享同一个账户,即只有一个账户名
但由于账户名是ThreadLocal类型的,所以两条线程将
导致有同一个Account,但有两个账户名的副本,每条线程
都完全拥有各自的账户名副本,所以从i == 6之后,将看到两条
线程访问同一个账户时看到不同的账户名。
*/
new MyTest(at , "线程甲").start();
new MyTest(at , "线程乙").start();
}
}
同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上避免了多个线程之间的共享资源(变量),也就不需要对多个线程进行同步了。
7 包装线程不安全的集合
java集合ArrayList、LinkedList、HashSet、TreeSet、HashMap等都是线程不安全的,也就是有可能当多个线程向这些集合中放入一个元素时,可能会破坏这些集合数据的完整性。
如果有多条线程可能访问以上这些集合,我们可以使用Collections提供的静态方法来把这些集合包装成线程安全的集合。
如:
//使用Collections的synchronizedMap方法将一个普通的HashMap包装成线程安全的类
HashMap hashMap = Collections.synchronizedMap(new HashMap());