本总结参考java核心技术中文版 第7版
为什么要多任务
传统的单线程程序是一个程序执行完后再执行下一个,同一个时间段就只能干一件事情。不言而喻效率不高,因为普通的程序很难让cpu全负荷运转起来,cpu大量的时间在闲置。一个程序也很难满足用户的需求。
多任务的解决方案
我们考虑是否程序能同时执行多个程序,最终的解决方案是把cpu调用程序的物理过程分成若干小的时间片断,这样就可以可已在每个时间片断上调用不同的程序。从电脑的内部看cpu的使用效率大大提高了。从用户角度根本感觉不到这些及其细小的时间片断的切换,制造出了一种可同时进行多任务处理的假象。举例:XP操作系统就是一个最典型的多线程程序,如:一边听歌一边编程。
注:多核处理器情况暂不讨论,原理类似。
在这样的方式实现多多任务处理,一个关键问题是操作系统是如何中断程序的。分为两种方式:抢占式多任务和协作式多任务。
线程与进程和多任务有什么关系
要想实现一个多任务程序,线程和进程这两个概念非常关键,他们也是我们程序员真正可以控制的地方。
进程是指在系统中正在运行的一个应用程序;线程是操作系统分配cpu时间片资源的基本单元,或者说进程之内独立执行的一个单元。对于操作系统而言,其调度单元是线程。一个进程至少包括一个线程,通常将该线程称为主线程。一个进程从主线程的执行开始进而创建一个或多个附加线程,就是所谓基于多线程的多任务。
java核心技术第七版谈到多线程和多进程的本质区别,是每个进程有它自己的变量的完备集,线程则共享相同的数据。
java多线程编程
默认情况下,我们编写的java程序是单线程的,也就是说代码是一条接着一条执行,直到程序结束,不能同时干多件事。通过上面的原理,我们要实现多任务就要把程序改成多线程的。
java中线程由Thread类表示,一个线程就是一个Thread对象。调用Thread对象的start方法就开始了一个线程。
下面是在主线程中运行另一个线程的简单过程:
1)将新线程中要执行的任务放到一个实现了Runnable接口的类的run方法中,这个run方法就是Runnalbe接口的唯一的一个方法。
public interface Runnable
{
void run();
)
我们只要这样一个实现类
Class MyTask implements Runnable
{
public void run()
{
task code
}
}
2)创建任务类对象:
Runnable r = new MyTask();
3)由Runnable对象构造一个Thread对象;
Thread t = new Thread(r);
4)启动线程
t.start();
这通过实现Runnable接口生成的多线程程序,还可以通过继承Thread类覆盖run方法来实现。不过这个方法现在不建议使用了。
我们看下这个过程中,我用到了那些java API。 java.lang.Thread 1.0
-
Thread(Runnable target)
-
void start()
启动这个线程,调用自己的run()方法。这个方法将立即返回,并且新线程将并发运行。
- void run()
java.lang.Runnable 1.0
- void run()
必须重载这个方法,并且在这个方法中添加执行相应任务的相关代码。
中断线程
正常情况线程(除主线程外)在它的run方法返回时终止。我们可以同一些方法让线程非正常终止,让线程进入到死亡状态。
线程状态
线程有以下4种状态
New (新生)
Runnable (可运行)
Blocked (被阻塞)
Dead (死亡)
1)新生线程
当用new操作符创建一个线程时,线程还没有开始运行。
2)可运行线程
一旦调用了start方法,该线程就成为可运行的。实际上线程在等待时间片运行和正在一个时间片上运行,这两种情况都称作是可运行线程。
3)被阻塞线程
当发生以下任意情况,线程进入阻塞状态:
- 线程通过调用sleep方法进入睡眠状态
- 线程调用一个在I/O上被阻塞的操作
- 线程试图得到一个锁,而该锁正被其他线程持有
- 线程等待某个触发条件
4)死线程
有两个原因会导致线程死亡:
- 因为run方法正常退出而自然死亡;
- 因为一个未捕获的异常终止了run方法而使线程猝死。
线程属性
线程属性包括:线程优先级、守护线程、线程组、以及未捕获异常处理器这个几方面内容。
同步
在实际的多线程应用中,通常会由两个或多个线程需要对相同的对象进行共享访问。这将会产生被腐蚀的对象。这种情况也通称竞争条件。
1)竞争条件产生的原因
一条语句编译成机器语言可能变成多条,并且不是原子操作。
2)锁对象
JDK5.0开始,有两种机制来保护代码块不受并行访问的干扰。synchronized关键字和ReentrantLock类。
//用ReentrantLock保护代码块的基本结构如下:
myLock.lock(); //a ReentrantLock object
try
{
critical section
}
finally
{
myLock.unlock(); //make sure the lock is unlocked even if an exception is thrown
}
3)条件对象
条件对象是用来控制已经获得锁,并且进入临界区的线程。
public void transfer(int from, int to , int amout)
{
bamlLock.lock();
try
{
while(accounts[from] < amout)
sufficientFunds.await();
//transfer funds
....
sufficientFunds.signalAll();
}finally
{
bankLock.unlock();
}
}
private Condition ufficientFunds = bankLock.newCondition();
java.util.concurrent.locks.Lock 5.0
- Condition newCondition()
返回与该锁相关的一个条件对象
java.util.concurrent.locks.Condition 5.0
- void await()
把该线程放到条件的等待集中(就是等待池)
- void signalAll()
解除该条件的等待集中所有线程的阻塞状态。
- void signal()
在该条件的等待集中随机选择一个线程,解除其阻塞状态
synchronized关键字 class Bank { public synchronized void transfer(int from, int to, int amount) throws InterruptedException { while(accouts[from] < amount) wait(); accounts[from] -= amount; accounts[to] += amount; notifyAll(); } public synchronized double getTotalBalance(){...} private double accounts[]; }
可以看到使用synchronized关键字来编码要简洁得多。当然,为了理解代码,你必须知道每个对象都有一个隐式得锁,并且每个锁都有一个隐
式条件。
在编码中使用那一种?
同步块
如果要处理遗留代码,需要知道内置得同步原语。别忘了每个对象都有一个锁。实际上线程有两种方法可以获得锁:1、调用一个同步方法。2
进入一个同步块。
synchronized(obj) { critical section }
Volatile域
volatile关键字为对一个实例的域的同步访问提供了一个免锁(lock-free)机制。如果你把域声名为volatile,那么编辑器和虚拟机就知道
该域可能会被另一个线程并发更新。
例如:假设一个对象一个布尔标记,由一个线程设置它的值,而由另一个线程来查询它,那么有以下两个方法:
1)使用锁 public synchronized boolean isDone() {return done;} private boolean done; 2)将域声名为volatile public boolean isDone(){return done;} private volatile boolean done;
借着介绍Volatile关键字的作用,总结下一个域都有那些情况是线程安全的: 域是volatile的 域是final的,并且在构造器调用完成后被访问。 对域的访问有锁保护
死锁
学习多线程就必须要明白什么死锁现象,这是因为锁和条件不能解决多线程中的所有问题。
我觉得死锁的根源就是调用obj.wait()这样的方法造成的,我就不明白为什么不满足条件就退出就完了,为什么一定要等待。书里没说明白。
锁测试和超时 没弄明白
读写锁 实际是对ReentrantLock的细化,ReentrantReadWriteLock类有两个细化了的锁 ReentrantReadWriteLock.readLock() ReentrantReadWriteLock.writeLock()
阻塞队列 阻塞队列本质还是一个队列,只不过为了用在两个线程间通讯加上了些阻塞的特性。 BlockingQueue LinkedBlockingQueue ArrayBlockingQueue PriorityBlockingQueue