1.1. 第一章 多线程基础

1.1.1. 1、线程与进程区别

进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。

线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。一个线程只能属于一个进程,但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。

1.1.2. 3、多线程应用场景

任务量比较大,通过多线程可以提高效率时,需要异步处理时,占用系统资源,造成阻塞的工作时,都可以采用多线程提高效率

  • 后台任务:如定时向大量(100W以上)的用户发送邮件;定期更新配置文件、任务调度(如quartz),一些监控用于定期信息采集
  • 自动作业处理:比如定期备份日志、定期备份数据库
  • 异步处理:如发微博、记录日志
  • 页面异步处理:比如大批量数据的核对工作(有10万个手机号码,核对哪些是已有用户)
  • 数据库的数据分析(待分析的数据太多),数据迁移
  • 多步骤的任务处理,可根据步骤特征选用不同个数和特征的线程来协作处理,多任务的分割,由一个主线程分割给多个线程完成

1.1.3. 4、线程基本操作

【1】线程创建

【1.1】继承Thread类
package com.itheima.thread.create;

/***
 * @description 继承Thread创建线程
 */
public class ExtendsThread {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
    // 继承Thread类  实现run方法
    static class MyThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println("输出打印"+i);
            }
        }
    }
}
【1.2】实现Runnable接口
package com.itheima.thread.create;

/***
 * @description 实现Runnable创建线程
 */
public class ImplementsRunnable {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
    // 实现Runnable接口  实现run方法
    static class MyRunnable implements Runnable  {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                System.out.println("输出:"+i);
            }
        }
    }
}

==到底是实现?还是继承?上面两个哪个好?== Thread类继承存在单继承的局限性,而接口不会 体现数据共享的概念(JMM内存模型图),代码可以被多个线程共享,代码和数据独立 Runnable实现线程可以对线程进行复用,因为runnable是轻量级对象,而Thread不行,它是重量级对象

【1.3】使用匿名内部类
package com.itheima.thread.create;

/**
 * @Description 匿名线程
 */
public class anonymityThread {

    public static void main(String[] args) {
        //使用匿名内部类方式创建Runnable实例
        Thread t1 = new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("输出"+i);
                }
            }
        });
        t1.start();
        // lambda 表达式简化语法
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                System.out.println("输出"+i);
            }
        });
        t2.start();
    }
}
【1.4】实现Callable接口
package com.itheima.thread.create;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @description 实现Callable创建线程
 */
public class ImplementsCallable {
    public static void main(String[] args) {
        //FutureTask包装我们的任务,FutureTask可以用于获取执行结果
        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
        //创建线程执行线程任务
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            //得到线程的执行结果
            Integer num = futureTask.get();
            System.out.println("得到线程处理结果:" + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
    // 实现Callable接口,实现带返回值的任务
    static class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            int num = 0;
            for (int i = 0; i < 1000; i++) {
                System.out.println("输出"+i);
                num += i;
            }
            return num;
        }
    }
}

【2】用户线程与守护线程

Java分为两种线程:用户线程和守护线程

守护线程:

​ 在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

守护线程和用户线程本质的区别:

​ 唯一的不同之处就在于虚拟机的离开,==如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。== 因为没有了用户线程,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意一下几点:

  • thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。==你不能把正在运行的常规线程设置为守护线程。==

  • 在Daemon线程中产生的新线程也是Daemon的。

  • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

==下面我们举个例子,线程t2运行时,伴随t1线程,当t2执行完成后,t1也会终止而不是一致执行==

package com.itheima.thread.basic;

/***
 * @description 守护线程与用户线程
 */
public class Daemon {
    public static void main(String[] args) {
        // 守护线程
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(200);
                    System.out.println("t1 输出" + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.setDaemon(true);// 守护线程
        t1.start();
        // 用户线程,如果注释掉T2线程,则守护线程不执行
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2 输出" + i);
            }
        });
        t2.start();
    }
}

【3】线程优先级

​ ==线程的切换是由线程调度控制的,我们无法通过代码来干涉,但是我们通过提高线程的优先级来最大程度的改善线程获取时间片的几率,当然我们也只是尽力干扰,而不是绝对说优先级高的一定先执行==

线程的优先级被划分为10级,值分别为1-10【1最低,10最高】线程提供了3个常量来表示最低,最高,以及默认优先级:

Thread.MIN_PRIORITY 1

Thread.MAX_PRIORITY 10

Thread.NORM_PRIORITY 5

void setPriority(int priority):设置线程的优先级。

package com.itheima.thread.basic;

import java.util.concurrent.CountDownLatch;

/***
 * @description 线程优先级:注意观察谁最后输出,而不是说线程优先级越高就一定先执行
 * 而是,CPU会尽量优先执行优先级高的线程
 */
public class Priority {
    public static void main(String[] args) throws InterruptedException {
        // 线程t1
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                try {
                    System.out.println("t1 输出" + i);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        t1.setPriority(1);
        t1.start();
        // 线程t2
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                System.out.println("t2 输出" + i);
            }
        });
        t2.setPriority(10);
        t2.start();
    }
}

【4】线程运行状态

新建(NEW)

当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码

可运行(RUNNABLE)

​ 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权

运行(RUNNING)

​ 可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码

阻塞(BLOCKED) 阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种

  • 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
  • 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
  • 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

    死亡(DEAD)

有两个原因会导致线程死亡:

  • run方法正常退出而自然死亡
  • 一个未捕获的异常终止了run方法而使线程猝死。

为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.

image-20210119152054846

【5】线程常用API

【5.1】基础API
常用线程api方法
start() 启动线程
getID() 获取当前线程ID Thread-编号 该编号从0开始
getName() 获取当前线程名称
Stop() 停止线程,(已废弃)
getPriority(); 返回线程的优先级【1-10】
boolean isAlive() 测试线程是否处于活动状态
isDaemon(): 测试线程是否为守护线程【伴随用户线程执行】
isInterrupted(); 测试线程是否已经中断
Thread.currentThread() 获取当前线程对象
Thread.state getState() 获取线程的状态
常用线程构造**函数**
Thread() 分配一个新的 Thread 对象
Thread(String name) 分配一个新的 Thread对象,具有指定的 name正如其名。
Thread(Runable r) 分配一个新的 Thread对象
Thread(Runable r, String name) 分配一个新的 Thread对象
package com.itheima.thread.basic;

/**
 * @Description 线程API操作
 */
public class Operation {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(50);
//                    System.out.println("t1 输出" + i);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            System.out.println("执行完毕");
        });
        t1.setName("呼啦嘿线程1号");
        t1.start();
        Thread.sleep(30);
        long id = t1.getId();// 分配的线程ID
        String name = t1.getName();// 线程的名称
        int priority = t1.getPriority();// 优先级
        Thread.State state = t1.getState();// 线程的状态
        boolean alive = t1.isAlive(); //true 活动   false 运行完毕
        boolean daemon = t1.isDaemon();//true 守护线程  false 用户线程
        boolean interrupted = t1.isInterrupted();
        System.out.println("线程ID:"+id + " 线程名称:"+name + " 线程优先级:" + priority);
        System.out.println("线程状态:"+state + " 线程是否活动中:"+alive + " 线程是否守护线程:" + daemon);
        System.out.println("线程是否中断:"+interrupted );
    }
}
【5.2】sleep方法

Thread的静态方法sleep用于使当前线程进入阻塞状态:

static void sleep(long ms)

该方法会使当前线程进入阻塞状态指定毫秒,当阻塞指定毫秒后,当前线程会重写进入Runnable状态,等待分配时间片。

【5.3】yield方法

Thread的静态方法yield:

static void yield()

该方法用于使当前线程主动让片出当次CPU时间片回到Runnable状态,等待分配时间片.

【5.4】join方法

Thread的方法join: 线程调用了join方法,那么就要一直运行到该线程运行结束,才会运行其他进程. 这样可以控制线程执行顺序。

现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行

package com.itheima.thread.basic;

/**
 * @Description 线程join
 */
public class Join {

    // 现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
    public static void main(String[] args) {
        final Thread t1 = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println("t1 ========> " + i);
            }
        });
        final Thread t2 = new Thread(()->{
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println("t2 ========> " + i);
            }
        });
        Thread t3 = new Thread(()->{
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println("t3 ========> " + i);
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }

}
【5.5】stop 和 interrupt

​ 在开发中,经常会遇到需要停止一个正在运行的线程的场景,以前的做法是通过Thread.stop() 的方式来停止具体的线程,但是这个方法目前是被废弃掉的,不推荐使用。不推荐使用的原因如下:

该方式是通过立即抛出ThreadDeath异常来达到停止线程的目的,而且此异常抛出可能发生在程序的任何一个地方,包括catch、finally等语句块中。
由于抛出ThreadDeath异常,会导致该线程释放所持有的所有的锁,而且这种释放的时间点是不可控制的,可能会导致出现线程安全问题和数据不一致情况,比如在同步代码块中在执行数据更新操作时线程被突然停止。

因此,为了避免Thread.stop()带来的问题,推荐使用被称作为Interrupt(中断)的协作机制来停止一个正在运行的线程。在JVM中,每个线程都有一个与之关联的Boolean属性,被称之为中断状态,可以通过Thread.currentThread().isInterrupted()来获取当前线程的中断状态,初始值为false。中断状态仅仅是线程的一个属性,用以表明该线程是否被中断。

package com.itheima.thread.basic;

/**
 * @Description Interrupted终止
 */
public class Interrupted {

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (true){
                try {
                    boolean interrupted = Thread.currentThread().isInterrupted();
                    if (interrupted){
                        System.out.println("当前线程为中断状态");
                        break;
                    }else {
                        System.out.println("线程执行中....");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        try {
            Thread.sleep(2000);
            t1.interrupt(); //中断线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

1.2. 第二章 线程安全

1.2.1. 1、什么是线程安全?

​ 当多个线程访问更改共享变量时候,就会出现线程安全问题,导致脏数据。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

举例说明:假设售票系统有1000张票,A和B同时来买票,如果是线程不安全,那么可能售票系统可能出现1000-1去同时执行的情况,最终结果是A和B都买完后剩下999张票,而不是998张。

1571373658783

模拟售票案例

  • 我们来模拟电影院的售票窗口,实现多个窗口同时卖“速度与激情8”这场电影票,多个窗口一起卖这100张票。

  • 什么是线程安全问题。

    线程安全问题的原因:多个线程在操作同一个共享资源的时候,可能出现线程安全问题。

  • 通过加睡眠实现模拟多线程下访问共享变量的线程安全问题。

package com.itheima.thread.safe;

/**
 * @Description 售票演示
 */
public class Ticketing {
    // 总票数
    static  int ticket = 100;
    public static void main(String[] args) {
        Runnable runnable = () ->{
            // 循环买票
            while (true){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (ticket > 0) {
                    ticket--;
                    System.out.println(Thread.currentThread().getName()+
                            "卖了一张票,剩余:" + ticket);
                } else {
                    // 票没了
                    break;
                }
            }
        };

        // 创建3个线程
        Thread t1 = new Thread(runnable,"窗口1");
        Thread t2 = new Thread(runnable,"窗口2");
        Thread t3 = new Thread(runnable,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

测试结果:

image-20210119154336702

这也是超买的的现象,为什么会出现这种情况呢?要从jvm的内存模型设计开始。

1.2.2. 2、JMM内存模型

Java内存模型(即Java Memory Model,简称JMM)。 JMM本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据。而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。 线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

1568170938949

1568180340358

  • lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
  • unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
  • write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中

面试区分: 需要注意的是,JMM与Java内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式。 JMM是围绕原子性,有序性、可见性展开的。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义

1.2.3. 3、并发三大特性

正因为有了JMM内存模型,以及java语言的设计,所以在并发编程当中我们可能会经常遇到下面几种问题。 这几种问题 我们称为并发编程的三大特性:

原子性

原子性,即一个操作或多个操作,要么全部执行并且在执行的过程中不被打断,要么全部不执行。(提供了互斥访问,在同一时刻只有一个线程进行访问)

互斥锁:这种线程一旦得到锁,其他需要锁的线程就会阻塞等待锁的释放 (悲观锁)

  • Synchronized (互斥锁)
  • Lock(互斥锁)

乐观锁:CAS操作的,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就做判定,直到成功为止。

  • 原子类(CAS 乐观锁)

分布式情况下处理分布式所

  • Redis命令执行是原子性的【单线程】
  • zookeeper也可以实现,同一个节点不可能创建相同文件名的文件

可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题

已经将结果设置为true为什么还一直在运行呢? 原因:线程之间是不可见的,读取的是副本,没有及时读取到主内存结果。

使用: volatile 关键字即可保证变量的可见性

有序性

程序执行的顺序按照代码的先后顺序执行。 一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。如下:

int a = 10;   //语句1
a = a + 3;    //语句2
int r = 2;    //语句3
r = a*a;      //语句4   

则因为指令重排序(happen-before),他还可能执行顺序为 3-1-2-4,1-3-2-4
但绝不可能 2-1-4-3,因为这打破了依赖关系。

显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

package com.itheima.thread.safe;

/**
 * @Description 有序性
 */
public class Orderliness {

    public static void main(String[] args) throws InterruptedException {

        for(int i=0;i<500;i++){
            Status state = new Status();
            ThreadA threadA=new ThreadA(state);
            ThreadB threadB=new ThreadB(state);
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
        }

    }
    static class ThreadA extends Thread{
        private  Status state;
        ThreadA(Status state) {
            this.state = state;
        }
        public void run(){
            // 初始化状态类里面的2个字段
            state.a=1;
            state.b=true;
        }
    }
    static class ThreadB extends Thread{
        private  Status state;
        ThreadB(Status state) {
            this.state = state;
        }
        public void run(){
            if (state.b){
                System.out.println("TRUE-获得:"+state.a);
            }else {
                System.out.println("FALSE-获得:"+state.a);
            }
        }
    }
    static class Status {
        public  int a = 0;
        public  boolean b = false;
    }
}

1.2.4. 4、volatile

volatile 关键字的作用是变量在多个线程之间可见。并且能够保证所修饰变量的有序性:

1、 保证变量的可见性:当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中。当一个线程用到被volatile关键字修饰的值的时候,虚拟机会强制要求它从主内存中读取。

2、 屏蔽指令重排序:指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。非常经典的例子是在单例方法中同时对字段加入volatile,就是为了防止指令重排序。

package com.itheima.thread.safe;

/**
 * @Description 可见性
 */
public class Visibility {

    private static volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println("1号线程启动 执行while循环");
            long num =0;
            while (!flag){ //true
                num++;
            }
            System.out.println("num = " + num);
        }).start();
        Thread.sleep(1000);// 1秒后启动2号
        new Thread(()->{
            System.out.println("2号线程启动 更改变量 flag为true");
            setStop();
        }).start();
    }
    private static void setStop(){
        flag = true;
    }

}

1.2.5. 5、synchronized

关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile

tips:深入理解synchronized底层原理

【1】 使用方法

1.普通同步方法,锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
2.静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
3.同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
package com.itheima.thread.safe;

/**
 * @Description synchronized售票
 */
public class TicketingSyn {
    // 总票数
    static  int ticket = 100;

    public static void main(String[] args) {

        Object object = new Object();

        Runnable runnable = () ->{
            // 循环买票
            while (true){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object){
                    if (ticket > 0) {
                        ticket--;
                        System.out.println(Thread.currentThread().getName()+
                                "卖了一张票,剩余:" + ticket);
                    } else {
                        // 票没了
                        break;
                    }
                }
            }
        };

        // 创建3个线程
        Thread t1 = new Thread(runnable,"窗口1");
        Thread t2 = new Thread(runnable,"窗口2");
        Thread t3 = new Thread(runnable,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

【2】 多线程死锁

package com.itheima.thread.safe;

/**
 * @Description 死锁
 */
public class DeadLock {

    public static void main(String[] args) throws InterruptedException {
        Object o1 = new Object();//一号锁
        Object o2 = new Object();//二号锁
        new Thread(()->{
            synchronized (o1){//获取o1锁
                try {
                    Thread.sleep(1000);
                    synchronized (o2){//获取o2锁
                        System.out.println("线程1执行");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(()->{
            synchronized (o2){//获取o2锁
                try {
                    Thread.sleep(1000);
                    synchronized (o1){//获取o1锁
                        System.out.println("线程2执行");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

1.2.6. 6、Lock

在 jdk1.5 之后,并发包中新增了 Lock 接口(以及相关实现类)用来实现锁功能,Lock 接口提供了与 synchronized 关键字类似的同步功能,但需要在使用时手动获取锁和释放锁。

首先要说明的就是Lock,通过查看Lock的源码可知,Lock是一个接口:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    //试着加锁
    boolean tryLock();
    //试着加锁,指定等待时间时间
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    //解锁
    void unlock();
    Condition newCondition();
}

【1】lock基本使用

首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

package com.itheima.thread.safe;

import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Description lock基本使用
 */
public class LockExample {

    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方

    public static void main(String[] args)  {

        final LockExample lockExample = new LockExample();

        new Thread(){
            public void run() {
                lockExample.insert(Thread.currentThread());
            };
        }.start();

        new Thread(){
            public void run() {
                lockExample.insert(Thread.currentThread());
            };
        }.start();
    }

    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }

}

tryLock()

方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

  tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

package com.itheima.thread.safe;

import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Description TryLock演示
 */
public class TryLockExample {

    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方
    public static void main(String[] args)  {
        final TryLockExample tryLockExample = new TryLockExample();

        new Thread(){
            public void run() {
                tryLockExample.insert(Thread.currentThread());
            };
        }.start();

        new Thread(){
            public void run() {
                tryLockExample.insert(Thread.currentThread());
            };
        }.start();
    }

    public void insert(Thread thread) {
        if(lock.tryLock()) {
            try {
                System.out.println(thread.getName()+"得到了锁");
                for(int i=0;i<5;i++) {
                    Thread.sleep(1000);
                    arrayList.add(i);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                System.out.println(thread.getName()+"释放了锁");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName()+"获取锁失败");
        }
    }

}

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

public interface ReadWriteLock {

    Lock readLock();

    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。

ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。

package com.itheima.thread.safe;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Description 读写锁
 */
public class ReadWriteLockExample {

    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public static void main(String[] args)  {
        final ReadWriteLockExample readWriteLockExample = new ReadWriteLockExample();

        new Thread(){
            public void run() {
                readWriteLockExample.get(Thread.currentThread());
            };
        }.start();

        new Thread(){
            public void run() {
                readWriteLockExample.get(Thread.currentThread());
            };
        }.start();

    }

    public void get(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在进行读操作");
            }
            System.out.println(thread.getName()+"读操作完毕");
        } finally {
            rwl.readLock().unlock();
        }
    }
}

说明thread1和thread2在同时进行读操作。

  这样就大大提升了读操作的效率。

  不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。

  如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

【2】lock与synchronized区别

  • Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  • synchronized关键字可以直接修饰方法,也可以修饰代码块, 而lock只能修饰代码块
  • synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  • Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  • 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。(提供tryLock)

  • Lock可以提高多个线程进行读操作的效率。(提供读写锁)

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized.

1.2.7. 7、ThreadLocal

框架中:

  • dubbo的隐式传参 RpcCentext
  • 事务管理器

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

package com.itheima.thread.safe;

/**
 * @Description ThreadLocal演示
 */
public class ThreadLocalExample {

    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
            @Override
            protected String initialValue() {
                return "";
            }
        };
        //在主线程中调用ThreadLocal的set()方法保存一个变量
        threadLocal.set("第一杀");

        new Thread(){
            public void run() {
                //在第一个子线程中调用ThreadLocal的set()方法保存一个变量
                threadLocal.set("第二杀");
                System.out.println("ThreadLocal保存的第一个子线程的变量值:"+threadLocal.get());
            };
        }.start();
        new Thread(){
            public void run() {
                //在第二个子线程中调用ThreadLocal的set()方法保存一个变量
                threadLocal.set("第三杀");
                System.out.println("ThreadLocal保存的第二个子线程的变量值:"+threadLocal.get());
            };
        }.start();


        try {
            Thread.sleep(3000);
            //验证在第一个和第二个子线程对于ThreadLocal存储的变量值的改动没有影响到ThreadLocal存的主线程变量
            System.out.println("ThreadLocal保存的主线的变量值:"+threadLocal.get());
        } catch (Exception e) {
        }
    }

}

1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

2、线程间数据隔离,【你访问你的,我访问我的,不会相互干扰】

3、进行事务操作,用于存储线程事务信息。

4、数据库连接,Session会话管理。

1.2.8. 8、CAS乐观锁

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操作数:内存地址V【要修改的变量】,旧的预期值A【修改前读取的值】,要修改的新值B。

更新一个变量的时候,只有当变量的==预期值A和内存地址V当中的实际值相同时==,才会将内存地址V对应的值修改为B。

Java 中 java.util.concurrent.atomic包相关类就是 CAS 的实现

如: AtomicInteger int类型的原子类, 提供int的修改原子操作。

1.3. 第三章 线程池相关

1.3.1. 1、线程池简介

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

1.3.2. 2、线程池体系结构

java.util.concurrent.Executor 负责线程的使用和调度的根接口 |--ExecutorService 子接口: 线程池的主要接口 |--ThreadPoolExecutor 线程池的实现类 |--ScheduledExceutorService 子接口: 负责线程的调度 |--ScheduledThreadPoolExecutor : 继承ThreadPoolExecutor,实现了ScheduledExecutorService

【1】 Executor

​ 线程池就是线程的集合,线程池集中管理线程,以实现线程的重用,降低资源消耗,提高响应速度等。线程用于执行异步任务,单个的线程既是工作单元也是执行机制,从JDK1.5开始,为了把工作单元与执行机制分离开,Executor框架诞生了,它是一个用于统一创建任务与运行任务的接口。框架就是异步执行任务的线程池框架。

【2】 ThreadPoolExecutor

​ Executor框架的最核心实现是ThreadPoolExecutor类,通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池,那么它的底层原理是怎样实现的呢,下面就来介绍下ThreadPoolExecutor线程池的构造函数

public ThreadPoolExecutor(
        int corePoolSize,
        int maximumPoolSize,
        long keepAliveTime,
        TimeUnit unit,
        BlockingQueue<Runnable> workQueue,
        ThreadFactory threadFactory,
        RejectedExecutionHandler handler){
            .....
        }
  • corePoolSize:核心线程数定义了最小可以同时运行的线程数量
  • maximumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。【如果使用的无界队列,这个参数就没啥效果】
  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到核心线程数的话,新任务就会被存放在队列中
  • unit: keepAliveTime 的时间单位。
  • threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  • handler:饱和策略,当前同时运行的线程数量达到最大线程数量【maximumPoolSize】并且队列也已经被放满时,执行饱和策略。

线程池的三种队列

SynchronousQueue

SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
使用SynchronousQueue阻塞队列一般要求maximumPoolSizes为无界,避免线程拒绝执行操作。

LinkedBlockingQueue

LinkedBlockingQueue是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。

ArrayBlockingQueue

ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错。
package com.itheima.thread.pool;

import java.util.Scanner;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @Description 线程池用例
 */
public class ThreadPoolExecutorExample {

    public static void main(String[] args) {

        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,
                5,
                5,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10), // 有界队列
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());
        executor.allowCoreThreadTimeOut(true);
        for (int i = 0; i < 20; i++) {
            try {
                // 循环执行 20个任务
                executor.execute(new MyRunnable("第"+(i+1)+"号任务"));
            } catch (Throwable e) {
                System.out.println("丢弃任务: " + (i+1) );
            }
        }

        // 会立刻停止线程池,返回所有未执行的任务
//        List<Runnable> runnables = executor.shutdownNow();
//        System.out.println(runnables);
        // 临时线程  如果获取不到新任务 会在keepAliveTime时间后 销毁
        // 核心线程默认不销毁 但如果将 allowCoreThreadTimeOut设置为true 也会在keepAliveTime时间后 销毁
        // 如果调用 executor.shutDown(); 方法会在执行完任务队列的全部任务后关闭
        executor.shutdown();
        // 如果调用 executor.shutDownNow(); 不会在执行任务队列内的任务
//        executor.shutdownNow();
    }
    static class MyRunnable implements Runnable{
        private String name;
        public MyRunnable(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() +"==>" +name);
        }
    }

}

【3】 线程池原理剖析

img

提交一个任务到线程池中,线程池的处理流程如下:

1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。

2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

1.3.3. 3、Executors线程池工具类

Executors是线程池的工具类,提供了四种快捷创建线程池的方法:

  • newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

【1】 newCachedThreadPool

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

package com.itheima.thread.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Description 创建一个可缓存线程池,
 * 如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
 */
public class CachedThreadPoolExample {

    public static void main(String[] args) {
        // 创建线程池可缓存的线程池
        ExecutorService es = Executors.newCachedThreadPool();
        // 会创建出10个线程   分别执行任务
        for (int i = 0; i < 10; i++) {
            es.execute(()->{
                for (int j = 0; j < 10; j++) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + j);
                }
            });
        }
        es.shutdown();
    }
}

总结: 线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

image-20210120152254191

【2】 newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。示例代码如下:

package com.itheima.thread.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @Description 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
 */
public class FixedThreadPoolExample {

    public static void main(String[] args) {
        // 创建线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        // 会创建出10个线程   分别执行任务
        for (int i = 0; i < 10; i++) {
            es.execute(()->{
                try {
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int j = 0; j < 10; j++) {
                    System.out.println(Thread.currentThread().getName() + ":" + j);
                }
            });
        }
        es.shutdown();
    }
}

总结:定长线程池的大小并发最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()

image-20210120152547727

【3】 newSingleThreadExecutor

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

package com.itheima.thread.pool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

    public static void main(String[] args) {
        // 创建线程池
        ExecutorService es = Executors.newSingleThreadExecutor();
        // 会创建出10个线程   分别执行任务
        for (int i = 0; i < 10; i++) {
            es.execute(()->{
                try {
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int j = 0; j < 10; j++) {
                    System.out.println(Thread.currentThread().getName() + ":" + j);
                }
            });
        }
        es.shutdown();
    }
}

总结: 结果依次输出,相当于顺序执行各个任务。

image-20210120152854654

【4】 newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:

package com.itheima.thread.pool;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @Description 创建一个定长线程池,支持定时及周期性任务执行
 */
public class ScheduledThreadPoolExample {

    public static void main(String[] args) {
        ScheduledExecutorService newScheduledThreadPool = 
                Executors.newScheduledThreadPool(5);
        // 延迟执行任务  3s
//        newScheduledThreadPool.schedule(()->{
//            System.out.println("执行任务");
//        },5,TimeUnit.SECONDS);
        // 周期性执行任务
        newScheduledThreadPool.scheduleAtFixedRate(()->{
            System.out.println("执行任务");
        },0,3, TimeUnit.SECONDS);
    }

}

总结: 定时调度或周期性调度

image-20210120153212096

1.4. 第四章 线程间的通信

1.4.1. 1、 什么是多线程之间通信

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 于是我们引出了等待唤醒机制:(wait()、notify())

wait()、notify()、notifyAll()是三个定义在Object类里的方法,可以用来控制线程的状态。

这三个方法最终调用的都是jvm级的native方法。随着jvm运行平台的不同可能有些许差异。

如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态。

如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行。

如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。

==注意: wait()方法的调用必须放在synchronized方法或synchronized块中。==

1.4.2. 2、 wait与sleep区别

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

获取对象锁进入运行状态。

1.4.3. 3、线程通讯实现方式

首先,要短信线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析:

题目:有两个线程A、B,A线程向一个集合里面依次添加元素"abc"字符串,一共添加十次,当添加到第五次的时候,希望B线程能够收到A线程的通知,然后B线程执行相关的业务操作

【1】使用 volatile 关键字

基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式

package com.itheima.thread.message;

import java.util.ArrayList;
import java.util.List;

/**
 * @Description 使用Volatile实现线程通讯
 */
public class VolatileMessageThread {
    // 定义一个共享变量来实现通信,它需要是volatile修饰,否则线程不能及时感知
    static volatile boolean notice = false;

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // 实现线程A
        Thread threadA = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                if (list.size() == 5){
                    notice = true;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                list.add("abc");
                System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
            }
        });
        // 实现线程B
        Thread threadB = new Thread(() -> {
            while (true) {
                if (notice) {
                    System.out.println("线程B收到通知,开始执行自己的业务...");
                    break;
                }
            }
        });
        // 需要先启动线程B
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 再启动线程A
        threadA.start();
    }
}

image-20210120163510538

【2】使用Object类的wait() 和 notify() 方法

众所周知,Object类提供了线程间通信的方法:wait()notify()notifyaAl(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。

注意:==wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁==

package com.itheima.thread.message;

import java.util.ArrayList;
import java.util.List;

/**
 * @Description 使用Object类的wait() 和 notify() 方法
 */
public class WaitMessageThread {

    public static void main(String[] args) {
        // 定义一个锁对象
        Object lock = new Object();
        List<String> list = new ArrayList<>();
        // 实现线程A
        Thread threadA = new Thread(() -> {
            synchronized (lock) {
                for (int i = 1; i <= 10; i++) {
                    if (list.size() == 5){
                        lock.notify();// 唤醒B线程
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    list.add("abc");
                    System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
                }
            }
        });
        // 实现线程B
        Thread threadB = new Thread(() -> {
            while (true) {
                synchronized (lock) {
                    if (list.size() != 5) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("线程B收到通知,开始执行自己的业务...");
                    lock.notify();
                    break;
                }
            }
        });
        // 需要先启动线程B
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 再启动线程A
        threadA.start();
    }
}

image-20210120164755915

【3】使用CountDownLatch

jdk1.5之后在java.util.concurrent包下提供了很多并发编程相关的工具类,简化了我们的并发编程代码的书写,CountDownLatch基于AQS框架,相当于也是维护了一个线程间共享变量state

package com.itheima.thread.message;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * @Description 使用JUC工具类 CountDownLatch
 */
public class CountDownLatchMessageThread {

    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        List<String> list = new ArrayList<>();
        // 实现线程A
        Thread threadA = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                if (list.size() == 5){
                    countDownLatch.countDown();
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                list.add("abc");
                System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());

            }
        });
        // 实现线程B
        Thread threadB = new Thread(() -> {
            while (true) {
                if (list.size() != 5) {
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程B收到通知,开始执行自己的业务...");
                break;
            }
        });
        // 需要先启动线程B
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 再启动线程A
        threadA.start();
    }

}

image-20210120165109450

【4】基本LockSupport

LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。

package com.itheima.thread.message;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.LockSupport;

/**
 * @Description 基本LockSupport实现线程间的阻塞和唤醒
 */
public class LockSupportMessageThread {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // 实现线程B
        final Thread threadB = new Thread(() -> {
            if (list.size() != 5) {
                LockSupport.park();
            }
            System.out.println("线程B收到通知,开始执行自己的业务...");
        });
        // 实现线程A
        Thread threadA = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                if (list.size() == 5){
                    LockSupport.unpark(threadB);
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                list.add("abc");
                System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());

            }
        });
        threadA.start();
        threadB.start();
    }
}

image-20210120165431208

results matching ""

    No results matching ""