多线程

基本概念

  • 程序:为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  • 进程:程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:它有自身的产生、存在和消亡。——生命周期

    • 程序是静态的,进程是动态的。
    • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
  • 线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。

    • 若一个进程同时执行多个线程,就是支持多线程的。
    • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小。
  • 并行与并发

    • 并行:多个CPU同时执行多个任务。
    • 并发:一个CPU同时执行多个任务。

线程的创建和使用

  • Java语言的JVM允许程序运行多个线程,它通过Java.lang.Thread类来体现。
  • Thread类的特性

    • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主题称为线程体。
    • 通过Thread对象的start()方法来启动这个线程,而非直接调用run()。

代码练习:

  1. package day01;
  2. /**
  3. * 多线程的创建,方式一:继承鱼=于Thread类
  4. *1.创建一个继承于Thread类的子类
  5. * 2.重写Thread类的run() --->将此线程执行的操作声明在run()中
  6. * 3.创建Thread类的子类的对象
  7. * 4.通过此对象调用strat()
  8. *
  9. */
  10. //1.创建一个继承于Thread类的子类
  11. class MyThread extends Thread{
  12. //2.重写Thread类的run()
  13. @Override
  14. public void run() {
  15. for (int i = 0; i < 100; i++) {
  16. if (i%2==0)
  17. System.out.println(Thread.currentThread().getName()+":"+i);
  18. }
  19. }
  20. }
  21. public class ThreadTest {
  22. public static void main(String[] args) {
  23. //3.创建Thread类的子类对象
  24. MyThread myThread = new MyThread();
  25. //4.调用start()
  26. myThread.start();
  27. //问题一:我们不能通过直接调用run()的方式启动线程,这样没有创建多线程,只是使用了对象。
  28. // myThread.run();
  29. //问题二:再启动一个线程,遍历一百以内的偶数。会报错,原因是不可以让已经start()的线程去执行。需要通过新建一个对象去实现。
  30. // myThread.start();
  31. MyThread myThread1 = new MyThread();
  32. myThread1.start();
  33. //main方法中的主线程
  34. for (int i = 0; i < 100; i++) {
  35. if (i%2==0)
  36. System.out.println(Thread.currentThread().getName()+":"+i);
  37. }
  38. }
  39. }
  • Thread类的有关方法

    • void start():启动线程,并执行对象的run()方法。
    • run():线程在被调度时执行的操作。
    • String getName():返回线程的名称。
    • void setName(String name):设置该线程名称。
    • static Thread currentThread():返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类。
    • static void yield():线程让步。

      • 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程。
      • 若队列中没有同优先级的线程,忽略此方法。
    • join():当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。

      • 低优先级的线程也可以获得执行。
    • static void sleep(long millis):(指定时间:毫秒)

      • 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
      • 抛出IntettuptedException异常。
    • stop():强制线程生命期结束,不推荐使用。
    • boolean isAlive():返回boolean,判断线程是否还活着。
  • 线程的调度:

    • 调度策略

      • 时间片。
      • 抢占式:高优先级的线程抢占CPU。
    • Java的调度方法:

      • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略。
      • 对高优先级,使用优先调度的抢占式策略。
  • 线程的优先级:

    • 线程的优先级等级:

      • MAX_PRIORITY:10
      • MIN_PRIORITY:1

        • NORM_PRIORITY:5 默认的优先级值
    • 涉及到的方法:

      • getPriority():返回线程的优先值。
      • setPriority(int newPriority):改变线程的优先级。
    • 说明:

      • 线程创建时继承父线程的优先级。
      • 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用。
  • 代码练习:
  1. package day01;
  2. class MyThreadTest extends Thread{
  3. @Override
  4. public void run() {
  5. for (int i = 0; i < 100; i++) {
  6. if (i%2==0)
  7. System.out.println(Thread.currentThread().getName()+":"+getPriority()+":"+i);
  8. // if (i%20==0)
  9. // this.yield();
  10. }
  11. }
  12. }
  13. public class ThreadMethodTest {
  14. public static void main(String[] args) {
  15. MyThreadTest myThread = new MyThreadTest();
  16. //设置分线程的名字
  17. myThread.setName("线程1:遍历100以内的偶数");
  18. //设置分线程的优先级
  19. myThread.setPriority(Thread.MAX_PRIORITY);
  20. myThread.start();
  21. //给主线程命名
  22. Thread.currentThread().setName("主线程:遍历100以内的奇数"); //获取当前主线程
  23. Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
  24. for (int i = 0; i < 100; i++) {
  25. if (i%2!=0)
  26. System.out.println(Thread.currentThread().getName()+":"+Thread.currentThread().getPriority()+":"+i);
  27. }
  28. System.out.println(myThread.isAlive());
  29. }
  30. }
  • 创建线程的方式二:实现Runnable接口

    • 创建一个实现了Runnable接口的类。

    • 实现类去实现Runnable中的抽象方法:run()。

    • 创建实现类的对象。

    • 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象。

    • 通过Thread类的对象调用start()。

    • 代码练习

    • ```java package day01;

class MThread1 implements Runnable{ @Override public void run() { for (int i = 0; i < 100; i++) { if (i%2==0) System.out.println(i); } } } public class MThread { public static void main(String[] args) { MThread1 mThread1 = new MThread1(); Thread thread = new Thread(mThread1); thread.start(); } }

  1. -
  2. 创建线程的两种方式的比较:
  3. - 开发中优先选择实现Runnable接口的方式。
  4. - 原因:
  5. - 实现的方式没有类的单继承性的局限性。
  6. - 实现的方式更适合来处理多个线程有共享数据的情况。
  7. ---
  8. <a name="c32ea57d"></a>
  9. ## 线程的生命周期
  10. - 线程生命周期的五种状态:
  11. - 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
  12. - 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
  13. - 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
  14. - 阻塞:在某种特俗情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
  15. - 死亡:线程完成了它的全部工作或线程被提前强制性中止或出现异常导致结束。
  16. <a name="4de0e2ae"></a>
  17. ## 线程的同步
  18. -
  19. Java通过同步机制来解决线程的安全问题。
  20. -
  21. 方式一:同步代码块
  22. -
  23. ```java
  24. synchronized(同步监视器){
  25. //需要被同步的代码
  26. }
  • 说明:

    • 操作共享数据的代码,即为需要被同步的代码。
    • 共享数据:多个线程共同操作的变量。
    • 同步监视器:俗称:锁。任何一个类的对象都可以充当锁。要求:多个线程必须共用同一把锁。
  • 代码练习: ```java package dayTest;

/**

  • 例子:创建三个窗口卖票,总票数为100张 *
  • 问题:卖票过程中出现了重票和错票。
  • 问题出现的原因:当某个线程操作车票的过程中,尚未完成操作时,其他线程参与进来,也操作车票。
  • 如何解决:当一个线程操作车票时,其他线程不能参与进来。直到该线程操作完成,其他线程才被允许操作车票。
  • 在Java中,通过同步机制来解决线程的安全问题。 */

class Window implements Runnable{

  1. private int ticket=100;
  2. Object obj=new Object();
  3. @Override
  4. public void run() {
  5. while (true){
  6. synchronized (obj){
  7. if (ticket>0){
  8. try {
  9. Thread.sleep(10);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
  14. ticket--;
  15. }else break;
  16. }
  17. }
  18. }

} public class WindowTest { public static void main(String[] args) { Window w = new Window();

  1. Thread w1 = new Thread(w);
  2. Thread w2 = new Thread(w);
  3. Thread w3 = new Thread(w);
  4. //给线程命名
  5. w2.setName("窗口二");
  6. w1.setName("窗口一");
  7. w3.setName("窗口三");
  8. //给线程设定优先级

// w1.setPriority(Thread.MAX_PRIORITY); // w2.setPriority(Thread.MIN_PRIORITY); // w3.setPriority(Thread.NORM_PRIORITY);

  1. //执行线程
  2. w1.start();
  3. w2.start();
  4. w3.start();
  5. }

}

  1. -
  2. 方式二:同步方法
  3. -
  4. 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步。
  5. -
  6. 代码练习:
  7. ```java
  8. package dayTest;
  9. /**
  10. * 使用同步方法解决实现Runnable接口的线程安全问题
  11. */
  12. class Window1 implements Runnable{
  13. private int ticket=100;
  14. Object obj=new Object();
  15. @Override
  16. public void run() {
  17. while (true){
  18. show();
  19. }
  20. }
  21. private synchronized void show(){
  22. if (ticket>0){
  23. try {
  24. Thread.sleep(10);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
  29. ticket--;
  30. }
  31. }
  32. }
  33. public class WindowTest1 {
  34. public static void main(String[] args) {
  35. Window w = new Window();
  36. Thread w1 = new Thread(w);
  37. Thread w2 = new Thread(w);
  38. Thread w3 = new Thread(w);
  39. //给线程命名
  40. w2.setName("窗口二");
  41. w1.setName("窗口一");
  42. w3.setName("窗口三");
  43. //给线程设定优先级
  44. // w1.setPriority(Thread.MAX_PRIORITY);
  45. // w2.setPriority(Thread.MIN_PRIORITY);
  46. // w3.setPriority(Thread.NORM_PRIORITY);
  47. //执行线程
  48. w1.start();
  49. w2.start();
  50. w3.start();
  51. }
  52. }
  • 线程的死锁问题

    • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
    • 出现死锁后不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
  • 解决方法

    • 专门的算法。
    • 尽量减少同步资源的定义。
    • 尽量避免嵌套同步。
  • Lock锁

    • 从JDK5.0开始,Java提供了更强大的线程同步机制——通过定义同步锁对象来实现同步。同步锁使用Lock对象充当。

    • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

    • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。

    • 代码练习: ```java package day01;

import java.util.concurrent.locks.ReentrantLock;

class Window implements Runnable{ private int ticket=100;

  1. //创建对象
  2. private ReentrantLock lock=new ReentrantLock();
  3. @Override
  4. public void run() {
  5. while (true){
  6. try {
  7. //调用lock
  8. lock.lock();
  9. if (ticket>0){
  10. try {
  11. Thread.sleep(100);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. System.out.println(Thread.currentThread().getName()+":售票,票号为:"+ticket);
  16. ticket--;
  17. }else break;
  18. }
  19. finally {
  20. //解锁
  21. lock.unlock();
  22. }
  23. }
  24. }

} public class LockTest { public static void main(String[] args) { Window window = new Window();

  1. Thread w1=new Thread(window);
  2. Thread w2=new Thread(window);
  3. Thread w3=new Thread(window);
  4. w1.setName("窗口一");
  5. w2.setName("窗口二");
  6. w3.setName("窗口三");
  7. w1.start();
  8. w2.start();
  9. w3.start();
  10. }

}

  1. -
  2. synchronizedLock的异同
  3. - 相同点:二者都可以解决线程安全问题。
  4. - 不同点:前者是执行完相应的同步代码之后自动释放同步监视器,后者是手动的启动同步和释放。
  5. ---
  6. <a name="ffc5004b"></a>
  7. ## 线程的通信
  8. -
  9. 线程通信的三个方法
  10. -
  11. wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
  12. -
  13. notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
  14. -
  15. notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
  16. - 说明
  17. - 三个方法必须使用在同步代码块或者同步方法中
  18. - 三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
  19. -
  20. 代码练习:
  21. ```java
  22. package day01;
  23. /**
  24. * 线程通信的例子:使用两个线程打印1-100.线程1,线程2交替打印。
  25. */
  26. class Number implements Runnable{
  27. private int number=1;
  28. @Override
  29. public void run() {
  30. while (true){
  31. synchronized (this){
  32. notify();
  33. if (number<=100){
  34. System.out.println(Thread.currentThread().getName()+":"+number);
  35. number++;
  36. try {
  37. wait();
  38. } catch (InterruptedException e) {
  39. e.printStackTrace();
  40. }
  41. }else break;
  42. }
  43. }
  44. }
  45. }
  46. public class CommunicationTest {
  47. public static void main(String[] args) {
  48. Number number = new Number();
  49. Thread n1 = new Thread(number);
  50. Thread n2 = new Thread(number);
  51. n1.setName("线程1");
  52. n2.setName("线程2");
  53. n1.start();
  54. n2.start();
  55. }
  56. }

sleep()和wait()的异同

  • 相同点:一旦执行方法,都可以使得当前线程进入阻塞状态,
  • 不同点

    • 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()。
    • 调用的要求不同:sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或同步方法之中。
    • 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不释放同步监视器,wait()释放同步监视器。

生产者消费者问题

代码演示:

  1. package day01;
  2. /**
  3. * 线程通信的应用:经典例题:生产者、消费者问题
  4. * 生产者将产品交给店员,而消费者从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图
  5. * 生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会
  6. * 告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
  7. */
  8. class Clerk{
  9. private int productCount=0;
  10. //生产产品
  11. public synchronized void produceProduct() {
  12. if (productCount<20){
  13. productCount++;
  14. System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个产品");
  15. notify();
  16. }
  17. else {
  18. //等待
  19. try {
  20. wait();
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. }
  26. //消费产品
  27. public synchronized void consumeProduct() {
  28. if (productCount>0){
  29. System.out.println(Thread.currentThread().getName()+":开始消费第"+productCount+"个产品");
  30. productCount--;
  31. notify();
  32. }else {
  33. //等待
  34. try {
  35. wait();
  36. } catch (InterruptedException e) {
  37. e.printStackTrace();
  38. }
  39. }
  40. }
  41. }
  42. class Producer extends Thread{//生产者
  43. private Clerk clerk;
  44. public Producer(Clerk clerk) {
  45. this.clerk = clerk;
  46. }
  47. @Override
  48. public void run() {
  49. System.out.println(getName()+":开始生产产品······");
  50. while (true){
  51. try {
  52. Thread.sleep(50);
  53. } catch (InterruptedException e) {
  54. e.printStackTrace();
  55. }
  56. clerk.produceProduct();
  57. }
  58. }
  59. }
  60. class Consumer extends Thread{//消费者
  61. private Clerk clerk;
  62. public Consumer(Clerk clerk) {
  63. this.clerk = clerk;
  64. }
  65. @Override
  66. public void run() {
  67. System.out.println(getName()+":开始消费产品·····");
  68. while (true){
  69. try {
  70. Thread.sleep(100);
  71. } catch (InterruptedException e) {
  72. e.printStackTrace();
  73. }
  74. clerk.consumeProduct();
  75. }
  76. }
  77. }
  78. public class ProductTest {
  79. public static void main(String[] args) {
  80. Clerk clerk = new Clerk();
  81. Producer p1 = new Producer(clerk);
  82. p1.setName("生产者1");
  83. Consumer c1 = new Consumer(clerk);
  84. c1.setName("消费者1");
  85. p1.start();
  86. c1.start();
  87. }
  88. }

饿汉式和懒汉式