Java基础知识(三)
一、线程状态转化
线程状态生命周期如下:
- 新建状态(New):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的
start()
方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。 - 运行状态(Runnning):就绪状态的线程获取了CPU,执行程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某个原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- 等待阻塞(Waiting):运行的线程执行
wait()
方法,JVM会把该线程放入等待池中。 - 同步阻塞(Blocked):运行的线程在获取对象的同步锁时,若该同步锁被别的线程占有,则JVM会把该线程放入锁池中。
- 超时阻塞(Time_Waiting):运行的线程执行
sleep(long)
或join(long)
方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
线程状态转化
- 等待阻塞(Waiting):运行的线程执行
- 死亡状态(Dead):线程执行完了或者因异常退出了
run()
方法,该线程结束生命周期。
相关方法简单介绍:
Thread.sleep(long):使当前线程进入阻塞状态,在指定时间内暂停执行,但不会释放"锁标志"。
Object.wait()、Object.wait(long):使当前线程处于等待状态,会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。wait()
和notify()
必须在synchronized函数或synchronized方法代码块中进行调用。如果没在里面执行,虽然编译通过,但在运行时会发生lllegalMonitorStateException
的异常。
Object.notifyAll():则从对象等待池中唤醒所有等待线程。
Object.notify():则从对象等待池中唤醒其中一个线程。
Object.yield():只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入可执行状态后马上又被执行,yield()只能使同优先级或更高优先级的线程有执行的机会。
二、线程同步解决方案
先来一个线程不安全的例子:
public class Ticket implements Runnable {
//当前拥有的票数
private int num = 100;
public void run() {
while(true) {
if(num>0) {
try{
Thread.sleep(10);
}catch (InterruptedException e){
}
//输出卖票信息
System.out.println(Thread.currentThread().getName()+".....sale...."+num--);
}
}
}
}
上面是卖票线程类,下来再来看看执行类:
public class TicketDemo {
public static void main(String[] args) {
Ticket t = new Ticket();//创建一个线程任务对象。
//创建4个线程同时卖票
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行程序结果如下(仅截取部分数据):
线程不安全运行结果
从运行结果,我们就可以看出我们4个售票窗口同时卖出了1号票,这显然是不合逻辑的,其实这个问题就是我们前面所说的线程同步问题。不同的线程都对同一个数据进了操作这就容易导致数据错乱的问题,也就是线程不同步。那么这个问题该怎么解决呢?
在java中有两种机制可以防止线程不安全的发生,java语言提供了一个synchronized关键字来解决这问题,同时在Java SE5.0引入Lock锁对象的相关类。
2.1 通过锁(Lock)对象的方式
Lock在使用过程中,需要显式地获取和释放锁。Lock接口的主要API如下:
方法 | 相关描述内容 |
---|---|
void lock() | 调用该方法,当前线程会获取锁对象 |
void lockInterruptibly() | 在获取锁过程中,中断当前线程 |
boolean tryLock() | 尝试非阻塞获取锁,如果能够获取锁则返回true;否则返回false |
boolean tryLock(long time, TimeUnit unit) | 超时获取锁,当前线程在以下3中情况返回:1.当前线程在超时时间内获取了锁;2.当前线程在超时时间呗中断;3.当前线程超时时间结束,返回false ; |
void unlock() | 释放锁 |
Condition newCondition() | 条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁。 |
ReentrantLock(重入锁)
重入锁,顾名思义就是支持重新进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。也就是说在调用lock()方法时,已经获取到锁的线程,能狗再次调用lock()方法获取锁而不被阻塞,同时还支持获取锁的公平性和非公平性。这里的公平是在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁时公平锁;反之,是不公平的。
(1). 同步执行的代码跟synchronized类似
ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
ReentrantLock lock = new ReentrantLock(true); //公平锁
lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
try {
//操作
} finally {
lock.unlock(); //释放锁
}
(2). 防止重复执行代码
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) { //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果
try {
//操作
} finally {
lock.unlock();
}
}
(3). 尝试等待执行的代码
ReentrantLock lock = new ReentrantLock(true); //公平锁
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
//如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行
try {
//操作
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException
}
通过ReentrantLock来解决前面卖票线程的线程同步(安全)问题,代码如下
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author zejian
* @time 2016年3月12日 下午2:55:42
* @decrition 模拟卖票线程
*/
public class Ticket implements Runnable {
//创建锁对象
private Lock ticketLock = new ReentrantLock();
//当前拥有的票数
private int num = 100;
public void run() {
while(true) {
ticketLock.lock();//获取锁
if(num>0) {
try{
Thread.sleep(10);
//输出卖票信息
System.out.println(Thread.currentThread().getName()+".....sale...."+num--);
}catch (InterruptedException e){
Thread.currentThread().interrupt();//出现异常就中断
}finally{
ticketLock.unlock();//释放锁
}
}
}
}
}
2.2 通过synchronized关键字的方式
在Java中内置了语言级的同步原语-synchronized,这个可以大大简化了Java中多线程同步的使用。从JAVA SE1.0开始,java中的每一个对象都有一个内部锁,如果一个方法使用synchronized关键字进行声明,那么这个对象将保护整个方法,也就是说调用该方法线程必须获得内部的对象锁。
public synchronized void method{
//method body
}
等价于
private Lock ticketLock = new ReentrantLock();
public void method{
ticketLock.lock();
try{
//.......
}finally{
ticketLock.unlock();
}
}
从这里可以看出使用synchronized关键字来编写代码要简洁得多了。当然,要理解这一代码,我们必须知道每个对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管那些调用wait的线程(wait()/notifyAll/notify())。同时我们必须明白一旦有一个线程通过synchronied方法获取到内部锁,该类的所有synchronied方法或者代码块都无法被其他线程访问直到当前线程释放了内部锁。刚才上面说的是同步方法,synchronized还有一种同步代码块的实现方式:
Object obj = new Object();
synchronized(obj){
//需要同步的代码
}
其中obj是对象锁,可以是任意对象。那么我们就通过其中的一个方法来解决售票系统的线程同步问题:
class Ticket implements Runnable {
private int num = 100;
Object obj = new Object();
public void run() {
while(true) {
synchronized(obj) {
if(num>0) {
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName()+".....sale...."+num--);
}
}
}
}
}
同步的好处:解决了线程的安全问题。
同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。
同步的前提:同步中必须有多个线程并使用同一个锁。
三、线程间通信机制
线程开始运行,就会生成一个自己独有的栈空间。在java中多线程间的通信使用的是等待./通知机制来实现的。
synchronized关键字等待/通知机制:
是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述的两个线程通过对象O来完成交互,而对象上的wait()和notify()/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。这些方法使用的前提是对调用对象加锁,也就是说只能在同步函数或者同步代码块中使用。
条件对象的等待/通知机制:
所谓的条件对象也就是配合前面我们分析的Lock锁对象,通过锁对象的条件对象来实现等待/通知机制。那么条件对象是怎么创建的呢?
//创建条件对象
Condition conditionObj=ticketLock.newCondition();
方法 | 函数方法对应的描述 |
---|---|
void await() | 将该线程放到条件等待池中(对应wait()方法) |
void signalAll() | 解除该条件等待池中所有线程的阻塞状态(对应notifyAll()方法) |
void signal() | 从该条件的等待池中随机地选择一个线程,解除其阻塞状态(对应notify()方法) |
就这样我们创建了一个条件对象。注意这里返回的对象是与该锁(ticketLock)相关的条件对象。下面是条件对象的API:
方法 | 函数方法对应的描述 |
---|---|
void await() | 将该线程放到条件等待池中(对应wait()方法) |
void signalAll() | 解除该条件等待池中所有线程的阻塞状态(对应notifyAll()方法) |
void signal() | 从该条件的等待池中随机地选择一个线程,解除其阻塞状态(对应notify()方法) |
上述方法的过程分析:一个线程A调用了条件对象的await()方法进入等待状态,而另一个线程B调用了条件对象的signal()或者signalAll()方法,线程A收到通知后从条件对象的await()方法返回,进而执行后续操作。上述的两个线程通过条件对象来完成交互,而对象上的await()和signal()/signalAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。当然这样的操作都是必须基于对象锁的,当前线程只有获取了锁,才能调用该条件对象的await()方法,而调用后,当前线程将释放锁。
这里有点要特别注意的是,上述两种等待/通知机制中,无论是调用了signal()/signalAll()方法还是调用了notify()/notifyAll()方法并不会立即激活一个等待线程。它们仅仅都只是解除等待线程的阻塞状态,以便这些线程可以在当前线程解锁或者退出同步方法后,通过争夺CPU执行权实现对对象的访问。到此,线程通信机制的概念分析完,我们下面通过生产者消费者模式来实现等待/通知机制。