Java多线程

一、进程和线程

二者的区别:
进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
进程和线程都是单位名词,就例如 厘米、分钟、克。厘米是一个描述距离的单位,分钟是描述时间单位,克是一个重量的单位。进程是操作系统分配资源的单位,也就是说 操作系统为我们的程序分配了一个资源我们就称之为 一个进程。
线程是 任务调度和执行的单位,也就是说 程序 有一个执行的资源就是一个线程。
在这里插入图片描述
在这里插入图片描述
开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

程序运行到cpu中 是 进程,在进程中 执行代码是线程。

二、多线程

2.1 什么是线程

线程,有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。

2.2 什么是多线程

多线程,是指从软件或者硬件上实现多个线程并发执行的技术。进程中执行代码的是线程,一个线程执行代码 称之为单线程,多个线程同时执行代码称之为多线程
在这里插入图片描述
在这里插入图片描述
进程中肯定至少有一个线程,肯定只要要有一个线程去执行代码 要是一个都没有 代码就无法执行 程序就不能运行,称之为 主线程。
有个时候 我们需要再去开辟新的线程去执行其他的代码 ,此时新的线程我们称之为 分线程。
主线程+分线程 = 多线程
在这里插入图片描述

2.3 为什么需要多线程

煮一个鸡蛋三分钟,煮三个鸡蛋几分钟?
在这里插入图片描述
多线程是为了同步完成多项任务,不是为了提高运行效率,
而是为了提高资源使用效率来提高系统的效率。
在这里插入图片描述
线程是在同一时间需要完成多项任务的时候实现的。
最简单的比喻多线程就像火车的每一节车厢,而进程则是火车。车厢离开火车是无法跑动的,同理火车也不可能只有一节车厢。多线程的出现就是为了提高效率。同时它的出现也带来了一些问题。

2.4 java如何开启多线程

我们创建两个 0-10000输出的for循环 并观测其执行完所需的时间
在这里插入图片描述

2.4.1 多线程创建的第一种方式

创建一个类 继承Thread类 并重写run方法
在这里插入图片描述

2.4.2 实现Runnable

在这里插入图片描述

2.4.3 匿名内部类创建

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.4.5 实现Callable接口

在这里插入图片描述

2.4.6 线程函数

在这里插入图片描述

2.5 守护线程

在我们线程中 ,我们可以分为 主线程 和 分线程。
也有一种分法是用户线程和守护线程。
用户线程可以理解成就是用来写多线程业务代码的线程,
守护线程可以理解成守护用户线程 为用户线程提供服务的线程。
例如:垃圾回收GC线程Garbage Collection
我们曾经说过int a = 10;变量创建之后 在内存中就占据4个字节的内存空间。试想一下,如果这个变量a使用完成,此时相当于a没有价值。此时a依然占据着其4个字节的内存,把这个问题放大化 如果每一个变量是使用完成之后 都不去管它,此时我们的内存将存在严重的浪费,并且有内存不足。
所以 每种编程语言都有自己的垃圾(没有的内存)回收机制GC
C 语言:手动回收,需要程序员自己去判断某个变量在哪个阶段需要回收 然后调用free()函数.
C++/java:自动回收,不需要程序员去考虑变量的回收问题,因为有专门负责回收的程序。在我们程序执行的时候 有一个线程 负责专门去回收垃圾,这就是GC线程。JVM调优 垃圾回收算法。
Objective-C:面向对象的c语言。自动/手动。
在这里插入图片描述
Java分为两种线程:用户线程和守护线程
所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
守护线程和用户线程的没啥本质的区别:唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护
线程也就没有工作可做了,也就没有继续运行程序的必要了。
将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意一下几点:
(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3) 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
在这里插入图片描述
注意: 不是守护线程就是GC线程 而是 GC线程是一种守护线程。
注意我们线程的所有测试运行 全部使用main函数 不能使用测试函数。

2.6 线程的优先级

多线程在执行的时候,需要各自争抢时间片。每个线程能否抢到时间片完全取决于脸。线程的优先级其实就是认为控制时间片资源的分配。
 在操作系统中,线程可以划分优先级,优先级高的线程得到的CPU资源较多,也是CPU优先执行优先级较高的线程对象中的任务。
  设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
  设置优先级使用setPriority()方法。
  在这里插入图片描述
①线程开启的四种方式
②线程的常用函数
③守护线程和用户线程
④线程的优先级

三、同步和异步

同步:多个任务情况下,一个任务A执行结束,才可以执行另一个任务B。只存在一个线程。
异步:多个任务情况下,一个任务A正在执行,同时可以执行另一个任务B。任务B不用等待任务A结束才执行。存在多条线程。
在这里插入图片描述
在这里插入图片描述
并行/并发,串行。很多人大概会混淆这些概念。
在这里插入图片描述
在这里插入图片描述

并发和并行其实是异步线程实现的两种形式。并行其实是真正的异步,多核CUP可以同时开启多条线程供多个任务同时执行,互补干扰,如上图的并行,其实和异步图例一样。但是并发就不一样了,是一个伪异步。在单核CUP中只能有一条线程,但是又想执行多个任务。这个时候,只能在一条线程上不停的切换任务,比如任务A执行了20%,任务A停下里,线程让给任务B,任务执行了30%停下,再让任务A执行。这样我们用的时候,由于CUP处理速度快,你看起来好像是同时执行,其实不是的,同一时间只会执行单个任务。
那么串行是什么呢,它是同步线程的实现方式,就是任务A执行结束才能开始执行B,单个线程只能执行一个任务,就如单行道只能行驶一辆车。

四、线程安全

问题:多线程模拟并发售票
在这里插入图片描述
输出结果:
在这里插入图片描述
为什么会出现多个窗口售出同一张票?
如何解决?

4.1 同步代码块

当线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,而且无论在什么时候,只能有一个线程可以获得对同步监视器的锁定,当同步代码执行完成之后,这个线程会释放该线程的同步监视器的锁定。
任何线程在修改指定的参数资源之前,都需要对该参数资源加锁,在加锁期间其他的线程是不能对这个参数资源进行修改的,只有在该线程完成修改并且释放对该参数的锁定之后其他线程才有机会对该参数进行修改。这样做也就是符合了“加锁–>修改–>释放”的逻辑顺序。
在这里插入图片描述

4.2 同步方法

在这里插入图片描述
非静态同步方法使用的是 this锁相当于synchronized (this){}
静态同步方法使用类锁 相当于synchronized (Ticket.class) {}
1 进程和线程
2 多线程 主线程+分线程
3 开启线程四种方式
4 线程函数:id name 线程休眠 设置守护线程 设置线程优先级
5 同步 异步 串行 并行 并发
6 线程安全 重要
在这里插入图片描述

4.3 同步锁

在这里插入图片描述

4.4 死锁

所谓死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
在这里插入图片描述
在这里插入图片描述
线程通信:等待唤醒
生产者和消费者案例
在这里插入图片描述
生产者
在这里插入图片描述
消费者
在这里插入图片描述
中间件
在这里插入图片描述
调用
在这里插入图片描述
在这里插入图片描述
其实 我们这个地方的核心问题就是两个线程 工作的时候没有配合。
如果 生产者的线程 给 变量赋值完成之后 消费者的线程再把时间片抢走 此时就没有问题了。
所以 线程通信 就是多线程之前配合的一种方式。

五、线程通信

5.1、为什么要线程通信?

多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
当然如果我们没有使用线程通信来使用多线程共同操作同一份数据的话,虽然可以实现,但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失!
所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺。

5.2、什么是线程通信?

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。于是我们引出了等待唤醒机制:

(wait()、notify())
就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify);
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.3 线程通信解决生产消费问题

两个线程 各自打印
A : 1234567
B : ABCDEFG

需求 让线程 有个规律的打印
例如 1a2b3c4d
记住 wait 和 notify是Object的函数
面试题 String StringBuilder 和 StringBuffer的 区别
AJAX的异步。底层原理 v8引擎的消息轮询通知机制
Sleep 和 wait 的区别

六、线程状态

Java中线程的状态分为6种。
1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3.阻塞(BLOCKED):表示线程阻塞于锁。
4.等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5.超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。
在这里插入图片描述

七、原子性和可见性、有序性

事务的ACID:原子性 唯一性 隔离性 持久性
/------------*****************
原子 不能再分
********************************************/

7.1、原子性(Atomicity)

原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。如果一个操作时原子性的,那么多线程并发的情况下,就不会出现变量被修改的情况比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等 面试重灾区。(由Java内存模型来直接保证的原子性变量操作包括read、load、use、assign、store和write六个,大致可以认为基础数据类型的访问和读写是具备原子性的。如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock与unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐匿地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块—synchronized关键字,因此在synchronized块之间的操作也具备原子性。)
在这里插入图片描述
在这里插入图片描述

7.2、可见性(Visibility)

在这里插入图片描述
可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。
除了volatile之外,Java还有两个关键字能实现可见性,它们是synchronized。同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这条规则获得的,而final关键字的可见性是指:被final修饰的字段是构造器一旦初始化完成,并且构造器没有把“this”引用传递出去,那么在其它线程中就能看见final字段的值。(可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。
volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就这这个操作同样存在线程安全问题。)
在这里插入图片描述
Java内存模型概述
Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图
在这里插入图片描述

7.3、有序性(Ordering)

Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存中主内存同步延迟”现象。Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。
先行发生原则:
如果Java内存模型中所有的有序性都只靠volatile和synchronized来完成,那么有一些操作将会变得很啰嗦,但是我们在编写Java并发代码的时候并没有感觉到这一点,这是因为Java语言中有一个“先行发生”(Happen-Before)的原则。这个原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依赖。
先行发生原则是指Java内存模型中定义的两项操作之间的依序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包含了修改了内
存中共享变量的值、发送了消息、调用了方法等。它意味着什么呢?如下例:
//线程A中执行
i = 1;
//线程B中执行
j = i;
//线程C中执行
i = 2;
假设线程A中的操作”i=1“先行发生于线程B的操作”j=i“,那么我们就可以确定在线程B的操作执行后,变量j的值一定是等于1,结出这个结论的依据有两个,一是根据先行发生原
则,”i=1“的结果可以被观察到;二是线程C登场之前,线程A操作结束之后没有其它线程会修改变量i的值。现在再来考虑线程C,我们依然保持线程A和B之间的先行发生关系,而线程C出现在线程A和B操作之间,但是C与B没有先行发生关系,那么j的值可能是1,也可能是2,因为线程C对应变量i的影响可能会被线程B观察到,也可能观察不到,这时线程B就存在读取到过期数据的风险,不具备多线程的安全性。
下面是Java内存模型下一些”天然的“先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则
推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随意地重排序。
a.程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。
b.管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面“是指时间上的先后顺序。
c.volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。
d.线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
e.线程终于规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。
f.线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。
g.对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。
g.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生“呢?也是不成立的,一个典型的例子就是
指令重排序。所以时间上的先后顺序与先生发生原则之间基本没有什么关系,所以衡量并发安全问题一切必须以先行发生原则为准

八、线程池

8.1 newCachedThreadPool

在这里插入图片描述
特点
会根据需要创建新线程,如果线程池中有可复用的,会先服用可用的。
这个线程池典型的场景是改善 任务耗时短小的 异步任务。
使用execute可以复用可用的线程。如果没有可用线程,会创建新线程并添加到线程池里。
那些1分钟没有被使用的线程将被停止并从缓存里移除。

8.2 newFixedThreadPool(int threadCount)

在这里插入图片描述
可以复用指定数目的线程
如果请求的线程数目大于目前idle的,那么多余的请求将被等待,直到线程池中有可用的线程。
如果有任何线程执行过程中停止了,将会新建一个线程代替。
线程池中的线程一直存活,直到显式的使用shutdown关闭。

8.3 newScheduledThreadPool

在这里插入图片描述
特点
支持定时及循环任务执行
延迟定时,延迟的时间间隔是从调用开始开始计算的,并不受线程执行时间长短的影响

8.4 newSingleThreadScheduledExecutor

在这里插入图片描述
corePoolSize = 1 maxPoolSize = 1;
最多只开启一个线程,对于队列中的Runnable,挨个执行

九、锁

9.1 悲观锁和乐观锁

悲观锁(Pessimistic Lock):
每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。
Mysql加锁:利用数据库内部锁机制,管理事务mysql数据库内部提供两种常用的锁机制 共享锁(读锁)和排它锁(写锁)
锁必须在事务中添加,事务结束了锁就释放了允许一张数据表中数据记录添加多个共享锁,添加共享锁记录,对于其他事务可读不可写 可添加一个排他锁,防止其他事务修改
mysql添加 共享锁方式 select * from account lock in share mode;
mysql添加 排他锁方式 select * from account for update;
解决丢失更新的方法:事务在修改记录过程中,锁定记录,别的事务无法并发修改

乐观锁(Optimistic Lock):
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。
采用记录的版本字段,来判断记录是否修改过
timestamp 可以自动更新
create table product(
id int,
name varchar(20),
updatetime timestamp );
解决丢失更新的方法:在数据表中添加版本字段,每次修改过记录后,版本字段都会更新,如果读取的版本字段与修改时不一致,证明被修改过

适用场景:
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。

9.2 读写锁

在这里插入图片描述
读写锁特点:
1 多个读者可以同时进行读
2 写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
3 写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

互斥锁特点:一次只能一个线程拥有互斥锁,其他线程只有等待

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值