​Java基础复习笔记 第10章:多线程

1. 相关概念

  • 掌握:程序、进程、线程
  • 熟悉:线程的调度机制:分时调度、抢占式调度
  • 了解:单核CPU、多核CPU
  • 了解:并行与并发

2. 创建多线程的两种经典方式(重点)

1. 线程的创建方式一:
1.1 步骤:
① 创建一个继承于Thread类的子类
② 重写Thread类的run()方法:将此线程要执行的操作编写在此方法体中。
③ 创建Thread类的子类的对象
④ 调用start()方法: 1、启动线程 2、调用线程的run()


1.2 例题:创建一个分线程1,用于遍历100以内的偶数
【拓展】 再创建一个分线程2,用于遍历100以内的偶数


2. 线程的创建方式二:
2.1 步骤:
① 创建实现Runnable接口的实现类
② 实现接口中的抽象方法run():将此线程要执行的操作编写在此方法体中。
③ 创建此实现类的对象
④ 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
⑤ 通过Thread类的对象调用start():1、启动线程 2、调用线程的run()



2.2 例题:创建分线程遍历100以内的偶数


3. 对比两种方式?
   共同点:① 创建的线程都是Thread类或其子类的对象
         ② 启动线程,调用的都是Thread类中的start()

   不同点:一种是继承的方式,一种是实现的方式(推荐);
        推荐实现的方式的原因: ① 类的单继承的局限性 ②实现的方式更适合、方便的用来处理共享数据的场景。

   联系:
        public class Thread implements Runnable

3. 线程的常用方法、生命周期

一、线程的常用结构
1. 线程中的构造器
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :指定创建线程的目标对象,它实现了Runnable接口中的run方法
- public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

2.线程中的常用方法:
> run():在继承的方式中,需要被重写的方法。
> start():要想启动一个线程,必须要调用此方法:①启动线程 ② 调用线程的run()
> static currentThread():获取当前执行的代码所属的线程。
> getName():获取线程名
> setName(String name):设置线程名
> yield():一旦线程执行此方法,当前线程就释放cpu的执行权
> join(): 在线程a中调用线程b的join()方法,此时线程a就进入阻塞状态,直到线程b执行结束以后,线程a才可以从被阻塞的位置继续执行
> static sleep(long millis):指定线程"睡眠"多少毫秒
> isAlive() : 判断当前线程是否存活

过时方法:
> stop():强行结束一个线程的执行,直接进入死亡状态。
> suspend() / resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。


3. 线程的优先级:
3.1 线程的优先级的范围:[1,10]
int MAX_PRIORITY = 10; //最大优先级
int MIN_PRIORITY = 1; //最小优先级
int NORM_PRIORITY = 5; //默认优先级

3.2 如何设置/获取优先级:
setPriority(int priority):设置线程的优先级
getPriority() : 获取线程的优先级
  • 生命周期

    • jdk5.0之前:
    • jdk5.0

4. 线程的安全问题与同步机制(重点)

线程的安全问题与线程的同步机制

1. 多线程卖票,出现的问题:出现了重票、错票

2. 什么原因导致的?一个线程在没有操作完ticket的情况下,其他线程参与进来,导致出现了重票、错票

3. 如何解决?
  应该包装一个线程在操作完共享数据ticket的情况下,其它线程才能参与进来继续操作ticket。

4. Java是如何解决线程的安全问题的? 同步机制

方式1:同步代码块

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

说明:
> 需要被同步的代码,即为操作共享数据的代码。
> 什么是共享数据:即为多个线程共同操作的数据。比如:ticket
> 使用synchronized将操作共享数据的代码包起来,确保这部分代码作为一个整体出现。只有当一个线程操作完此部分代码
  之后,其他线程才有机会操作同样的这部分代码。
> 同步监视器,俗称锁。哪个线程获取了同步监视器,这个线程就能执行操作共享数据的代码。没有获取同步监视器的线程就只能等待。

注意:
> 操作共享数据的代码,不能包多了,也不能包少了。
> 同步监视器:任何一个类的对象,都可以充当同步监视器。但是,多个线程必须共用同一个同步监视器。
> 实现Runnable的方式中,使用的同步监视器可以考虑this。
  继承Thread类的方式中,使用的同步监视器慎重this,可以考虑使用当前类。

方式2:同步方法
如果操作共享数据的代码完整的声明在一个方法中。我们也可以考虑将此方法声明为同步方法。

说明:
> 非静态的同步方法,其默认的同步监视器是:this
> 静态的同步方法,其默认的同步监视器是:当前类本身

5. synchronized好处:解决了线程的安全问题
   弊端:串行的执行,是得多线程的性能受限

5. 同步机制的相关问题

5.1 解决懒汉式的线程安全问题(重点)
package com.atguigu04.threadsafemore.singleton;

/**
 * ClassName: BankTest
 * Description:
 *
 * @Author 尚硅谷-宋红康
 * @Create 2023/2/24 11:50
 */
public class BankTest {
    static Bank b1 = null;
    static Bank b2 = null;
    public static void main(String[] args) {


        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                b1 = Bank.getInstance();
            }
        });


        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                b2 = Bank.getInstance();
            }
        });

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


        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1 == b2);

    }
}


//懒汉式
class Bank{

    private Bank(){}

    private static Bank bank = null;

    //方式1:使用同步方法
//    public static synchronized Bank getInstance(){
//
//        if(bank == null){
//
//            try {
//                Thread.sleep(1000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//
//            bank = new Bank();
//        }
//        return bank;
//
//    }

    //方式2:使用同步代码块
    public static Bank getInstance(){

        synchronized (Bank.class) {
            if(bank == null){

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

                bank = new Bank();
            }
            return bank;
        }
    }

    //思考:使用同步代码块,存在指令重排
//    public static Bank getInstance(){
//
//        if(bank == null){
//
//            synchronized (Bank.class) {
//                if(bank == null){
//
//                    try {
//                        Thread.sleep(1000);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
//
//                    bank = new Bank();
//                }
//
//            }
//        }
//
//        return bank;
//    }

}
5.2 死锁问题
线程的同步机制带来的问题:死锁

1. 如何看待死锁?
> 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
> 一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
> 我们编程中,要避免出现死锁

2. 诱发死锁的原因?
- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待

以上4个条件,同时出现就会触发死锁。


3. 如何避免死锁?
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
5.3 JDK5.0新增解决安全问题的方式:Lock
除了使用synchronized同步机制处理线程安全问题之外,还可以使用jdk5.0提供的Lock锁的方式

1. 步骤:
步骤1. 创建ReentrantLock的实例,必须保证多个线程共用一个。
步骤2. 调用lock(),锁住共享数据的代码
步骤3. 调用unlock(),解锁共享数据的代码

2. 面试题:
synchronized同步的方式 与Lock的对比 ?
    > synchronized同步机制,利用同步监视器,确保同步监视器的唯一性。
        > 同步代码块、同步方法对应的一对{}中的代码是需要被同步的,只能有一个线程执行。
    > Lock,确保Lock的实例的唯一性
        > 在lock()和unlock()方法之间的操作,确保只有一个线程在执行。


官方文档:
Lock implementations provide more extensive locking operations
than can be obtained using synchronized methods and statements.

6. 线程的通信

1. 线程间的通信

为什么需要线程间的通信?
当我们`需要多个线程`来共同完成一件任务,并且我们希望他们`有规律的执行`,那么多线程之间需要一些通信机制,
可以协调它们的工作,以此实现多线程共同操作一份数据。

2. 涉及到三个方法的使用:
wait(): 一旦执行此方法,对应的线程就进入阻塞状态,并释放同步监视器的调用
notify():唤醒被wait的线程中优先级最高的那一个。如果被wait的多个线程优先级相同,则会随机唤醒其中被wait的线程。
notifyAll():唤醒所有被wait的线程。


3. 注意点:
> 此三个方法的调用者必须是同步监视器
> 此三个方法声明在java.lang.Object类中
> 此三个方法的使用,必须在同步方法或同步代码块中。
    ---> 在Lock方式解决线程安全问题的前提下,不能使用此三个方法。在Lock的情况下,使用Condition实现通信。


4. 案例:
案例1:使用两个线程打印 1-100。线程1, 线程2 交替打印

案例2:生产者&消费者
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有
固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品
了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来
取走产品。


5. 【面试题】wait() 和 sleep()的区别?
> 所属的类:wait()存在于Object类的非静态方法;sleep()存在于Thread类的静态方法
> 使用环境:wait() 必须使用在同步代码块或同步方法中;sleep():在调用时没有任何限制
> 都使用在同步代码块或同步方法情况下,区别:wait()一旦调用会释放同步监视器;sleep()不会释放同步监视器
> 相同点:二者一旦执行都可以使得当前线程进入阻塞状态
    > 但是结束阻塞的方式不同:wait()的线程需要被notify()/notifyAll();sleep()的线程在指定时间结束后就结束阻塞。

7. JDK5.0新增两种创建多线程的方式

  • 使用Callable接口
  • 使用线程池
1. 创建多线程的方式三:实现Callable (jdk5.0新增的)

与之前的方式的对比:对比Runnable
> call()方法有返回值类型,比run()灵活
> call()声明有throws结构,内部有异常的话,不必非要使用try-catch
> Callable接口使用了泛型,call()的返回值类型更加灵活。(超纲)



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

此方式的好处:
> 提高了程序执行的效率
> 提高了资源的重用率
> 设置相关的参数,实现对线程的管理

联想:与后续讲的数据库连接池的好处是相同的。
  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值