多线程篇

1、简介

1.1、进程

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。

1.2、线程

程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。

1.3、进程与线程的区别

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位。

  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线。

  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见。

  4. 调度和切换:线程上下文切换比进程上下文切换要快得多。

2、线程的三种创建方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

补充:Runnable与Callable的相同与不同:

  • 相同:都是接口,都可以编写多线程程序,都采用Thread.start()启动线程。
  • 不同:Runnable没有返回值,Callable可以返回执行结果。Callable的call()方法允许抛出异常,Runnable的run方法不能抛出异常。Callable的返回结果需要调用FutureTask.get()得到,此方法会阻塞主线程,不调用则不会阻塞。

3、并发与并行

3.1、并发

把任务在不同的时间点交给处理器进行处理。在同一时间点,多个任务并不会同时运行。

多个线程操作同一个资源下,线程不安全,导致数据混乱的问题。

3.2、并行

把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行的。

4、线程的五个状态

  1. 创建线程:构建出一个线程实例。
  2. 就绪:调用start()方法。
  3. 运行:获得CPU资源,开始运行。
  4. 阻塞:因为某种原因放弃CPU使用权。
    • 等待阻塞:wait()方法,进入等待队列,返回就绪状态。
    • 同步阻塞:对象的同步锁被别的线程占用,放入锁池(lock pool)。
    • 其他阻塞:sleep()或者join()方法,只有当sleep()方法超时或者join()方法等待线程终止或超时,线程重新转入可运行状态。
  5. 死亡:线程正常执行完毕,或者因异常退出了run()方法,死亡的线程不可再运行。

5、线程的方法

方法 说明
setPriority(int newPriority) 更该线程的优先级
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
void join() 等待该线程终止
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() 中断线程,别用这种方式
boolean isAlive() 测试线程是否处于活动状态

5.1、停止线程

不推荐JDK的方法,推荐让线程自己停下来。

使用一个标志位进行终止变量。

如下:

// 1.线程中定义线程体使用的标识
private boolean flag = true;

@Override
public void run() {
    // 2.线程体使用该标识
	while (flag) {
        // 执行内容代码...
    }
}

// 3.对外提供方法改变标识
public void stop() {
    this.flag = false;
}

5.2、线程休眠

  • sleep(时间)指定当前线程阻塞的毫秒数。
  • sleep存在异常InterruptedException。
  • sleep时间达到后线程进入就绪状态。
  • sleep可以模拟网络延时,倒计时等。
  • 每一个对象都有一个锁,sleep不会释放锁。

5.3、线程礼让

  • 让当前正在执行的线程暂停,但不阻塞。
  • 将线程从运行状态转未就绪状态。
  • 让CPU重新调度,但是礼让不一定成功,取决于CPU调度。

5.4、线程合并

  • 等当前线程执行完成后,再执行其他线程,其他线程阻塞。
  • 可以想象成插队。

6、观测线程状态

调用getState()方法可以查看当前线程的当前状态。

7、线程的优先级

  • java提供了一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
  • 线程的优先级用数字表示,取值范围1~10。
  • 使用以下方式更改或者获取优先级:
    • getPriority()
    • setPriority(int priority)
  • 优先级不代表级别越高的就一定比级别低的先执行,只是被CPU调度到的概率被提高了。
  • 优先级建议设定在start()方法之前。

8、守护线程

  • 线程分为用户线程与守护线程。
  • 虚拟机必须确保用户线程执行完毕。
  • 虚拟机不用等待守护线程执行完毕,如后台记录操作日志、监控内存、垃圾回收等待...
  • 设置方式为setDaemon(boolean on),默认false表示是用户线程,true为守护线程。

9、线程同步

为了解决在多线程中出现的并发问题。

线程同步就是一种等待机制。多个需要同时访问此对象的线程进入这个对象的等待池形成队列,让前面的线程使用完毕后,下一个线程再使用。

9.1、synchronized

9.1.1、修饰对象的几种方式

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

    1. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
    2. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
    3. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。

9.1.2、同步监视器

  • 同步块:

    synchronized(Obj) {
    	// 此处省略...
    }
    
  • Obj被称之为同步监视器。

    • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器。
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是调用该同步方法的对象。
  • 同步监视器的执行过程

    1. 第一个线程访问,锁定同步监视器,执行其中代码。
    2. 第二个线程访问,发现同步监视器被锁定,无法执行。
    3. 第一个线程执行完毕,解锁同步监视器。
    4. 第二个线程访问,发现同步监视器没有锁定,然后锁定并访问。

9.2、死锁

多个线程持有对方的锁,同时都需要对方释放锁从而导致僵持的就是死锁。

9.2.1、产生死锁的必要条件

  • 互斥条件:一个资源仅为一进程所占用。

  • 请求和保持条件:一个进程请求资源而阻塞时,对已获得的资源保持不放。

  • 不剥夺条件:进程已获得的资源在未使用完之前,不能强势剥夺。

  • 循环等待条件:在发生死锁时,必然存在一个循环链。

9.2.2、预防死锁

避免或者破坏产生死锁的四个必要条件中的一个或几个来预防死锁的产生。

9.2.3、检测死锁

使用jps命令。

  1. jps -l查看JAVA进程号。
  2. jstack pid查看进程的堆栈情况。

9.3、Lock

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。ReentrantLock类(可重入锁)拥有与synchronized相同的线程同步机制。

9.3.1、ReentrantLock

可重入锁。无参构造方法是默认(false)的非公平锁,传boolean值true给构造方法则是公平锁。

可重入就是说某个线程已经获得某个锁,可以再次获取这个锁而不会出现死锁。

注意:

  • ReentrantLock 和 synchronized 不一样,需要手动释放锁,所以使用 ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样。

9.4、synchronized与Lock的区别

  • synchronized是Java语言的关键字,Lock是JUC包下的一个接口类。
  • Lock是显示锁(手动开启和关闭锁);synchronized是隐式锁,出了作用域自动释放。
  • Lock锁只有代码块锁,synchronized有代码块锁和方发锁。
  • synchronized无法判断是否获取锁的状态,Lock可以通过tryLock()方法判断是否获取到锁。
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。

10、线程协作(通信)

方法名 作用
wait() 表示线程一直等待,直到其他线程通知。与sleep不同,他会释放锁。
wait(long timeout) 指定等待的毫秒数。
notify() 唤醒一个处于等待状态的线程。
notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度。

注意:以上均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegalMonitorStateException。

10.1、生产者--消费者例子

10.1.1、管程法

  • 生产者:负责生产数据。
  • 消费者:负责处理数据。
  • 缓冲区:消费者不能直接使用生产者的数据,在生产者与消费者之间有一个数据的缓冲区。

img

代码实例:

package com.thread.gaoji;

//测试: 生产者消费者模型-->利用缓冲区解决:管程法

//生产者 , 消费者 , 产品 , 缓冲区
public class TestPC {

    public static void main(String[] args) {
        SynContainer container = new SynContainer();

        new Productor(container).start();
        new Consumer(container).start();
    }
}

//生产者
class Productor extends Thread {
    SynContainer container;

    public Productor(SynContainer container) {
        this.container = container;
    }

    //生产
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.push(new Chicken(i));
            System.out.println("生产了" + i + "只鸡");
        }
    }
}

//消费者
class Consumer extends Thread {
    SynContainer container;

    public Consumer(SynContainer container) {
        this.container = container;
    }

    //消费
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了-->" + container.pop().id + "只鸡");
        }
    }
}

//产品
class Chicken {
    int id;//编号

    public Chicken(int id) {
        this.id = id;
    }
}

//缓冲区
class SynContainer {

    //需要一个容器大小
    Chicken[] chickens = new Chicken[10];

    //容器计数器
    int count = 0;

    //生产者放入产品
    public synchronized void push(Chicken chicken) {
        //如果容器满了,就需要等待消费者消费
        if (count == chickens.length) {
            //生产者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果没有满,我们需要丢入产品
        chickens[count] = chicken;
        count++;

        //可以通知消费者消费了.
        this.notifyAll();
    }

    //消费者消费产品
    public synchronized Chicken pop() {
        //判断能否消费
        if (count == 0) {
            //消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //如果可以消费
        count--;
        Chicken chicken = chickens[count];

        //吃完了,通知生产者生产
        this.notifyAll();
        return chicken;
    }
}

10.1.2、信号灯法

  • 生产者:负责生产数据。
  • 消费者:负责处理数据。
  • 标志位:通过标志位boolean值来决定生产者和消费者线程的运行。通过这样一个判断方式,只要来判断什么时候让他等待,什么时候将他唤醒就ok。

代码实例:

package com.thread.gaoji;

//测试生产者消费者问题2:信号灯法,通过标志位解决

public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

//生产者-->演员
class Player extends Thread {
    TV tv;

    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.tv.play("快乐大本营播放中");
            } else {
                this.tv.play("抖音:记录美好生活");
            }
        }
    }
}

//消费者-->观众
class Watcher extends Thread {
    TV tv;

    public Watcher(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

//产品-->节目
class TV {
    //演员表演,观众等待 T
    //观众观看,演员等待 F
    String voice; // 表演的节目
    boolean flag = true;


    //表演
    public synchronized void play(String voice) {

        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员表演了:" + voice);
        //通知观众观看
        this.notifyAll();
        this.voice = voice;
        this.flag = !this.flag;
    }

    //观看
    public synchronized void watch() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看了:" + voice);
        //通知演员表演
        this.notifyAll();
        this.flag = !this.flag;
    }
}
posted @ 2022-02-17 21:07  是老胡啊  阅读(38)  评论(0)    收藏  举报