6.多线程
6.1 线程与进程
进程:是正在运行的程序
- 是系统进行资源分配和调用的独立单位
- 每一个进程都有它自己的内存空间和系统资源
- 一般而言进程有三个特征:独立性、动态性、并发性
线程:是进程中的单个顺序控制流,是一条执行路径
- 单线程:一个进程只有一条执行路径。
- 多线程:一个进程有多条执行路径。
线程调度
两种调度模型:
- 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占有CPU的时间片
- 抢占调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些
Java使用的是抢占式调度模型。
6.2 线程的创建与启动
方式一:继承Thread类创建线程类
- 定义Thread子类,并重写该类的run()方法。(run()方法中是线程需要完成的任务)
- 创建子类实例并调用其中的start()方法启动该线程。
例:
public class FirstThread extends Thread {
@Override
public void run() {
for(int i = 0; i < 10; ++i) System.out.println(this.getName() + " " + i);
}
public static void main(String[] args) {
FirstThread t1 = new FirstThread();
t1.setName("线程1");
FirstThread t2 = new FirstThread();
t2.setName("线程2");
t1.start();
t2.start();
}
}
方式二:实现Runnable接口创建线程类
- 定义Runnable接口的实现类,并重写run()方法。
- 创建Runnnable实现类的实例,并将此实例作为Thread的target来创建Thread对象。(该Thread对象才是真正的线程对象)
- 调用线程对象的start()方法启动该线程
例:
public class SecondThread implements Runnable {
private int i = 0;
@Override
public void run() {
for(; i < 10; ++i) {
//只能使用此方式获取线程名称
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
Runnable r = new SecondThread();
new Thread(r, "线程1").start();
new Thread(r, "线程2").start();
}
}
方式三:使用Callable和Future创建线程
- 创建Callable接口的实现类,并实现call()方法,再创建Callable实现类的实例。(call()方法将作为线程的线程执行体,且该call()方法有返回值)
- 使用FutureTask类来包装Callable对象。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
例:
public class ThirdThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int i = 1;
while(i < 10) {
System.out.println(Thread.currentThread().getName() + " " + i++);
}
return Integer.valueOf(i);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new ThirdThread());
Thread t = new Thread(task,"ThirdThread");
t.start();
System.out.println("ThirdThread线程的返回值为:" + task.get());
}
}
三种创建线程的方式比较:
采用实现Runnable、Callable接口的方式创建多线程的优缺点:
- 线程类还可以实现其他接口和继承其他类
- 再此方式下,多线程可以共享同一个target对象,所以适合多个相同的线程来处理同一份资源的情况从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想
- 缺点是比较复杂
采用继承Thread类的方式创建多线程的优缺点:
- 缺点是,因为线程类已经继承了Thread类,就不能再继承其他父类了
- 优点是,编写简单
PS:一般推荐采用实现Runnable或Callable接口来创建多线程
6.3 线程的生命周期
线程的生命周期中,它一般会经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。并且当一个线程被创建并被启动后,它并不会一直霸占着CPU独自运行,而是多次在运行、就绪之间切换直到该线程死亡。
以下是线程的生命状态转换图:
新建和就绪状态:
当一个线程类被实例化后创建出的实例对象就是处于新建状态的线程,当它被调用start()方法后,该线程就处于就绪状态了。
PS:只能对处于新建状态的线程调用 start() 方法!!!
运行和阻塞状态:
当线程就绪状态的线程有了CPU的执行权后,该线程就会进入运行状态。但是一个线程不可能一直有着执行权的,因为其他线程的抢夺或一些方法本来处于运行状态的线程会丧失CPU的执行权,而丧失了执行权的线程就会进入就绪或阻塞状态。处于阻塞状态的就等待着阻塞的接触进入就绪状态,处于就绪状态的就等着重新夺回执行权后再次进入运行状态。
以下情况线程会进入阻塞状态:
- 线程调用sleep()方法主动放弃执行权
- 线程调用了一个阻塞式的IO方法,在该方法返回前,该线程会处于阻塞状态
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
- 线程在等待着某个通知(notify)
- 程序调用了线程的suspend()方法将该线程挂起。(这个方法容易导致死锁)
与上面相对的接触阻塞的方法:
- 调用sleep()方法的线程经过了指定时间
- 线程调用的阻塞式IO方法已经返回
- 线程成功获得了试图取得的同步监视器
- 线程正在等待某个通知时,其他线程发出了一个通知
- 处于被挂起状态的线程被调用了resume()恢复方法
PS:调用yield()方法可以让运行状态的线程转入就绪状态。
线程的死亡:
线程会以以下方式结束,结束后就处于死亡状态
- run()或call()方法执行完成,线程正常结束
- 线程抛出一个未捕获的Exception或Error
- 直接调用该线程的stop()方法来结束该线程(此方式容易导致死锁)
当主线程结束时,其他线程不受任何影响,并不会随之结束。不要试图对一个已死亡的线程调用start()方法使它重新启动,线程死亡就是死亡了。
6.5 控制线程
join线程: 在执行流中通过调用其他线程的join()方法使某个线程处于阻塞状态等待其他线程执行。
线程睡眠: 通过调用线程的sleep()方法使其暂停并进入阻塞状态。
join和sleep的区别:
- sleep(long)方法在睡眠时不释放对象锁
- join(long)方法在等待的过程中释放对象锁
- sleep是让调用线程进入阻塞状态
后台线程: 又称守护线程或精灵线程,其特征为:如果所有前台线程都死亡,后台线程就会自动死亡。可以通过setDaemon()方法将线程设置为后台线程。前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认为后台线程。
线程的优先级: 每个线程执行时都会具有一定的优先级,优先级高的线程获得CPU执行权的几率会更高。每个线程默认的优先级都与创建它的父线程的优先级相同。可以通过setPriority(int newPriority)、getPriority()设置和获取线程的优先级。
线程的默认优先级是5,以下是Thread类中的三个静态常量:
- MAX_PRIORITY:值为10
- MIN_PRIORITY:值为1
- NORM_PRIORITY:值为5
6.6 线程同步
多线程很有趣,但也容易出现错误。如当使用多个线程来访问同一个数据时,就容易“偶然”出现线程安全问题。而线程安全问题一般用以下方法解决。
同步代码块
synchronized(任意对象) {
...//操作共享数据的代码块
}
同步方法
//非静态
修饰符 synchronized 返回值类型 方法名(参数列表) { }
//静态
修饰符 static synchronized 返回值类型 方法名(参数列表) { }
- 非静态同步方法的锁对象为 this
- 静态同步方法的锁对象为 类名.class
PS:synchronized关键字不可修饰构造器和成员变量
Lock锁
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
void lock(); //获得锁
void unlock(); //释放锁
Lock是一个接口,所以需采用它的实现类ReentrantLock来实例化。
6.7 死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁。JVM没有关于死锁的监测,并且也没有采取措施处理死锁情况,一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
常用以下方法解决死锁问题:
- 避免多次锁定:尽量避免同一个线程对多个同步监视器进行锁定。
- 具有相同的加锁顺序
- 使用定时锁:可以使用Lock对象的tryLock()方法设置时间参数加锁,当超时后会自动释放对Lock的锁定。
- 死锁检测
6.8 线程池
系统启动一个新线程的成本是比较高的,这时使用线程池就可以很好的提高性能。线程池在系统启动时即创建大量的空闲线程,程序将一Runnable对象或Callable对象传给线程池,线程池在启动一个空闲的线程来执行它们的run()或call()方法,当run()或callI()方法执行结束后,该线程并不会死亡,而是再次返回线程池中称为空闲状态等待下一个线程对象的run()或call()方法。
6.9 线程相关类
包装线程不安全的集合:
在Java的集合类中,许多都是线程不安全的,也就是说,当多个并发线程向这些集合中存、取元素时,就可能破坏这些集合的数据完整性。如果一定要使用这些集合类,可以使用Collections集合工具类提供的类方法将这些包装成线程安全的集合。
自身线程安全的类: StringBuffer、Vector、Hashtable等。