Unit 8 多线程

1 基本概念

  • 程序(Program),是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

  • 进程(process),是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程——生命周期

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

    • 若一个进程同一时间并行执行多个线程,就是支持多线程的。
    • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
    • 一个进程中的多个线程共享相同的内存单元/内存地址空间 → 它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能会带来安全的隐患。
  • 单核 CPU 和多核 CPU 的理解

    • 单核 CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如,虽然多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么 CPU 就好比收费人员,如果有某个人不想交钱,收费人员可以把它挂起。但是CPU时间单元特别短,因此感觉不出来。
    • 如果是多核的话,才能更好地发挥多线程的效率(现在的服务器都是多核的)
    • 一个Java应用程序 java.exe,其实至少三个线程:main( ) 主线程,gc( ) 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
  • 并行与并发

    • 并行:多个 CPU 同时执行多个任务。比如:多个人同时做不同的事。
    • 并发:一个 CPU(采用时间片)“同时”执行多个任务。比如:秒杀、多个人做同一件事。

使用多线程的好处

  • 提高应用程序的响应。对图像化界面更有意义
  • 提高计算机 CPU 利用率
  • 改善程序结构,代码结构更清晰

何时需要多线程

  • 程序需要同时执行两个或多个任务。
  • 程序需要实现一些需要等待的任务时,如用户输入、操作、网络操作、搜索等。
  • 需要一些后台运行的程序时。

2 线程的创建和使用

在这里插入图片描述

1)多线程创建方式一:继承于Thread类

  • 1.创建一个继承于Thread类的子类
  • 2.重写 Thread 类的run() -->将此线程执行的操作声明在 run( ) 中
  • 3.创建 Thread 类的子类的对象
  • 4.通过此对象调用start():作用:①启动当前线程 ②调用当前线程的 run( )

​ 注意: 问题一:我们启动一个线程,必须调用 start( ),不能调用 run( ) 的方式启动线程。
​ 问题二:如果再启动一个线程,必须重新创建一个 Thread 子类的对象,调用此对象的 start( )。

例子:遍历100以内继承于Thread类的子类
//1. 创建一个继承于Thread类的子类
class MyThread extends Thread{
    //2. 重写Thread类的run():将此线程要做的事声明在run方法中
    @Override
    public void run() {
        for(int i = 0;i < 100;i++){
            if(i % 2 == 0){
                System.out.println(i);
            }
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        //3. 创建Thread类的子类的对象
        MyThread t1 = new MyThread();

        //4. 通过此对象调用start():作用:① 启动当前线程 ② 调用当前线程的run()
        t1.start();
        //问题一:我们不能通过直接调用run()的方式启动线程。
        // t1.run();

        //问题二:再启动一个线程,遍历100以内的偶数.不可以还让已经start()的线程去执行,会
        //报Illegal ThreadStateException
        // t1.start();
        //我们需要重新创建一个线程的对象
        MyThread t2 = new MyThread();
        t2.start();

        //如下操作仍然是在main线程中执行的。
        for(int i = 0;i < 100;i++){
            if(i % 2 == 0){
                System.out.println(i + "***********");
            }
        }
        // System.out.println("hello");
    }
}
/*
练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数
*/
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread1 m1 = new MyThread1();
        MyThread2 m2 = new MyThread2();
        m1.start();
        m2.start();
        //创建Thread类的匿名子类也可以
    }
}

class MyThread1 extends Thread{
    @Override
    public void run() {
        for(int i = 0;i < 100;i++){
            if(i % 2 ==0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

class MyThread2 extends Thread{
    @Override
    public void run() {
        for(int i = 0;i < 100;i++){
            if(i % 2 !=0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

2)Thread 类的有关方法

( 代码在java包中的ThreadMethodTest.java中)

  1. start():启动当前线程;调用当前线程的run()

  2. run():通常需要重写 Thread 类中的此方法,将创建的线程执行的操作声明在此方法中

  3. currentThread():静态方法,返回执行当前代码的线程

  4. getName():获取当前线程的名字

  5. setName():设置当前线程的名字

  6. yield():释放当前 CPU 的执行权

  7. join():在线程 a 中调用线程b的 join( ),此时线程 a 就进入阻塞状态,直到线程 b 完全执行完以后,线程a才结束阻塞状态。

  8. stop():已过时。当执行此方法时,强制结束当前线程。

  9. sleep(longmillitime):当前线程“睡眠”指定的 millitime 毫秒。在指定的 millitime 毫秒时间内,当前线程是阻塞状态的。

  10. isAlive():判断当前线程是否存活。

3)线程的调度

在这里插入图片描述

4)线程的优先级

​ > 设置线程的优先级,在 start( ) 之前设置
​ > 高优先级的线程要抢占低优先级线程的 CPU 的执行权,但是只是从概率上讲,高优先级的线程高概率的情况下被执行,并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。

在这里插入图片描述

5)多线程创建方式二:实现Runnable接口

  • 1.创建一个实现了 Runnable 接口的类
  • 2.实现类去实现 Runnable 中的抽象方法:run( )
  • 3.创建实现类的对象
  • 4.将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
  • 5.通过 Thread 类的对象调用 start( )
//示例:
//创建一个实现Runnable接口的类
class MThread implements Runnable{
    //2. 实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        for(int i = 0;i < 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
public class ThreadTest1 {
    public static void main(String[] args) {
        //3. 创建实现类的对象
        MThread mThread = new MThread();
        //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mThread);
        //5. 通过Thread类的对象调用start():① 启动线程 ② 调用当前线程的run()
        t1.setName("线程一:");
        t1.start();

        Thread t2 = new Thread(mThread);
        t2.setName("线程二:");
        t2.start();
    }
}

6)比较创建两种线程的两种方式

开发中优先选择实现 Runnable 接口的方式

原因:1.实现的方式没有类的单继承性的局限性。
2.实现的方式更适合来处理多个线程有共享数据的情况。

联系:public class Thread implements Runnable

相同点:两种方式都需要重写 run( ),将线程要执行的逻辑声明在 run( ) 中。
目前两种方式,要想启动线程,都是调用的 Thread 类中的 start( ) 。

在这里插入图片描述

3 线程的生命周期

在这里插入图片描述

在这里插入图片描述

4 线程的同步

例子:创建三个窗口卖票,总票数为100张

存在线程安全问题,待解决

​ 1.问题:卖票过程中,出现了重票、错票—>出现了线程的安全问题
​ 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票
​ 3.如何解决:当一个线程 a 在操作 ticket 的时候,其他线程不能参与进来,直到线程 a 操作完 ticket 时,其他线程才可以开始操作 ticket。这种情况即使线程 a 出现了阻塞,也不能被改变。
​ 4.在 Java中,我们通过同步机制,来解决线程安全问题

方式一:同步代码块

synchronized(同步监视器){
	//需要被同步的代码
}

说明: 1.操作共享数据的代码,即为需要被同步的代码。—>不能包含代码多了,也不能包含代码少了。
2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
3.同步监视器,俗称,锁。任何一个类的对象,都可以充当锁。

​ 要求:多个线程必须要共用同一把锁。

​ 补充:在实现 Runnable 接口创建多线程的方式中,我们可以考虑使用 this 充当同步监视器。

方式二:同步方法

如果操作共享数据的代码完整地声明在一个方法中,我们不妨将此方法声明同步的。

​ 1.同步方法仍然涉及到同步监视器,只是不需要我们显式地声明。
​ 2.非静态的同步方法,同步监视器是:this
​ 静态的同步方法,同步监视器:当前类本身

​ 同步的方式,解决了线程的安全问题。—> 好处

​ 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程;效率低。—> 局限性

public class BankTest {
}

class Bank {
    private Bank() {
    }

    private static Bank instance = null;

    public static synchronized Bank getInstance() {
        //方式一:效率稍差
//        synchronized (Bank.class) {
//            if(instance == null){
//                instance = new Bank();
//            }
//            return instance;
//        }
        //方式二:效率更高
        if (instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

死锁问题

在这里插入图片描述

在这里插入图片描述

public class ThreadTest {
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run(){
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s2){
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

面试题:synchronized与Lock的异同

相同:二者都可以解决线程安全问题

不同:synchronized机制在执行完响应的代码以后,自动地释放同步监视器

​ Lock需要手动地启动同步(Lock()),同时结束同步也需要手动地实现(unlock())

优先使用顺序:

​ Lock → 同步代码块(已经进入了方法体,分配了相应资源)→ 同步方法(在方法体之外)

练习

在这里插入图片描述

//设置的一个账户
class Account{
    private double balance;

    public Account(double balance) {
        this.balance = balance;
    }

    //存钱:为了处理线程不安全问题加上synchronized来同步方法,操作同步代码时,只能有一个线程参与,其他线程等待
    public synchronized void deposit(double amt){
        if(amt > 0){
            balance += amt;

            //这里用一个sleep()将存钱动作放慢一些
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread() + "存钱成功。余额为:" + balance);
        }
    }
}

class Customer extends Thread{
    private Account acct;

    public Customer(Account acct){
        this.acct = acct;
    }

    @Override
    public void run() {
        for(int i = 0;i < 3;i++){
            acct.deposit(1000);
        }
    }
}

public class AccountTest {
    public static void main(String[] args) {
        Account acct = new Account(0);
        //如下操作就实现了两个用户共用一个账户
        Customer c1 = new Customer(acct);
        Customer c2 = new Customer(acct);

        //各自起个名字
        c1.setName("甲");
        c2.setName("乙");

        c1.start();
        c2.start();
    }
}

5 线程的通信

所谓线程通信就是线程之间的交替执行

//实现Runnable接口的方式创建多线程
class Number implements Runnable{//①
    private int number = 1;
    @Override
    public void run() { //②实现run方法

        while (true){
            
            //用synchronized创建同步代码块解决多线程安全问题
            synchronized (this) {

                //在此处唤醒一个线程,进行下去
                this.notify();

                if(number <= 100){

                    //为了展示更明显的多线程同步效果,故意让线程停留一会,如果有安全问题,提高出现的概率
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    //先进来的一个线程要阻塞,另一个线程才能进,先在此阻塞一下
                    //但是注意阻塞完要唤醒,不然就两个线程一直卡在那
                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}

public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();  //③创建实现类的对象
        Thread t1 = new Thread(number); //将实现类的对象传入Thread类的构造器中创建Thread类的对象
        Thread t2 = new Thread(number);

        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}
/*
执行过程:假设线程1先进来,此时notify没有用(因为另外一个线程没有阻塞),打印出数字1,接下来遇到wait后阻塞(会释放同步监视器,这样线程2拿到同步监视器权限才能进来),线程2进来执行notify会唤醒线程1,但此时线程1还是不能进入同步监视器,因为线程2还没有释放权限,接着线程2打印出数字2,遇到wait后阻塞(执行到wait遇到阻塞的同时也会释放同步监视器,此时线程1就可以进入监视器执行了...),
*/

涉及到的三个方法:

wait( ):一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。

notify( ):一旦执行此方法,就会唤醒 wait 的一个线程。如果有多个线程被 wait,就唤醒优先级高的那个,notifyAll( ):一旦执行此方法,就唤醒所有 wait 的线程。

​ 说明:

1.wait( ),notify( ),notifyAll( ) 三个方法必须使用在同步代码块或同步方法中。

2.wait( ),notify( ),notifyAll( ) 三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则,会出现IllegalMonitorStateException 异常

3.wait( ),notify( ),notifyAll( ) 三个方法是定义在 java.lang.Object 类中。

面试题:sleep( )和wait( )的异同?

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

2.不同点: 1)两个方法声明的位置不同;Thread 类中声明 sleep( ),Object 类中声明 wait( )

​ 2)调用的要求不同:sleep( ) 可以在任何需要的场景下调用(任何地方你想让线程睡一会都可以调用sleep)。wait( ) 必须使用在同步代码块或同步方法中

​ 3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中 sleep( ) 不会释放锁,wait( ) 会释放锁。

练习

/**
 * 线程通信的应用:经典例题:生产者/消费者问题
 *
 * 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品。
 * 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员
 * 会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,
 * 店员会告诉消费者等一下,如果店中有产品了再通知消费者取走产品。
 *
 * 分析:
 * 1. 是否多线程问题?是,生产者线程,消费者线程
 * 2. 是否有共享数据?是,店员(或产品)
 * 3. 如何解决线程安全问题?同步机制,有三种方法
 * 4. 是否涉及到线程通信?是
 */
class Clerk{//店员
    private int productCount = 0;
    //生产产品
    public synchronized void produceProduct(){
        if(productCount < 20){
            productCount++;
            System.out.println(Thread.currentThread().getName() + "开始生产第" + productCount + "个产品");

            //生产了一个产品,就可以唤醒消费者了
            notify();

        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //消费产品
    public synchronized void consumeProduct(){
        if(productCount > 0){
            System.out.println(Thread.currentThread().getName() + ":开始消费的第" + productCount + "个产品");
            productCount--;

            notify();

        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Producer extends Thread{//生产者
    private Clerk clerk;
    public Producer(Clerk clerk){
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":开始生产产品...");
        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProduct();
        }
    }
}
class Consumer extends Thread{//消费者
    private  Clerk clerk;
    public Consumer(Clerk clerk){
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":开始消费产品...");
        while (true){
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumeProduct();
        }
    }
}

public class ProductTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer p1 = new Producer(clerk);
        p1.setName("生产者1");

        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者1");

        p1.start();
        c1.start();
    }
}

6 JDK5.0新增线程创建方式

多线程创建方式三:实现Callable接口

在这里插入图片描述

何理解实现 Callable 接口的方式创建多线程比实现 Runnable 接口创建多线程方式强大?

​ 1.call( ) 可以有返回值的

​ 2.call( ) 可以抛出异常,被外面的操作捕获,获取异常的信息

​ 3.Callable 是支持泛型的

//1. 创建一个实现Callable的实现类
class NumThread implements Callable{
    //2. 实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}
public class ThreadNew {
    public static void main(String[] args) {
        //3. 需要创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4. 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();

        try {
            //6. 获取Callable中call方法的返回值
            //get()方法的返回值即为FutureTask构造器Callable实现类重写的call()的返回值。即上面的sum
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

多线程创建方式四:使用线程池

在这里插入图片描述
在这里插入图片描述

好处:

​ 1.提高响应速度(减少了创建新线程的时间)

​ 2.降低资源消耗(重复利用线程池中的线程,不需要每次都创建)

​ 3.便于线程管理

corePoolSize:核心池的大小

maximumPoolSize:最大线程数

keepAliveTime:线程没有任务时最多保持多久时间会终止

面试题:创建多线程有几种方式?四种

class NumberThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
public class ThreadPool {
    public static void main(String[] args) {

        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //实际上返回的对象就是service1,且是ThreadPoolExecutor类的
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;

        //设置线程池的属性
//        System.out.println(service.getClass());

        //2. 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合使用于Runnable

        //service.submit();//适合使用于Callable

        //3. 关闭连接池
        service.shutdown();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值