Hefery 的个人网站

Hefery's Personal Website

Contact:hefery@126.com
  menu
73 文章
0 浏览
3 当前访客
ღゝ◡╹)ノ❤️

Java基础—并发编程

并发概念

线程相关概念

同步和异步

用户线程与内核的交互方式

  • 同步:用户线程发起 IO 请求后需要等待或者轮询内核,IO 操作完成后才能继续执行
  • 异步:用户线程发起 IO 请求后仍继续执行,当内核 IO 操作完成后会通知用户线程,或者调用用户线程注册的回调函数

阻塞和非阻塞

用户线程调用内核IO操作的方式
阻塞:IO操作需要彻底完成后才返回到用户空间
非阻塞:IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成

并发和并行

  • 并发:多任务在同一时间段发生(同⼀时间段,多个任务都在执行,单位时间内不⼀定同时执行)
  • 并行:多任务在同一时刻发生(单位时间内,多个任务同时执行)
  • 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题

并发 = 两个队列和一台咖啡机
并行 = 两个队列和两台咖啡机
串行 = 一个队列和一台咖啡机

进程和线程

  • 进程:内存中运行的应用程序,系统资源(CPU、内存等)分配和调度的基本单位
    程序的一次执行过程,是系统运行程序的基本单位(系统运行程序即是进程从创建、运行到消亡的过程)
    每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程
  • 线程:进程中的一个执行单元,程序执行的基本单位,负责当前进程中程序的执行(CPU上真正运行的是线程)

一个程序运行后至少有一个进程,一个进程中可以包含多个线程,多个线程可共享数据

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

  • 资源开销:
    每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
  • 包含关系:
    如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程
  • 内存分配:
    同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
  • 影响关系:
    一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮
  • 执行过程:
    每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

多进程和多线程的区别

多线程:一个进程中有多个线程,称为多线程,程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务(腾讯电脑管家:体检、杀毒、清理、加速、分析可以同时运行)

Java 线程数过多会造成什么异常?
  • 线程的生命周期开销非常高
  • 消耗过多的 CPU
  • 资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销
  • 降低稳定性JVM
  • 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出 OOM
线程状态
线程状态

public static enum Thread.State

  • new
    初始状态,线程被构建,但是还没有调用 start() 方法
  • runnable
    运行状态,正在 JVM 中执行的线程状态。Java线程将OS中的就绪和运行两种状态笼统地称作“运行中”
  • blocked
    阻塞状态,表示线程阻塞于锁
  • waiting
    等待状态,线程进入等待状态,当前线程需要等待其他线程做出一些特定动作(通知或中断)
  • timed_waiting
    超时等待状态,该状态不同于waiting,它是可以在指定的时间自行返回的
  • terminated
    终止状态,表示当前线程已经执行完毕
状态转换

image.png

线程通信

多线程间通讯:多个线程在操作同一资源,但是操作的动作不同

为什么通信:用于线程同步。多线程并发执行的时候, 如果需要指定线程等待或者唤醒指定线程, 那么就需要通信,比如生产者消费者的问题。生产一个消费一个,生产的时候需要负责消费的进程等待,生产一个后完成后需要唤醒负责消费的线程,同时让自己处于等待,消费的时候负责消费的线程被唤醒,消费完生产的产品后又将等待的生产线程唤醒,然后使自己线程处于等待。这样来回通信,以达到生产一个消费一个的目的

怎么通信:

  • 中断—等待唤醒机制:wait/notify,在同步代码块中, 使用锁对象的wait()方法可以让当前线程等待, 直到有其他线程唤醒为止。使用锁对象的 notify() 方法可以唤醒一个等待的线程,或者 notifyAll() 唤醒所有等待的线程。多线程间通信用 sleep() 很难实现,睡眠时间很难把握
  • 内存共享:volatile

进程通信

  • 管道(pipe):一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系
  • 有名管道 (namedpipe):半双工的通信方式,但是它允许无亲缘关系进程间的通信
  • 信号量(semaphore):计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段
  • 消息队列(messagequeue):由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点
  • 信号 (sinal):比较复杂的通信方式,用于通知接收进程某个事件已经发生
  • 共享内存(shared memory):映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信
  • 套接字(socket):进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信
为什么wait(), notify()和notifyAll()必须在同步方法或同步块中被调用?

当一个线程需要调用对象的 wait() 的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()。同样的,当一个线程需要调用对象的 notify() 时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用

为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类?

Java中任何对象都可以作为锁,JAVA提供的锁是对象级的而不是线程级的。并且 wait(),notify() 等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在 Object 类中。wait(), notify()和 notifyAll() 这些方法在同步代码块中调用。有的人会说,既然是线程放弃对象锁,那也可以把 wait() 定义在Thread 类里面啊,新定义的线程继承于 Thread 类,也不需要重新定义 wait() 的实现。然而,这样做有一个非常大的问题,一个线程完全可以持有很多锁,一个线程放弃锁的时候,到底要放弃哪个锁?当然了,这种设计并不是不能实现,只是管理起来更加复杂

对volatile关键字的了解

当前的Java内存模型下,线程可以把变量保存本地内存中,⽽不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,⽽另外⼀个线程还继续使⽤它在寄存器中的变量值的拷⻉,造成数据的不⼀致。要解决这个问题,就需要把变量声明为volatile,主要作⽤就是保证变量的可⻅性然后还有⼀个作⽤是防⽌指令重排序。一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
  • 禁止进行指令重排序:volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住
线程优先级

每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级

Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级

setPriority(int newPriority):更改线程优先级
int getPriority():返回线程的优先级

线程的调度

线程调度器选择优先级最高的线程运行,但是,如果发生以下情况,就会终止线程的运行:

1.线程体中调用了 yield 方法让出了对 CPU 的占用权利
2.线程体中调用了 sleep 方法使线程进入睡眠状态
3.线程由于 IO 操作受到阻塞
4.另外一个更高优先级线程出现
5.在支持时间片的系统中,该线程的时间片用完

守护线程

守护线程和用户线程:

  • 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
  • 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。守护线程是 JVM 中非守护线程的 “佣人”
    一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作

注意:

  • setDaemon(true) 必须在 start() 方法前执行,否则会抛出 IllegalThreadStateException 异常
  • 在守护线程中产生的新线程也是守护线程
  • 守护线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护线程中的 finally 语句块可能无法被执行

并发内存模型

Java多线程内存模型

Java内存模型

Java内存模型:Java Memory Model,JMM,定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步

srchttppic1.zhimg.comv2c2ccd7456c624f065d0723c09e9f5c0cb.jpgreferhttppic1.zhimg.jpg

JMM原子操作

  • read(读取):从主内存读取数据
  • load(载入):将主内存读取到的数据写入工作内存
  • use(使用):从工作内存读取数据来计算
  • assign(赋值):将计算好的值重新赋值到工作内存中
  • store(存储):将工作内存数据写入主内存
  • write(写入):将store过去的变量值赋值给主内存中的变量
  • lock(锁定):将主内存变量加锁,标识为线程独占状态
  • unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量

并发编程

并发编程概念

  • 目的:提高系统资源的利用率;提高并发能力和CPU利用率;提高程序的执行效率;
  • 优点:
    • 充分利用多核CPU计算能力:通过并发编程可将多核CPU的计算能力发挥到极致,性能得到提升
    • 方便进行业务拆分,提升系统并发能力和性能:在特殊的业务场景下,先天适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制
    • 大大提高系统整体的并发能力以及性能:面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分
  • 缺点:容易引发:内存泄漏、上下文切换、线程安全、死锁等问题
  • 并发编程三要素
    • 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败
    • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
    • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

创建多线程

Java 使用java.lang.Thread类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。每个线程的作用是完成一定任务,实际上就是执行一段程序流。Java使用线程执行体来代表这段程序流

创建多线程方式:

  • 继承 Thread 类, 重写 run() 方法:不建议使用, 因为 Java 是单继承,继承了 Thread 就没办法继承其它类
  • 实现 Runnable 接口,重写 run() 方法:实现接口,比 Thread 类更加灵活,没有单继承的限制
  • 实现 Callable 接口,重写 call() 方法,可以获取线程执行结果,可以接收任何类型的返回值

继承Thread类和实现Runnable接口区别

  • 实现Runnable接口避免了单继承的局限性
  • 继承Thread类线程代码存放在Thread子类的run方法中,实现Runnable接口线程代码存放在接口的子类的run方法中

Thread

//1.创建 Thread 子类
public class MyThread extends Thread{
    //2.在 Thread 子类中重写 run 方法,设置线程任务
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("run " + i);
        }
    }
}
//3.创建 Thread 子类对象
MyThread mt = new MyThread();
//4.调用 Thread 类的 start() 方法,开启新线程,执行 run 方法
mt.start();
public class InnerThread {
    public static void main(String[] args) {
        // 继承 Thread 类,创建线程
        new Thread(){
          // 重写 run 方法
          @Override
          public void run(){
              for (int i = 1; i <= 20; i++) {
                  System.out.println("Thread: " + Thread.currentThread().getName() + "..." + i);
              }
          }
        }.start();
}
数据类型方法说明
StringgetName()返回该线程的名称
static ThreadcurrentThread()返回对当前正在执行的线程对象的引用
voidsetName(String name)改变线程名称,使之与参数 name 相同
static voidsleep(long millis)在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

start()和run()区别

调用 start() 启动线程;run() 存储线程要运行的代码即线程体

  • start() 用来启动线程,实现了多线程运行。这时无需等待 run() 方法体代码执行完毕而直接继续执行下面的代码。通过调用 Thread 类的 start() 来启动一个线程,这时此线程处于就绪(可运行)状态,但是并没有运行,一旦得到 CPU 时间片(执行权),就开始执行线程体 run() 方法,它包含了要执行的这个线程的内容,run() 运行结束,此线程随即终止
  • run() 只是 Thread 类的一个普通方法,如果直接调用 run() ,程序中依然只有主线程这一个线程,
    其程序执行路径还是只有一条,还是要等待 run() 方法体执行完毕后才可继续执行下面的代码,
    这样就没有达到多线程的目的

sleep()和wait()区别

两者都可以暂停线程的执行。wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行

  1. sleep() 来自 Thread 类,wait() 来自 Object 类
  2. sleep() 是线程类 Thread 的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用 sleep() 不会释放对象锁;wait() 是 Object 类的方法,对此对象调用 wait() 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出 notify() 方法后本线程才进入对象锁定池准备获得对象锁进入运行状态
  3. sleep() 释放资源不释放锁,wait() 释放资源释放锁
  4. sleep() 通常被用于暂停执行,wait() 通常被用于线程间交互
  5. 使用范围:wait、notify、notifyAll只能在同步控制方法或同步控制块里面使用,sleep 可在任何地方使用

sleep()和yield()区别

  • sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会
    yield()方法只会给相同优先级或更高优先级的线程以运行的机会
  • 线程执行 sleep() 后转入阻塞blocked状态;线程执行 yield() 后转入就绪ready状态
  • sleep() 声明抛出 InterruptedException;yield() 没有声明任何异常
  • sleep() 比 yield() 具有更好的可移植性,通常不建议使用 yield() 来控制并发线程的执行

如何停止一个正在运行的线程?

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
  • 使用 stop() 方法强行终止,不推荐这个方法,因为 stop 和 suspend 及 resume 一样都是过期作废的方法
  • 使用 interrupt() 方法中断线程

Runnable

public class InnerThread {
    public static void main(String[] args) {
        // 实现 Runnable 接口,创建线程
        Runnable r = new Runnable(){
            // 重写 run 方法
            @Override
            public void run() {
                for (int i = 1; i <= 20; i++) {
                    System.out.println("Runnable: " + Thread.currentThread().getName() + "..."  + i);
                }
            }
        };
        new Thread(r).start();
    }
}
public class InnerThread {
    public static void main(String[] args) {
        //继承 Thread 类,创建线程
        new Thread(){
          //重写 run 方法
          @Override
          public void run(){
              for (int i = 1; i <= 20; i++) {
                  System.out.println("Thread: " + Thread.currentThread().getName() + "..." + i);
              }
          }
        }.start();
}

线程类的构造方法、静态块是被哪个线程调用

线程类的构造方法、静态块被 new 这个线程类所在的线程所调用,run 方法里面的代码才是被线程自身所调用

举个例子,假设 Thread2 中 new 了 Thread1,main 函数中 new 了 Thread2,那么:
1.Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是 Thread2 自己调用的
2.Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是 Thread1 自己调用的

线程池

线程池相关概念

好处:

  • 降低资源消耗:减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
  • 提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行
  • 提高线程的可管理性:可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)

线程池创建方式

  • Executors.FixedThreadPool、CachedThreadPool、ScheduledThreadPool、SingleThreadExecutor
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {
    public static void main(String[] args) {
        //1.使用线程池的工厂类 Executors 里边的静态方法 newFixedThreadPool(int nThreads) 生产一个指定线程数量的线程池
        ExecutorService es = Executors.newFixedThreadPool(2);

        //3.调用 ExecutorService 中的 submit(Runnable task) 方法,传递线程任务(实现类),开启线程,执行 run() 方法
        es.submit(new RunnableDemo());
        es.submit(new RunnableDemo());
        es.submit(new RunnableDemo());

        /*
            pool-1-thread-1创建了新的线程
            pool-1-thread-2创建了新的线程
            pool-1-thread-1创建了新的线程
         */

        //销毁线程池(不建议使用)
        es.shutdown();
    }
}

//2.创建一个类,实现 Runnable 接口,重写 run() 方法,布置线程任务
public class RunnableDemo implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "创建了新的线程");
    }
}

《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而通过 ThreadPoolExecutor 的方式

Executors 返回线程池对象的弊端如下:

  • FixedThreadPool和SingleThreadExecutor:允许请求的队列长度为 Integer.MAX_VALUE,可能堆积⼤量的请求,导致OOM
  • CachedThreadPool和ScheduledThreadPool:允许创建的线程数量为 Integer.MAX_VALUE,可能会创建⼤量线程,导致OOM

线程池状态

  • running:正在运行
  • shutdown:shutdown() 不再接收新任务,直到队列中的任务执行完毕
  • stop:shutdownNow() 不再接收新任务,丢弃队列中的任务,中断正在执行的任务
  • tidying:队列为空,线程池执行任务为空
  • terminted:terminted()

线程池核心参数

  • corePoolSize:线程池核心线程个数
  • maximunPoolSize:线程池最大线程个数
  • workQueue:任务队列,用于保存等待执行的任务
    • 无界队列:
    • 有界队列:ArrayBlockingQueue、LinkedBlockingDeque
    • 同步移交队列:SynchronousQueue
  • ThreadFactory:创建线程的工厂
  • RejectedExecutionHandler:拒绝策略,当任务队列井且线程池个数达到 maximunPoolsize 后采取策略
    • AbortPolicy:终止策略(默认)
    • DiscardPolicy:抛弃策略
    • DiscardOldestPolicy:抛弃旧任务策略
    • CallerRunsPolicy:调用者运行策略
  • keeyAliveTime:最大存活时间,当线程池的线程数量比核心线程数量要多,且闲置
  • TimeUnit:线程存活时间的时间单位

Java线程池种类

  • FixedThreadPool:固定线程数量的线程池。该线程池中的线程数量始终不变。可控制线程最⼤并发数,超出的线程会在队列中等待
    ExecutorService fPool = Executors.newFixedThreadPool(3);

    public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(
              nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
              new LinkedBlockingQueue<Runnable>()
            );
    }
    
  • CachedThreadPool:可缓存线程池程,不会对线程池大小做限制,如果线程池长度超过处理需要,可灵活回收空闲线程,线程池大小完全依赖于操作系统(或JVM)能够创建的最大线程大小
    ExecutorService cPool = Executors.newCachedThreadPool();

    public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(
    		0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, 
    		new SynchronousQueue<Runnable>()
            );
    }
    
  • ScheduledThreadPool:周期性执行任务的线程池,大小无限,支持定时以及周期性执行任务的需求
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool();

    public ScheduledThreadPoolExecutor(int corePoolSize) {
            super(
              corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue()
            );
    }
    
  • SingleThreadExecutor:只有⼀个线程的线程池,保证所有任务的执行顺序按照任务的提交顺序执行
    ExecutorService sPool = Executors.newSingleThreadExecutor();

    public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService(
                new ThreadPoolExecutor(
    		1, 1, 0L, TimeUnit.MILLISECONDS, 
    		new LinkedBlockingQueue<Runnable>())
            );
    }
    

线程池工作流程

srchttpprocesson.comchartimageid591133e2e4b0065b564344d4.pngreferhttpprocesson.jpg

  • 线程池创建时,里面没有线程。任务队列是作为参数传进来的。就算队列有任务,线程池也不会马上执行
  • 当调用execute() 方法添加一个任务时,线程池会做如下判断:
    • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
    • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
    • 如果队列满了,且正在运行的线程数量小于maximumPoolSize,那么还是要创建线程运行这个任务
    • 如果队列满了,且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常
  • 当一个线程完成任务时,它会从队列中取下一个任务来执行
  • 当线程无事可做,超过 keepAliveTime 时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小

自定义的线程池

  • 线程池:开启 | 初始化 | 关闭
  • 线程:获取 | 归还
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {
	public static void main(String[] args) {
	    // corePoolSize : 核⼼线程数为 5
		final int CORE_POOL_SIZE = 5;
		// maximumPoolSize :最⼤线程数 10
		final int MAX_POOL_SIZE = 10;
		// workQueue :任务队列为ArrayBlockingQueue,容量为 100
		final int QUEUE_CAPACITY = 100;
		final Long KEEP_ALIVE_TIME = 1L;

		// 通过ThreadPoolExecutor构造函数⾃定义参数创建
		ThreadPoolExecutor executor = new ThreadPoolExecutor(CORE_POOL_SIZE,
				MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,  // unit : 等待时间的单位为 TimeUnit.SECONDS
				new ArrayBlockingQueue<>(QUEUE_CAPACITY),
				new ThreadPoolExecutor.CallerRunsPolicy());  // handler :饱和策略为 CallerRunsPolicy 
		for (int i = 0; i < 10; i++) {
			// 创建WorkerThread对象(WorkerThread类实现了Runnable 接⼝)
			Runnable worker = new MyRunnable("" + i);
			// 执⾏Runnable
			executor.execute(worker);
		}

		// 终⽌线程池
		executor.shutdown();
		while (!executor.isTerminated()) {
		}
		System.out.println("Finished all threads");
	}

}

public class MyRunnable implements Runnable {
	private String command;

	public MyRunnable(String s) {
		this.command = s;
	}

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + " Start.Time = " + new Date());
		processCommand();
		System.out.println(Thread.currentThread().getName() + " End.Time = " + new Date());
	}

	private void processCommand() {
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	@Override
	public String toString() {
		return this.command;
	}

}
execute()和submit()区别
  • execute():用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
  • submit():用于提交需要返回值的任务。线程池会返回⼀个 Future 类型的对象,通过 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get() 来获取返回值,get() 会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit) 则会阻塞当前线程⼀段时间后立即返回,这时候有可能任务没有执行完
获取Java线程返回值
  • Callable接口类似于Runnable,但 Runnable 不会返回结果,并且无法抛出返回结果的异常。Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到拿到异步执行任务的返回值。Callable 可以认为是带有回调的 Runnable。Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable 用于产生结果,Future 用于获取

主线程调用子线程,要等待子线程返回的结果来进行下一步动作的业务

并发问题

多线程并发问题

上下文切换

上下文切换:CPU处理器给每个线程分配CPU时间片(Time Slice),线程在分配获得的时间片内执行任务,当一个线程被暂停或剥夺CPU的使用权,另外的线程开始或者继续运行的这个过程

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换

  • 切出:线程被剥夺处理器的使用权而被暂停运行
  • 切入:线程被选中占用处理器开始或者继续运行
  • 上下文:在这种切出切入的过程中,操作系统需要保存和恢复相应的进度信息

上下文切换发生的时机:

  • 线程被分配的时间片用完
  • 使用 synchronized 或 lock

线程安全

线程安全:方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全

当程序的多条语句在操作线程共享数据时,由于线程的随机性导致一个线程对多条语句,执行了一部分还没执行完,另一个线程抢夺到cpu执行权参与进来执行,此时就导致共享数据发生错误。解决:对多条操作共享数据的语句进行同步;一个线程在执行过程中其他线程不可以参与进来

Java程序如何保证多线程运行安全?

具体问题具体分析:首先判断有没有线程安全问题,再想方案

  • 涉及到操作的原子性,可以考虑 atomic 包的原子类
  • 涉及到对线程的控制,可以考虑线程工具类 CountDownLatch/Semaphore 等
  • 涉及到集合类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
  • 使用自动锁 synchronized
  • 使用手动锁 Lock

线程死锁

死锁:两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的线程称为死锁进线程

形成死锁的必要条件:

  • 互斥条件:共享资源只能被一个线程占用一互斥锁
  • 请求与保持:线程因请求被占用资源而发生阻塞时,对已获得的资源保持不放—等待对方释放资源
  • 不可抢占:线程已获得的资源在末使用完前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源—无法释放对方资源
  • 循环等待:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞—两个线程互相等待

如何避免线程死锁:我们只要破坏产生死锁的四个条件中的其中一个就可以了

  1. 破坏互斥条件 :这个条件没法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)
  2. 破坏请求与保持条件 :⼀次性申请所有的资源
  3. 破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可主动释放它占有的资源
  4. 破坏循环等待条件 :靠按序申请资源来预防。按顺序申请资源,释放资源则反序释放。破坏循环等待条件
查看死锁

使用jps查看在运行的 Java 程序及其 PID

使用jstack PID查看是否有 Found N deadlock,也会打印出堆栈信息

乐观锁和悲观锁区别
  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。Java里面的同步原语 synchronized 实现是悲观锁
  • 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。在Java中的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现

并发问题解决方案

线程同步与互斥

当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系:

  • 线程同步:当一个线程对共享的数据进行操作时,应使之成为一个”原子操作“,即在没有完成相关操作之前,不允许其他线程打断它,否则,就会破坏数据的完整性,必然会得到错误的处理结果
  • 线程互斥:对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步
实现线程同步方法
  • 同步代码方法:sychronized 关键字修饰的方法
    public synchronized void method(){
        // 可能产生线程安全问题的代码
    }
    
  • 同步代码块:sychronized 关键字修饰的代码块,表示只对这个区块的资源实行互斥访问
    synchronized(同步锁){
        // 需要同步的代码
    }
    
  • volatile:保证了不同线程对该变量操作的内存可见性;禁止指令重排序
  • 使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义

同步锁:

  • 锁对象可以是任意类型
  • 多个线程对象,要使用同一把锁
  • 在任何时候,至多只允许一个进程拥有同步锁进入代码块
同步方法和同步块哪个更好?

同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象),同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。原则:同步的范围越小越好

对synchronized的了解

synchronized关键字解决的是多个线程之间访问资源的同步性,可以保证被它修饰的⽅法或者代码块在任意时刻只能有⼀个线程执行

  • 修饰实例方法:作用于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的对象锁
  • 修饰静态方法:相当于给当前类加锁,会作用于类的所有对象实例。修饰静态方法以及同步代码块的synchronized锁的是类,线程想要执行对应同步代码,需要获得类锁。因为静态成员不属于任何⼀个实例对象,是类成员。所以如果⼀个线程 A 调用⼀个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法是允许的,不会发生互斥现象, 因为访问静态synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
  • 修饰代码块:指定加锁对象,对给定对象加锁,进⼊同步代码库前要获得给定对象的锁
Lock和Synchronized区别

原理:

相同点:Lock 能完成 synchronized 所实现的所有功能

不同点:Lock 有比 synchronized 更精确的线程语义和更好的性能

用法:

  • synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁
  • Lock可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到
synchronized和ReenterantLock区别
  • 两者都是可重⼊锁:⾃⼰可以再次获取⾃⼰的内部锁。⽐如⼀个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重⼊的话,就会造成死锁。同⼀个线程每次获取锁,锁的计数器都⾃增1,所以要等到锁的计数器下降为0时才能释放锁
  • synchronized依赖于JVM⽽ReentrantLock依赖于 API:synchronized依赖于JVM实现的,前⾯我们也讲到了虚拟机团队在JDK1.6为synchronized关键字进⾏了很多优化,但是这些优化都是在虚拟机层⾯实现的,并没有直接暴露。ReentrantLock是JDK 层⾯实现的(也就是 API 层⾯,需要lock()和unlock()配合try/finally 语句块来完成)
  • ReentrantLock ⽐ synchronized 增加了⼀些⾼级功能:等待可中断;可实现公平锁;可实现选择性通知(锁可以绑定多个条件)
对volatile关键字的了解

当前的Java内存模型下,线程可以把变量保存本地内存中,⽽不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,⽽另外⼀个线程还继续使⽤它在寄存器中的变量值的拷⻉,造成数据的不⼀致。要解决这个问题,就需要把变量声明为volatile,主要作⽤就是保证变量的可⻅性然后还有⼀个作⽤是防⽌指令重排序。一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的
  • 禁止进行指令重排序:volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住
synchronized和volatile区别
  • 场景:volatile解决变量在多个线程之间的可见性;synchronized解决多线程间访问资源的同步性
  • 使用:volatile只能用于变量;synchronized可以修饰方法以及代码块
  • 性能:volatile(线程同步的轻量级实现)性能比synchronized关键字要好
  • 阻塞::多线程访问 volatile 不会发⽣阻塞,而 synchronized 可能会阻塞
  • 并发:volatile能保证数据的可见性,但不能保证数据的原子性; synchronized都能保证

原理:

Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒⼀个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高

public class RunnableImpl implements Runnable{
    // 定义多线程共享的票源
    private int ticket = 100;
    // 定义锁对象
    Object obj = new Object();
    // 线程任务:卖票
    @Override
    public void run() {
        while (true){
            payTicket();
        }
    }

    public synchronized void payTicket(){
        // 判断票是否存在
        if (ticket > 0){
            // 提高出错概率
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 票存在,卖票
            System.out.println(Thread.currentThread().getName() + "--> 正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();

        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);

        t0.start();
        t1.start();
        t2.start();
        /*
            出现线程安全问题:
                Thread-0--> 正在卖第92张票
                Thread-1--> 正在卖第92张票
                Thread-2--> 正在卖第92张票
         */
    }
}

ThreadLocal

ThreadLocal:不是一个Thread,而是Thread的局部变量

作用:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度

  • 线程并发:多线程并发的场景
  • 传递数据:通过 ThreadLocal 在同一线程,不同组件中传递公共变量
  • 线程隔离:每个线程的变量都是独立的,不会互相影响

应用场景:

在并发编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象

构造函数:ThreadLocal()
类型方法说明
voidset(T value)设置当前线程绑定的局部变量
voidremove()移除当前线程绑定的局部变量
Tget()获取当前线程绑定的局部变量
/**
 * @Description:
 *  并发问题:
 *      线程+1 --> 线程+2的数据
 */
public class ThreadLocalTest {

    ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    private String content;

    private String getContent() {
        // return content;
        return threadLocal.get();
    }

    private void setContent(String content) {
        // this.content = content;
        threadLocal.set(content);
    }

    public static void main(String[] args) {
        ThreadLocalTest threadLocalTest = new ThreadLocalTest();

        for (int i = 1; i <= 20; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() { 
                    // 存一个变量,过一会取出这个变量
                    //synchronized (ThreadLocalTest.class) {
                        threadLocalTest.setContent(Thread.currentThread().getName() + "的数据");
                        System.out.println(Thread.currentThread().getName() + " --> " + threadLocalTest.getContent());
                    //}
                }
            });
            thread.setName("线程+" + i);
            thread.start();
        }
    }

}
ThreadLocal与synchronized区别

ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同

  • synchronized:侧重多个线程之间访问资源的同步。同步机制采用“以时间换空间的方式,只提供了一份变量,让不同的线程排队访问
  • ThreadLocal:侧重多线程中让每个线程之间的数据相互隔离。ThreadLocal采用“以空间换时间的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
ThreadLocal源码
  • 每个Thread线程内部都有一个ThreadLocalMap
  • ThreadLocalMap里面存储ThreadLocal对象(key)和线程的变量副本(value)
  • Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰

设计好处:

  • 每个 Map 存储的 Entry 数量变少
  • 当 Thread 销毁时,ThreadLocalMap 也会随之销毁,减少内存的使用
set()
  • 首先获取当前线程,并根据当前线程获取一个 Map
  • 如果获取的 Map 不为空,则将参数设置到 Map 中(当前 ThreadLocal 的引用作为 key)
  • 如果 Map 为空,则给该线程创建 Map,并设置初始值
public class ThreadLocal<T> {

    /**
     * 设置当前线程对应的 ThreadLocal 的值
     * @param value 将要保存在当前线程对应的ThreadLocal的值
     */
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的 ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用 map.set 设置此实体 entry
            map.set(this, value);
        else
            // 当前线程 Thread 不存在 ThreadLocalMap 对象,则调用 createMap 进行 ThreadLocalMap 对象的初始化
            // 并将 t(当前线程)和 value(t对应的值)作为第一个 entry 存放至 ThreadLocalMap 中
            createMap(t, value);
    }

    /**
     * 获取当前线程 Thread 对应维护的 ThreadLocalMap
     * @param  t 当前线程
     * @return   对应维护的 ThreadLocalMap
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
        // ThreadLocal.ThreadLocalMap threadLocals = null;
    }

    /**
     * 创建当前线程Thread对应维护的ThreadLocalMap
     * @param t          当前线程
     * @param firstValue 存放到 map 中第一个 entry 的值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

}
get()
  • A. 首先获取当前线程,根据当前线程获取一个Map
  • B. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entrye,否则转到D
  • C. 如果e不为null,则返回e.value,否则转到D
  • D. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值

public class ThreadLocal<T> {

    /**
     * 返回当前线程中保存 ThreadLocal 的值
     * 如果当前线程没有此 ThreadLocal 变量,则它会通过调用 initialvalue 方法进行初始化值
     * @return 返回当前线程对应此 ThreadLocal 的值
     */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的 ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        // 如果此 map 存在
        if (map != null) {
            // 以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 对 e 进行判空
            if (e != null) {
                // 获取存储实体 e 对应的 value 值,即为我们想要的当前线程对应此 ThreadLocal 的值
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 初始化:有两种情况有执行当前代码
        // 1.map 不存在,表示此线程没有维护的 ThreadLocalMap 对象
        // 2.map 存在,但是没有与当前 ThreadLocal 关联的 entry
        return setInitialValue();
    } 

    /**
     * 获取当前线程 Thread 对应维护的 ThreadLocalMap
     * @param  t 当前线程
     * @return   对应维护的 ThreadLocalMap
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
        // ThreadLocal.ThreadLocalMap threadLocals = null;
    }

    /**
     * 初始化
     * @return 初始化后的值
     */
    private T setInitialValue() {
        // 调用initialvalue获取初始化的值,此方法可以被子类重写,如果不重写默认返回nu11
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的 ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        // 判断 map 是否存在
        if (map != null)
            // 存在则调用 map.set 设置此实体 entry
            map.set(this, value);
        else
            // 当前线程 Thread 不存在 ThreadLocalMa p对象,则调用 createMap 进行 ThreadLocalMap 对象的初始化
            // 并将 t(当前线程)和 value(t对应的值)作为第一个 entry 存放至 ThreadLocalMap 中
            createMap(t, value);
        // 返回设置的值 value
        return value;
    }

    /**
     * 创建当前线程Thread对应维护的ThreadLocalMap
     * @param t          当前线程
     * @param firstValue 存放到 map 中第一个 entry 的值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

}
remove()

首先获取当前线程,并根据当前线程获取一个 Map。如果获取的 Map 不为空,则移除当前 ThreadLocal 对象对应的 entry

public class ThreadLocal<T> {

    /**
     * 删除当前线程中保存的ThreadLocal对应的实体entry
     */
    public void remove() {
        // 获取当前线程对象中维护的Thr eadLocalMap对象
        ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
        if (m != null)
            // 存在则调用 map.remove,以当前 ThreadLocal 为 key 删除对应的实体 entry
            m.remove(this);
    }

    /**
     * 获取当前线程 Thread 对应维护的 ThreadLocalMap
     * @param  t 当前线程
     * @return   对应维护的 ThreadLocalMap
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
        // ThreadLocal.ThreadLocalMap threadLocals = null;
    }

    /**
     * Remove the entry for key.
     * @param key [description]
     */
    private void remove(ThreadLocal<?> key) {
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }

}

并发项目

两线程交替打印

多线程模拟100分钱随机分给20个人,每个人最少分配到2分钱

实现所有线程一起等待某个事件发生,当事件发生时,这些线程一起执行,有什么好的办法?

栅栏-CyclicBarrier

ThreadLocal

作用:用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据

应用场景:


标题:Java基础—并发编程
作者:Hefery
地址:http://hefery.icu/articles/2022/02/28/1645981305860.html