20.java-多线程

多线程

进程和线程

进程

​ 简单理解:进程就是正在运行的程序

进程的特性:
  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。

    • 进程是操作系统的最小调度单元
    • 在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他的进程的地址空间。
  • 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。

  • 并发性:任何进程都可以同其他进程一起并发执行

    • 并行:在同一时刻,有多个指令在多个CPU上【同时】执行
    • 并发:在同一时刻,有多个指令在单个CPU上【交替】执行
线程
  • 线程(Thread)是一个程序内部的一条执行流程

  • 程序中如果只有一条执行流程,那这个程序就是单线程的程序。

  • 操作系统可以同时执行多个任务,每个任务就是进程,进程可以同时执行多个任务,每个任务就是线程。

    • 线程依赖于进程,线程是进程执行的最小单元。一个进程至少应该存在一个线程。
  • 进程在执行的时候其实是执行的是进程中对应的线程。

多线程是什么
  • 多线程指的是从软硬件上实现的多条执行流程的技术(多线程由CPU负责调度执行)。
多线程的本质

​ 对于一个CPU而言,他在某个时间点只能执行一个程序,即一个进程,其实CPU是不断地在这些进程间轮换执行的,由于执行速度相对人的感觉来说过快,所以我们感觉不到而已。

多线程的意义

​ 使用多线程可以提高程序的执行效率

问题 : 什么是进程 ? 什么是线程?
  • 正在运行的程序.
  • 线程指的就是程序中的任务.
    • 单线程 : 程序只有一条执行路径
    • 多线程 : 程序有多条执行路径

线程的创建方式

方式一:继承Thread类

方法介绍

方法名 说明
void run() 在线程开启后,此方法将被调用执行
void start() 使此线程开始执行,Java虚拟机会调用run方法()
//1.让子类继承Thread线程类
class MyThread extends Thread{
    //2.必须重写Thread类的run方法
    @Override
    public void run() {
        //描述线程的执行任务
        System.out.println("子线程");
    }
}

//main方法是由一条默认的主线程负责执行
    public static void main(String[] args) {
        //4.创建MyThread线程类的对象代表一个线程
        MyThread t = new MyThread();
        //4.启动线程(自动执行run方法)
        t.start();
        System.out.println("主线程");
    }

方式一优缺点:

优点:编码简单

缺点:线程类已经继承Thread类,无法继承其他类,不利于功能的扩展。

注意:

  • 启动线程必须是调用start方法,不是调用run方法

    直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。

    只有调用start方法才是启动一个新线程

  • 不要把主线程任务放在启动子线程之前

    这样主线程一直是先跑完的,相当于是一个单线程的效果

方式二:实现Runnable接口

Thread构造方法

方法名 说明
Thread(Runnable target) 分配一个新的Thread对象
Thread(Runnable target, String name) 分配一个新的Thread对象
//定义一个任务类,实现Runnable接口
class MyRunnable implements Runnable{
    //重写Runnable的run方法
    @Override
    public void run() {
        //线程要执行的任务
        System.out.println("子线程任务");
    }
}

public static void main(String[] args) {
        //创建任务对象
        Runnable target = new MyRunnable();
        //把任务对象交给一个线程对象处理
        //public Thread(Runnable target)
        new Thread(target).start();
        System.out.println("主线程任务");
    }

方式二优缺点:

  • 优点:任务类只是实现接口,可以继续继承其他类,实现其他接口,扩展性强
  • 缺点:需要多一个Runnable对象
多线程创建方式二的匿名内部类写法
 public static void main(String[] args) {
        //直接创建Runnable接口的匿名内部类形式(任务对象)
        Runnable target = new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程任务一");
            }
        };
        new Thread(target);
        //形式二
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程任务二");
            }
        }).start();
        System.out.println("主线程任务");
    }
}
方式三:实现Callable接口

方法介绍

方法名 说明
V call() 计算结果,如果无法计算结果,则抛出一个异常
FutureTask(Callable callable) 创建一个 FutureTask,一旦运行就执行给定的 Callable
V get() 如有必要,等待计算完成,然后获取其结果
//定义一个任务类,实现Callable接口
class MyCallable implements Callable<Integer>{
//重写Call方法,编写线程任务
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

 public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建Callable实现类对象
        MyCallable mc = new MyCallable();
        //创建线程任务对象
        FutureTask<Integer> ift = new FutureTask<>(mc);
        //创建线程对象,传递线程任务对象
        Thread thread = new Thread(ift);
        //调用start方法开启线程
        thread.start();
        //通过线程任务对象,获取线程执行完毕后的结果
        System.out.println(ift.get());
    }

方式三优缺点:

  • 好处: 扩展性强,实现该接口的同时还可以继承其他的类
  • 缺点: 编程相对复杂,不能直接使用Thread类中的方法
开启线程的方式有几种?各自的区别
  • 继承Thread类

  • 实现Runnbale接口

  • -实现Callable接口
    继承线程类好处可以直接调用线程类的方法,使用方便,弊端是扩展性降低了,如果已经有了其他父类,就不能使用这一种了。
    实现Runnbale接口,实现可调用接口好处是扩展性强,就算有了其他的父类,也可以继续实现机接口。
    弊端:调用方法不是那么方便,需要先获取到当前的线程对象,再调用方法.

    实现Runnable接口,重写的Run方法,方法没有返回值
    实现Cal1able接口,重写的CA 11方法,方法有返回值.

线程中的相关方法

设置和获取线程名称
方法名 说明
void setName(String name) 将此线程的名称更改为等于参数name
String getName() 返回此线程的名称
Thread currentThread() 返回对当前正在执行的线程对象的引用
线程休眠
方法名 说明
static void sleep(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数
线程优先级
优先级相关方法
方法名 说明
final int getPriority() 返回此线程的优先级
final void setPriority(int newPriority) 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10
线程调度
  • 两种调度方式

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

  • 随机性

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

守护线程

​ 当程序中的其他线程结束时,守护线程也会随之结束

方法名 说明
final void setDaemon(boolean on) 设置为守护线程

线程同步

同步代码块解决数据安全问题
  • 安全问题出现的条件

    • 是多线程环境

    • 有共享数据

    • 有多条语句操作共享数据

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

    • 基本思想:让程序没有安全问题的环境
  • 怎么实现呢?

    • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可

    • Java提供了同步代码块的方式来解决

  • 同步代码块格式:

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

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

  • 同步的好处和弊端

    • 好处:解决了多线程的数据安全问题

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

  • 代码演示

    public class SellTicket implements Runnable {
        private int tickets = 100;
        private Object obj = new Object();
    
        @Override
        public void run() {
            while (true) {
                synchronized (obj) { // 对可能有安全问题的代码加锁,多个线程必须使用同一把锁
                    // t1进来后,就会把这段代码给锁起来
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                            // t1休息100毫秒
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 窗口1正在出售第100张票
                        System.out.println(Thread.currentThread().getName() + "卖出了第" + tickets + "号票");
                        tickets--; //tickets = 99;
                    }
                }
                // t1出来了,这段代码的锁就被释放了
            }
        }
    }
    
    public class SellTicketDemo {
        public static void main(String[] args) {
            SellTicket st = new SellTicket();
    
            Thread t1 = new Thread(st, "窗口1");
            Thread t2 = new Thread(st, "窗口2");
            Thread t3 = new Thread(st, "窗口3");
    
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
同步方法解决数据安全问题
  • 同步方法的格式

    同步方法:就是把 synchronized 关键字加到方法上

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

    同步方法的锁对象是什么呢? this

  • 静态同步方法

    同步静态方法:就是把synchronized关键字加到静态方法上

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

    同步静态方法的锁对象是什么呢? 类名.class

Lock锁

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

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

  • ReentrantLock构造方法

    方法名 说明
    ReentrantLock() 创建一个ReentrantLock的实例
  • 加锁解锁方法

    方法名 说明
    void lock() 获得锁
    void unlock() 释放锁
  • 代码演示

    public class Ticket implements Runnable {
        //票的数量
        private int ticket = 100;
        private Object obj = new Object();
        private ReentrantLock lock = new ReentrantLock();
    
        @Override
        public void run() {
            while (true) {
                //synchronized (obj){//多个线程必须使用同一把锁.
                try {
                    lock.lock();
                    if (ticket <= 0) {
                        //卖完了
                        break;
                    } else {
                        Thread.sleep(100);
                        ticket--;
                        System.out.println(Thread.currentThread().getName() 
                                           					+ "在卖票,还剩下" + ticket + "张票");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
    
            Thread t1 = new Thread(ticket);
            Thread t2 = new Thread(ticket);
            Thread t3 = new Thread(ticket);
    
            t1.setName("窗口一");
            t2.setName("窗口二");
            t3.setName("窗口三");
    
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
死锁
  • 概述

    线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行

  • 什么情况下会产生死锁

    1. 资源有限
    2. 同步嵌套
  • 代码演示

    public class Demo {
        public static void main(String[] args) {
            Object objA = new Object();
            Object objB = new Object();
    
            new Thread(()->{
                while(true){
                    synchronized (objA){
                        // 线程一
                        System.out.println(Thread.currentThread().getName() + "----抢到了执行权, 上A锁");
                        synchronized (objB){
                            System.out.println(Thread.currentThread().getName() + "----抢到了执行权, 上B锁");
                        }
                    }
                }
            }).start();
    
            new Thread(()->{
                while(true){
                    synchronized (objB){
                        // 线程二
                        System.out.println(Thread.currentThread().getName() + "----抢到了执行权, 上B锁");
                        synchronized (objA){
                            System.out.println(Thread.currentThread().getName() + "----抢到了执行权, 上A锁");
                        }
                    }
                }
            }).start();
        }
    }
    

线程等待唤醒机制 (线程间的通讯)

生产者和消费者模式概述
  • 概述

    生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。

    所谓生产者消费者问题,实际上主要是包含了两类线程:

    ​ 一类是生产者线程用于生产数据

    ​ 一类是消费者线程用于消费数据

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

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

    消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

  • Object类的等待和唤醒方法

    方法名 说明
    void wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
    void notify() 唤醒正在等待对象监视器的单个线程
    void notifyAll() 唤醒正在等待对象监视器的所有线程
生产者和消费者案例
public class Box {
    public static boolean flag = false;
}


public class Consumer extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (Box.class) {
                if (Box.flag == false) {
                    // 等待
                    try {
                        System.out.println("没有包子了, 消费者等待...");
                        Box.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("消费者开吃....");
                    Box.flag = false;
                    Box.class.notifyAll();
                }
            }
        }
    }
}

public class Maker extends Thread{
    @Override
    public void run() {
        while(true){
            synchronized (Box.class) {
                if(Box.flag == false){
                    // 没包子了, 生产者制作
                    System.out.println("生产制作包子....");
                    Box.flag = true;
                    Box.class.notifyAll();
                }else {
                    // 有包子, 生产者等待
                    System.out.println("有包子, 生产者等待");
                    try {
                        Box.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Consumer c = new Consumer();
        Maker m = new Maker();

        c.start();
        m.start();
    }
}

面试题:sleep方法和wait方法的区别

  • sleep()休眠线程,需要指定一份时间,时间到了自动醒来
  • wait()线程等待,需要由其它线程调用通知对其唤醒
  • sleep()方法在休眠的时候,不会释放锁对象
  • wait()在等待的时候,会释放锁对象
  • sleep()可以使用在任何地方
  • wait()必须在同步代码块中使用
  • sleep()需要捕获异常
  • wait()不需要捕获异常
  • sleep()来自Thread类
  • wait()来自Object类

你可能感兴趣的:(java,开发语言)