1.认识线程(Thread)
1.1概念
1.线程是什么?
线程就是一个“执行流”,每个线程之间都可以按照顺序执行自己的代码,多个线程之间可以执行多份代码。
2.为啥要有线程
首先,“并发编程”成为“刚需”,最后虽然多进程编程也可以实现并发编程,但进程没有线程轻量。
最后,线程虽然比进程要轻量,但是人们还不满足,于是就有了线程池(ThreadPool)和“协程”(Coroutine)
3.进程和线程的区别?
进程是包含线程的,每个进程至少有一个线程
进程和进程之间不共用一个内存资源,同一个进程里的线程,共用一个内存空间
进程是系统分配资源的最小单位,线程是系统调度的最小单位。
一个进程挂了一般不会影响其他进程,但一个线程挂了会影响同一个进程里的其他线程,从而使这个进程挂了。
4.java的线程和操作系统线程的关系
线程是操作操作系统中的概念,操作系统内核实现的线程这样的机制,并对外提供一些API接口供用户使用(例如Linux的Pthread库)
java标志库中Thread类就是对操作系统提供的API的抽象和封装
1.2第一个多线程程序
在这里我说下用jconsole命令观察线程,这个东西可以观察你的线程
jconsole的位置是在你java jdk bin目下就可以找到。

1.3创建线程
方法1.继承Thread类(重写Thread的run方法里面就是你线程所要干的事情)
继承Thread类来创建一个线程

方法2.实现Runnable接口
1.类去实现Runnable接口重写里面的run方法
2.在我们在main方法里调用这个线程通过实例化Thread把实现Runnable接口的这个类的对象做为Thread构造函数的参数,然后Thread的引用去.start(这个创建线程和上面那个有点不一样注意)

方法3.其他变形
1.使用匿名内部类的方法创建线程

2.匿名内部类创建Runnable子类对象

3.lambda表达式创建Runnable子类对象

2.Thread类及其常见方法
2.1Thread类的常见构造方法


2.2Thread的几个常见属性

ID 获取线程ID每一个线程ID都不一样
名称 获取线程名称
状态 获取线程状态
优先级 获取线程优先级
是否为后台线程 前台线程执行完毕后直接结束,不管后台线程执行没执行完
是否存活 也就是看是否run完
是否被中断
2.3启动一个线程 start
之前我们已经看到了如何通过覆写run⽅法创建⼀个线程对象,但线程对象被创建出来并不意味着线 程就开始运⾏了。
调用start方法才能真正在操作系统底层创建出一个线程
2.4中断一个线程
常见的有以下俩种方法
1.通过共享的标记来进行沟通



当我们一start的时候,代码会一直每1秒打印一个别管我,我忙着转账,持续10秒(因为休眠了10秒),结束后,isQuit变成了true,那么你手中设置的标志位就变了,就不会进入到while循环执行打印啊!险些误了大事的代码
2.通过Interrupt()方法来通知(中断对象关联的线程,如果线程处于阻塞状态则以异常方式通知,否则设置标志位)
使⽤Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替⾃定义标志位.
这俩个的去别的Thread.interrupted()中断后清除标志位,Thread.currentThread.isInterrupted中断后不清除标志位。(我们一般用会用Thread.currentThread.isInterrupted)
2.5等待一个线程join
哪个线程调用.join就是它先工作另一个线程等待(等待线程结束)
join(long millis)等待线程结束,最多等millis毫秒
join(long millis , int nanos)等待线程结束,可以更高精度


2.6获取当前线程的引用
currentThread()获取当前线程的引用

2.7休眠当前程序
就是调用sleep()方法,注意:sleep方法要和try catch或者throw一起使用不然代码报错
sleep(int millis)
sleep(int millis, int nanos)精度更高
3.线程状态
3.1观察线程的所有状态
线程的状态是一个枚举类型Thread.State


3.2线程状态图(方便大家理解):

4.多线程带来的风险-线程安全(重点)
4.1观察线程不安全

运行结果:于我们所预期的不一样,且每次运行都是一个新的结果

针对上述现象我们看下面的相关知识
4.2线程安全的概念
如果多线程环境下代码运⾏的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是 线程安全的。
4.3线程不安全的原因
ONE.线程调度是随机的
TWO.内存可见性
造成内存可见性问题原因:编译器优化,缓存机制(就是我们下面那个图里面说的),指令从排序
THREE.指令重排序
线程调度是随机的, 这是线程安全问题的罪魁祸⾸, 随机调度使⼀个程序在多线程环境下,执⾏顺序存在很多的变数. 程序猿必须保证在任意执⾏顺序下,代码都能正常⼯作.
修改共享数据
多个线程修改同⼀个变量,我们这里引入操作原子性和一个图
原子性:简单理解,就是你的操作是最小操作不涉及其他微观操作
例子:我们上面的count++就不是一个原子性的操作。它可以分三步操作,1.从主内存读取count,2.在工作内存上++,3.加完后把值返回到主内存里

工作内存=cpu寄存器内存+缓存
线程之间的共享变量存在主内存(MainMemory).
•每⼀个线程都有⾃⼰的"⼯作内存"(WorkingMemory).
//有个上述原子性概念和这个图的概念,我们就可以开始解释,上面那个代码为什么和我们预期结果不一样:因为count++不是原子性的,操作分为3步,又因为我们线程执行是抢占式的,你第一个线程在工作内存加1返回到了主内存,第二个线程可能在那期间在工作内存加1,导致返回给主内存的还是1,导致看似加了俩次实际还是1这种情况,依次类推,最后结果绝不可能是10000
指令重排序
概念:比如我们有三步操作,由于我们jvm,会有一个优化概念,有的时候就会把你的指令打乱
所有因为上面的概念,我们就容易出现线程安全性问题,预期结果,与输出结果不一样
4.4解决线程步安全问题
我们可以用synchronize(加锁),或者volatile来解决
synchronize特性:1.互斥性2.可重入性(线程可以重复加锁)
synchronize:可以保证原子性(把不是原子性的操作看作一个整体),还可以保证内存可见性,当一个线程进入同步块时,它会锁定线程共享资源,其他线程必须等带到这个线程解锁后才能加锁使用。这确保了所有线程都能看到最新状态。
volatile:可以确保变量的修改对所有线程立即可见。(不能保证原子性)
针对我们上面那个预期值不一样代码我们对其进行下面的修改:

注意:synchronize(对象或者是this){},注意锁的格式。当多个线程针对同一个对象进行加锁的时候,不会出现线程安全问题。其他情况都会出现.

代码在写⼊volatile修饰的变量的时候,
•改变线程⼯作内存中volatile变量副本的值
•将改变后的副本的值从⼯作内存刷新到主内存
代码在读取volatile修饰的变量的时候,
•从主内存中读取volatile变量的最新值到线程的⼯作内存中
•从⼯作内存中读取volatile变量的副本
代码示例:

注意:因为我t1线程flag读到的是t1的工作内存(从主内存过来的副本)上的flag,t2线程修改的是主内存的,所有t1线程就不会出了那个循环。
解决方法:在flag这个变量前面加上volatile即可
5.wait和notify
由于线程之间是抢占式执⾏的,因此线程之间执⾏的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执⾏先后顺序.
所有我们引入了wait和notify
完成这个协调⼯作,主要涉及到三个⽅法
•wait()/wait(long timeout):让当前线程进⼊等待状态.•notify()/notifyAll():唤醒在当前对象上等待的线程.
注意:wait,notify,notifyAll都是Object类的⽅法.
5.1wait()方法
wait做的事情:
•使当前执⾏代码的线程进⾏等待.(把线程放到等待队列中)
•释放当前的锁
•满⾜⼀定条件时被唤醒,重新尝试获取这个锁.
wait要搭配synchronized来使⽤.脱离synchronized使⽤wait会直接抛出异常.
wait结束等待的条件:
•其他线程调⽤该对象的notify⽅法.
•wait等待时间超时(wait⽅法提供⼀个带有timeout参数的版本,来指定等待时间).
•其他线程调⽤该等待线程的interrupted⽅法,导致wait抛出InterruptedException异常.
代码实例:

说明:这样这个线程就可以一直等待下去了,但我们肯定还要利用notify取唤醒它
5.3notify()方法
notify⽅法是唤醒等待的线程.
•⽅法notify()也要在同步⽅法或同步块中调⽤,该⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
•⽅法notify()也要在同步⽅法或同步块中调⽤,该⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
完,也就是退出同步代码块之后才会释放对象锁。
notify⽅法只是唤醒某⼀个等待线程.使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程.
5.4wait和sleep的区别:(重点理解)
从类上来看:
1.wait是Object的方法,sleep是Thread类的静态方法(静态方法不依赖对象Thread.sleep())
从使用方法上来看:
2.wait必须和synchronized配合使用,而sleep不需要
6.多线程案例
6.1单例模式:
(是校招最常考的设计模式之一)
概念:单例模式能保证某个类在程序中只存在唯⼀⼀份实例,⽽不会创建出多个实例.
实现单例模式又分为俩个模式:
1.饿汉模式(在类一加载就实例了)

2.懒汉模式(类在加载的时候不创建,第一次使用的时候创建实例)
懒汉模式本身就是线程不安全的

注意:1.当我们多线程取调用懒汉模式,要注意线程安全,1.因为线程调度为抢占式的,会有线程安全问题,所有我们要在第一次实例的时候加锁,2.因为我们只需要第一次去加锁即可,且如果多次加锁会耗费cpu资源,所有我们要判断是不是第一次加锁,3.同时我们要确保内存可见性问题要加volatile。
7.阻塞队列
阻塞队列是什么?
阻塞队列是⼀种特殊的队列.也遵守"先进先出"的原则.
阻塞队列能是⼀种线程安全的数据结构,并且具有以下特性:
•当队列满的时候,继续⼊队列就会阻塞,直到有其他线程从队列中取⾛元素.
•当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插⼊元素.
阻塞队列的⼀个典型应⽤场景就是"⽣产者消费者模型".这是⼀种⾮常典型的开发模型.
生产者消费者模型?
⽣产者消费者模式就是通过⼀个容器来解决⽣产者和消费者的强耦合问题。
阻塞队列作用?
1.平衡生产者消费者之间生产消费能力(削峰填谷)
可以这么理解,当生产者生产太多导致队列满了的时候,就进行阻塞当消费者消费后再生产,
队列里的东西被消费完了之后,进行阻塞当生产者入队列的时候在消费。
2.降低生产者消费者之间的耦关系
标准库里面的阻塞队列
•BlockingQueue是⼀个接.真正实现的类是LinkedBlockingQueue.
•put⽅法⽤于阻塞式的⼊队列,take⽤于阻塞式的出队列.
•BlockingQueue也有offer,poll,peek等⽅法,但是这些⽅法不带有阻塞特性.
代码实例:

我们自己来实现一个阻塞队列
代码实例:



8.定时器
概念:定时器也是软件开发中的⼀个重要组件.类似于⼀个"闹钟".达到⼀个设定的时间之后,就执⾏某个指定好的代码.
标志库中的定时器
标准库中提供了⼀个Timer类.Timer类的核⼼⽅法为schedule
schedule包含两个参数.第⼀个参数指定即将要执⾏的任务代码,第⼆个参数指定多⻓时间之后执⾏(单位为毫秒).

实现一个定时器






9.线程池
线程池最⼤的好处就是减少每次启动、销毁线程的损耗。
标准库中的线程池
•使⽤Executors.newFixedThreadPool(10)能创建出固定包含10个线程的线程池.•返回值类型为ExecutorService
•通过ExecutorService.submit可以注册⼀个任务到线程池中.

Executors创建线程池的⼏种⽅式
•newFixedThreadPool:创建固定线程数的线程池
•newCachedThreadPool:创建线程数⽬动态增⻓的线程池.
•newSingleThreadExecutor:创建只包含单个线程的线程池.
•newScheduledThreadPool:设定延迟时间后执⾏命令,或者定期执⾏命令.是进阶版的Timer.Executors本质上是ThreadPoolExecutor类的封装.
ThreadPoolExecutor提供了更多的可选参数,可以进⼀步细化线程池⾏为的设定.

corePoolSize:正式员⼯的数量.正式员⼯,⼀旦录⽤,永不辞退)
•maximumPoolSize:正式员⼯+临时⼯的数⽬.(临时⼯:⼀段时间不⼲活,就被辞退).•keepAliveTime:临时⼯允许的空闲时间.
•unit:keepaliveTime的时间单位,是秒,分钟,还是其他值.
•workQueue:传递任务的阻塞队列
threadFactory:创建线程的⼯⼚,参与具体的创建线程⼯作.通过不同线程⼯⼚创建出的线程相当于对⼀些属性进⾏了不同的初始化设置.
RejectedExecutionHandler:拒绝策略,如果任务量超出公司的负荷了接下来怎么处理.◦AbortPolicy():超过负荷,直接抛出异常.
◦CallerRunsPolicy():调⽤者负责处理多出来的任务.◦DiscardOldestPolicy():丢弃队列中最⽼的任务.
◦DiscardPolicy():丢弃新来的任务.
---CHACHA

4836

被折叠的 条评论
为什么被折叠?



