多线程

进程与线程

进程

进程表示正在运行的程序,是系统进行资源分配和调用的独立单位,每一个进程都有自己的内存空间和系统资源。CPU把时间分成很多细小的时间片,一个时间片给一个程序用,一个时间片给另一个程序用,并在它们之间反复切换。

线程

线程是进程中单个执行顺序流,是一条执行路径。如果一个进程只有一条执行路径,则称为单线程程序;如果一个进程有多条执行路径,则称为多线程程序。

并发和并行

实现多线程

继承Thread类

为什么要重写run方法

因为run方法是用来封装被线程执行的代码。

调用run方法和调用start方法的区别

  • run封装线程执行的代码,直接调用,相当于普通方法调用
  • start启动线程,然后由Java虚拟机调用此线程的run方法

实现Runnable接口

创建一个Runnable实现类,重写run方法;创建一个Thread类实例,将Runnable实现类对象作为参数传递给Thread类构造方法。

相比继承Thread类,实现Runnable接口有如下好处:

  • 避免了Java单继承的局限性
  • 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想。

实现Callable接口

通过线程池创建线程

线程对象概述

设置和获取线程名称

Thread类中提供了设置和获取线程名称的方法:

  1. void setName(String name);
  2. String getName();
  3. public Thread() {
  4. this(null, null, "Thread-" + nextThreadNum(), 0);
  5. }
  6. // 每次调用+1,默认值为0
  7. private static int threadInitNumber;
  8. private static synchronized int nextThreadNum(){
  9. return threadInitNumber++;
  10. }

线程调度

线程有两种调度模型:

  1. 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
  2. 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。

Java使用的是抢占式调度模型,假如计算机只有一个CPU,那么CPU在某一时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性的,因为谁抢到CPU的使用权是不一定的。

Thread类中提供了设置和获取线程优先级的方法:

  1. // 返回此线程的优先级
  2. public final int getPriority();
  3. // 更改此线程的优先级
  4. public final void setPriority(int newPriority);
  5. public final static int MIN_PRIORITY = 1;
  6. public final static int NORM_PRIORITY = 5;
  7. public final static int MAX_PRIORITY = 10;

线程的默认优先级是5,线程优先级的范围是1-10,线程优先级高仅仅代表线程获取的CPU时间片的几率高,但是要在次数比较多,或者多次运行之后才能看到想要的结果。

线程控制

  1. // 是当前正在执行的线程停留(暂停执行)指定的毫秒数
  2. static void sleep(long millis);
  3. // 等待这个线程死亡
  4. // 如果线程对象调用了join方法,那么其他的线程会等待这个线程执行完毕,才有机会执行
  5. void join();
  6. // 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
  7. // 守护线程等待所有线程执行完毕才会退出,不是立即退出,虚拟机GC机制就是一个守护线程
  8. void setDaemon(boolean on);

线程的生命周期

线程同步

判断多线程是否会有数据安全问题的标准。

  • 是否是多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

如何解决多线程安全问题?

  • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行。
  • Java提供了同步代码块来解决。

同步代码块

锁多条语句操作共享数据,可以使用同步代码块实现。

  1. synchronized(任意对象){
  2. 多条语句操作共享数据的代码
  3. }

synchronize(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁。

同步的好处和弊端:

  • 解决了多线程的数据安全问题。
  • 当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形之中会降低程序的运行效率。

同步方法

同步方法就是把synchronized关键字加到方法上,同步方法的锁是this。

  1. 修饰符 synchronized 返回值类型 方法名(方法参数){}

同步静态方法就是把synchronized关键字加到静态方法上,同步静态方法的锁是类名.class。

  1. 修饰符 static synchronized 返回值类型 方法名(方法参数){}

线程安全的类

StringBuffer

  • 线程安全,可变的字符序列
  • 从JDK5开始,被StringBuilder替代。通常应该使用StringBuilder,因为它支持所有相同的操作,但它更快,因为它不执行同步。

Vector

  • 从Java 2平台开始,该类该进了List接口,使其成为Java Collection Framework的成员。与新的集合实现不同,Vector被同步。如果不需要线程安全,建议使用ArrayList代替Vector。

Hashtable

  • 该类实现了一个哈希表,它将键映射到值。任何非null对象都可以用作键或者值。
  • 从java2平台开始,该类实现了改进,实现了map接口,使其成为Java Collection Framework的成员。与新的集合实现不同,HashTable被同步。如果不需要线程安全的实现,建议使用HashMap代替HashTable。

可以使用Collections类中的synchronizedList包装成一个线程安全的类。

Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5之后提供了一个新的锁对象Lock。

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作,Lock中提供了获得锁和释放锁的方法:

  1. // 获得锁
  2. void lock();
  3. // 释放锁
  4. void unlock();

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化ReentrantLock的构造方法。

  1. ReentrantLock();

生产者消费者

生产者和消费者是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻所谓生产者和消费者问题,实际上主要是包含了两类线程:

  • 一个是生产者线程用于生产数据
  • 一个是消费者线程用于消费数据

为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库。

  • 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
  • 消费者只需要从共享数据区中获取数据,并不需要关心生产者的行为

为了体现生产和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用,这几个方法在Object类中,Object类中的等待和唤醒方法:

  1. // 导致当前线程等待,知道另一个线程调用该对象的notify或notifyAll方法
  2. void wait();
  3. // 唤醒正在等待对象监视器的单个线程
  4. void notify();
  5. // 唤醒正在等待对象监视器的所有线程
  6. void notifyAll();

案例

生产者消费者案例中包含的类:

  • 奶箱类(milkBox):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作。
  • 生产者类(Producer):实现Runnable接口,重写run方法,调用存储牛奶的操作
  • 消费者类(Customer):实现Runnable接口,重写run方法,调用获取牛奶的操作
  • 测试类(milkBoxTest):
  1. package com.shaw.thread.producer.customer;
  2. public class MilkBox {
  3. private int milkCount;
  4. private boolean isEmpty = true;
  5. public MilkBox() {
  6. }
  7. // 使用wait方法需要共享锁 synchronized 或者 Lock锁
  8. public synchronized void get() {
  9. // 如果没有有牛奶就等待生产牛奶
  10. if (this.isEmpty) {
  11. try {
  12. wait();
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. // 如果有牛奶就消费牛奶
  18. System.out.println("用户将第" + this.milkCount + "瓶奶拿走");
  19. // 修改状态
  20. this.isEmpty = true;
  21. // 唤醒其他线程
  22. notifyAll();
  23. }
  24. public synchronized void putMilk(int milkCount) {
  25. // 如果有牛奶就等待消费
  26. if (!this.isEmpty) {
  27. try {
  28. wait();
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. this.milkCount = milkCount;
  34. // 如果没有牛奶就生产牛奶
  35. System.out.println("送奶工将第" + this.milkCount + "瓶奶放入奶箱");
  36. // 修改状态
  37. this.isEmpty = false;
  38. // 唤醒其他线程
  39. notifyAll();
  40. }
  41. }
  1. package com.shaw.thread.producer.customer;
  2. public class Producer implements Runnable {
  3. private MilkBox milkBox;
  4. private Customer customer;
  5. public Producer(MilkBox milkBox){
  6. this.milkBox = milkBox;
  7. }
  8. @Override
  9. public void run() {
  10. for (int i = 1; i <= 30; i++) {
  11. milkBox.putMilk(i);
  12. }
  13. }
  14. }
  1. package com.shaw.thread.producer.customer;
  2. public class Customer implements Runnable {
  3. private MilkBox milkBox;
  4. private Producer producer;
  5. public Customer(MilkBox milkBox) {
  6. this.milkBox = milkBox;
  7. }
  8. @Override
  9. public void run() {
  10. while (true) {
  11. milkBox.get();
  12. }
  13. }
  14. }
  1. package com.shaw.thread.producer.customer;
  2. public class MilkBoxTest {
  3. public static void main(String[] args) {
  4. // 共享数据区域
  5. MilkBox milkBox = new MilkBox();
  6. // 生产者对象,把共享数据区域作为构造方法参数传入,因为这个类中需要调用存储牛奶的操作
  7. Producer p = new Producer(milkBox);
  8. // 消费者对象,把共享数据区域作为构造方法参数传入,因为这个类中需要调用获取牛奶的操作
  9. Customer c = new Customer(milkBox);
  10. // 创建生产者线程和消费者线程
  11. Thread producerThread = new Thread(p);
  12. Thread customerThread = new Thread(c);
  13. producerThread.start();
  14. customerThread.start();
  15. }
  16. }

Wait和Sleep的区别

  • Wait是Object实例方法,而sleep是线程的静态方法
  • sleep方法不会释放lock,但是wait方法会释放lock,而且会加入到等待队列中
  • sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
  • sleep不需要被唤醒,但是wait需要。