Day10
1.多线程
1.1 相关概念
1.1.1 程序、进程和线程
- 程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码。
- 进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。如:运行中的 QQ,运行中的网易音乐播放器。
- 每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程。(生命周期)
- 程序是静态的,进程是动态的。
- 进程作为操作系统调度和分配资源的最小单位(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。
- 现代的操作系统,大都是支持多进程的,支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos 窗口等软件。
- 线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。一个进程中至少有一个线程。
- 一个进程同一时间若并行执行多个线程,就是支持多线程的。
- 线程作为 CPU 调度和执行的最小单位。
- 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
1.1.2 线程调度
- 分时调度:所有线程轮流使用 CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。
- 抢占式调度:让优先级高的线程以较大的概率优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java 使用的为抢占式调度。
1.1.3 多线程程序的优点
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统 CPU 的利用率。
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
1.2 创建和启动线程
- Java 语言的 JVM 允许程序运行多个线程,使用 java.lang.Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。
- Thread 类的特性:
- 每个线程都是通过某个特定 Thread 对象的 run()方法来完成操作的,因此把 run()方法体称为线程执行体。
- 通过该 Thread 对象的 start()方法来启动这个线程,而非直接调用 run()。
- 要想实现多线程,必须在主线程中创建新的线程对象。
1.2.1 方式1:继承Thread类
Java 通过继承 Thread 类来创建并启动多线程的步骤如下:
- 定义 Thread 类的子类,并重写该类的 run()方法,该 run()方法的方法体就代表了线程需要完成的任务。
- 创建 Thread 子类的实例,即创建了线程对象。
- 调用线程对象的 start()方法来启动该线程。其目的是①启动线程;②调用当前线程的run()方法。
public class Thread1 extends Thread{public static void main(String[] args) {PrintNumber t1 = new PrintNumber();t1.start();}
}class PrintNumber extends Thread{@Overridepublic void run() {for (int i = 1; i <= 100; i++) {if (i % 2 == 0){System.out.println(i);}}}
}
1.2.2 方式2:实现 Runnable 接口
Java 有单继承的限制,当我们无法继承 Thread 类时,那么该如何做呢?在核心类库中提供了 Runnable 接口,我们可以实现 Runnable 接口,重写 run()方法,然后再通过 Thread 类的对象代理启动和执行我们的线程体 run()方法。
步骤如下:
- 定义 Runnable 接口的实现类,并重写该接口的 run()方法,该 run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 参数来创建Thread 对象,该 Thread 对象才是真正 的线程对象。
- 调用线程对象的 start()方法,启动线程。调用 Runnable 接口实现类的 run 方法。
public class Thread2{public static void main(String[] args) {//创建当前实现类的对象EvenNumberPrint t1 = new EvenNumberPrint();//将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例Thread thread = new Thread(t1);//Thread类的实例调用start()方法thread.start();}
}class EvenNumberPrint implements Runnable{@Overridepublic void run() {for (int i = 1; i <= 100; i++) {if (i % 2 == 0){System.out.println(i);}}}
}
1.2.3 两种方式的对比
- 共同点:
- 启动线程使用的都是Thread类中定义的start()方法。
- 创建的线程对象,都是Thread类或其子类的实例。
- 不同点:一个是类的继承,一个是接口的实现。
建议使用实现Runnable接口的方式。因为①Runnable实现方式避免了类的单继承的局限性;②更适合处理有共享数据的问题;③实现了代码和数据的分离。
1.3 Thread 类的常用结构
1.3.1 构造器
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :指定创建线程的目标对象,它实现了 Runnable 接口中的 run 方法。
- public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
1.3.2 常用方法
- public void run() :此线程要执行的任务在此处定义代码。
- public void start() :导致此线程开始执行; Java 虚拟机调用此线程的 run 方法。
- public String getName() :获取当前线程名称。
- public void setName(String name):设置该线程名称。
- public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在Thread 子类中就是 this,通常用于主线程和 Runnable 实现类。
- public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
- public static void yield():yield 只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这
个不能保证,完全有可能的情况是,当某个线程调用了 yield 方法暂停之后,线程调度器又将其调度出来重新执行。 - void join() :等待该线程终止。
- void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果 millis 时间到,将不再等待。
- void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
- public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。
1.3.3 线程优先级相关方法
每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。Thread 类的三个优先级常量:
- MAX_PRIORITY(10):最高优先级。
- MIN _PRIORITY (1):最低优先级。
- NORM_PRIORITY (5):普通优先级,默认情况下 main 线程具有普通优先级。
- public final int getPriority() :返回线程优先级。
- public final void setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。
1.4 多线程的生命周期
Java 语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下一些状态:
1.4.1 JDK5.0之前:5种状态
线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。CPU 需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。

1.4.2 JDK5.0之后:6种状态
- NEW(新建):线程刚被创建,但是并未启动。还没调用 start 方法。
- RUNNABLE(可运行):这里没有区分就绪和运行状态。因为对于 Java 对象来说,只能标记为可运行,至于什么时候运行,不是 JVM 来控制的了,是 OS 来进行调度的,而且时间非常短暂,因此对于 Java 对象的状态来说,无法区分。
- Teminated(被终止):表明此线程已经结束生命周期,终止运行。
- 重点说明,根据 Thread.State 的定义,阻塞状态分为三种:BLOCKED、WAITING、TIMED_WAITING。
- BLOCKED(锁阻塞):在 API 中的介绍为:一个正在阻塞、等待一个监视器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才能有执行机会。比如,线程 A 与线程 B 代码中使用同一锁,如果线程 A 获取到锁,线程 A 进入到 Runnable 状态,那么线程 B 就进入到 Blocked锁阻塞状态。
- TIMED_WAITING(计时等待):在 API 中的介绍为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。当前线程执行过程中遇到 Thread 类的 sleep 或 join,Object 类的 wait,LockSupport 类的 park 方法,并且在调用这些方法时,设置了时间,那么当前线程会进入 TIMED_WAITING,直到时间到,或被中断。
- WAITING(无限等待):在 API 中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。当前线程执行过程中遇到遇到 Object 类的 wait,Thread 类的join,LockSupport 类的 park 方法,并且在调用这些方法时,没有指定时间,那么当前线程会进入 WAITING 状态,直到被唤醒。
- 通过 Object 类的 wait 进入 WAITING 状态的要有 Object 的notify/notifyAll 唤醒;
- 通过 Condition 的 await 进入 WAITING 状态的要有Condition 的 signal 方法唤醒;
- 通过 LockSupport 类的 park 方法进入 WAITING 状态的要有LockSupport 类的 unpark 方法唤醒
- 通过 Thread 类的 join 进入 WAITING 状态,只有调用 join方法的线程对象结束才能让当前线程恢复;
说明:当从 WAITING 或 TIMED_WAITING 恢复到 Runnable 状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入 BLOCKED 状态。

1.5 线程安全问题及解决方案
当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。Java使用线程的同步机制来解决线程的安全问题。
1.5.1 同步代码块
synchronized(同步监视器){//需要被同步的代码
}
- 需要被同步的代码,即为操作共享数据(多个线程需要操作的数据)的代码。
- 需要被同步的代码,在被synchronized包裹之后,就使得一个线程在操作这些代码的过程中,其他线程必须等待。
- 同步监视器,俗称锁。哪个线程获取了锁,那个线程就能执行需要被同步的代码。
- 同步监视器可以使用任何一个类的对象充当。但是多个线程必须共用一个同步监视器。
注意:
- 在实现Runnable接口的方式中,同步监视器可以考虑使用:this
- 在继承Thread类的方式种,同步监视器慎用this,可以考虑使用:当前类.class。
package p136;public class WindowTest {public static void main(String[] args) {SaleTicket s = new SaleTicket();Thread t1 = new Thread(s);Thread t2 = new Thread(s);Thread t3 = new Thread(s);t1.start();t2.start();t3.start();}
}class SaleTicket implements Runnable {int ticket = 100;@Overridepublic void run() {while (true) {synchronized (this) {if (ticket > 0) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);ticket--;} else {break;}}}}
}
1.5.2 同步方法
如果操作共享数据的代码完整的声明在了一个方法中,那么我们就可以将此方法声明为同步方法即可。
- 非静态的同步方法,默认同步监视器是this。
- 静态的同步方法,默认同步监视器是当前类本身。
package p136;public class WindowTest1 {public static void main(String[] args) {SaleTicket s = new SaleTicket();Thread t1 = new Thread(s);Thread t2 = new Thread(s);Thread t3 = new Thread(s);t1.start();t2.start();t3.start();}
}class SaleTicket1 implements Runnable {int ticket = 100;boolean isFlag = true;@Overridepublic void run() {while (isFlag){show();}}public synchronized void show() { //此时的同步监视器为thisif (ticket > 0) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);ticket--;} else {isFlag = false;}}
}
1.5.3 synchronized的优缺点
- 优点:解决了线程的安全问题。
- 缺点:在操作共享数据时,多线程其实是串行执行,意味着性能较低。
1.6 单例之懒汉式的线程安全问题
package p138;public class BankTest {static Bank b1 = null;static Bank b2 = null;public static void main(String[] args) {Thread t1 = new Thread() {@Overridepublic void run() {b1 = Bank.getInstance();}};Thread t2 = new Thread() {@Overridepublic void run() {b2 = Bank.getInstance();}};t1.start();t2.start();try {t1.join();} catch (InterruptedException e) {e.printStackTrace();}try {t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(b1);System.out.println(b2);System.out.println(b1 == b2);}
}class Bank {private Bank() {}private static volatile Bank instance = null;//实现线程安全的方式1public static synchronized Bank getInstance() { //同步监视器默认为Bank.classif (instance == null) {instance = new Bank();}return instance;}//实现线程安全的方式2public static Bank getInstance() { //同步监视器默认为Bank.classsynchronized (Bank.class) {if (instance == null) {instance = new Bank();}return instance;}}//实现线程安全的方式3,相较于方式1和方式2来讲,效率更高。public static Bank getInstance() { //同步监视器默认为Bank.classif (instance == null) {synchronized (Bank.class) {if (instance == null) {instance = new Bank();}}}return instance;}
}
/*注意:上述方式 3 中,有指令重排问题mem = allocate(); 为单例对象分配内存空间instance = mem; instance 引用现在非空,但还未初始化ctorSingleton(instance); 为单例对象通过 instance 调用构造器从 JDK2 开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,然后复制到主存储区。但是需要volatile 关键字,避免指令重排。*/
1.7 死锁问题
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
1.7.1 产生死锁的原因
- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待
以上 4 个条件,同时出现就会触发死锁。
1.7.2 解决死锁
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
- 针对条件 1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
- 针对条件 2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
- 针对条件 3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
- 针对条件 4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
1.8 JDK5.0提供的Lock(锁)的使用
- JDK5.0 的新增功能,保证线程的安全。与采用 synchronized 相比,Lock 可提供多种锁方案,更灵活、更强大。Lock 通过显式定义同步锁对象来实现同步。同步锁使用Lock 对象充当。
- java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。
- 在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。ReentrantLock 类实现了 Lock 接口,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
- Lock 锁也称同步锁,加锁与释放锁方法,如下:
- public void lock() :加同步锁。
- public void unlock() :释放同步锁。
- synchronized和lock的对比:
- synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监视器的调用。
- Lock是通过两个方法控制需要被同步的代码,更灵活一些。
- Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。
class Window extends Thread{static int ticket = 100;//1.创建Lock的实例,需要确保多个线程共用同一个Lock实例,需要考虑将此对象声明为static finalprivate static final ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while (true){try {//2.执行lock方法,锁定对共享资源的调用lock.lock();if (ticket > 0){try {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);ticket--;}}//3.执行unlock方法,释放对共享资源的调用lock.unlock();}}}
}
1.9 线程的通信
1.9.1 需要处理线程间通信的理由
当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。
1.9.2 等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。
- wait():线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用。
- notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。(加果被wait()的多个线程的优先级相同,会随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。
- notifyAll:—旦执行此方法,就会唤醒所有被wait的线程。
注意点:
- 此三个方法的使用,必须使用在同步代码块或同步方法中。
- Lock需要配合Condition来实现线程之间的通信。
- 此三个方法的调用者,必须是同步监视器,否则会报异常。
- 此三个方法声明在Object类中。
1.9.3 wait()方法和sleep()方法的区别
- 相同点:一旦执行,当前线程都会进入阻塞状态。
- 不同点:
- 声明的位置不同:wait()方法声明在Object类中;sleep()方法声明在Thread类中且为静态方法。
- 使用的场景不同:wait()方法只能使用在同步代码块或同步方法中;sleep()方法可以在任何需要使用的场景中。
- 使用在同步代码块或同步方法中:如果wait()方法和sleep()方法都使用在同步代码块或同步方法中,wait()方法一旦执行,会释放同步监视器;而sleep()方法一旦执行,不会释放同步监视器。
- 结束阻塞的方式:wait()方法到达指定时间自动结束阻塞或通过被notify()唤醒结束阻塞;sleep()方法到达指定时间自动结束阻塞。
package p139;/*** 两个线程交替打印1-100之间的数*/public class PrintNumberTest {public static void main(String[] args) {PrintNumber p = new PrintNumber();Thread t1 = new Thread(p,"线程1");Thread t2 = new Thread(p,"线程2");t1.start();t2.start();}
}class PrintNumber implements Runnable{private int number = 1;@Overridepublic void run() {while (true){synchronized (this) {notify(); //唤醒线程if (number <= 100){try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":"+ number);number++;try {wait(); //线程一旦执行此方法,就进入等待状态,会同时释放对同步监视器的调用} catch (InterruptedException e) {e.printStackTrace();}}else {break;}}}}
}
package p139;/*** 生产者和消费者问题*//*** 店员*/
class Clerk{private int product = 0; //产品数量/*** 增加产品数量*/public synchronized void addProduct(){if (product >= 20){//等待try {wait();} catch (InterruptedException e) {e.printStackTrace();}}else {product++;System.out.println(Thread.currentThread().getName()+"生产了第"+product+"个产品");//唤醒notifyAll();}}/*** 减少产品数量*/public synchronized void minusProduct(){if (product <= 0){try {wait();} catch (InterruptedException e) {e.printStackTrace();}}else {System.out.println(Thread.currentThread().getName()+"消费了第"+product+"个产品");product--;//唤醒notifyAll();}}
}/*** 生产者*/
class Producer extends Thread{//将Clerk进行共享private Clerk clerk;//在创建Producer时,通过构造器实例化Clerk对象public Producer(Clerk clerk) {this.clerk = clerk;}@Overridepublic void run() {while (true){System.out.println("生产商品");clerk.addProduct();}}
}/*** 消费者*/
class Consumer extends Thread{//将Clerk进行共享private Clerk clerk;//在创建Consumer时,通过构造器实例化Clerk对象public Consumer(Clerk clerk) {this.clerk = clerk;}@Overridepublic void run() {while (true){System.out.println("消费商品");clerk.minusProduct();}}
}public class ProducerAndConsumerTest {public static void main(String[] args) {Clerk clerk = new Clerk();Producer pro1 = new Producer(clerk);Consumer con1 = new Consumer(clerk);pro1.setName("生产者1");con1.setName("消费者1");pro1.start();con1.start();}
}
1.10 创建线程的其他两种方式
1.10.1 实现Callable(JDK5.0中新增)
- 与使用 Runnable 相比,Callable 功能更强大些。
- 相比 run()方法,可以有返回值。
- 方法可以使用throws的方式处理异常。
- 使用泛型参数,可以指明具体的call()的返回值类型(需要借助 FutureTask 类,获取返回结果)。
- Future 接口(了解)
- 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。
- FutureTask 是 Futrue 接口的唯一的实现类。
- FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。
- 缺点:如果在主线程中需要获取分线程call()的返回值,则此时的主线程是阻塞状态的。
1.10.2 使用线程池(JDK5.0中新增)
- 线程池的优点:
- 提高了程序执行的效率。(因为线程已经提前创建好)
- 提高了资源的复用率。(因为执行完的线程并未销毁,而是可以继续执行其他的任务)
- 可以设置相关的参数,对线程池中的线程的使用进行管理。
- corePoolSize:核心池的大小。
- maximumPoolSize:最大线程数。
- keepAliveTime:线程没有任务时最多保持多长时间后会终止。
public class ThreadPool {public static void main(String[] args) {//1.提供指定线程数量的线程池ExecutorService service = Executors.newFixedThreadPool(10);ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;//设置线程池的属性service1.setMaximumPoolSize(50);//2.执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象service.execute(new NumberThread()); //适用于Runnableservice.execute(new NumberThread1()); //适用于Runnableservice.submit(Callable callable); //适用于Callable//3.关闭连接池service.shutdown();}
}