Day66.多线程复习 -Java多线程

    科技2024-05-15  83

    多线程复习

    程序、进程、线程的理解

    1. 程序(program)

    概念:

    完成某种特定任务,并使用某种编写语言的一组指令集合;即是一段静态的代码。

    2. 进程(process)

    概念:

    程序的一次执行过程;或是正在运行的程序。

    说明:

    进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

    3. 线程(thread)

    概念:

    是程序内部的一条执行路径

    说明:

    线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开 销小

    补充:

    内存结构:

    进程可以细化为多个线程。

    每个线程,拥有自己的独立:栈、程序计数器(pc)

    多个线程,共享同一个进程的结构: 方法区、堆

    并行与并发

    1. 单核CPU与多核CPU的理解

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

    2. 并行与并发的理解

    并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。

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

    创建多线程的两种方式

    方式一: 继承Thread类的方式:

    * 方式一:继承与Thread类 * 1. 创建一个继承于Thread类的子类 * 2. 重写Thread类的run()方法 ---> 将此线程的操作声明在run()* 3. 创建Thread类的子类对象 * 4. 通过此对象调用start()

    说明两个问题:

    问题一: 我们启动一个线程,必须调用start();不能调用run()的方式去启动线程。

    问题二: 如果再启动一个线程,必须重新创建一个Thread子类的对象,再调用此对象的start()方法。会报错IllegalThreadStateException

    方式二: 实现Runnable接口的方式:

    * 1. 创建一个实现了Runnable接口的类 * 2. 实现类去实现Runnable中的抽象方法: run() * 3. 创建实现类的对象 * 4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 * 5. 通过Thread类的对象去调用start()

    两种方式的对比:

    * 开发中: 优先选择: 实现Runnable接口的方式 * 原因: 1. 实现方式没有类的单继承性局限性 * 2. 实现方式更适合来处理多个线程有共享数据的情况 * * 联系: public class Thread implements Runnable;都实现Runnable接口 * 相同点: 两种方式都需要重写run(),将创建的线程要执行的逻辑声明在run()中。 目前两种方式,想要启动线程,都需要调用Thread类中的start()。

    Thread类中的常用方法

    Thread类中的常用方法:

    * 测试Thread类中的常用方法: * 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(long millis): 让当前线程"睡眠"指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。 * 10. isAlive(): 判断当前线程是否存活 线程的优先级: * 1. * MAX_PRIORITY:10 * MIN_PRIORITY: 1 * NORM_PRIORITY:5 -->默认优先级 * 2.如何设置当前线程的优先级 * getPriority(): 获取程序的优先级 * setPriority(int p): 设置线程的优先级 * * 说明: 高优先级的线程要抢占低优先级线程CPU的执行权。但只是从概率上讲,高优先级的线程高概率的情况下呗执行。 * 并不意味着只有当高优先级线程执行完以后,低优先级线程才执行。

    线程通信: wait() / notify() / notifyAll() :此三个方法定义在Object类中的。

    补充: 线程的分类

    线程的生命周期

    图示:

    说明:

    生命周期关注两个概念: 状态、相应方法

    关注: 状态a --> 状态b :哪些方法执行了(回调方法)

    ​ 某个方法主动调用: 状态a -->状态b

    阻塞状态: 临时状态,不可作为最终状态。

    死亡: 最终状态。

    线程的同步机制

    1. 背景

    * 创建三个窗口,总票数为100张,使用实现Runnable接口的方式 * * 1. 卖票过程中,出现了重票、错票 --->线程安全问题 * 2. 问题出现的原因: 当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也来操作车票。 * 3. 如何解决: 当一个线程在操作ticket的时候,其他线程不能参与进来。直到线程A操作完ticket时 * 其他线程才可以开始操作ticket。这种情况即是线程A出现了阻塞,也不能被改变。

    2. Java解决方案: 同步机制

    在Java中,我们通过同步机制,来解决线程的安全问题。 方式一: 同步代码块 * * synchronized(同步监视器){ * //需要被同步的代码 * * } * 说明: 1. 操作共享数据的代码,即为需要被同步的代码 -->不能包含代码多了,也不能包含代码少了。 * 2. 共享数据: 多个线程共同操作的变量。比如: ticket就是共享数据 * 3. 同步监视器,俗称: 锁。任何一个类的对象,都可以充当锁。 * 要求: 多个线程必须要共用同一个把锁。 * * 补充: 在实现Runnable接口创建多线程的方式中,我们可以考虑,使用this充当同步监视器。 在继承Thread类创建多线程的方式中,慎用this充当同步监视器。考虑使用当前类充当同步监视器。 * * 方式二: 同步方法 * 如果操作共享数据的代码完整声明在一个方法中,我们不妨将此方法声明为同步的。 * * 关于同步方法的总结: * 1. 同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。 * 2. 非静态的同步方法,同步监视器是: this * 静态的同步方法,同步监视器是: 当前类本身 * 方式三: Lock锁 --- jdk5.0新增 * 1. 面试题: synchronized 与 Lock 的异同? * 相同: 二者都可以解决线程安全问题 * 不同: synchronized机制在执行完相应的同步代码后,自动释放同步监视器 * Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())。 * * 面试题: 如何解决线程安全问题?有几种方式? * 3种;同步代码块、同步方法、Lock锁 * *优秀使用顺序: * Lock --> 同步代码块(已经进入方法体,分配了相应资源) --> 同步方法(在方法体之外)

    3. 利弊

    同步的方式,解决了线程的安全问题。 --->好处 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 --->局限性

    4. 面试题:

    Java是如何解决线程安全问题的,有几种方式?并对比几种方式的不同。

    3种;

    Lock锁: 较为灵活,可自己指定何时锁 和何时解锁,注意使用的锁要唯一。

    同步方法块:指定包含代码为同步,同步监视器自己指定,但必须保证唯一,

    同步方法:修饰方法,将方法体声明为同步,同步监视器如果是非静态则为this,如果是静态则为当前类本身

    synchronized 和 Lock方式解决线程安全的区别

    相同点: 都能解决线程安全问题

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

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

    线程的同步机制

    1. 线程安全的单列模式(懒汉式)

    public class Singleton{ private static Singleton instance; private Sington(){} public static synchronized Singleton getinstance(){ if(instance == null){ Singleton instance = new Singleton(); } return instance; } } public class Singleton{ private Singleton(){} private static Singleton instance; public static Singleton getInstance(){ synchronized(Singleton.class){ if(instance == null){ Singleton instance = new Singleton(); } return instance; } } } 饿汉式 public class Singleton{ private Singleton(){} private static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; } }

    2. 死锁问题

    死锁的理解:

    不同线程分别占用对方的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

    说明:

    1) 出现死锁后,不会出现异常,不会出现提升,只是所有的线程都出于阻塞状态,无法继续。 2) 我们使用同步时,要避免出现死锁。

    举例:

    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(s1){ s1.append("d"); s2.append(4); System.out.println(s1); System.out.println(s2); } } } }).start(); }

    线程通信

    1. 线程通信涉及到的三个方法:

    * 1. wait(): 一旦执行此方法,当前线程就进入阻塞状态。并释放同步监视器。 * 2. notify(): 一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的线程。 * 3. notifyAll(): 一旦执行此方法,就会唤醒被所以wait的线程。

    2. 说明:

    * 1. wait(),notify(),notifyAll() 三个方法必须使用在同步代码块或同步方法中。 * 2. wait(),notify(),notifyAll() 三个方法的调用者必须是同步代码块或同步方法中的同步监视器。 * 否则会出现IllegalMonitorStateException异常 * 3. wait(),notify(),notifyAll() 三个方法是定义在java.lang.Object类中。

    3. 面试题:

    面试题: sleep() 和 wait() 的异同? * 相同点: 一旦执行方法都可以使当前线程进入阻塞状态 * 不同点: 1) 两个方法的声明位置不一样: Thread类中声明sleep(),Object类中声明wait() * 2) 调用的要求不同: sleep()可以在任何需要的场景下调用。 * wait()必须使用在同步代码块或同步方法中。 * 3) 关于是否释放同步监视器: * 如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁; * wait()会释放锁,并需要通过notify()来唤醒。

    4.

    小结释放锁的操作:

    小结不释放锁的操作:

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

    新增方式一: 实现Callable接口。 — jdk 5.0 新增

    //1. 创建一个实现Callable的实现类 class NumThread implements Callable{ //2. 重写call()方法,将此线程需要执行的操作声明在call()中。 @Override public Object call() throws Exception { int sum = 0; for (int i = 0; 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 f = new FutureTask<Integer>(numThread); //5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start() new Thread(f).start(); Object o = null; try { //6. 获取Callable中的call()的返回值 //get()的返回值 即为FutureTask构造器参数Callable实现类重写的call()的返回值。 o = f.get(); System.out.println("总合为: "+ o); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }

    说明:

    * 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程强大? * 1. call()可以有返回值。 * 2. call()可以抛出异常,被外面的操作捕获,获取异常的信息 * 3. Callable是支持泛型

    新增方式二: 使用线程池

    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 executorService = Executors.newFixedThreadPool(10); //2. 执行指定的线程操作。 需要提供提示Runnable接口或Callable接口实现类的对象 executorService.execute(new NumberThread());//适合使用于Runnable executorService.execute(new NumberThread1()); //executorService.submit();//适合使用于Callable //3. 关闭连接池 executorService.shutdown(); } } class NumberThread1 implements Runnable{ @Override public void run() { for (int i = 0; i <= 100; i++) { if (i % 2 !=0){ System.out.println(Thread.currentThread().getName()+": "+ i ); } } } }

    说明:

    好处: * 1. 提高响应效率(减小创建新线程的时间) * 2. 降低资源的消耗(重复利用,而不是再造) * 3. 便于线程的管理 * corePoolSize: 核心池的大小 * maximumPoolSize: 最大线程数 * keepAliveTime: 线程没有任务时最多保持多长时间会终止 * ...

    面试题: java中多线程的创建有几种方式?

    4种;

    Thread继承

    Runnable接口实现

    Callable接口实现

    线程池

    Processed: 0.015, SQL: 8