什么是Java多线程

53次阅读

共计 11264 个字符,预计需要花费 29 分钟才能阅读完成。

第五阶段 多线程

前言:

一个场景:周末,带着并不存在的女票去看电影,无论是现场买票也好,又或是手机买票也好,上一秒还有位置,迟钝了一下以后,就显示该座位已经无法选中,一不留神就没有座位了,影院的票是一定的,但是究竟是如何做到,多个窗口或者用户同时出票而又不重复的呢?这就是我们今天所要讲解的多线程问题

(一) 线程和进程的概述

(1) 进程

  • 进程:进程是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源
  • 多线程:在同一个时间段内可以执行多个任务,提高了 CPU 的使用率

(2) 线程

  • 线程:进程的执行单元,执行路径
  • 单线程:一个应用程序只有一条执行路径
  • 多线程:一个应用程序有多条执行路径
  • 多进程的意义?—— 提高 CPU 的使用率
  • 多线程的意义? —— 提高应用程序的使用率

(3) 补充

并行和并发

  • 并行 是逻辑上同时发生,指在某一个时间段内同时运行多个程序
  • 并发 是物理上同时发生,指在某一个时间点同时运行多个程序

Java 程序运行原理和 JVM 的启动是否是多线程的?

  • Java 程序的运行原理:

    • 由 java 命令启动 JVM,JVM 启动就相当于启动了一个进程
    • 接着有该进程创建了一个主线程去调用 main 方法
  • JVM 虚拟机的启动是单线程的还是多线程的 ?

    • 垃圾回收线程也要先启动,否则很容易会出现内存溢出
    • 现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,jvm 的启动其实是多线程的
    • JVM 启动至少启动了垃圾回收线程和主线程,所以是多线程的

(二) 多线程代码实现

需求:我们要实现多线程的程序。

如何实现呢?

由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。

而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。

Java 是 不能直接调用系统功能 的,所以,我们 没有办法直接实现多线程 程序。

但是呢?Java 可以去调用 C /C++ 写好的程序来实现多线程程序。

由 C /C++ 去调用系统功能创建进程,然后由 Java 去调用这样的东西,

然后提供一些类供我们使用。我们就可以实现多线程程序了。

通过查看 API,我们知道了有 2 种 方式实现多线程程序。

方式 1:继承 Thread 类

步骤:

  • 自定义 MyThread(自定义类名)继承 Thread 类
  • MyThread 类中重写 run()
  • 创建对象
  • 启动线程
public class MyThread extends Thread{public MyThread() { }
    
    @Override
    public void run() {for (int i = 0; i < 100; i++){System.out.println(getName() + ":" + i);
        }
    }
}
public class MyThreadTest {public static void main(String[] args) {
        // 创建线程对象
        MyThread my = new MyThread();
        // 启动线程,run()相当于普通方法的调用,单线程效果
        //my.run();
        // 首先启动了线程,然后再由 jvm 调用该线程的 run()方法,多线程效果
        my.start();

        // 两个线程演示,多线程效果需要创建多个对象而不是一个对象多次调用 start()方法
        MyThread my1 = new MyThread();
        MyThread my2 = new MyThread();

        my1.start();
        my2.start();}
}

// 运行结果
Thread-1:0
Thread-1:1
Thread-1:2
Thread-0:0
Thread-1:3
Thread-0:1
Thread-0:2
......
Thread-0:95
Thread-0:96
Thread-0:97
Thread-0:98
Thread-0:99

方式 2:实现 Runnable 接口 (推荐)

步骤:

  • 自定义类 MyuRunnable 实现 Runnable 接口
  • 重写 run()方法
  • 创建 MyRunable 类的对象
  • 创建 Thread 类的对象,并把 C 步骤的对象作为构造参数传递
public class MyRunnable implements Runnable {public MyRunnable() { }
    
    @Override
    public void run() {for (int i = 0; i < 100; i++){
            // 由于实现接口的方式不能直接使用 Thread 类的方法了,但是可以间接的使用
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
public class MyRunnableTest {public static void main(String[] args) {
        // 创建 MyRunnable 类的对象
        MyRunnable my = new MyRunnable();

        // 创建 Thread 类的对象,并把 C 步骤的对象作为构造参数传递
//        Thread t1 = new Thread(my);
//        Thread t2 = new Thread(my);
        // 下面具体讲解如何设置线程对象名称
//        t1.setName("User1");
//        t1.setName("User2");

        Thread t1 = new Thread(my,"User1");
        Thread t2 = new Thread(my,"User2");

        t1.start()
        t2.start();}
}

实现接口方式的好处

可以避免由于 Java 单继承带来的局限性

适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想

如何理解 —— 可以避免由于 Java 单继承带来的局限性

比如说,某个类已经有父类了,而这个类想实现多线程,但是这个时候它已经不能直接继承 Thread 类了

(接口可以多实现 implements,但是继承 extends 只能单继承),它的父类也不想继承 Thread 因为不需要实现多线程

(三) 获取和设置线程对象

// 获取线程的名称
public final String getName()

// 设置线程的名称
public final void setName(String name)

设置线程的名称 (如果不设置名称的话,默认是 Thread-? (编号) )

方法一:无参构造 + setXxx (推荐)

// 创建 MyRunnable 类的对象
MyRunnable my = new MyRunnable();

// 创建 Thread 类的对象,并把 C 步骤的对象作为构造参数传递
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
t1.setName("User1");
t1.setName("User2");
        
// 与上面代码等价
Thread t1 = new Thread(my,"User1");
Thread t2 = new Thread(my,"User2");

方法二:(稍微麻烦,要手动写 MyThread 的带参构造方法,方法一不用)

//MyThread 类中

public MyThread(String name){super(name);// 直接调用父类的就好
}

//MyThreadTest 类中
MyThread my = new MyThread("admin");

获取线程名称

注意:重写 run 方法内获取线程名称的方式

//Thread
getName()

//Runnable
// 由于实现接口的方式不能直接使用 Thread 类的方法了,但是可以间接的使用
Thread.currentThread().getName()

使用实现 Runnable 接口方法的时候注意:main 方法所在的测试类并不继承 Thread 类,因此并不能直接使用 getName()方法来获取名称。

// 这种情况 Thread 类提供了一个方法:
//public static Thread currentThread():

// 返回当前正在执行的线程对象, 返回值是 Thread, 而 Thread 恰巧可以调用 getName()方法
System.out.println(Thread.currentThread().getName());

(四) 线程调度及获取和设置线程优先级

假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU 时间片,也就是使用权,才可以执行指令。那么 Java 是如何对线程进行调用的呢?

线程有两种调度模型:

分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片

抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。

Java 使用的是抢占式调度模型

// 演示如何设置和获取线程优先级

// 返回线程对象的优先级
public final int getPriority()

// 更改线程的优先级
public final void setPriority(int newPriority)

线程默认优先级是 5。

线程优先级的范围是:1-10。

线程优先级高仅仅表示线程获取的 CPU 时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。

(五) 线程控制

在后面的案例中会用到一些,这些控制功能不是很难,可以自行测试。

// 线程休眠
public static void sleep(long millis)

// 线程加入(等待该线程终止,主线程结束后,其余线程开始抢占资源)
public final void join()

// 线程礼让(暂停当前正在执行的线程对象,并且执行其他线程让多个线程的执行更加和谐,但是不能保证一人一次)
public static void yield()

// 后台线程(某线程结束后,其他线程也结束)public final void setDaemon(boolean on)

//(过时了但还可以用)public final void stop()

// 中断线程
public void interrupt()

(六) 线程的生命周期

新建 —— 创建线程对象

就绪 —— 线程对象已经启动,但是还没有获取到 CPU 的执行权

运行 —— 获取到了 CPU 的执行权

  • 阻塞 —— 没有 CPU 的执权,回到就绪

死亡 —— 代码运行完毕,线程消亡

(七) 多线程电影院出票案例

public class SellTickets implements Runnable {
    private int tickets = 100;

    @Override
    public void run() {while (true){if (tickets > 0){
                try {Thread.sleep(100);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() 
                                   + "正在出售第" + (tickets--) + "张票");
            }
        }
    }
}
public class SellTicketsTest {public static void main(String[] args) {
        // 创建资源对象
        SellTickets st = new SellTickets();

        // 创建线程对象
        Thread t1 = new Thread(st, "窗口 1");
        Thread t2 = new Thread(st, "窗口 2");
        Thread t3 = new Thread(st, "窗口 3");

        // 启动线程
        t1.start();
        t2.start();
        t3.start();}
}

在 SellTicket 类中添加 sleep 方法,延迟一下线程,拖慢一下执行的速度

通过加入延迟后,就产生了连个问题:

A: 相同的票卖了多次

CPU 的一次操作必须是原子性(最简单的)的 (在读取 tickets– 的原来的数值和减 1 之后的中间挤进了两个线程而出现重复)

B: 出现了负数票

随机性和延迟导致的 (三个线程同时挤进一个循环里,tickets– 的减法操作有可能在同一个循环中被执行了多次而出现越界的情况,比如说 tickets 要大于 0 却越界到了 -1)

也就是说,线程 1 执行的同时线程 2 也可能在执行,而不是线程 1 执行的时候线程 2 不能执行。

我们先要知道一下哪些问题会导致出问题:

而且这些原因也是以后我们 判断一个程序是否会有线程安全问题的标准

A: 是否是多线程环境

B: 是否有共享数据

C: 是否有多条语句操作共享数据

我们对照起来,我们的程序确实存在上面的问题,因为它满足上面的条件

那我们怎么来解决这个问题呢?

把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行

Java 给我们提供了:同步机制

// 同步代码块:synchronized(对象){需要同步的代码;}

同步的好处

同步的出现解决了多线程的安全问题

同步的弊端

当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

概述:

A: 同步代码块的锁对象是谁呢?

任意对象

B: 同步方法的格式及锁对象问题?

把同步关键字加在方法上

同步方法的锁对象是谁呢?

this

C: 静态方法及锁对象问题?

静态方法的锁对象是谁呢?

类的字节码文件对象。

我们使用 synchronized 改进我们上面的程序,前面线程安全的问题,

public class SellTickets implements Runnable {
    private int tickets = 100;

    // 创建锁对象
    // 把这个关键的锁对象定义到 run()方法(独立于线程之外),造成同一把锁
    private Object obj = new Object();

    @Override
    public void run() {while (true) {synchronized (obj) {if (tickets > 0) {
                    try {Thread.sleep(100);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() 
                                       + "正在出售第" + (tickets--) + "张票");
                }
            }
        }
    }
}

(八) lock 锁的概述和使用

为了更清晰的表达如何加锁和释放锁,JDK5 以后提供了一个新的锁对象 Lock

(可以更清晰的看到在哪里加上了锁,在哪里释放了锁,)

void lock() 加锁

void unlock() 释放锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SellTickets2 implements Runnable {

    private int tickets = 100;

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {while (true) {
            try {lock.lock();
                ;
                if (tickets > 0) {
                    try {Thread.sleep(150);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
                }
            } finally {lock.unlock();
            }
        }
    }
}

(九) 死锁问题 (简单认识)

同步弊端

效率低

如果出现了同步嵌套,就容易产生死锁问题

死锁问题

是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象

(十) 等待唤醒机制

我们前面假定的电影院场景,其实还是有一定局限的,我们所假定的票数是一定的,但是实际生活中,往往是一种供需共存的状态,例如去买早点,当消费者买走一些后,而作为生产者的店家就会补充一些商品,为了研究这一种场景,我们所要学习的就是 Java 的等待唤醒机制

生产者消费者问题 (英语:Producer-consumer problem),也称 有限缓冲问题(英语:Bounded-buffer problem),是一个多进程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

我们用通俗一点的话来解释一下这个问题

Java 使用的是抢占式调度模型

  • A:如果消费者先抢到了 CPU 的执行权,它就会去消费数据,但是现在的数据是默认值,如果没有意义,应该等数据有意义再消费。就好比买家进了店铺早点却还没有做出来,只能等早点做出来了再消费
  • B:如果生产者先抢到 CPU 的执行权,它就回去生产数据,但是,当它产生完数据后,还继续拥有执行权,它还能继续产生数据,这是不合理的,你应该等待消费者将数据消费掉,再进行生产。这又好比,店铺不能无止境的做早点,卖一些,再做,避免亏本

梳理思路

  • A:生产者 —— 先看是否有数据,有就等待,没有就生产,生产完之后通知消费者来消费数据
  • B:消费者 —— 先看是否有数据,有就消费,没有就等待,通知生产者生产数据

解释 唤醒——让线程池中的线程具备执行资格

Object 类提供了三个方法:

// 等待
wait()
// 唤醒单个线程
notify()
// 唤醒所有线程
notifyAll()

注意:这三个方法都必须在同步代码块中执行 (例如 synchronized 块),同时在使用时必须标明所属锁,这样才可以得出这些方法操作的到底是哪个锁上的线程

为什么这些方法不定义在 Thread 类中呢 ?

这些方法的调用必须通过锁对象调用,而我们刚才使用的锁对象是任意锁对象。

所以,这些方法必须定义在 Object 类中。

我们来写一段简单的代码 实现等待唤醒机制

public class Student {
    String name;
    int age;
    boolean flag;// 默认情况是没有数据(false),如果是 true,说明有数据

    public Student() {}
}
public class SetThread implements Runnable {
    private Student s;
    private int x = 0;

    public SetThread(Student s) {this.s = s;}

    @Override
    public void run() {while (true){synchronized (s) {
                // 判断有没有数据
                // 如果有数据,就 wait
                if (s.flag) {
                    try {s.wait(); //t1 等待,释放锁
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }

                // 没有数据,就生产数据
                if (x % 2 == 0) {
                    s.name = "admin";
                    s.age = 20;
                } else {
                    s.name = "User";
                    s.age = 30;
                }
                x++;
                // 现在数据就已经存在了,修改标记
                s.flag = true;

                // 唤醒线程
                // 唤醒 t2, 唤醒并不表示你立马可以执行,必须还得抢 CPU 的执行权。s.notify();}
        }
    }
}
package cn.bwh_05_Notify;

public class GetThread implements Runnable {
    private Student s;

    public GetThread(Student s) {this.s = s;}

    @Override
    public void run() {while (true){synchronized (s){
                // 如果没有数据,就等待
                if (!s.flag){
                    try {s.wait();
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                }

                System.out.println(s.name + "---" + s.age);

                // 修改标记
                s.flag = false;
                // 唤醒线程 t1
                s.notify();}
        }
    }
}
package cn.bwh_05_Notify;

public class StudentTest {public static void main(String[] args) {Student s = new Student();

        // 设置和获取的类
        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);

        // 线程类
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

        // 启动线程
        t1.start();
        t2.start();}
}
// 运行结果依次交替出现

生产者消费者之等待唤醒机制代码优化

最终版代码(在 Student 类中有大改动,然后 GetThread 类和 SetThread 类简洁很多)

public class Student {
    private String name;
    private int age;
    private boolean flag;

    public synchronized void set(String name, int age) {if (this.flag) {
            try {this.wait();
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }

        this.name = name;
        this.age = age;

        this.flag = true;
        this.notify();}

    public synchronized void get() {if (!this.flag) {
            try {this.wait();
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }

        System.out.println(this.name + "---" + this.age);

        this.flag = false;
        this.notify();}
}
public class SetThread implements Runnable {
    private Student s;
    private int x = 0;

    public SetThread(Student s) {this.s = s;}

    @Override
    public void run() {while (true) {if (x % 2 == 0) {s.set("admin", 20);
            } else {s.set("User", 30);
            }
            x++;
        }
    }
}
public class GetThread implements Runnable{
    private Student s;

    public GetThread(Student s) {this.s = s;}

    @Override
    public void run() {while (true){s.get();
        }
    }
}
public class StudentTest {public static void main(String[] args) {Student s = new Student();
        // 设置和获取的类

        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);

        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

        t1.start();
        t2.start();}
}

最终版代码特点:

  • 把 Student 的成员变量给私有的了。
  • 把设置和获取的操作给封装成了功能,并加了同步。
  • 设置或者获取的线程里面只需要调用方法即可

(十一) 线程池

程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池

线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用

在 JDK5 之前,我们必须手动实现自己的线程池,从 JDK5 开始,Java 内置支持线程池

JDK5 新增了一个 Executors 工厂类来产生线程池,有如下几个方法
// 创建一个具有缓存功能的线程池
// 缓存:百度浏览过的信息再次访问
public static ExecutorService newCachedThreadPool()

// 创建一个可重用的,具有固定线程数的线程池
public static ExecutorService newFixedThreadPool(intnThreads)
                       
// 创建一个只有单线程的线程池,相当于上个方法的参数是 1 
public static ExecutorService newSingleThreadExecutor()
                       
这些方法的返回值是 ExecutorService 对象,该对象表示一个线程池,可以执行 Runnable 对象或者 Callable 对象代表的线程。它提供了如下方法

Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorDemo {public static void main(String[] args) {
        // 创建一个线程池对象,控制要创建几个线程对象
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 可以执行 Runnalble 对象或者 Callable 对象代表的线程
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());

        // 结束线程池
        pool.shutdown();}
}

(十二) 匿名内部类的方式实现多线程程序

匿名内部类的格式:

new 类名或者接口名( ) {重写方法;};

本质:是该类或者接口的子类对象

public class ThreadDemo {public static void main(String[] args) {new Thread() {
            @Override
            public void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + i);
                }
            }
        }.start();}
}
public class RunnableDemo {public static void main(String[] args) {new Thread(new Runnable() {
            @Override
            public void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + i);
                }
            }
        }).start();}
}

(十三) 定时器

定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在 Java 中,可以通过 Timer 和 TimerTask 类来实现定义调度的功能

Timer

·public Timer()

public void schedule(TimerTask task, long delay)

public void schedule(TimerTask task,long delay,long period)

TimerTask

abstract void run()

public boolean cancel()

开发中

Quartz 是一个完全由 java 编写的开源调度框架

结尾:

如果内容中有什么不足,或者错误的地方,欢迎大家给我留言提出意见, 蟹蟹大家!^_^

如果能帮到你的话,那就来关注我吧!(系列文章均会在公众号第一时间更新)

在这里的我们素不相识,却都在为了自己的梦而努力 ❤

一个坚持推送原创 Java 技术的公众号:理想二旬不止

正文完
 0