一、线程的概念
一个程序同时执行多个任务,每个任务称为一个线程。
进程有自己单独的变量,线程共享数据。
单核多线程不算真正的多线程,因为CPU在同一时间只能处理一个线程。
创建线程类的方法:实现Runnable接口或继承Thread类。
//线程中执行任务过程
class MyRunnable implements Runnable{
//执行run方法中的代码
public void run(){
task code;
//sleep方法使线程休眠DELAY的秒数,需捕获异常
Thread.sleep(DELAY);
}
}
//创建类对象
Runnable r = new MyRunnable();
//由Runnable对象创建线程
Thread t = new Thread(r);
//启动线程
t.start();
sleep方法、wait方法:使线程中断。
创建新线程调用start方法而不是run方法,否则不能创建新线程,只是执行同一个线程中的任务。
start方法:创建一个执行run方法的新线程。
run方法:被start方法调用后执行方法内代码。
二、中断线程
用interrupt方法来强制终止线程。
//应时常检查当前线程是否被中断
while(Thread.currentThread().isInterrupted() && ...){
code;
}
若线程被阻塞就无法检测中断状态,产生InterruptedException。
被中断的线程对中断有不同的响应,很重要的线程处理异常后继续执行,不理会中断。但线程中断后终止的情况更普遍。
interrupted方法:静态方法,检测当前线程是否被中断,同时会清除该线程中断状态。
isInterrupted方法:实例方法,检验是否有线程被中断,不改变中断状态。
三、线程的状态
5种。New(新生),Runnable(可运行),Blocked(被阻塞),Waiting(等待),Terminated(被终止)。
新生线程(New)
new创建新线程后该线程还未开始运行。
可运行线程(Runnable)
调用start方法后线程处于可运行状态(不一定运行)。即就绪状态,等待CPU调度后运行。
被阻塞线程(Blocked)和等待线程
处于上述两种状态时暂时不活动。
阻塞状态:线程试图获取被其他线程持有的内部的对象锁时,进入阻塞状态。其他线程释放锁并线程调度器重新调度该线程时,线程变为非阻塞状态。
等待状态:线程在调度器中等待条件时,进入等待状态。(Object.wait方法,Thread.join方法,等待java.util.concurrent中的Lock或Condition时)
/**
* join方法的含义:使当前进程暂停,等待调用了join方法的线程执行完毕后当前线程再执行。
*/
public class JoinTest {
public static void main(String [] args) throws InterruptedException {
Test t1 = new ThreadJoinTest("aaa");
Test t2 = new ThreadJoinTest("bbb");
t1.start();
// main线程暂停,返回t1线程,t1线程执行完毕后,main线程重新执行
t1.join();
t2.start();
}
}
class Test extends Thread{
public Test(String name){
super(name);
}
public void run(){
for(int i=0;i<1000;i++){
System.out.println(this.getName() + ":" + i);
}
}
}
计时等待:调用有超时参数的方法时导致线程进入计时等待状态(规定时间内暂停或在被激活前暂停)。
被终止线程
当run方法执行完毕或因为未捕获异常而使run方法终止时,线程终止。
四、线程的属性
线程优先级
线程都有优先级。线程继承其父线程的优先级。
setPriority:设置优先级:在MIN_PRIORITY(1)与MAX_PRIORITY(10)之间,NORM_PRIORITY为5。调度器先选择有高优先级的线程,如果在低优先级线程执行的过程中出现高优先级线程,则暂停执行低优先级线程,去执行高优先级线程。
yield:静态方法,使当前线程处于让步状态。当前线程将自己对CPU的占用让给同优先级的其他线程,但这是概率问题,并不确定结果。
守护线程
SetDaemon(true):将线程转换为守护线程。
守护线程用途:为其他线程提供服务(例如计时)。当只剩守护线程时虚拟机退出。
未捕获异常处理器
run方法中的不被检测的异常在线程死亡前被传递到用于未捕获异常的处理器(实现Thread.UncaughtExceptionHandler接口类)。
setUncaughtExceptionHandler方法为线程安装默认处理器。若不安装,处理器为线程的ThreadGroup对象。
五、同步
竞争条件:多个线程共享数据,同时对数据进行操作时,容易互相覆盖操作导致结果出错。
解决方法:各线程执行过程中不被中断则不会出现错误。
锁对象
防止代码块受并发访问干扰:synchronized关键字,ReentrantLock类(可重入锁)。
//任何时刻只有一个线程进入临界区,
//一个线程封锁了锁对象,其他任何线程都无法调用lock(被阻塞)
//直到线程释放了锁对象时才能停止阻塞
//该类的每个对象都有一个自己的锁对象,若两个线程分别访问不同的锁对象,则不会阻塞。
public class Bank{
private Lock myLock = new ReentrantLock();
public void transfer(){
myLock.lock();//ReentrantLock对象
try{
...
}
finally{
myLock.unlock();//锁必须被释放,所以放在finally子句中
}
}
}
Reentrant锁可被用来保护临界区,是可重入的。线程可重复获得已经持有的锁(线程在已经获得锁对象后可以进入与它同步的其他代码块,再次获得自己已经持有的锁。比如在同一个类中,有锁的方法内几次调用自身或其他有锁的方法)。锁在被重复调用时计数,调用就++,释放就–,当计数为0时释放锁。
ReentrantLock(boolean fair):构建一个带有公平策略的锁。公平锁对等待时间长的线程优先,但会降低性能,也无法保证完全公平,所以一般不强制使用。
条件对象
条件对象:管理已经获得锁但不能运行的线程(进入临界区后必须满足条件才能执行)。
锁可以拥有一个或多个相关的条件对象。
比如账户中没有足够余额时需要另一个线程向账户中注入资金,但这一线程获取了锁且没有释放,其他线程无法操作。此时需要条件对象。
class Bank{
public Bank(){
...
//锁对象获取条件对象,命名为“余额充足”
sufficientFunds = bankLock.newCondition();//给条件对象赋值
}
...
private Condition sufficientFunds;//定义条件对象
}
线程拥有条件锁时,仅可以调用如下方法:
await方法:如果当前进程负责转账的方法发现余额不足无法转账,则调用sufficientFunds.await(),使当前进程被阻塞,放弃锁。此时另一个线程开始转账。
while(不能继续执行){
condition.await();
}
signalAll方法:通知等待的线程可能满足条件,需要再次检测。解除所有等待线程的阻塞。
另一个线程转账完成后,调用sufficientFunds.signalAll(),激活之前因为调用了await方法而进入条件的等待集的线程,让它们成为可运行的。被激活的其中一个线程重新返回调用await处,获得锁,继续执行,并再次测试条件。
signal方法:随机解除处于等待集中的某个线程的阻塞状态。易发生死锁:随机选择的线程可能不能运行,再次阻塞。
synchronized关键字
Java中每个对象都有一个内部锁,如果方法用synchronized关键字声明,则对象的锁保护整个方法。(获取内部锁才能调用该方法)
wait方法:添加线程到等待集中,相当于condition.await()方法。
notify / notifyAll方法:解除等待线程的阻塞状态,相当于condition.signalAll方法。
以上三个方法为Object类的final方法。
尽量使用synchronized关键字,会减少编码数量与出错几率。
每个Java对象有一个锁,线程可调用同步方法或进入一个同步阻塞来获得锁。
synchronized(obj){
临界区;//获得obj的锁
}
------------------------------------------------------------
//特殊的锁
public class Bank{
public void transfer(){
synchronized(lock);//lock的作用仅为创建每个对象都拥有的锁
{
...
}
}
private Object lock = new Object();
}