Java多线程

Java多线程编程

Java线程的创建和启动

继承Thread类创建线程类

  1. 继承Thread类,并重写run()方法,该run()方法的方法体代表线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即线程对象。
  3. 调用线程对象的start()来启动线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class FirstThread extends Thread
{
private int i ;
// 重写run方法,run方法的方法体就是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
// 当线程类继承Thread类时,直接使用this即可获取当前线程
// Thread对象的getName()返回当前该线程的名字
// 因此可以直接调用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();
}
}
}
}

Java程序运行时默认的主线程总共是main()方法的方法体。

  • Thread.currentThread(), Thread类的静态方法,总是返回当前正在执行的线程对象。
  • getName(): Thread类的实例方法,返回调用该方法的线程名字。

使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。

实现Runnable接口创建线程类

  1. 定义Runnable接口的实现类,重写run()方法。
  2. 创建Runnable实现类的实例,并依次实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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对象只是线程的target,多个线程可共享一个target,所以可以共享这个实例的实例变量。

使用Callable和Future创建线程

类似与Runnable接口的用法,Callable接口提供了一个call()方法作为线程执行体,不同的是,call()可以有返回值。,且可以声明抛出异常。

Future接口代表Callable接口call()方法的返回值,并为接口提供了一个FutureTask的实现了,该实现类实现了Future接口,实现了Runnable接口—可以作为Thread类的target。

几个方法:

  • boolean cancle(boolean mayInterruptIfRunning)
  • V get(): 返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。
  • V get(long timeout, TimeUnit unit)
  • boolean isCancelled()
  • boolean isDone()

步骤:

  1. 创建Callable接口实现类,并实现call()方法
  2. 使用FutureTask类包装Callable对象
  3. 使用FutureTask对象作为Thread对象的target创建并启动线程
  4. 通过调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ThirdThread implements Callable<Integer>
{
// 实现call方法,作为线程执行体
public Integer call()
{
int i = 0;
for ( ; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" + i);
}
// call()方法可以有返回值
return i;
}

public static void main(String[] args)
{
// 创建Callable对象
ThirdThread rt = new ThirdThread();
// 使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>(rt);
for (int i = 0 ; i < 100 ; i++)
{
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" + i);
if (i == 20)
{
// 实质还是以Callable对象来创建、并启动线程
new Thread(task , "有返回值的线程").start();
}
}
try
{
// 获取线程返回值
System.out.println("子线程的返回值:" + task.get());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}

线程的生命周期

新建-就绪-运行—(就绪)-运行-阻塞-(就绪)-死亡

新建和就绪状态

当程序使用new关键字创建了一个线程后,该线程就处于新建状态。
只有当线程对象调用了start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,但并没有直接运行,只是可以运行了。至于何时运行,取决于线程调度器的调度。

启动线程要使用start()方法,不是run()方法,否则只是一个普通的方法调用!

运行和阻塞状态

现代操作系统都采用抢占式调度策略,也就是系统会给每个可执行的线程一个小时间片段来处理任务,该时间段用完后,系统就会剥夺该线程所占用的资源,将其放入就绪状态,然其他线程获得执行机会。如果线程执行时被阻塞,系统也会将资源重新给其他线程。

发生如下情况时,线程将会进入阻塞状态:

  • 线程调用sleep()方法主动放弃占用的处理器资源
  • 线程调用了阻塞式IO方法,在该方法返回前,该线程被阻塞。
  • 线程试图获得一个同步监视器,但该监视器正在被其他线程所持有。
  • 线程在等待某个notify
  • 程序调用了线程的suspend()将其挂起。

当前正在执行的线程被阻塞后,其他线程就可以获得执行的机会。被阻塞的线程会在阻塞基础后重新进入就绪状态,等待再次被调用。

针对上面几种情况,下面情况可以解除上面的阻塞:

  • sleep()方法的时间超过
  • 线程调用的阻塞式IO方法已经返回。
  • 线程成功获得试图取得的同步监视器
  • 获得notify
  • 挂起的线程被调用了resume()恢复方法。

注意yield()方法是直接放弃资源重新进入就绪队列。

线程死亡

几种情况:

  • run()或call()方法执行完成。
  • 线程抛出一个未捕获的Exception或Error。
  • 直接调用该线程的stop()方法。

当主线程结束时,其他线程不收任何影响,并不会随之结束。一旦子线程启动起来,就拥有和主线程相同的地位,不会受主线程的影响。

isAlive(),新建,死亡时返回true,其他返回false。

不能再start已经启动并死亡的线程。

控制线程

join线程

Thread提供了让一个线程等待另一个线程执行完的方法—join(). 当某个程序执行流中调用其他线程的join()方法时,调用线程即被阻塞,直到被join()方法加入的join线程执行完为止.

通常有使用线程的程序调用,将大问题划分为许多小问题,每个小问题分配一个线程。

  • join()
  • join(long millis)
  • join(long millis, int nanos)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class JoinThread extends Thread
{
// 提供一个有参数的构造器,用于设置该线程的名字
public JoinThread(String name)
{
super(name);
}
// 重写run方法,定义线程执行体
public void run()
{
for (int i = 0; i < 100 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)throws Exception
{
// 启动子线程
new JoinThread("新线程").start();
for (int i = 0; i < 100 ; i++ )
{
if (i == 20)
{
JoinThread jt = new JoinThread("被Join的线程");
jt.start();
// main线程调用了jt线程的join()方法,main线程
// 必须等jt执行结束才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
}
}

后台进程

后台线程的任务是为其他线程提供服务。如果所有前台线程都死亡,后台线程就会自动死亡。

必须要在启动前设置。

  • setDaemon(true)
  • isDaemon()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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线程)结束------
// 后台线程也应该随之结束
}
}

线程睡眠:sleep

让当前正在执行的线程暂停一段时间,并进入阻塞状态。

  • static void sleep(long millis)
  • static void sleep(long millis, int nanos)
1
2
3
4
5
6
7
8
9
10
11
12
13
public class SleepTest
{
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);
}
}
}

线程让步:yield

放弃资源,但不会进入阻塞状态,有可能马上又得到调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class YieldTest extends Thread
{
public YieldTest(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
{
// 启动两条并发线程
YieldTest yt1 = new YieldTest("高级");
// 将ty1线程设置成最高优先级
// yt1.setPriority(Thread.MAX_PRIORITY);
yt1.start();
YieldTest yt2 = new YieldTest("低级");
// 将yt2线程设置成最低优先级
// yt2.setPriority(Thread.MIN_PRIORITY);
yt2.start();
}
}

改变线程优先级

setPriority(int newPriority)
getPriotiry()

优先级为1-10之间,最好使用下面几个静态常量

  • MAX_PRIORITY: 10
  • MIN_PRIORITY: 0
  • NORM_PRIORITY: 5

高优先级的线程将会获得更多的执行机会。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class PriorityTest extends Thread
{
// 定义一个有参数的构造器,用于创建线程时指定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);
}
}
}
}

线程同步

由于线程调度的随机性,当多个线程需要访问并修改同一个数据时,很容易出现线程安全问题。

同步代码块

1
2
3
synchronized(obj) {
// 同步代码块
}

obj就是同步监视器,线程在执行同步代码块之前,必须先获得对同步监视器的锁定。

任何时刻只能有一个线程可以获得对同步监视器的锁定,执行完成后,则会释放该同步监视器的锁定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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() + "取钱失败!余额不足!");
}
}
//同步代码块结束,该线程释放同步锁
}
}

任何时候只有一个线程可以进入修改共享资源的代码区(临界区)。

同步方法

对于synchronized修饰的实例方法(非static方法),无需显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。

使用同步方法可以非常方便的实现线程安全的类,该类的对象可以被多个线程安全地访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class Account
{
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}

// 提供一个线程安全draw()方法来完成取钱操作
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()
+ "取钱失败!余额不足!");
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

synchronized可以修饰代码块,方法,但不能修饰变量,构造器

释放同步监视器

无法显式释放,下面几种情况会释放对同步监视器的锁定:

  • 当前线程同步方法,同步代码块执行结束
  • 当前线程在同步方法,同步代码中遇到break,retrun终止了该代码块,该方法继续执行
  • 当前线程在同步方法,同步代码出现了未处理的Error或Exception
  • 当前线程在同步方法,同步代码中调用了同步检索器的wait()方法,则当前线程暂停,释放同步监视器。

下面情况不会释放:

  • 当前线程调用Thread.sleep(), Thread.yield()方法来暂停当前线程的执行
  • 其他线程调用了该线程的suspend()方法将该线程挂起。

同步锁(Lock)

通过显式定义同步锁对象来实现同步,这种机制下,同步锁由Lock对象充当。

某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock,ReadWriteLock是Java 5的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。

比较常用的是ReentrantLock。可以显式的加锁,释放锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public class Account
{
// 定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}

// 提供一个线程安全draw()方法来完成取钱操作
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();
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

对应一个Account对象,同一时刻只能有一个线程进入临界区。

当获取了多个锁时,必须已相反的顺序释放,且必须在所有锁被获取的相同的范围内释放所有锁。

ReetrantLock锁具有可重入性,也就是说一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用。

死锁

两个线程互相等待对方释放同步监视器就会发生死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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();
}
}

线程通信

当线程在系统内运行时,线程调度具有一定的透明性,程序通常无法准确控制线程的轮换执行。但Java也提供了一些机制来保证线程协调运行。

传统的线程通信

Oject类提供了wait(), notify(), notifyAll()三个方法。这三个方法必须由同步监视器对象来调用,这可分为一下两种情况。

  • synchroninzed, 因为该类默认实例(this)就是同步监视器,所以可以在同步方法直接调用这三个方法。
  • 对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这个三个方法(只适用于Synchronized同步的线程)。

  • wait(), 导致当前线程释放同步监视器并阻塞等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。可以带时间参数。

  • notify(): 唤醒在此同步监视器上等待的一个线程,如果有多个线程在等待(调用了wait方法的线程),则会随机选择唤醒其中一个线程,重新进入尝试获得锁的队列。
  • notiifyAll(): 唤醒在此同步监视器等待的所有线程(调用了wait方法的线程),重新进入尝试获得锁的队列。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public class Account
{
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
//标识账户中是否已有存款的旗标
private boolean flag = false;

public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
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();
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

使用Condition控制线程通信

如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则不存在隐式的同步监视器,也就是不能用上面的方法进行线程通信了。

当使用Lock对象来保证同步时,Java提供了一个Condition类,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。

Condition实例被绑定在一个Lock对象上,可以通过调用Lock对象的newCondition()方法即可。Condition类提供了如下三个方法:

  • await(): 类似与隐式同步监视器上的wait()方法,导致当前线程释放锁并等待,直到其他线程调用该Condition的signal()方法或signalAll()方法来唤醒该线程。
  • signal(): 唤醒在此lock对象上等待的单个线程(获得锁又通过调用await()的放弃锁的线程)。如果有多个线程,则随机选择一个。
  • signalAll(): 唤醒在此lock对象上等待的所有线程(获得锁又通过调用await()的放弃锁的线程)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
public class Account
{
// 显式定义Lock对象
private final Lock lock = new ReentrantLock();
// 获得指定Lock对象对应的Condition
private final Condition cond = lock.newCondition();
// 封装账户编号、账户余额两个Field
private String accountNo;
private double balance;
//标识账户中是否已有存款的旗标
private boolean flag = false;

public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}

public void draw(double drawAmount)
{
// 加锁
lock.lock();
try
{
// 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
if (!flag)
{
cond.wait();
}
else
{
// 执行取钱
System.out.println(Thread.currentThread().getName()
+ " 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
// 将标识账户是否已有存款的旗标设为false。
flag = false;
// 唤醒其他线程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 使用finally块来释放锁
finally
{
lock.unlock();
}
}
public void deposit(double depositAmount)
{
lock.lock();
try
{
// 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
if (flag) //①
{
cond.wait();
}
else
{
// 执行存款
System.out.println(Thread.currentThread().getName()
+ " 存款:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
// 将表示账户是否已有存款的旗标设为true
flag = true;
// 唤醒其他线程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 使用finally块来释放锁
finally
{
lock.unlock();
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

使用阻塞队列(BlockingQueue)控制线程通信

BlockingQueue的主要用途是作为线程同步的工具,当生产者线程试图向BlockingQueue中放入元素时,如果队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程被阻塞。

  • put(E e)
  • take()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class Producer extends Thread
{
private BlockingQueue<String> bq;
public Producer(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
String[] strArr = new String[]
{
"Java",
"Struts",
"Spring"
};
for (int i = 0 ; i < 999999999 ; i++ )
{
System.out.println(getName() + "生产者准备生产集合元素!");
try
{
Thread.sleep(200);
// 尝试放入元素,如果队列已满,线程被阻塞
bq.put(strArr[i % 3]);
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "生产完成:" + bq);
}
}
}
class Consumer extends Thread
{
private BlockingQueue<String> bq;
public Consumer(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
while(true)
{
System.out.println(getName() + "消费者准备消费集合元素!");
try
{
Thread.sleep(200);
// 尝试取出元素,如果队列已空,线程被阻塞
bq.take();
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "消费完成:" + bq);
}
}
}
public class BlockingQueueTest2
{
public static void main(String[] args)
{
// 创建一个容量为3的BlockingQueue
BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
// 启动3条生产者线程
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
// 启动一条消费者线程
new Consumer(bq).start();
}
}

线程组和未处理的异常

线程池

线程相关类