Java多线程

多线程概述

🐤并行与并发

  • 并行:指两个或多个事件在同一时刻发生(同时发生)
  • 并发:指两个或多个事件在同一个时间段内发生(交替执行)

🐱‍👤举例

  • 你吃饭吃到一半, 电话来了, 你一直吃饭, 吃完了才接电话, 这就说明你不支持并发也不支持并行
  • 你吃饭吃到一半, 电话来了, 你停止了吃饭, 接了电话, 接完后继续吃饭, 这说明支持并发 (轮流,串行)
  • 你吃饭吃到一半, 电话来了, 你一边打电话一边吃饭, 这说明你支持并行 (一起执行)

🐪进程与线程

  • 进程
    • 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
    • 一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位
    • 系统运行一个程序即是一个进程从创建、运行到消亡的过程
  • 线程
    • 进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程
    • 一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序
  • 进程与线程的区别
    • 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程
    • 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多

🐮内存结构

线程

🐪线程类

  • Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例
  • 每个线程的作用是完成一定的任务,实际上就是执行一段程序即执行一段顺序的代码
  • Java使用线程执行体来代表这段程序

🐸创建并启动多线程的步骤

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务, 因此把run()方法称为线程执行体
/**
 * description: MyThread
 * date: 2020-08-05 10:29
 * author: 30315
 * version: 1.0
 */
public class MyThread extends Thread {

    @Override
    public void run() {
        // 需要完成的任务
        for (int i = 1; i <= 100; i++) {
            System.out.println(getName() + i);
        }
    }
}
  1. 创建Thread子类的实例,就是创建了线程对象
public class Test {
    public static void main(String[] args) {
        MyThread Mt = new MyThread();
    }
}
  1. 调用线程对象的start()方法来启动该线程
public class Test {
    public static void main(String[] args) {
        MyThread Mt = new MyThread();
        Mt.start();
    }
}

运行过程图

🦙执行过程

  • 程序启动后运行main的时候,java虚拟机启动一个进程,主线程main在调用main()的时候被创建
  • 随着调用Mt对象的start方法,另外一个新的线程也启动了,这样整个应用就在多线程下运行

Thread类

🦝构造方法

构造方法名称 概述
public Thread() 分配一个新的线程对象, 创建一个新的线程
public Thread(String name) 分配一个指定名字的, 新的线程对象, 在创建新线程的时候可以顺便给个名称
public Thread(Runnable target) 分配一个带有指定目标(Runnable的实现类可以资源共享), 新的线程对象
public Thread(Runnable target,String name) 分配一个带有指定目标(Runnable的实现类可以资源共享), 新的线程对象, 并指定名字

🐬常用方法

方法名称 概述
public String getName() 获取当前线程名称
public void start() 导致此线程开始执行, Java虚拟机调用此线程的run方法
public void run() 此线程要执行的任务在此处定义代码
public static void sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行, 1秒=1000毫秒)
public static Thread currentThread() 返回当前正在执行的线程对象的引用

Runnable接口

  • 多线程程序的第二种实现方式

🦝构造方法

构造方法名称 概述
public Thread(Runnable target) 分配新的 Thread 对象, 创建新的线程
public Thread(Runnable target,String name) 分配新的 Thread 对象, 创建新的线程, 可以指定线程名称

实现步骤

  1. 创建一个类实现Runnable接口
public class RunnableImpl implements Runnable {
    
}
  1. 重写Runnable接口中的run方法, 设置线程任务
public class RunnableImpl implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}
  1. 创建Runnable接口的实现类对象
public class Test {
    public static void main(String[] args) {
        RunnableImpl rb = new RunnableImpl();
    }
}
  1. 创建Thread类对象, 构造方法中传递Runnable接口的实现类对象
public class Test {
    public static void main(String[] args) {
        RunnableImpl rb = new RunnableImpl();
        Thread thread = new Thread(rb);
    }
}
  1. 调用Thread类中的start方法, 开启新的线程, 执行run方法
public class Test {
    public static void main(String[] args) {
        RunnableImpl rb = new RunnableImpl();
        Thread thread = new Thread(rb);
        thread.start();
    }
}

使用Runnable接口的好处

  1. 避免了单继承的局限性
    • 一个类继承了Thread类就不能继续继承其他的类
    • 一个类实现了Runnable接口, 还可以继续继承别的类, 实现其他的接口
  2. 增强了程序的扩展性, 降低程序的耦合度
    • 使用Runnable接口把设置线程任务和开启线程相分离
    • 实现类当中, 重写run方法, 设置线程任务
    • 创建Thread类对象, 调用start方法, 开启新线程

如果一个类继承Thread,则不适合资源共享, 但是如果实现了Runable接口的话,则很容易实现资源共享

匿名内部类实现多现程

🐤匿名内部类

  • 把子类继承父类, 重写父类的方法, 创建子类对象, 合成一步完成
  • 把实现类实现接口, 重写接口库的方法, 创建实现类对象, 合成一步完成
  • 最终得要子类对象或实现类对象
new 父类/接口(){
	重写父类/接口中的方法
};

🐬Thread匿名内部类实现方式

public class Test {
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                for (int i = 1; i <= 20; i++) {
                    System.out.println(getName() + i);
                }
            }
        }.start();
    }
}

🐬Runnable匿名内部类实现方式

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

线程安全

  • 多线程访问了共享数据, 就会产生线程的安全

🐸举例

  • 多个窗口, 同时卖一种票
  • 如果不进行控制, 可以会出现卖重复票的现象

🐤卖票任务

public class TicketRunnableImpl implements Runnable {

    // 定义共享的票源
    private int ticket = 100;

    // 线程任务: 卖票
    @Override
    public void run() {
        while (ticket > 0) {
            try {
                // 为了提高线程安全问题出现的几率,让线程睡眠10毫秒,放弃cpu的执行权
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            // 卖票操作,ticket--
            ticket--;
        }
    }
}

🦄开启多线程同时执行

public class Test {
    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        TicketRunnableImpl r = new TicketRunnableImpl();

        //创建3个线程
        Thread t0 = new Thread(r);
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        
        //开启新的线程
        t0.start();
        t1.start();
        t2.start();
    }
}

🐥使用synchronized解决线程安全, 同步代码块

synchronized(锁对象){
	出现安全问题的代码(访问了共享数据的代码)
}
  1. 锁对象可以是任意对象
  2. 必须保证多个线程使用的是同一个锁对象
  3. 锁对象的作用, 把{}中代码锁住, 只让一个线程进去执行
public class TicketRunnableImpl implements Runnable {

    // 定义共享的票源
    private int ticket = 100;
    // 锁对象
    private Object obj = new Object();

    // 线程任务: 卖票
    @Override
    public void run() {
        synchronized (obj){
            while (ticket > 0) {
                try {
                    // 为了提高线程安全问题出现的几率,让线程睡眠10毫秒,放弃cpu的执行权
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                // 卖票操作,ticket--
                ticket--;
            }
        }
    }
}
  • 同步中的线程, 没有执行完毕, 不会释放锁对象, 同步外的线程没有锁对象进不去同步代码块当中
  • 当没有锁对象时, 进入阻塞状态, 一直等待
  • 出了同步后, 会把锁对象归还
  • 同步保证了只能有一个线程在同步中执行共享数据
  • 保证了安全, 但是程序频繁的判断锁, 释放锁, 程序的效率会降低

🍧同步方法解决线程安全

修饰符 synchronized 返回值类型 方法名(参数列表){
	出现安全问题的代码(访问了共享数据的代码)
}
  1. 创建一个方法, 方法的修饰符后面添加synchronized
  2. 把访问了共享数据的代码放入方法中
  3. 调用同步方法
public class TicketRunnableImpl implements Runnable {

    // 定义共享的票源
    private int ticket = 100;
    // 锁对象
    private Object obj = new Object();

    // 线程任务: 卖票
    @Override
    public void run() {
        ticketMethod();
    }

    public synchronized void ticketMethod() {
        while (ticket > 0) {
            try {
                // 为了提高线程安全问题出现的几率,让线程睡眠10毫秒,放弃cpu的执行权
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            // 卖票操作,ticket--
            ticket--;
        }
    }
}

🐤锁对象是谁

  • 锁对象为this
public void ticketMethod() {
    synchronized (this) {
        while (ticket > 0) {
            try {
                // 为了提高线程安全问题出现的几率,让线程睡眠10毫秒,放弃cpu的执行权
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            // 卖票操作,ticket--
            ticket--;
        }
    }
}

🐸静态同步方法

public class TicketRunnableImpl implements Runnable {

    // 定义共享的票源
    private static int ticket = 100;
    // 锁对象
    private Object obj = new Object();

    // 线程任务: 卖票
    @Override
    public void run() {
        ticketMethod();
    }

    public static synchronized void ticketMethod() {
        while (ticket > 0) {
            try {
                // 为了提高线程安全问题出现的几率,让线程睡眠10毫秒,放弃cpu的执行权
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            // 卖票操作,ticket--
            ticket--;
        }
    }
}

锁对象是谁

  • 对于static方法, 我们使用当前方法所在类的字节码对象作为锁对象(类名.class)
public static void ticketMethod() {
    synchronized (TicketRunnableImpl.class) {
        while (ticket > 0) {
            try {
                // 为了提高线程安全问题出现的几率,让线程睡眠10毫秒,放弃cpu的执行权
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            // 卖票操作,ticket--
            ticket--;
        }
    }
}

🐤使用Lock锁解决线程安全

  • java.util.concurrent.locks.Lock接口
  • JDK1.5之后的新特性
  • Lock实现提供了, 比使用synchronized方法和语句, 可获得的更广泛的锁定操作

🦄Lock接口中的方法

方法名称 概述
void lock() 获取锁
void unlock() 释放锁

🐇Lock锁使用步骤

  1. 在成员位置创建一个Lock接口的实现类对象ReentrantLock
  2. 在可能会出现安全问题的代码前, 调用lock方法获取锁对象
  3. 在可能会出现安全问题的代码后, 调用unlock方法释放锁对象
public class TicketRunnableImpl implements Runnable {
    // 定义共享的票源
    private  int ticket = 100;
    // 在成员位置创建一个Lock接口的实现类对象ReentrantLock
    Lock l = new ReentrantLock();
    // 线程任务: 卖票
    @Override
    public void run() {
        while (true) {
            l.lock();
            if (ticket > 0){
                try {
                    // 为了提高线程安全问题出现的几率,让线程睡眠10毫秒,放弃cpu的执行权
                    Thread.sleep(10);
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    // 卖票操作, ticket--
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    // 在可能会出现安全问题的代码后, 调用unlock方法释放锁对象
                    // 无论程序是否异常, 都会把锁对象释放,节约内存提高程序的效率
                    l.unlock();
                }
            }
        }
    }
}

线程状态

六种线程状态

  • new(新建): 线程刚被创建,但是并未启动。还没调用start方法
  • Runnable(可运行): 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器
  • Blocked(锁阻塞): 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态, 当该线程持有锁时,该线程将变成Runnable状态
  • Waiting(无限等待): 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态, 进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒
  • TimedWaiting(计时等待): 同waiting状态,有几个方法有超时参数,调用他们将进入TimedWaiting状态, 这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、 Object.wait
  • Teminated(被终止): 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡

🐤等待与唤醒

方法名称 概述
public void wait() 让当前线程进入到等待状态此方法必须锁对象调用
public void notify() 唤醒当前锁对象上等待状态的线程, 此方法必须锁对象调用, 会继续执行wait()方法之后的代码
  • 顾客与老板线程
  • 创建一个顾客线程(消息者) 告诉老板要吃什么, 调用wait方法, 放弃cpu的执行, 进入wating状态(无限等待)
  • 创建一个老板线程(生产者) 花5秒做好, 做好后, 调用notify方法, 唤醒顾客, 开吃

顾客与老板线程必须使用同步代码块包裹起来, 保证等待和唤醒只能有一个在执行同步使用的锁必须要保证唯一, 只有锁对象才能调用wait和notify方法

👦代码实现

public class Test {
    public static void main(String[] args) {

        Object obj = new Object();

        // 顾客线程
        new Thread() {
            @Override
            public void run() {
                synchronized (obj) {
                    System.out.println("来份面条");
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("开吃");
                }
            }
        }.start();

        // 老板线程
        new Thread() {
            @Override
            public void run() {
                synchronized (obj){
                    try {
                        Thread.sleep(3000);
                        System.out.println("做好了");
                        // 唤醒当前锁对象上的等待线程
                        obj.notify();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}

进入计时等待状态的两种方式

  1. 使用sleep(long m)方法, 在毫秒值结束后, 线程睡醒, 进入Runnable/Blocked状态
  2. 使用wait(long m)方法, wait方法如果在毫秒值结束之后, 还没有被唤醒, 就会自动醒来, 进入Runnable/Blocked状态

两种唤醒的方法

方法名称 概述
public void notify() 随机唤醒1个
public void notifyall() 唤醒锁对象上所有等待的线程

线程池

  • 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了
  • 这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间

🐤线程池

  • 有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务
  • 在Java中可以通过线程池来达到这样的效果
  • 线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用
  • 省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源

🐸线程池工作模型

  • 线程池的工作模型主要两部分组成,一部分是运行Runnable的Thread对象,另一部分就是阻塞队列
  • 由线程池创建的Thread对象其内部的run方法会通过阻塞队列的take方法获取一个Runnable对象,然后执行这个Runnable对象的run方法
  • 在Thread的run方法中调用Runnable对象的run方法
  • 当Runnable对象的run方法执行完毕以后,Thread中的run方法又循环的从阻塞队列中获取下一个Runnable对象继续执行
  • 这样就实现了Thread对象的重复利用,也就减少了创建线程和销毁线程所消耗的资源

🐤合理利用线程池能够带来三个好处

  1. 降低资源消耗, 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
  2. 提高响应速度, 当任务到达时,任务可以不需要等到线程创建就能立即执行
  3. 提高线程的可管理性, 可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)

Executors类

  • JDK1.5的时候java提供了线程池
  • java.util.concurrent.Executors类, 线程池的工厂类, 用来生产线程

🐸方法

方法名称 概述
static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程, int nThreads, 创建线程池中线程的个数
submit(Runnable task) 提交一个Runnable任务用于执行
oid shutdown() 用于销毁线程池, 一般不建议使用, 注意线程池销毁之后, 就在内存中消失了, 就不能在执行线程任务了

🐤Executors使用步骤

  1. 使用线程池工厂类Executors提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
  2. 调用线程池ExecutorService中的方法submit, 传递线程任务, 执行线程任务
public class Test {
    public static void main(String[] args) {
        // 使用线程池工厂类Executors提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
        ExecutorService ex = Executors.newFixedThreadPool(2);
        // 调用线程池ExecutorService中的方法submit,传递线程任务,执行线程任务
        // 相当于new Thread(new Runnable(){}).start();
        ex.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "线程任务1执行了!");
            }
        });
        ex.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "线程任务2执行了!");
            }
        });
        // 销毁线程池
        ex.shutdown();
        // 会报错
        ex.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "线程任务3执行了!");
            }
        });
    }
}
posted @ 2020-08-05 09:57  BNTang  阅读(128)  评论(0编辑  收藏  举报