java - 多线程 , 线程的概念,线程的创建方式,多线程的状态,线程调度,线程安全,线程同步,等待唤醒机制,Lock锁,线程池

第十一章、多线程

10.1、进程、线程的概念

1)程序和进程是什么?

​ 程序是对数据描述和操作的代码集合。比如我们说的暴风影音、Office中的word、QQ、微信。程序是一个静态的概念。程序运行后就是进程。

​ 进程:是 运行中的程序 ,或者是程序的一次动态执行,它对应从代码的加载、执行、执行完毕。它是一个动态的概念,它有生命周期。

​ 一句话概括:系统运行的一个或者多个程序,叫做进程。

特点:

1)进程是系统运行程序的基本单元。

2)每个进程有自己的独立的一块内存空间、一组系统资源。

3)每一个进程的内部数据和状态是完全独立的。

图示:

image

2)程序1在 CPU 中运行状态

1、cpu 同一时刻只能被一个进程使用

2、由于 cpu 在各个程序之间切换的时间片非常短,用户就会觉得 cpu 同时被多个程序使用。

3、时间片:它是操作系统调度 cpu 的单位。比如:0.00001毫秒时间片被迅雷下载使用

图示:

image

3)线程是什么?它和进程之间有什么关系?

每个正在系统上运行的程序就是一个进程。每个进程可以包含多个线程。线程是一组指令的集合,它可以在程序中独立的运行。

我们可以把线程理解成代码运行的上下文。线程是轻量级的进程,

它负责在单个程序中执行任务,通常它的调度是由操作系统负责和执行。

一句话概括线程是运行在进程中的,一个进程中可以有很多对线程,这些线程可以相互影响,也会独立运行,叫做多线程

4)为什么要用多线程?

1、进程之间没有关联(不共享内存),线程之间有关系(共享内存)

2、线程的使用效率比进程要高

3、JAVA 提供了对多线程的支持,编码的效率高

图示:

image

5)线程的分类以及特点 = 面试题

1)主线程:main 方法

​ main 方法会启动一个主线程,其他的线程都是这个线程的子线程

2)守护线程:也称“服务线程”,在没有用户线程课可服务时会自动离开。优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。

例如:

垃圾回收线程就是一个经典的守护线程,当我们的程序中不在有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

如果把用户线程用 setDaemon(true) 方法进行设置,那么就变成守护线程。当进程不存在或者主线程停止,守护线程会被停止。

3)用户线程:是用户自己定义的线程,主线程停止的时候,用户线程不会停止。

6)多线程的优势

就是可以提高 程序执行的效率 。实际生活中迅雷软件去下载电

影。它实际上是采用多线程的,多线程不能提高程序下载的速度,

只是提高程序的执行效率。

7)程序中的同步和异步

图示:

image

10.2、线程的创建方式 = 面试题

1)线程的常用构造方法:

Thread() 
          分配新的 Thread 对象。 
Thread(Runnable target) 
          分配新的 Thread 对象。 
Thread(Runnable target, String name) 
          分配新的 Thread 对象。 
Thread(String name) 
          分配新的 Thread 对象。 

name:表示线程的名字

2)主要由如下几种创建方式:

​ 继承方式、实现接口方式、匿名内部类、线程池方式

A)继承的方式

实现步骤:

  1. 写一个A类,继承 java.lang.Thread类
  2. 要重写 Thread 类的 run()方法
  3. 在 run() 方法里,写要执行的一些操作
  4. 在 main 方法里(主线程),创建线程对象,调用 start() 方法
//1.继承线程类 :Thread
public class MyThread01 extends Thread{

    //有参构造
    public MyThread01(String name) {
        super(name);
    }

    //run(): 表示线程运行的方法,会把核心的代码放在run方法中。
    @Override
    public void run() {
        /**
         * 案例:采用不同的方式创建线程,在线程中输出1-100的整数。
         */
        for (int i = 1; i <= 100; i++) {
            try {
                //sleep(毫秒值):线程休眠:让当前线程进入休眠状态(阻塞状态),停止运行,
                //休眠期到了之后,再次让当前线程进入运行状态。
                Thread.sleep(300);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //线程名称 + 循环次数
            //currentThread(): 当前线程对象的引用
            //getName():当前线程对象的名称
            System.out.println(Thread.currentThread().getName()+">>>"+i);
        }
    }
}

class Test01{
    public static void main(String[] args) {
        //主线程(main)
        //创建子线程(用户线程)
        //Thread-0: 如果提供是无参构造,那么它会使用java提供的线程名称
      /*  MyThread01 th01 = new MyThread01();
        MyThread01 th02 = new MyThread01();*/
        MyThread01 th01 = new MyThread01("线程1");
        MyThread01 th02 = new MyThread01("线程2");
        //一定要调用start()方法
        //start():线程执行的方法,在start方法执行之前,线程都是处于就绪状态。
        th01.start();
        th02.start();
    }
}

问题:为什么代码不是从上往下顺序进行,而是主线程有自己的执行路径,自定义线程有另外的执行路径的原因

main方法先执行,然后就执行main方法里面的内容,当main方法执行完毕之后,子线程才运行,然后打印内容。

//观察输出顺序
public class Example01 {

    public static void main(String[] args) throws InterruptedException {
        //多线程的运行方式:谁抢夺到了cpu的使用权,谁就执行!!!
        //main(主线程)
        System.out.println("==========线程开始创建===============");
        //子线程
        MyThread01 th03 = new MyThread01("线程3");
        System.out.println("==========线程创建结束===============");
        th03.start();
        Thread.sleep(301); //把主线程阻塞一定时间,改变睡眠时间试试
        System.out.println("==========线程运行结束===============");
    }
}

问题:线程执行有什么特点?

​ 1)交替执行,没有规则,具体执行哪个线程是由cpu调度的,人为不能干涉cpu的调度。

​ 2)线程会跟其他的线程抢夺CPU的资源,谁抢到了,谁调用,结果都是随机的。

B)实现接口方式

实现步骤:

  1. 写一个A类,实现 java.lang.Runnable接口
  2. 要实现Runnable接口的run()方法。
  3. 在run方法里,写要执行的一些操作
  4. 在main方法里(主线程),创建线程对象,调用start()方法
//实现Runnable接口
public class MyThread02 implements Runnable{

    @Override
    public void run() {
        /**
         * 案例:采用不同的方式创建线程,在线程中输出1-100的整数。
         */
        for (int i = 1; i <= 100; i++) {
            try {
                //sleep(毫秒值):线程休眠:让当前线程进入休眠状态(阻塞状态),停止运行,
                //休眠期到了之后,再次让当前线程进入运行状态。
                Thread.sleep(300);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //线程名称 + 循环次数
            //currentThread(): 当前线程对象的引用
            //getName():当前线程对象的名称
            System.out.println(Thread.currentThread().getName()+">>>"+i);
        }
    }
}

class Test02{
    public static void main(String[] args) {
        //创建线程对象
        MyThread02 th01 = new MyThread02();
        //创建Thread对象
        Thread thread = new Thread(th01, "线程1");
        thread.start();
    }
}

C)匿名内部类

Runnable:函数式接口

语法:

new THread(new Runnable(){
    @Override
    public void run(){
        //...
    }
}).start();

案例:

public class Example03 {

    public static void main(String[] args) {
        //使用匿名内部类的方式=创建线程对象
        //这种方式虽然简单,但是对象只能使用一次。
        new Thread(new Runnable() {
            @Override
            public void run() {
                /**
                 * 案例:采用不同的方式创建线程,在线程中输出1-100的整数。
                 */
                for (int i = 1; i <= 100; i++) {
                    try {
                        //sleep(毫秒值):线程休眠:让当前线程进入休眠状态(阻塞状态),停止运行,
                        //休眠期到了之后,再次让当前线程进入运行状态。
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    //线程名称 + 循环次数
                    //currentThread(): 当前线程对象的引用
                    //getName():当前线程对象的名称
                    System.out.println(Thread.currentThread().getName()+">>>"+i);
                }
            }
        }, "线程3").start();

    }
}

函数式接口的特有写法:后面会专门讲

//java的新语法 - 箭头函数
        new Thread(()->{
            /**
             * 案例:采用不同的方式创建线程,在线程中输出1-100的整数。
             */
            for (int i = 1; i <= 100; i++) {
                try {
                    //sleep(毫秒值):线程休眠:让当前线程进入休眠状态(阻塞状态),停止运行,
                    //休眠期到了之后,再次让当前线程进入运行状态。
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //线程名称 + 循环次数
                //currentThread(): 当前线程对象的引用
                //getName():当前线程对象的名称
                System.out.println(Thread.currentThread().getName()+">>>"+i);
            }
        },"线程4").start();

10.3、多线程状态 = 面试题

线程是有生命周期的,它有5个状态:

1)新建状态

当采用 new 关键字创建一个线程对象的时候,此时线程还没有运行。比如:Thread t = new Thread();

2)就绪状态

当调用了 start(),系统为该线程分配除了 cpu 之外所有的资源。此时该线程具备运行的机会,此时处于 就绪状态。此时线程是没有运行的。

3)运行状态

处于就绪状态的线程,获得了 cpu 的使用资格,才开始运行。

4)阻塞状态

yige1正在运行的线程由于某种原因丧失了 cpu 的使用权,不能继续运行,就由运行状态进入阻塞状态。处于阻塞状态的线程在得到某个特点的事件后会转入可运行状态。

A、线程调用了 sleep() 方法进入睡眠状态。

B、线程执行到一个 I/O 操作,如果 I/O 还没有完成,线程就被阻塞。

C、线程视图得到一个锁,而该锁正被其他的线程所持有。

D、执行了 suspend() 方法,线程被挂起。但是容易导致死锁。被 jdk 列为过时,基本不用。

5)死亡状态

肯定就是线程的 run() 方法执行完毕,自然死亡。肯是一个未捕获的异常终止了线程的 run() 的执行。

图示:

image

10.4、线程调度

常用API

image

1)线程调度的机制:

同一时刻可能有多个线程处于就绪状态。线程调度器负责线程的排队和 CPU 在线程之间的分配。线程调度器按照一定的调度的算法进行调度,当线程调度器选中了某个线程后该线程就可以进入运行状态

​ 线程的调度它是抢占式的,如果当前的线程在执行的过程中来了一个优先级更高的线程进入了就绪状态,则该优先级高的线程会被调度执行。

2)join() 方法:

 void join() 
          等待该线程终止。 

在哪个线程内调用该方法就会让当前的线程暂停执行,就是获得了 CPU 也会让出控制权,等待调用该方法的线程执行完毕后,再执行本线程。

需求:创建一个线程,子线程执行完毕后,主线程才能执行。在子线程中输出0-9的数字,主线程输出0-19的数字。

public class Example01 {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @SneakyThrows //代替try...catch/抛出异常
            @Override
            public void run() {
                for (int i = 0; i <= 9; i++) {
                    Thread.sleep(300);
                    System.out.println(Thread.currentThread().getName() + i);
                }
            }
        }, "子线程>>>");
        thread.start();

        //主线程让步,让子线程先执行
        //join()方法:在哪个线程内部调用,那个线程就让步。
        //thread.join();
        //yield():暂停当前线程,下一次继续和其他线程抢夺资源。
        Thread.yield();
        //主线程
        for (int i = 0; i <= 19; i++) {
            Thread.sleep(300);
            System.out.println("主线程>>>"+i);
        }
    }
}

案例:假设有三个线程t1、t2、t3,怎么保证让 t1 执行完毕后执行 t2 ,t2 执行完毕后执行 t3 线程?

public class MyThread02 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + ">>>" + i);
        }
    }
}


class Test02{
    public static void main(String[] args) throws InterruptedException {
        MyThread02 myThread02 = new MyThread02();
        Thread t1 = new Thread(myThread02, "t1");
        Thread t2 = new Thread(myThread02, "t2");
        Thread t3 = new Thread(myThread02, "t3");

        t1.start();
        t1.join();
        t2.start();
        t2.join();
        t3.start();
    }
}

3)yield() 方法:

static void 		yield() 
          暂停当前正在执行的线程对象,并执行其他线程。 

暂停当前的线程,运行其他线程执行,但是该线程还是处于就绪状态,不转为阻塞状态。可能下次的时候仍然获得 CPU 的控制权。并不能保证线程让步的目的。

详细说明:

​ Java线程中的Thread.yield( )方法,译为线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。

​ yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!

​ 举个例子:一帮朋友在排队上公交车,轮到Yield的时候,他突然说:我不想先上去了,咱们大家来竞赛上公交车。然后所有人就一块冲向公交车,有可能是其他人先上车了,也有可能是Yield先上车了。

​ 但是线程是有优先级的,优先级越高的人,就一定能第一个上车吗?这是不一定的,优先级高的人仅仅只是第一个上车的概率大了一点而已,最终第一个上车的,也有可能是优先级最低的人。并且所谓的优先级执行,是在大量执行次数中才能体现出来的。

4)sleep() 方法:

1.方法的说明:

Thread.sleep()是Thread类的一个静态方法,使当前线程休眠,进入阻塞状态(暂停执行),如果线程在睡眠状态被中断,将会抛出IterruptedException中断异常。

2.常用 API 方法:

static void 		sleep(long millis)
          			在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。

3.使用方法

在哪个线程里面调用 sleep() 方法就阻塞哪个线程。

练习:定义一个线程,每隔一秒输出 “HelloWorld” 的一个字母

4.注意问题

  1. sleep()方法是Thread类的静态方法,如果调用线程对象.sleep()方法并不是该线程就休眠,而是在哪一个线程里面执行了sleep()方法哪一个线程就休眠。
  2. 线程睡眠到期自动苏醒,并返回到可运行状态(就绪),不是运行状态。然后等待线程的调度。

5.问题:sleep() 和 yield() 方法的区别? = 面试题

sleep() 会转入阻塞状态,进行休眠,即使没有其他等待运行的线程也会等待指定的时间。而 yield() 方法暂停线程的执行,不会进行阻塞。如果没有其他等待的线程会马上恢复执行。它会和其他就绪的线程一起竞争 CPU 资源的使用。它可能被选中。

5)setPriority() 方法(更改线程的优先级)

更改线程的优先级。

  1. 优先级表示重要程度或者紧急程度. 但是能不能抢到资源也是不一定。
  2. 分配优先级:反映线程的重要或紧急程度

​ 线程的优先级用1~10表示,1的优先级最低,10的优先级最高,默认值是5.

public class Example01 {

    public static void main(String[] args) {
        //创建3个线程
        Thread th01 = new Thread(new Runnable() {

            @SneakyThrows
            @Override
            public void run() {
                for (int i = 0; i < 9; i++) {
                    Thread.sleep(300);
                    System.out.println(Thread.currentThread().getName()+i);
                }
            }
        },"线程1>>>");

        Thread th02 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                //线程2让步
                th01.join();
                for (int i = 0; i < 9; i++) {
                    Thread.sleep(300);
                    System.out.println(Thread.currentThread().getName()+i);
                }
            }
        },"线程2>>>");

        Thread th03 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                //线程3让步
                th02.join();
                for (int i = 0; i < 9; i++) {
                    Thread.sleep(300);
                    System.out.println(Thread.currentThread().getName()+i);
                }
            }
        },"线程3>>>");
        th01.start();
        th02.start();
        th03.start();
    }
}

10.5、线程安全 = 重点

什么是线程安全?

​ 如果多个线程同时运行,而这些线程可能会运行相同的代码,**在这段代码中要对公共的变量(对象)进行非读操作(写操作),会出现意想不到的情况,和预期的值不一样,这就是所谓的线程不安全。反之就是线程安全的

案例:

电影院卖票,放的是独行月球,假设本次座位有100个位置(意味着只能卖出100张票)

100张票:共享数据(变量)

窗口1:线程

窗口2:线程

窗口3:线程

图示:

image

import lombok.SneakyThrows;

public class Ticket implements Runnable{

    //创建变量:总共的票数
    private int count = 100;

    //创建锁对象
    Object lock = new Object();

    @SneakyThrows
    @Override
    public void run() {
        //循环
        while (count > 0){
            //如果count>0 卖票
            Thread.sleep(100);
            //对可能会发生线程安全的代码,加上锁(代码块)
            synchronized (lock){
               //必须要添加if判断, count > 0 才能卖票
               if(count > 0){
                   System.out.println(Thread.currentThread().getName()+"卖了第"+(100-count+1)+"张票");
                   //每次卖一张票
                   //这里可能会发生线程安全的问题!!!
                   count--;
               }
            }
        }
    }
}

class Test01{
    public static void main(String[] args) {
        //创建线程对象
        Ticket ticket = new Ticket();
        //创建Thread对象
        //注意:这3个Thread对象,必须使用同一个ticket线程对象:数据共享
        Thread th1 = new Thread(ticket,"窗口1");
        Thread th2 = new Thread(ticket,"窗口2");
        Thread th3 = new Thread(ticket,"窗口3");
        th1.start();
        th2.start();
        th3.start();
    }
}

10.6、线程同步

1)什么是线程同步?

当线程出现安全问题的时候,java提供了 线程的同步机制 来解

决上面的安全问题。

2)什么情况下,需要使用同步解决线程安全

当多线程操作同一个 共享变量 ,并且对变量值进行修改。

3)如何解决线程安全问题?(如何实现线程的同步)

同步方法、同步代码块

思想:对可能出现线程安全的代码,用锁把它锁定,代码执行完毕后释放锁,让其他线程进来执行。这样可以解决安全问题。不能让多个线程同时去操作共享的变量,在一个线程执行完代码前,不让其他线程进入执行代码。

图示:

image

4)同步代码块

语法:

synchronized(锁对象){
    //可能出现线程安全的代码
}

同步代码块中的锁对象是任意的对象,但是多个线程,要使用同一把锁才能保证线程安全

5)同步方法

1. 同步普通方法

public synchronized void method(){
    //可能出现线程安全的代码
}

同步方法中的锁对象是 this ,但是多个线程,要使用同一把锁才能保证线程安全。

2. 同步静态方法

语法:

public static synchronized void method(){
    //可能出现线程安全的代码
}

要保证共享的变量是static,它的锁对象是类名.class

说明:除了同步代码块需要定义锁对象,对应同步方法和静态同步方法它们的锁对象是隐式的,一个是this,一个是类名.class

12.7、等待唤醒机制

案例演示:

需求:第一个线程写入(input)用户,偶数输入男生,奇数输入女生,另一个线程取读取(out)用户的信息.实现读一个,写一个操作。

分析:

实现步骤:

1)定义一个共享的资源Resource类

2)编写输入线程InputThread

3)编写输出线程outputThread

4)测试类

分析问题:

线程安全的问题:输入线程还没有来得及修改属性,就被输出线程抢占了资源,那么就会导致输出信息有问题。 如何解决?使用线程锁来解决 。

public class InputThread implements Runnable{

    //引入资源类属性
    private Resource r;

    //定义变量:奇数或者是偶数
    private int x;

    public InputThread(Resource r) {
        this.r = r;
    }

    @SneakyThrows
    @Override
    public void run() {
        while (true){
            Thread.sleep(300);
            //偶数输入男生,奇数输入女生
            //注意:这里必须使用同一把锁对象
            synchronized (r){
                if(x%2==0){
                    //给属性赋值
                    r.userName = "小米";
                    r.sex = '男';
                }else{
                    r.userName = "小红";
                    r.sex = '女';
                }
                x++;
            }
        }
    }
}


public class OutputThread implements Runnable{

    private Resource r;

    public OutputThread(Resource r) {
        this.r = r;
    }

    @SneakyThrows
    @Override
    public void run() {
        while (true){
            Thread.sleep(300);
            synchronized (r){
                //输出属性- 打印
                if(r.userName!=null){
                    System.out.println(r.userName+","+r.sex);
                }
            }
        }
    }
}


//资源类
public class Resource {

    public String userName;

    public char sex;
}


public class Test01 {

    public static void main(String[] args) {
        //创建共享对象
        Resource r = new Resource();
        //创建线程对象
        InputThread it = new InputThread(r);
        OutputThread ot = new OutputThread(r);
        //创建Thread对象
        Thread th01 = new Thread(it, "输入线程>>>");
        Thread th02 = new Thread(ot, "输出线程>>>");
        th01.start();
        th02.start();

    }
}

案例2:

需求:用两个线程一个线程输入、一个线程输出,输入一个用户完毕,然后马上输出一个用户的信息。而不是出现同时生成用户和输出用户?

1、需求说明:

image

2、线程之间的通信?

多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

3、等待唤醒机制所涉及到的方法:

wait() : 等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。

notify(): 唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。

notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。

4、什么是唤醒?

所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程

仔细查看JavaAPI之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?

因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。

image

public class InputThread implements Runnable{

    //引入资源类属性
    private Resource r;

    //定义变量:奇数或者是偶数
    private int x;

    public InputThread(Resource r) {
        this.r = r;
    }

    @SneakyThrows
    @Override
    public void run() {
        while (true){
            Thread.sleep(300);
            //偶数输入男生,奇数输入女生
            //注意:这里必须使用同一把锁对象
            synchronized (r){
                if(r.flag == false){
                    if(x%2==0){
                        //给属性赋值
                        r.userName = "小米";
                        r.sex = '男';
                    }else{
                        r.userName = "小红";
                        r.sex = '女';
                    }
                    x++;
                }
                r.flag = true; //给输出线程判断使用
                //唤醒输出线程
                r.notify();
                //让当前线程进入等待状态
                r.wait();
            }
        }
    }
}


public class OutputThread implements Runnable{

    private Resource r;

    public OutputThread(Resource r) {
        this.r = r;
    }

    @SneakyThrows
    @Override
    public void run() {
        while (true){
            Thread.sleep(300);
            synchronized (r){
                if(r.flag == true){
                    //输出属性- 打印
                    if(r.userName!=null){
                        System.out.println(r.userName+","+r.sex);
                    }
                }
                r.flag = false; //给输入线程判断使用
                //唤醒输入线程
                r.notify();
                //让当前线程等待
                r.wait();
            }
        }
    }
}


//资源类
public class Resource {

    public String userName;

    public char sex;

    //定义一个标记变量
    public boolean flag = false;
}

12.8、Lock 锁

Lock 锁的特点:

  1. Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构
  2. 使用lock锁来实现等待唤醒机制,实现线程之间的通信。

Lock 接口中的常用方法:

image

说明:Lock锁也可以使用等待和唤醒,不过它的方法发现了改变。它需要用到一个和Lock关联的对象Conditon接口。该接口的await()用来等待,而signal() 唤醒一个等待线程。signalAll() 唤醒所有等待线程。

Lock锁相当于代替了synchronized,Conditon 相当于代替了Object

如何使用 Lock 锁实现步骤:

1)如何创建一个 Lock 对象?

​ Lock lock = new ReentrantLock();

2)如何获得 Lock 对象关联的 Condition 接口?

Condition condition= lock.newCondition();

​ condition.await()等待,condition.signal() 唤醒

问题:

什么时候使用唤醒全部?多个输入和输出线程的时候要注意唤醒的

不能只唤醒锁池中的第1个,而应该是全部唤醒。用notifyAll()或者

是signalAll()去处理。

代码案例:

public class Example02 {

    public static void main(String[] args) {
        //创建共享对象
        Resource r = new Resource();
        //通过共享锁对象,获取Condition对象
        Condition con = r.lock.newCondition();
        //创建线程对象
        InputThread it = new InputThread(r,con);
        OutputThread ot = new OutputThread(r,con);
        //创建Thread对象
        Thread th01 = new Thread(it, "输入线程>>>");
        Thread th02 = new Thread(ot, "输出线程>>>");
        th01.start();
        th02.start();

    }
}


public class InputThread implements Runnable{

    //引入资源类属性
    private Resource r;

    //定义变量:奇数或者是偶数
    private int x;

    //定义Condition对象属性
    private Condition condition;

    public InputThread(Resource r, Condition con) {
        this.r = r;
        this.condition = con;
    }

    @SneakyThrows
    @Override
    public void run() {
        while (true){
            Thread.sleep(300);
            //偶数输入男生,奇数输入女生
            //注意:这里必须使用同一把锁对象
            //加锁
            r.lock.lock();
            try{
                if(r.flag == false){
                    if(x%2==0){
                        //给属性赋值
                        r.userName = "小米";
                        r.sex = '男';
                    }else{
                        r.userName = "小红";
                        r.sex = '女';
                    }
                    x++;
                }
                r.flag = true; //给输出线程判断使用
                //唤醒输出线程
                condition.signal();
                //让当前线程进入等待状态
                condition.await();
            }finally {
                //解锁
                r.lock.unlock();
            }
        }
    }
}


//输出线程
public class OutputThread implements Runnable{

    private Resource r;

    private Condition condition;

    public OutputThread(Resource r, Condition con) {
        this.r = r;
        this.condition = con;
    }

    @SneakyThrows
    @Override
    public void run() {
        while (true){
            Thread.sleep(300);
            r.lock.lock();
            try{
                if(r.flag == true){
                    //输出属性- 打印
                    if(r.userName!=null){
                        System.out.println(r.userName+","+r.sex);
                    }
                }
                r.flag = false; //给输入线程判断使用
                //唤醒输入线程
                condition.signal();
                //让当前线程等待
                condition.await();
            }finally {
                r.lock.unlock();
            }
        }
    }
}


//资源类
public class Resource {

    public String userName;

    public char sex;

    //定义一个标记变量
    public boolean flag = false;

    //定义一个共享的锁对象
    public Lock lock = new ReentrantLock();

}

12.9、线程池

1、什么是线程池?

​ Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

2、线程池的工作原理

image

3、为什么要使用线程池?

​ 在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

4、线程池的优点

1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

3)提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用

5、线程池的分类

1)newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

2) newFixedThreadPool : 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

4)创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

6、参数的含义

1)corePoolSize :线程池的核心池大小,在创建线程池之后,线程池默认没有任何线程。

当有任务过来的时候才会去创建创建线程执行任务。换个说法,线程池创建之后,线程池中的线程数为0,当任务过来就会创建一个线程去执行,直到线程数达到corePoolSize 之后,就会被到达的任务放在队列中。(注意是到达的任务)。换句更精炼的话:corePoolSize 表示允许线程池中允许同时运行的最大线程数。如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

2)maximumPoolSize :线程池允许的最大线程数,他表示最大能创建多少个线程。maximumPoolSize肯定是大于等于corePoolSize。

3)keepAliveTime :表示线程没有任务时最多保持多久然后停止。默认情况下,只有线程池中线程数大于corePoolSize 时,keepAliveTime 才会起作用。换句话说,当线程池中的线程数大于corePoolSize,并且一个线程空闲时间达到了keepAliveTime,那么就是shutdown。

4)Unit:keepAliveTime 的单位。

5)workQueue :一个阻塞队列,用来存储等待执行的任务,当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能

6)threadFactory :线程工厂,用来创建线程。

7)handler :表示当拒绝处理任务时的策略。

7、线程池的创建和使用

A)和线程池相关的API

1)Executors:是创建线程池的工厂类,它专门用来创建线程池。

static ExecutorService			newFixedThreadPool(int nThreads)
          创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。

2)ExecutorService:线程池对象,用来执行任务。

void 				shutdown()
            启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
    
<T> Future<T>		submit(Callable<T> task)
     		提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。
Future<?>			submit(Runnable task)
     		提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。

3)Future接口 :用来记录线程任务执行完毕后产生的结果

V 					get()
       		如有必要,等待计算完成,然后获取其结果。

B)如何使用

实现步骤:

1)Executors创建线程池

2)创建一个实现了Runnable接口的线程类

3)提交线程类到线程池中,线程池中调度的线程执行线程类中的run()方法

4)关闭线程池

public class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println("========线程开始========");
        System.out.println(Thread.currentThread().getName());
        System.out.println("========线程结束========");
    }
}

class Test01{
    public static void main(String[] args) {
        //创建线程的第4种方式:通过线程池的方式创建线程
        //该线程池中,可以放3个线程
        //service:线程池对象
        ExecutorService service = Executors.newFixedThreadPool(3);
        //创建4个线程对象
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();
        MyThread m3 = new MyThread();
        MyThread m4 = new MyThread(); //不会产生线程4,会重新执行线程1
        //把线程对象放入线程池中,并且启动线程。
        service.submit(m1);
        service.submit(m2);
        service.submit(m3);
        service.submit(m4);
        //关闭线程池
        service.shutdown();
    }
}

8、如何获得方法的返回值?

线程池可以获得线程执行后的返回值:而采用原来的继承Thread类或者是实现Runnable接口是无法获得线程执行方法run()中的返回值的。

实现步骤:

​ 1)实现Callable接口,接口中的 泛型类型决定了返回值的类型

​ 2)submit()提交后会有一个接口Future

​ 3)Future的get()方法就可以获得返回值

注意:实现Callable 接口的方式有2种

​ 1)使用匿名内部类的方式

​ 2)使用一个类实现 Callable 接口的方式

public class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return "call()返回的结果...";
    }
}


class Test01{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(3);
        MyThread myThread = new MyThread();
        //把线程放入线程池,并且执行
        //service.submit(myThread); //但是这个线程执行的方法是有返回值的,需要接收
        Future<String> future = service.submit(myThread);
        System.out.println("线程执行返回的结果 = " + future.get());

    }
}


class Test02{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(3);
        Future<String> future = service.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "call()方法返回的结果...";
            }
        });
        System.out.println("线程执行返回的结果 = " + future.get());
    }
}


class Test03{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(3);
        String s = service.submit(() -> "call...").get();
        System.out.println("线程执行返回的结果 = " + s);
    }
}
posted @ 2022-08-16 10:21  Thecong  阅读(33)  评论(0编辑  收藏  举报