尚硅谷Java入门笔记 - P406 ~ P446

多线程

目录

前言

本文为B站Java教学视频BV1Kb411W75N的相关笔记,主要用于个人记录与分享,如有错误欢迎留言指出。
本章笔记涵盖视频内容P406~P446

1. 基本概念

1.1 什么是线程

  • 程序(program):是为完成特定任务,用某种语言编写的指令的集合。即一段静态的代码,静态对象。
  • 进程(process):程序 的一次执行过程,或正在运行的一个程序。是一个动态 的过程。
    • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
  • 线程(thread):进程可以进一步细化为线程,是一个程序内部的一条执行路径。
    • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器
  • 并行与并发:
    • 并行:多个CPU同时执行多个任务
    • 并发:一个CPU同时执行多个任务

1.2 多线程程序的优点

  1. 提高应用程序的响应。对图形化界面更有意义,可以增强用户体验
  2. 提高计算机系统CPU的利用率
  3. 改善程序结构,将长而复杂的进程分为多个线程,独立运行,利于理解和修改

2.创建多线程

2.1 方式一:继承于Thread类

  1. 创建一个继承于Thread类的子类
  2. 重写Thread类的run() (将此线程执行的操作声明在run()中)
  3. 创建Thread类的子类的对象
  4. 通过此对象调用start() (start()是Thread类内的方法)
//1.创建一个继承于Thread类的子类
class MyThread extends Thread{
    //2.重写Thread类的run()
    public void run(){
        for(int i = 0;i < 100;i++){
            if(i % 2 == 0){
                System.out.println(i);
            }
        }
    }
}

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

        //4.通过此对象调用start()
        t1.start();
    }
}
  • 注意事项:

    1. 不能通过直接调用run()的方式启动线程
    2. 不可以让已经start()的线程再去执行其它线程,会报错IllegalThreadExecption;若要执行其它线程,需要重新创建一个线程的对象
  • Thread类的匿名子类

    new Thread(){
        public void run(){
            //执行体
        }
    }.start();
    

2.2 方式二:实现Runnable接口

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

public class ThreadTest{
    public static void main(String[] args){
        //3.创建实现类的对象
        MThread mThread = new MThread();
        //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mThread);
        //5.通过Thread类的对象调用start()
        t1.start();
    }
}

/*
Runnable接口创建和Thread直接创建有诸多不同
Runnable本质只是一个接口,内部没有Thread的诸多方法,所以调用Runnable的对象不能使用Thread内的方法
比如上面的MThread,但是t1可以,此处用mThread作为构造器参数创建了一个Thread对象
同时由于Runnable不是Thread的子类,此处run()也不应该是被"重写"了,但一些特殊的设置使得其效果等同于被重写
*/

2.3 比较两种创建线程的方式

  • 开发中优先选择:实现Runnable接口的方式
    • 实现的方式没有类的单继承性的局限性
    • 实现的方式更适合来处理多个线程有共享数据的情况
  • 两种方式的相同点:
    • 两种方式都需要重写run(),将线程要执行的逻辑声明在run()中

2.4 方式三:实现Callable接口

  1. 创建一个实现Callable的实现类
  2. 实现call方法,将此线程所需要执行的操作声明在call()中
  3. 创建Callable接口实现类的对象
  4. 将此Callable接口实现类的对象,作为创建FutureTask的参数,传递到FutureTask构造器中
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
  6. 获取Callable中call方法的返回值(可选)
//创建一个实现Callable的实现类
class NumThread implements Callable{
    //实现call方法,将此线程所需要执行的操作声明在call()中(相当于run())
    public Object call() throws Exception{
        //......
    }
}

public class ThreadNew{
    public static void main(String[] ards){
        //创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
      	//将此Callable接口实现类的对象,作为创建FutureTask的参数,传递到FutureTask构造器中
        FutureTask futuretask = new FutureTask(numThread);
     	//将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futuretask).start();
        //获取Callable中call方法的返回值
        //get()返回值即为FutureTask构造器参数——Callable实现类重写的call()的返回值
        try {
            Object sum = futuretask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
  • Callable()相较于Runnable()优点
    • 相比run()方法,可以有返回值
    • 方法可以抛出异常
    • 支持泛型的返回值

2.5 方式四:线程池

  • 定义:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁线程,实现重复利用。
public class ThreadPool{
    public static void main(String[] args){
        //1.提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //2.执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(...);//适用于Runnable
        service.submit(...);//适用于Callable
        //3.关闭线程池
        service.shutdown();
    }
}
  • 线程池的优点:
    • 提高响应速度,减少了创建新线程的时间
    • 降低资源消耗,重复利用线程池中的线程,不需要重复创建
    • 便于线程管理
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会中止

3. 多线程的常用方法

方法 功能
start() 启动当前线程,调用当前线程的run()
run() 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
currentThread() 静态方法,返回执行当前代码的线程
getName() 获取当前线程的名字
setName() 设置当前线程的名字
yield() 释放当前cpu的执行权
join() 在线程a中调用线程b的join(),此时线程a进入阻塞状态,直到线程b完全执行后,线程a才会结束阻塞状态
sleep(long milli) 让当前线程"睡眠"指定的milli毫秒,在指定的millitime毫秒时间内,当前的线程是阻塞状态
isAlive() 判断当前线程是否存活

4. 多线程的优先级

  1. MAX_PRIORITY:10

    MIN_PRIORITY:1

    NORM_PRIORITY:5(默认优先级)

  2. 获取和设置当前线程的优先级

    getPriority():获取线程的优先级

    setPriority(int p):设置线程的优先级

  3. 注意:高优先级的线程会抢占低优先级线程cpu的执行权,但这只是从概率上讲,高优先级的线程被执行的概率较高。并不意味着只有当高优先级的线程执行完后,低优先级的线程才会被执行。(和阻塞不同)

HelloThread h1 = new HelloThread("Thread:1"); //用构造器初始化线程名

//设置线程的优先级
h1.setPriority(Thread.MAX_PRIORITY);	//设置当前线程优先级为10
h1.setPriority(8);	//设置当前线程优先级为8
h1.getPriority();	//获取当前线程的优先级

5. 线程的生命周期

  • 线程的一个完整的生命周期中通常要经历如下的五种状态:
    1. 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
    2. 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已经具备了运行的条件,只是没分配到CPU资源
    3. 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
    4. 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作 时,让出CPU并临时中止自己的执行,进入阻塞状态
    5. 死亡:线程完成了全部工作或线程被提前强制性中止或出现异常导致结束

尚硅谷Java入门笔记 - P406 ~ P446

6. 线程安全问题

  • 定义:当某个线程操作过程中,尚未操作完成时,其它线程也参与进来进行操作,此时数据就会出现重复,溢出等问题。为了解决这个问题,需要当一个线程a在操作的时候,其它线程不能参与进来,直到线程a操作完成时,其它线程才可以开始操作。这种情况即便线程a出现了阻塞也不能被改变。Java中,通过同步机制来解决线程安全的问题。

6.1 方式一:同步代码块

  • 格式:synchronized (同步监视器) { //需要被监视的代码 }
6.1.1 处理实现Runnable的线程安全问题
class Windows implements Runnable{
	//不需要static,接口实现类创建的多线程,内部属性天然就是共用的
    private int ticket = 100;

    public void run() {
        while(true){
            
			//使用自身充当锁,由于调用者是Windows且唯一所以可以用this指定;用obj做锁也可以
            synchronized (this){        
                if (ticket > 0){

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

                    ticket--;
                }
            }
        }
    }
}
6.1.2 处理继承Thread类的线程安全问题
class Windows2 extends Thread{
	//必须要用static,继承类的内部变量默认是不共用的,每个对象内都有自己的ticket
    private static int ticket = 100;

    public void run() {
        while(true){
            //由于继承类创建的对象不唯一,所以不能做锁,此处使用特殊的方法将Window2作为一个对象做锁;当然Object obj = new Object;然后用obj做锁也可以
            synchronized (Windows2.class){   
                if (ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    ticket--;
                }
            }
        }
    }
}
6.1.3 注意事项
  1. 操作共享数据的代码,即为需要被同步的代码
  2. 共享数据:多个线程共同操作的变量(如上方代码中的ticket)
  3. 理论上任何一个类的对象,都可以充当同步监视器(锁)。但要求多个线程必须共用同一把锁
  4. 同步的方式,解决了线程的安全问题。但是操作同步代码时,只能有一个线程参与,其它线程等待。相当于是一个单线程的过程,效率低。
  5. 使用synchronized时不能包含太多或太少的代码;少则线程不安全,多则全程一个线程执行任务

6.2 方式二:同步方法

  • 格式:在方法中添加synchronized关键字
6.2.1 处理实现Runnable的线程安全问题
class Window1_ implements Runnable{

    private int ticket = 100;

    public void run() {
        while(true){
            show();
        }
    }

    //直接添加synchronized关键字即可,默认监视器是this
    public synchronized void show(){
            if (ticket > 0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;
            }
    }
}
6.2.2 处理继承Thread类的线程安全问题
class Window2_ extends Thread{

    private static int ticket = 100;

    public synchronized void run() {
        while(true){
            show();
        }
    }

    //因为默认的是this,而继承类不能共用锁,所以必须要加static
    public static synchronized void show(){
        if (ticket > 0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticket--;
        }
    }
}
6.2.3 注意事项
  1. 同步方法仍然涉及到同步监视器,只是不需要显式声明

  2. 非静态的同步方法,同步监视器是:this

    静态的同步方法,同步监视是:当前类本身

6.3 方式三:Lock锁

  • 格式:ReentrantLock 变量名 = new ReentrantLock();
class Window1 implements Runnable{
    private int ticket = 100;
	//实例化ReentrantLock
    ReentrantLock lock = new ReentrantLock();
    
    public void run() {
        while(true){     
            	//调用锁定方法:lock()
            	lock.lock();
            
                if (ticket > 0){

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

                    ticket--;
            }
            //调用解锁方法:unlock()
            lock.unlock();
        }
    }
}
  • synchronized 与 Lock的异同

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

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

      ​ Lock需要手动的启动同步监视器,同时结束同步也需要手动的实现

6.4 死锁问题

  • 定义:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方释放自己需要的同步资源。出现死锁后,不会出现异常或提示,只是所有的线程都处于阻塞状态,无法继续。
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();

new Thread(){
    public void run(){
        synchronized(s1){
            s1.append("a");
            s2.append("1");
            
            synchronized(s2){
                s1.append("b");
                s2.append("2");
            }
        }
    }
}.start();

new Thread(){
    public void run(){
        synchronized(s2){		//和上一个线程所需要的锁,顺序刚好相反
            s1.append("c");
            s2.append("3");
            
        synchronized(s1){
            s1.append("d");
            s2.append("4");
            }
        }
    }
}.start();
/*
线程1->s1->s2(等待线程2释放)
线程2->s2->s1(等待线程1释放)
这样就形成了死锁
*/
  • 解决方法:
    • 设计专门的算法/原则
    • 尽量减少同步资源的定义
    • 尽量避免嵌套同步

7. 线程通信

  • 关键字

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

    1. wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中
    2. wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器;否则会出现IllegalMonitorStateException异常
    3. wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中
    4. wait()和notify()必须同时出现在代码中,否则阻塞的线程无法被唤醒
  • sleep()和wait()的异同

    • 相同点
      • 一旦执行方法,都可以使当前线程进入阻塞状态
    • 不同点
      • 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
      • 调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中
      • 是否释放同步监视器:sleep()不会释放锁,wait()会释放锁
上一篇:线程安全问题


下一篇:多线程