与线程同步以及线程调度相关的方法
wait()
:使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep()
:使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
notify()
:唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll()
:唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
补充:Java 5通过Lock接口提供了显式的锁机制(explicit lock),增强了灵活性以及对线程的协调。Lock接口中定义了加锁(
lock()
)和解锁(unlock()
)的方法,同时还提供了newCondition()
方法来产生用于线程之间通信的Condition对象;此外,Java 5还提供了信号量机制(semaphore),信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()
方法);在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()
方法)。
显示锁
基于synchronized关键字的锁机制有以下问题:
- 锁只有一种类型,而且对所有同步操作都是一样的作用
- 锁只能在代码块或方法开始的地方获得,在结束的地方释放
- 线程要么得到锁,要么阻塞,没有其他的可能性
Java 5对锁机制进行了重构,提供了显示的锁,这样可以在以下几个方面提升锁机制:
- 可以添加不同类型的锁,例如读取锁和写入锁
- 可以在一个方法中加锁,在另一个方法中解锁
- 可以使用tryLock方式尝试获得锁,如果得不到锁可以等待、回退或者干点别的事情,当然也可以在超时之后放弃操作
显示的锁都实现了java.util.concurrent.Lock接口,主要有两个实现类:
- ReentrantLock - 比synchronized稍微灵活一些的重入锁
- ReentrantReadWriteLock - 在读操作很多写操作很少时性能更好的一种重入锁
只有一点需要提醒,解锁的方法unlock的调用最好能够在finally块中,因为这里是释放外部资源最好的地方,当然也是释放锁的最佳位置,因为不管正常异常可能都要释放掉锁来给其他线程以运行的机会。
如何使用显示锁
下面的例子演示了100个线程同时向一个银行账户中存入1元钱,在没有使用同步机制和使用同步机制情况下的执行情况。
银行账户类:
|
|
存钱线程类:
|
|
测试类:
|
|
在没有同步的情况下,执行结果通常是显示账户余额在10元以下,出现这种状况的原因是,当一个线程A试图存入1元的时候,另外一个线程B也能够进入存款的方法中,线程B读取到的账户余额仍然是线程A存入1元钱之前的账户余额,因此也是在原来的余额0上面做了加1元的操作,同理线程C也会做类似的事情,所以最后100个线程执行结束 ,本来期望账户余额为100元,但实际得到的通常在10元以下(很可能是1元哦)。解决这个问题的办法就是同步,当一个线程对银行账户存钱时,需要将此账户锁定,待其操作完成后才允许其他的线程进行操作,代码有如下几种调整方案:
在银行账户的存款(deposit)方法上同步(synchronized)关键字
synchronized关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用
synchronized(对象) { … }定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符。
|
|
- 在线程调用存款方法时对银行账户进行同步
|
|
- 通过Java 5显式的锁机制,为每个银行账户创建一个锁对象,在存款操作进行加锁和解锁的操作
|
|
按照上述三种方式对代码进行修改后,重写执行测试代码Test01,将看到最终的账户余额为100元。当然也可以使
用Semaphore或CountdownLatch来实现同步。
编写多线程程序的几种实现方式
Java 5以前实现多线程有两种实现方法:一种是继承Thread类;另一种是实现Runnable接口。两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。
补充:Java 5以后创建线程还有第三种方式:实现Callable接口,该接口中的call方法可以在线程执行结束时
产生一个返回值,代码如下所示:
|
|