目录
一 线程介绍
1 并发与并行
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。
在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。
2 进程和线程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程或是正在运行的一个程序,是一个动态的过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。 线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
线程和进程的区别
- 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
- 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
- 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
- 多进程是指操作系统能同时运行多个任务(程序);多线程是指在同一程序中有多个顺序流在执行。
在Java中,一个应用程序可以包含多个线程。每个线程执行特定的任务,并可与其他线程并发执行多线程使系统的空转时间最少,提高CPU利用率、多线程编程环境用方便的模型隐藏CPU在任务间切换的事实在Java程序启动时,一个线程立刻运行,该线程通常称为程序的主线程。
主线程的重要性体现在两个方面:
- 它是产生其他子线程的线程。
- 通常它必须最后完成执行,因为它执行各种关闭动作。
3 多线程的作用
发挥多核CPU的优势:随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。
防止阻塞:从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
便于建模:假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
二 创建线程的方法
Java提供了三种创建线程的方法:
- 通过继承Thread类本身
- 通过实现runnable接口
- 通过Callable和Future创建线程
java的线程是通过java.lang.Thread类实现的。所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
被Thread对象调用的常用方法有
Thread类的静态方法有
1 通过继承Thread来创建线程
Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
package com.itcast.test;
class MyThread extends Thread{
//定义指定线程名称的构造方法
public MyThread(String name){
//调用父类的String参数构造方法,指定线程名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for(int i = 0;i < 20; i++){
//getName()方法,来自父亲
System.out.println(getName()+":"+i);
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("小强");
//开启新线程
mt.start();
//在主方法中执行for循环
for(int i = 0;i < 20; i++){
System.out.println("旺财:"+i);
}
}
}
程序运行的流程图分析如下。当程序启动运行main时,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用mt的对象的 start方法,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。 当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。
2 通过实现 Runnable 接口来创建线程
采用 java.lang.Runnable 也是非常常见的一种,我们只需要重写run方法即可。
步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
package com.itcast.test;
class MyRunnable implements Runnable{
@Override
public void run() {
for(int i = 0;i < 20;i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
//创建自定义类对象,线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr,"小强");
t.start();
for(int i = 0;i < 20; i++){
System.out.println("旺财:"+i);
}
}
}
也可以使用匿名内部类的方式实现Runnable接口创建线程
package com.itcast.test;
public class ThreadDemo3 {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
for(int i = 0;i < 20; i++){
System.out.println("张宇:" + i);
}
}
};
new Thread(r).start();
for(int i = 0; i < 20; i++){
System.out.println("费玉清:" + i);
}
}
}
3 通过 Callable 和 Future 创建线程
步骤:
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
package com.itcast.test;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class CallableThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int i = 0;
for(; i < 100; i++){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
return i;
}
}
public class ThreadDemo4{
public static void main(String[] args) {
CallableThread ct = new CallableThread();
FutureTask<Integer> ft = new FutureTask<>(ct);
for(int i = 0;i < 100; i++){
System.out.println(Thread.currentThread().getName() + "的循环变量i的值" + i); //main的循环变量i的值
if(i == 20){
new Thread(ft,"有返回值的线程").start();
}
}
try{
System.out.println("子线程的返回值:" + ft.get());
}catch (Exception e){
e.printStackTrace();
}
}
}
4 创建线程的三种方式的对比
- 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。可以多个线程共享一个target对象,所以非常适合多个线程来处理同一份资源的情况。弊端:编程稍微复杂,不直观,如果要访问当前线程,必须使用Thread.currentThread() 方法
- 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。弊端:因为线程类已经继承了Thread类,则不能再继承其它类。因为java是单继承。 实际上大多数的多线程应用都可以采用实现Runnable接口的方式来实现【推荐使用匿名内部类】。
- 通过实现Callable接口并重写call方法,并把Callable实例传给FutureTask对象,再把FutureTask对象传给Thread对象。它与Thread、Runnable最大的不同是Callable能返回一个异步处理的结果Future对象并能抛出异常,而其他两种不能。
常用第二种实现Runnable接口实现,因为实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度。
start()方法和run()方法的区别
只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。
Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
Runnable接口和Callable接口的区别
- Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
- Callable规定的方法是 call(), Runnable规定的方法是 run()。
- call方法可以抛出异常, run方法不可以。
- 运行 Callable任务可以拿到一个 Future对象
因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。
三 线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
例如,电影院好多个窗口一起售卖电影票,票数一共是100张,用线程对象模拟,采用Runnable接口实现创建线程。
package com.itcast.test;
class Ticket implements Runnable{
private int ticket = 100;
/**
* 执行买票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
//有票可以卖
if(ticket > 0){
try{
//使用sleep模拟一下出票时间
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
//获取当前线程对象的名字,并出票
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
public class ThreadDemo5 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket,"窗口1");
Thread t2 = new Thread(ticket,"窗口2");
Thread t3 = new Thread(ticket,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
从结果看出,上述代码存在两个问题:
- 相同的票数,比如5这张票被卖了两回。
- 不存在的票,比如0票与-1票,是不存在的。
这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。
四 线程同步
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。 要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制 (synchronized)来解决。
案例描述:窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。 有三种方式完成同步操作:
- 同步代码块
- 同步方法
- 锁机制
1 同步代码块
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象可以是任意类型。
- 多个线程对象要使用同一把锁。
class Ticket implements Runnable{
private int ticket = 100;
Object lock = new Object();
/**
* 执行买票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while(true){
synchronized (lock){
//有票可以卖
if(ticket > 0){
try{
//使用sleep模拟一下出票时间
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
//获取当前线程对象的名字,并出票
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
}
2 同步方法
public synchronized void method(){可能会产生线程安全问题的代码}同步锁是谁 ?对于非 static 方法 , 同步锁就是 this 。对于 static 方法 , 我们使用当前方法所在类的字节码对象 ( 类名 .class)
使用同步方法代码如下
class Ticket implements Runnable{
private int ticket = 100;
/**
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口永远开启
while(true){
sellTicket();
}
}
/**
* 锁对象是谁调用这个方法就是谁
* 隐含锁对象就是this
*/
public synchronized void sellTicket(){
//有票可以卖
if(ticket > 0){
try{
//使用sleep模拟一下出票时间
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
3 Lock锁
java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
- public void lock() :加同步锁。
- public void unlock() :释放同步锁。
class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock();
if(ticket > 0){
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
lock.unlock();
}
}
}
4 Synchronized与lock的异同
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的代码逻辑以后,自动的释放同步监视器
lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())(同时lock的方式更为灵活)
优先使用顺序:
LOCK->同步代码块->同步方法
5 判断线程是否有安全问题,以及如何解决
- 先判断是否多线程
- 再判断是否有共享数据
- 是否并发的对共享数据进行操作
- 选择上述三种方法解决线程安全问题
6 乐观锁和悲观锁
乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,直接上了锁就操作资源了。共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
五 线程死锁
死锁是多线程编程中最常见的问题。若程序中有多个线程共同访问多个资源,相互之间存在竞争,就有可能产生死锁。如:当一个线程等待另一个线程持有的锁,而另一个线程也在等待第一个线程持有的锁,两个线程就进入阻塞状态。
线程死锁的理解:僵持,谁都不放手,一双筷子,我一只你一只,都等对方放手(死锁,两者都进入阻塞,谁都吃不了饭,进行不了下面吃饭的操作)
出现死锁以后,不会出现提示,只是所有线程都处于阻塞状态,无法继续
package com.itcast.thread;
class DeadClock extends Thread{
int flag;
DeadClock(String name,int flag){
super(name);
this.flag = flag;
}
@Override
public void run() {
if (flag == 1){
synchronized ("A"){
try {
System.out.println(Thread.currentThread().getName() + "拿到A锁");
Thread.sleep(5000);
}catch (InterruptedException e){};
synchronized ("B"){
System.out.println(Thread.currentThread().getName() + "拿到B锁");
}
System.out.println(Thread.currentThread().getName() + "释放B锁");
}
System.out.println(Thread.currentThread().getName() + "释放A锁");
}
if (flag == 0){
synchronized ("B"){
try {
System.out.println(Thread.currentThread().getName() + "拿到B锁");
Thread.sleep(5000);
}catch (InterruptedException e){};
synchronized ("A"){
System.out.println(Thread.currentThread().getName() + "拿到A锁");
}
System.out.println(Thread.currentThread().getName() + "释放A锁");
}
System.out.println(Thread.currentThread().getName() + "释放B锁");
}
}
}
public class ThreadTest04 {
public static void main(String[] args) {
DeadClock deadClock1 = new DeadClock("线程一",1);
DeadClock deadClock2 = new DeadClock("线程二",0);
deadClock1.start();
deadClock2.start();
}
}
代码结果分析:由于线程一已经拿到A锁,但是还需要再拿到B锁才会继续往下执行,所以线程一没有执行完毕前,A锁不可能被释放。而线程二已经拿到B锁,但是线程二需要拿到A锁才能往下执行。但是由于A锁已经被线程一拿走,故线程二要一直等待线程一执行完毕释放A锁,并拿到A锁才能继续执行。由于两者之间不可能有一个线程能够执行完成,所以就进入死锁状态。
死锁的解决办法:
- 减少同步共享变量
- 采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题
- 减少锁的嵌套。
六 线程生命周期
1 线程的5个状态
JDK中用Thread.State类定义了线程的几种状态,如下:
线程生命周期 | 描述 |
新建(new) | 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。 |
就绪(runnable) | 当处于新建状态的线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列等待CPU时间片,即要等待JVM里线程调度器的调度,此时已经具备运行条件,知识还没有分配到CPU资源。 |
运行(running) | 如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。 |
阻塞(blocked) | 如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种: 1.等待阻塞:运行状态中的线程调用 wait() 方法,使线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)。 2.同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。 3.其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态等待JVM的调度。 |
死亡(dead) | 一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。 |
2 sleep()/join()/yield()方法详解
- sleep()方法:让正在执行的线程休眠进入阻塞状态,在指定的毫秒数(millis)后进入就绪状态
- join()方法:表示该线程结束后才能跑主线程接下来的代码块。使当前线程停下来等待,直至所引用join()方法的另一个线程终止。
- yield()方法:使当前线程从执行状态(运行状态)变为可执行状态(就绪状态),并执行其他线程。cpu会从众多的可执行状态中选择,也就是说,当前刚刚的那个线程还有可能被再次选中,并不是说一定执行其他线程而该线程在下一次不会执行到。
3 Timed waiting(计时等待)
一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。需要记住以下几点:
- 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
- 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠。
- sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
4 Blocked (锁阻塞)
一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
5 waiting(无线等待)
一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
package com.itcast.test;
public class WaitingTest {
public static Object obj = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while(true){
synchronized (obj){
try{
System.out.println(Thread.currentThread().getName()
+"===获取到锁对象,调用wait方法,进入waiting状态");
obj.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "===从waiting状态醒来,获取到锁对象,继续执行了");
}
}
}
},"等待线程").start();
new Thread(new Runnable(){
@Override
public void run() {
while(true){
try{
System.out.println(Thread.currentThread().getName() + "----等待3秒钟");
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (obj){
System.out.println(Thread.currentThread().getName() + "----获取到锁对象,调用notify方法,释放锁对象");
obj.notify();
}
}
}
},"唤醒线程").start();
}
}
通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。
当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。
在翻阅API的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的,比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。
6 wait() 和 notify()
- public final void wait() throws InterruptedException:让当前线程等待,释放当前线程所持有的共享资源的锁,直到其他线程调用notify()方法或notifyall()方法
- public final void notify():唤醒等待此对象的共享资源的单个线程
- public final void notifyall():唤醒等待此对象的共享资源的所有线程
wait()使当前线程阻塞,前提是必须获得锁。一般配合synchronized关键字使用。即,一般在synchronized同步代码块里使用wait()、notify()、notifyall()方法。wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁。当线程执行wait()方法时,会释放当前的锁,然后让出CUP,进入等待状态。只有当notify()、notifyall()被执行时,才会唤醒一个或多个正在处于等待状态的线程,然后继续往下执行,直到执行完synchronized代码块的代码或者遇到wait(),再次释放锁。也就是说,notiyf()、notifyall()的执行只是唤醒沉睡的线程,二不会立即释放锁,锁的释放要看代码的具体执行情况。
wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器
。
7 sleep和wait的异同
相同点:一旦执行方法以后,都会使得当前的进程进入阻塞状态
不同点:
- 两个方法声明的位置不同,Thread类中声明sleep,Object类中声明wait。
- 调用的要求不同,sleep可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中
- 关于是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep不会释放,wait会释放
七 线程通信
概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
1 等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但是线程间也会有协作机制。在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后再将其唤醒notify();在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。
- wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
- notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
注意:哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下: 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态; 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态。
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
2 生产者和消费者问题-等待唤醒机制
就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:
包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子 (即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。 接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。
代码如下
package com.itcast.thread;
class Baozi{
boolean flag = false; //包子资源是否存在,代表包子资源状态
}
class ChiHuo extends Thread{
private Baozi bz;
public ChiHuo(String name,Baozi bz){
super(name);
this.bz = bz;
}
@Override
public void run() {
while (true){
synchronized (bz){
if(bz.flag == false){
try{
bz.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"正在吃包子");
bz.flag = false;
bz.notify();
}
}
}
}
class BaoziPu extends Thread{
private Baozi bz;
public BaoziPu(String name,Baozi bz){
super(name);
this.bz = bz;
}
@Override
public void run() {
while(true){
synchronized (bz){
if(bz.flag == true){
try{
bz.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
System.out.println("包子铺开始做包子,包子造好了");
bz.flag = true;
bz.notify();
}
}
}
}
public class ThreadTest01 {
public static void main(String[] args) {
Baozi bz = new Baozi();
ChiHuo ch = new ChiHuo("吃货",bz);
BaoziPu bzp = new BaoziPu("包子铺",bz);
ch.start();
bzp.start();
}
}
八 线程池
1 线程池概念和原理
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
2 使用线程池
Java里面线程池的顶级接口是 java.util.concurrent.Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。
真正的线程池接口是java.util.concurrent.ExecutorService。
在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。
Executors类中有个创建线程池的方法如下:
- public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
使用线程池对象的方法如下:
- public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不做)。
package com.itcast.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("我要一个教练");
try{
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("教练来了:"+Thread.currentThread().getName());
System.out.println("教我游泳,教完后,教练回到游泳池");
}
}
public class ThreadTest03 {
public static void main(String[] args) {
//创建线程池对象,包含两个线程对象
ExecutorService service = Executors.newFixedThreadPool(2);
//创建Runnable实例对象
MyRunnable r = new MyRunnable();
//自己创建线程对象的方式
//Thread t = new Thread(r);
//t.start();
//从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
//在获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);
//注意:submit()方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
//将使用完的线程又归还到了线程池中
//关闭线程池
//service.shutdown();
}
}