为啥要有进程?因为 *** 作系统支持多任务执行,程序员也需要“并发编程”
通过多进程,是完全可以实现并发编程的,但是是有点问题的!!!
如果需要频繁的创建进程/销毁进程,这个事情的成本是比较高的如果需要频繁的调度进程,这个事情的成本也是比较高的
那要如何去解决这样的问题呢???
为啥线程要比进程更轻量???
谈谈进程和线程的区别和联系?
1.进程包含线程.一个进程里可以有一个线程,也可以有多个线程.
2.进程和线程都是为了处理并发编程这样的场景.
但是进程有问题,频繁创建和释放的时候效率低.相比之下,线程更轻量,创建和释放效率更高.(为啥更轻量?少了申请释放 资源的过程)
3. *** 作系统创建进程,要给进程分配资源.进程是 *** 作系统分配资源的基本单位.
*** 作系统创建的线程,是要在CPU上调度执行.线程是 *** 作系统调度执行的基本单位
4.进程具有独立性.每个进程有各自的虚拟地址空间.一个进程挂了,不会影响到其他进程.
同一个进程中的多个线程,共用同一个内存空间,一个线程挂了,可能影响到其他线程的,甚至导致整个进程崩溃
什么是多线程???
在java标准库中,就提供了一个Thread类,来表示/ *** 作线程Thread这个类也可以视为一组java标准库中提供的API
实例的Thread其实是和 *** 作系统中的线程是一 一对应的关系
创建一个Thread实例 就是创建了一个线程
*** 作系统提供了一组有关线程的API(C语言风格),java对这组API进一步封装,就成了Thread类
class Mythread2 extends Thread{
@Override
public void run() {
while (true){
System.out.println("Thread");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Dome2 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Mythread2();
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
run方法里面执行的代码就是线程执行的内容
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Dome3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new MyRunnable());
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
2.1.3、匿名内部类
两种方法创建的线程(都采用匿名内部类)
public class Dome4 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
System.out.println("hello Thread");
}
};
t.start();
}
}
public class Dome5 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello Thread");
}
});
t.start();
}
}
2.1.4、使用lambda 表达式
public class Dome6 {
public static void main(String[] args) {
Thread t = new Thread(() ->{
System.out.println("hello Thread");
});
t.start();
}
}
三、串行执行和多线程执行代码的花费的时间
import java.util.concurrent.atomic.AtomicLong;
public class Dome7 {
private static final long cont = 100_0000_0000L;
//串行执行代码的时间
public static void chuanxing(){
//记录开始的时间
long beg = System.currentTimeMillis();//获取当前时间的时间戳
//分别计算a和b加到cont的时间
long a = 0;
for (long i = 0; i < cont; i++) {
a++;
}
long b = 0;
for (long i = 0; i < cont; i++) {
b++;
}
long end = System.currentTimeMillis();//程序执行完的时间戳
System.out.println("程序执行完的时间是多少:" + (end - beg) + "ms");
}
//多线程执行花费的时间
public static void duociancheng() throws InterruptedException {
//计算开始的时间戳
long beg = System.currentTimeMillis();
//建立一个线程计算a自增
Thread t1 = new Thread( () -> {
long a = 0;
for (long i = 0; i < cont; i++) {
a++;
}
});
t1.start();
//建立一个线程计算b自增
Thread t2 = new Thread( () -> {
long b = 0;
for (long i = 0; i < cont; i++) {
b++;
}
});
t2.start();
//
t1.join();
t2.join();
long end = System.currentTimeMillis();
System.out.println("程序执行完的时间是多少:" + (end - beg) + "ms");
}
public static void main(String[] args) throws InterruptedException {
//串行执行代码的时间
chuanxing();
//多线程执行花费的时间
duociancheng();
}
}
如果当你执行的任务本身是很小的,你在用多线程编程是得不偿失的,因为创建线程就要花费时间,所以多线程编程是要考虑场景的,多线程特别适合于那种CPU密集型的程序,程序要进行大量的计算,使用多线程就可以更充分的利用CPU的多核资源
Thread常见的构造方法
中断线程,让一个线程停下来
线程停下来的关键,是要让线程对应的run方法执行完
(还有一个特殊的;是main这个线程.对于main来说,得是 main方法执行完,线程就完了)
多个线程之间,调度顺序是不确定的
线程之间的执行是按照调度器来安排的.这个过程可以视为是"无序,随机"这样不太好.有些时候,我们需要能够控制线程之间的顺序.
线程等待,就是其中一种,控制线程执行顺序的手段
此处的线程等待,主要是控制线程结束的先后顺序
调用join的时候,哪个线程调用的join,哪个线程就会阻塞等待~~得到对应的线程执行完毕为止(对应线程的run执行完)
所谓的休眠到底是在干啥?
进程,PCB+双向链表~~这个说法是针对只有一个线程的进程,是如此的,如果是一个进程有多个线程,此时每个线程都有一个PCB
一个进程对应的就是一组PCB了
PCB上有一个字段tgroupld,这个id其实就相当于进程的id.同一个进程中的若干个线程的tgroupld是相同的!!!
process control block r进程控制块?和线程有啥关系?其实Linux内核不区分进程和线程.
进程线程是程序猿写应用程序代码,搞出来的词.实际上Linux内核只认PCB !!!
在内核里Linux把线程称为轻量级进程
进程有状态:就绪/阻塞
这是一个进程里只有一个线程的时候,线程的状态就是进程的状态
一个进程里有很多线程,此时说的状态就是线程的状态
在Linux中,一个进程对应一组PCB , PCB是和线程对应的
上面说的“就绪”和“阻塞”都是针对系统层面上的线程的状态(PCB)在Java中 Thread类中,对于线程的状态,又进一步的细化了~
什么是线程安全????
*** 作系统在调度线程的时候,是随机的~~(抢占式执行)
正式因为这样的随机性,就可能导致程序的执行出现一些bug~
如果因为这样的调度随机性引入了bug,就认为代码是线程不安全的!
如果是因为这样的调度随机性,也没有带来bug,就认为代码是线程安全的!!
通过一个典型的案例来看一下:让cont自增100000次
为什么会导致这种情况发生???
通过加锁的方式就可以避免发生这种问题
给方法直接加上synchronized关键字.此时进入方法,就会自动加锁.
离开方法,就会自动解锁~
当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待.(此时对应的线程,就处在BLOCKED状态)
阻塞会一直持续到,占用锁的线程把锁释放为止~~
线程安全:多线程并发执行某个代码,没有逻辑上的错误,就是“线程安全”。
线程不安全:多线程并发执行某个代码,产生逻辑上的错误,就是“线程不安全”。
1、线程是抢占式执行,线程间的调度充满随机性.
线程之间的调度完全由内核负责,用户代码中感知不到,也无法控制。
线程之间谁先执行,谁后执行,谁执行到哪里从CPU上下来,这样的过程都是用户无法控制也无法感知的。
2、针对变量的 *** 作不是原子的~~
自增 *** 作不是原子的
有时也把这个现象叫做同步互斥,表示 *** 作是互相排斥的。
一条 java 语句不一定是原子的,也不一定只是一条指令
比如刚才我们看到的 cont++,其实是由三步 *** 作组成的:
1. 从内存把数据读到 CPU
2. 进行数据更新
3. 把数据写回到 CPU
3.三个线程尝试修改同一个变量
a) 如果是一个线程修改一个变量,线程安全。
b) 如果多个线程读取同一个变量,线程安全。但是如果有多个线程,一个读取的变量,一个修改变量,则最后线程还是不安全的。(注意体会)
c) 如果多个线程修改不同的变量,线程安全。
4.内存可见性导致的线程安全问题。
一个具体的栗子:针对同一个变量~
一个线程(t1)进行读 *** 作(循环进行很多次),一个线程(t2)进行修改 *** 作(合适的时候执行一次)
当t1在循环读变量的时候(读内存的速度相对于读寄存器是很慢的,因为现代的编译器是会优化代码的(这是在保证逻辑不变的情况下,但是在单线程就不会出现线程不安全问题,多线程中如果进行优化可能就会翻车)),因此t1就会在寄存器中读取这个变量,这是t2在进行修改 *** 作的时候就会将这个变量改变,这是t1在去读变量的时候就会出错(原来的变量已经改变了),当发生这种情况的时候就会很危险
5.指令重排序(Java的编译器在编译代码时,会针对指令进行优化,调整指令的先后顺序,保证原有逻辑不变的情况下,提高程序的运行效率)
现代的编译器优化能力都非常强,优化后的代码,会比优化前快很多。
但是多线程的优化是不容易实现的,可能会导致一些问题。
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
synchronized不光能保证指令的原子性,同时也能保证内存可见性)被synchronized包裹起来的代码,编译器就不敢轻易的做出上述的优化代码,相当于手动禁用了编译器的优化
理解 “阻塞等待”.
针对每一把锁, *** 作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由 *** 作系统唤醒一个新的线程, 再来获取到这个锁.
注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠 *** 作系统来 “唤醒”. 这也就是 *** 作系统线程调度的一部分工作.假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
synchronized的底层是使用 *** 作系统的mutex lock实现的.
2) 刷新内存
synchronized 的工作过程:
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
1)直接修饰普通方法:锁的 SynchronizedDemo 对象
2) 修饰静态方法: 锁的 SynchronizedDemo 类的对象
- 修饰代码块: 明确指定锁哪个对象.
锁当前对象
锁类对象
我们重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产生竞争
什么情况下会导致死锁
所谓死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进
1.一个线程,一把锁
2.两个线程,两把锁
3.n个线程m把锁
如何解决这种死锁问题:
1.互斥使用~一个锁被一个线程占用了之后,其他线程占用不了(锁的本质,保证原子性)
2.不可抢占一个锁被一个线程占用了之后,其他的线程不能把这个锁给抢走.
3.请求和保持当一个线程占据了多把锁之后,除非显式的释放锁,否则这些锁始终都是被该线程持有的
4.环路等待,等待关系成环了~~A等B,B等C,C又等A
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder
但是还有一些是线程安全的. 使用了一些锁机制来控制.
Vector (不推荐使用)
HashTable (不推荐使用)
ConcurrentHashMap
StringBuffer
volatile关键字能保证内存可见性也就相当于禁止编译器的优化了,但是不能保证原子性
之所以存在 “内存可见性”问题,是因为我们的计算机的硬件所决定的
提到内存可见性,在Java中,经常往往会提到另外一词:JMM– Java Memory Model (Java内存模型/Java存储模型)JMM就是把上诉将的硬件结构,在Java中用专门的术语又重新抽象封装了一遍。
简单来说:就是换汤不换药,还是上述的硬件结构,只不过名称发生了改变。
代码示例
在这个代码中
创建两个线程 t1 和 t2
t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
预期当用户输入非 0 的值的时候, t1 线程结束.
如果不用volatile修饰flag,t1 读的是自己工作内存中的内容.
当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化.
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile
保证的是内存可见性.
代码示例
6.4.3、volatile 和 synchronized 的区别这个是最初的演示线程安全的代码. 给 increase 方法去掉 synchronized 给 count 加上 volatile 关键字.
这两个本来就没有什么联系。
只是在 Java 中 恰好都是关键字。
其他语言,加锁不是关键字,C++中的锁就是一个单独的普通类而已
拓展:
synchronized 不能 无脑使用,凡事都是有代价的。 代价就是一旦使用synchronized
很容易使线程阻塞,一旦线程阻塞(放弃CPU),下次回到CPU的时间就不可控了。【可能就是一辈子,因为 可能有几个线程在卡bug】
如果调度不回来,自然对应的任务执行时间也就是拖慢了。 用一句话来说 synchronized:一旦使用了
synchronized,这个代码大概率就和“高性能无缘了。 开发效率高,固然好,但有些时候还是需要考虑执行效率的。
volatile 就不会引起线程阻塞。
6.5、wait 和 notify :等待 和 通知wait和notify 为了处理线程调度随机性的问题。
还是那句话,多线程的调度,因为它的随机性,就导致代码谁先执行,谁后执行,存在太多的变数。前面讲到的 join 也是一种控制顺序的方式,但是join更倾向于控制线程结束。因此
join是有使用的局限性。就不像wait和notify用起来更加合适。wait和notify都是Object 对象的方法。 调用wait方法的线程,就会陷入阻塞,阻塞到有其它线程通过notify 来通知。
wait 做的事情:
使当前执行代码的线程进行等待. (把线程放到等待队列中) 释放当前的锁 满足一定条件时被唤醒, 重新尝试获取这个锁. wait 要搭配
synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
wait 结束等待的条件:
其他线程调用该对象的 notify 方法. wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本,
来指定等待时间). 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException
异常.
目前,我们只是使用了 wait 方法, 接下来 我们来实践一下: notify 和 wait 的组合使用 wait
使线程处于阻塞状态,notify 来唤醒 调用wait 方法陷入睡眠的线程
private static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
//进行 wait
synchronized (locker){
System.out.println("wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait 之后");
}
});
t1.start();
// 为了大家能更清楚的观察到现象,我这里使用 sleep 延迟3s
Thread.sleep(3000);
Thread t2 = new Thread(()->{
// 进行 notify
synchronized (locker){
System.out.println("notify 之前");
locker.notify();
System.out.println("notify 之后");
}
});
t2.start();
}
notify 方法是唤醒等待的线程.
方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait
状态的线程。(并没有 “先来后到”) 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行
完,也就是退出同步代码块之后才会释放对象锁
wait 和 notify的使用及注意
七、关于多线程的案例wait notify.都是针对同一个对象来 *** 作的.
例如现在有一个对象O~~ 有10个线程,都调用了o.wait.
此时10个线程都是阻塞状态. 如果调用了o.notify,就会把10个其中的一个给唤醒.(唤醒哪个?不确定)
针对notifyAll,就会把所有的10个线程都给唤醒
wait唤醒之后,会重新尝试获取到锁~(这个过程就会发生竞争)
线程安全,我们已经知道了。 但是 单例模式又是一个新的东西。 单例模式:设计模式之一。
设计模式:可以理解为“棋谱”,就是说:设计模式就是一些固定的代码套路。
当下棋,下到一种程度。都会去学习棋谱,也就是学习下棋的招数 和 应对之法。
代码中,有很多经典场景。 经典场景中,也就有一些经典的应对手段。
一些大佬们就把这些常见的应对手段,给收集整理起来,起了个名字,就叫“设计模式”。 不要想得太nb,就是收集资料整理得出的结果,俗称套路。
这些套路就可以让程序员(哪怕是新手),按照套路去写,就不至于把代码写的太差。(就像做PPT一样,下一个模板,自己去填充)
从客观角度出发,可能很多的程序水平一般。 因此,如何才能保证 即使程序员水平一般,也能代码写好? 那就需要通过 设计模式 来去做出规范。
另外,关于“设计模式”的书,有不少。 我推荐是不着急去学习更多关于“设计模式” 的东西。 我们当前需要解决的问题是从无到有。 直白点说 就是
从不会写,到能写出来。 这是因为 “设计模式” 这个东西,它从 有 到 优。
就是说:我们本来就会写,只是写得不是很好。现在就可以通过设计模式,来进行进一步的优化代码。
所以话说回来,我们目前的重点:是从无到有,从不会写到会写。 不过也别着急,等我们工作了,有了一定工作经验,这些东西你都会遇到的。
而且只要代码敲得多了,问题也就不存在了。
7.1、单例模式虽然 “设计模式” 不着急学。 但是!我们不能完全不会! 至少校招中,有两种“设计模式”是常提问的。
1、单例模式 2、工厂模式
这个我后面都会讲,这里先关注于 “单例模式”
要求我们代码中的某个类,只能有一个实例,不能有多个实例。
实例就是对象。 就是说某个类只能new 一个对象,不能new多个对象。
这种单例模式,在实际开发中是非常常见的,也是非常有用的。 开发中的很多“概念”,天然就是单例的。
比如说:我前面写的MySQL 的JDBC编程 里面,有一个DataSource(数据源)类,像数据源这样的对象就应该是单例的。 毕竟作为一个程序,数据源应该只有一个。有一个源就可以了,我们只要描述这些数据只来自于 一个 数据源, 就行了。 像这种,就应该像是一个单例。我在讲 JDBC并没讲那么多,现在我来说一下:在真实情况下,像这种数据库的数据源都会被设计成单例模式的。
大部分跟数据有关的东西,服务器里面只存一份。那么,就都可以使用“单例模式”来进行表示。
单例模式的两种典型实现
单例模式中有两个典型实现:
1、饿汉模式
2、懒汉模式
我们来通过一个生活上的例子来给大家讲讲什么是饿汉模式,什么是懒汉模式。
洗碗,这件事不陌生把?
第一种情况:
假设我们中午吃饭的时候,一家人用了4个碗。然后吃完之后,马上就把碗给洗了。
这种情况,就是饿汉模式。
注意!饿汉模式的 “饿” 指的是着急的意思,不是肚子饿。
第二种情况
中午吃饭的时候,一家人用了4个碗。然后吃完之后,碗先放着,不着急洗。
等待晚上吃饭的时候,发现只需要2个碗。
那么就将 4个没洗的碗 中,洗出2个碗,拿来用。吃完之后,碗先放着,不着急洗。
如果下一顿只用一个玩,就洗出1个碗。
简单来说:就是用多少,拿多少。少的不够,多的不要。
这就是懒汉模式
饿汉的单例模式,是比较着急的去进行创建实例的.
懒汉的单例模式,是不太着急的去创建实例,只是在用的时候,才真正创建
懒汉模式不推荐现实生活中使用
之所以,我们将其称为饿汉模式,是因为它是“比较着急的”。就是针对唯一实例的初始化,比较着急。
换个说法:在类加载的阶段,就会直接创建实例。
只要程序中用到了这个类,就会立即加载。
class Singleton {
//1.使用static创建一个实例,并且立即进行实例化.
// 这个 instance对应的实例,就是该类的唯一实例.
private static Singleton instance = new Singleton();
//2.为了防止程序猿在其他地方不小心的new这个Singleton,就可以把构造方法设为
private Singleton(){}
//3.提供一个方法,让外面能够拿到唯一实例.
public static Singleton getInstance(){
return instance;
}
}
7.1.2、懒汉模式
懒汉的单例模式,是不太着急的去进行创建实例的,只有在用的时候,才真正创建实例。
懒汉模式的代码 和 饿汉模式的代码非常相似。
class Singleton2{
//1、现在就不是立即初始化实例
private static Singleton2 instance;// 默认值:Null
//2、把构造方法设为 private
private Singleton2(){};
//3、提供一个公开的方法,来获取这个 单例模式的唯一实例
public static Singleton2 getInstance(){
// 只有当我们真正用到这个实例的时候,才会真正去创建这个实例
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
public class Dome14 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
}
}
7.2、实现一个线程安全的单例模式(懒汉模式)面试问题饿汉模式 和 懒汉模式 的唯一区别就在于 创建实例的时机不一样。
饿汉模式 是 类加载时,创建。
懒汉模式 是 首次使用时,创建。
所以懒汉模式就更懒一些,不用的时候,不创建;
等到用用的时候,再去创建。
这样做的目的,就是节省资源。如果像 饿汉模式一样,一开始就实例化对象。
此时这个对象就存储在堆上。【这是需要耗费资源】 我们也不确定 这个 对象 什么时候会被用到。
那么,我们一直不调用,这资源还是一直挂在那里。 这就不就是浪费嘛!
如果像 懒汉模式一样,到了真正用到的时候,才会去实例化唯一的对象。
//单例模式 - 懒汉模式
class Singleton2{
//1、现在就不是立即初始化实例
private static Singleton2 instance;// 默认值:Null
//2、把构造方法设为 private
private Singleton2(){};
//3、提供一个公开的方法,来获取这个 单例模式的唯一实例
public static Singleton2 getInstance(){
// 只有当我们真正用到这个实例的时候,才会真正去创建这个实例
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton2();
}
}
}
return instance;
}
}
真正要解决的问题,是实现一个线程安全的单例模式!
线程安全不安全,具体指的是多线程环境下,并发的调用getInstance方法,是否可能存在bug
现在知道懒汉模式存在线程安全问题,那又该怎么办呢???
此时可以看见有两个if语句进行判断,但是这两个条件判断的祈祷的作用是不同的
在这个代码中,看起来两个—样的if条件是相邻的.但是实际上这两个条件的执行时机是差别很大的!!!
加锁可能导致代码出现阻塞.外层条件是10:16执行的.里层条件可能是10:30执行的…
在这个执行的时间差中间, instance也是很可能被其他线程给修改的
当前这个代码中还存在一个重要的问题
如果多个线程,都去调用这里的getlnstance
就会造成大量的读instance 内存的 *** 作=>可能会让编译器把这个读内存 *** 作优化成读寄存器 *** 作
那该如何解决这问题呢???
此时,我们才完成一个线程安全的单例模式 - 懒汉模式
1、正确的位置加锁
2、双重if判定
3、volatile关键字
理解双重 if 判定 / volatile:
加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候.
因此后续使用的时候, 不必再进行加锁了.
外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了.
同时为了避免 “内存可见性” 导致读取的 instance 出现偏差, 于是补充上 volatile .
当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁,
其中竞争成功的线程, 再完成创建实例的 *** 作.
当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例
先进先出
阻塞队列同样也是一个符合先进先出规则的队列
相比于普通队列,阻塞队列又有一些其他方面的功能!!
1.线程安全
2产生阻塞效果.
1)如果队列为空,尝试出队列,就会出现阻塞.阻塞到队列不为空为止.
2)如果队列为满,尝试入队列,也会出现阻塞.阻塞到队列不为满为止.
基于上述特性,就可以实现"生产者消费者模型"
生产者消费者模型 是日常开发中,处理多线程问题的一个典型方式。
举个例子:过年,吃饺子 既然吃饺子,就需要包饺子这件事。 而包出一个完美的饺子这件事很麻烦。
【和面,擀饺子皮,包饺子,煮/蒸。大概是这么一个流程,其中细节是非常多的】 如果数量非常多,就需要多人分工进行协作。其中 和面 和煮饺子 不太好进行分工。【一般和面是一个人负责,煮饺子也是一个人】
毕竟和面这件事,一坨面一起和。没有说拆成两个部分来和面的。那样口感就不一样了。
煮饺子,那就更简单了,一个人拿着勺子不停的搅拌锅里的饺子,等到煮熟了,直接捞起来就行了。
擀饺子皮 和 包饺子 就比较好分工了。
毕竟面皮是一张一张擀出来了,饺子也是一个一个包的。
假设现在ABC三个人一起来擀饺子皮+包,就会有两种情况:
1)ABC分别每个人都是先斡一个皮,然后包一个饺子.(存在一定的问题,锁冲突比较激烈,(擀面杖只有一个,就会竞争))
待这个人使用完,让出来。然后,另外两个人就会出现竞争。 所以这个时候就会出现一系列的阻塞等待。擀起面皮就很难受了,要等。
2)A专门负责饺子皮,B和C专门负责包~~~[常见情况]
A就是饺子皮的生产者,要不断的生成一些饺子皮,BC就是饺子皮的消费者,要不断的使用/消耗饺子皮(A就擀饺子皮,BC就负责包饺子)
对于包饺子来说用来放饺子皮的那个"盖帘(存放饺子的地方)“就是"交易场所”
这种就是生产者消费者模型。
在这个模型中,既有生产者负责生产数据,消费者负责使用数据。
那么,生产者 和 消费者之间,需要有一个“桥梁” 来去进行沟通交互。我们将 “桥梁” 称其为 “交易场所”。放在 饺子 事件中,“交易场所” 就相当于 用来放饺子的那个“盖帘”。A将生产出来的饺子皮放在盖帘上,B、C消耗的饺子皮,要从盖帘上面拿。
得有这样的一个空间来存放饺子皮,得有这样的一个空间来存储需要使用的数据。 这就是“交易场所”。
阻塞队列 就可以作为 生产者消费者模型 中的 “交易场所”。
生产者消费者模型,是实际开发中非常有用的一种多线程开发手段!!!
尤其是在服务器开发的场景中。
实际开发中使用到的“阻塞队列”并不是一个简单的数据结构了,而是一个/一组﹐专门的服务器程序
这里我们只是说一个 Java中内置阻塞队列是哪一个,顺带将一个常用的入队和出队方法
1、先实现一个普通队列
2、加上线程安全
3、加上阻塞功能 因此 阻塞队列 是可以基于链表,也可以基于数组来实现、
但是基于数组来实现阻塞队列更简单,所以我们就直接写一个数组版本的阻塞队列。
数组实现队列的重点就在于 循环队列。
循环队列的实现
现在有两种方案实现队列:
1)浪费一个格子.head == tail认为是空 ,head == tail+1认为是满
2)额外创建一个变量,size,记录元素的个数,size == 0空 ,size == arr.length 为满
1.我们采用第二种方案实现一个普通队列:
class MyBlockingQueue{
//保存数据本体
private int[] date = new int[1000];
//有效元素的个数
private int size = 0;
//对头位置
private int head = 0;
//队尾位置
private int tail = 0;
//入队列
public void put(int value){
if(tail == date.length){
//tail == date.length 说明队列满了
//此时先不做处理
return;
}
//队列没有满的时候,把新的元素防在tail位置上
date[tail] = value;
tail++;
if(tail >= date.length){
//tail >= date.length 说明tail到达数组末尾了,就得处理,让tail重新回到0位置
tail = 0;
//tail = tail % date.length; 让tail重新回到0位置这种写法也行(但是不好)
}
size++;//插入元素之后让元素个数加1
}
//出队列
public Integer take(){//Integer 是为了返回非法值,int就不能返回null
if(size == 0){
//如果没有元素就返回一个非法值
return null;
}
int ret = date[head];
head++;//记录head的位置
if(head >= date.length){
//head >= date.length 说明 head 到数组最后了
head = 0;
}
size--;//记录出队列之后的元素个数
return ret;
}
}
public class Dome16 {
public static void main(String[] args) {
//验证这个普通队列有没有问题
MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
myBlockingQueue.put(1);
myBlockingQueue.put(2);
myBlockingQueue.put(3);
myBlockingQueue.put(4);
Integer a = myBlockingQueue.take();
Integer b = myBlockingQueue.take();
Integer c = myBlockingQueue.take();
Integer d = myBlockingQueue.take();
System.out.println(a + " " + b + " " + c +" " + d);
}
}
2、让队列在支持线程安全
保证多线程环境下,调用这里的put 和 take 是没有问题的。 使用加锁 *** 作 synchronized
3.让队列实现阻塞效果
关键要点:使用 wait 和 notify机制。
class MyBlockingQueue{
// 保存数据的本体
private int[] data = new int[1000];
// 有效元素个数
private int usedSize;
// 队头下标位置
private int head;
// 队尾下标位置
private int rear;
private Object locker = new Object();// 专门的锁对象
// 入队列
public void put(int value) throws InterruptedException {
synchronized(locker){
if(usedSize == this.data.length){
// 如果队列满了,暂时先返回。
//return;
locker.wait();
}
data[rear++] = value;
//处理 rear 到达数组末尾的情况。
if(rear >= data.length){
rear = 0;
}
usedSize++;// 入队成功,元素个数加一。
locker.notify();
}
}
// 出队列
public Integer take() throws InterruptedException {
synchronized(locker){
if(usedSize == 0){
// 如果队列为空,就返回一个 非法值
// return null;
locker.wait();
}
int tmp = data[head];
head++;
if(head == data.length){
head = 0;
}
usedSize--;
// 在 take成功之后,唤醒put中的等待。
locker.notify();
return tmp;
}
}
}
当前的场景中,只有一个消费者 和 一个 生产者。
如果多个生产者 和 消费者,那我们就多创建线程就行了。
为了更好看到效果,我们在给这这个程序中的“生产者”加上一个sleep。
让它生产的慢一些,此时消费者就只能跟生产的步伐走。
生产者生成一个,消费者就消费一个。
下面我们来看一下执行效果。【生产的速度 没有消费速度快】
我们再来将sleep代码的位置换到 消费者 代码中。
此时就是消费速度 没有生产速度快。
来看下面的效果
这几张图片来源于:细节狂魔
为了观察程序的效果,我们再利用 阻塞队列 来构造一个 生产者和消费者模型
我通过构造两个线程,来实现一个简易的消费者生产者模型。
public class Test22 {
private static MyBlockingQueue queue = new MyBlockingQueue();
public static void main(String[] args) {
// 实现一个 生产者消费者模型
Thread producer = new Thread(()->{
int num = 0;
while (true){
try {
System.out.println("生产了" + num);
queue.put(num);
num++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
Thread customer = new Thread(()->{
while (true){
try {
int num = queue.take();
System.out.println("消费了"+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
}
}
总程序(完整的生产者消费者模型 + 阻塞队列)
下面这个程序,是 生产速度非常,消费速度很慢。 取决于你给谁加上sleep
class MyBlockingQueue{
// 保存数据的本体
private int[] data = new int[1000];
// 有效元素个数
private int usedSize;
// 队头下标位置
private int head;
// 队尾下标位置
private int rear;
private Object locker = new Object();// 专门的锁对象
// 入队列
public void put(int value) throws InterruptedException {
synchronized(locker){
if(usedSize == this.data.length){
// 如果队列满了,暂时先返回。
//return;
locker.wait();
}
data[rear++] = value;
//处理 rear 到达数组末尾的情况。
if(rear >= data.length){
rear = 0;
}
usedSize++;// 入队成功,元素个数加一。
locker.notify();
}
}
// 出队列
public Integer take() throws InterruptedException {
synchronized(locker){
if(usedSize == 0){
// 如果队列为空,就返回一个 非法值
// return null;
locker.wait();
}
int tmp = data[head];
head++;
if(head == data.length){
head = 0;
}
usedSize--;
// 在 take成功之后,唤醒put中的等待。
locker.notify();
return tmp;
}
}
}
public class Test22 {
private static MyBlockingQueue queue = new MyBlockingQueue();
public static void main(String[] args) {
// 实现一个 生产者消费者模型
Thread producer = new Thread(()->{
int num = 0;
while (true){
try {
System.out.println("生产了" + num);
queue.put(num);
num++;
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
Thread customer = new Thread(()->{
while (true){
try {
int num = queue.take();
System.out.println("消费了"+num);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
}
}
7.3、定时器
7.3.1、标准库中的定时器用法类似于一个闹钟,可以进行定时,在一定时间之后,被唤醒并执行某个之间设定好的任务。
举个例子 在我们打开浏览器,随便打开一个网页 如果顺利,马上就能进入网站
如果不能进去的话就会执行,那就回出现一些提示,比如说这个页面就会一直转或者出现一个
这样的页面
此时我们很快就能想到 join,有一个用法,在括号里添加指定的 “超时时间” join(10000)。
sleep也可以达到这个效果,sleep(指定休眠时间) 。
join 和 sleep 都是基于系统内部的定时器,来实现的。
7.3.2、自己如何实现定时器java. uti1.Timer
核心方法就一个, schedule(安排),参数有两个:任务是啥,多长时间之后执行
7.3.2.1、描述任务首先,思考一个问题:Timer 类 的内部需要什么东西? 从Timer 的 工作内容入手
1、管理很多的任务
2、执行时间到了的任务管理任务又可以细分为 2个:
1、描述任务(创建一个专门的类来表示一个定时器中的任务【Timer Task】)
2、组织任务(使用一定的数据及结构进行组织数据,把一些任务放到一起。)
具体任务顺序为
1、描述任务(创建一个专门的类来表示一个定时器中的任务【Timer Task】)
2、组织任务(使用一定的数据及结构进行组织数据,把一些任务放到一起。)
3、执行时间到了的任务
创建—个专门的类来表示—个定时器中的任务.(TimerTask)
//描述一个任务
class MyTask{
//任务具体要做什么
private Runnable runnable;
// 任务具体的执行时间:保存任务要执行的毫秒级时间戳
private long time;
// after 是一个时间间隙,不是绝对的时间戳的值
public MyTask(Runnable runnable,long after){
this.runnable = runnable;
// 很简单,意思就是从当前时间开始, after 秒之后,这个任务被执行。
this.time = System.currentTimeMillis()+after;
}
// 通过调用这里 run方法,来执行我们任务具体要做什么
public void run(){
runnable.run();
}
}
7.3.2.2、组织任务(通过一些数据结构将任务放在一起并组织好)
7.3.2.3、执行时间到了的任务
有的人可能会说,为什么不用peek?
拿出来,有放进去,多麻烦,还会有多余的开销。
确实,peek 更好一些,但是这里的 take 也没有什么开销。
所以,这个点不重要。
现在我们先安排个任务看一下执行效果
7.3.2.4、自己实现定时器的代码当我们执行程序的时候就发现抛出了异常
这是因为这个代码还有两个重要的问题
//描述一个任务
class MyTask implements Comparable<MyTask>{
//任务具体要做什么
private Runnable runnable;
// 任务具体的执行时间:保存任务要执行的毫秒级时间戳
private long time;
// delay 是一个时间间隙,不是绝对的时间戳的值
public MyTask(Runnable runnable,long delay){
this.runnable = runnable;
// 很简单,意思就是从当前时间开始, delay 秒之后,这个任务被执行。
this.time = System.currentTimeMillis()+delay;
}
// 通过调用这里 run方法,来执行我们任务具体要做什么
public void run(){
runnable.run();
}
public long getTime(){
return time;
}
@Override
public int compareTo(MyTask o) {
return (int) (o.time - this.time);
}
}
class MyTimer{
//定时器内部要存放多个任务
//这是一个带有 线程安全 的优先级队列,有阻塞功能
PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable, long delay){
MyTask task = new MyTask(runnable,delay);
queue.put(task);
synchronized (locker){
locker.notify();
}
}
private Object locker = new Object();
//通过这个构造方法来来检查是否有任务应该执行了
//时间到了就执行任务
public MyTimer(){
//创建一个线程来不停检查任务是否执行
Thread t = new Thread( () -> {
while (true){
try {
//将当前的任务取出来
MyTask task = queue.take();
//获取当前的时间
long curTime = System.currentTimeMillis();
if(task.getTime() < curTime){
//任务还没到执行的时间,那就将任务放回到队列里
queue.put(task);
//指定一个等待时间
synchronized(locker){
locker.wait(task.getTime() - curTime);
}
} else{
//说明任务已经到执行的时间了
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
7.4、线程池
7.4.1、什么是线程池
下面,我们来看一下 Java标准库中是怎样的体现
老规矩,先来学习一下 Java 保准库中,线程池的使用。
然后再自己实现一个线程池。
Java 保准库中,线程池的使用
面试问题–有一个程序,这个程序要并发的/多线程的来完成一些任务~~,如果使用线程池的话,这里的线程数设为多少合适??Java 保准库中,线程池 对应的类 叫做 ThreadPoolExecutor
Thread:线程
Pool:池
Executor:执行者
ThreadPoolExecutor:这个东西用起来有点麻烦。
在这里点击https://docs.oracle.com/javase/8/docs/api/ 这个链接,这是java文档
7.4.2、模拟实现一个简化版本的线程池 Executors标准库中还提供了一个简化版本的线程池 Executors
本质是针对 ThreadPoolExecutor 进行了封装,提供了一些默认参数
看一下Executors是咋用的,仿照这个实现一个线程池
接下来我们使用 java标准库中的 Executors.newFixedThreadPool() 来创建一个任务
线程池里面都有什么?
1、先能够描述任务。【直接使用 Runable 即可】
2、需要组织任务 。【直接使用一个 BlockingQueue,阻塞队列】
3、描述工作线程。
4、组织工作线程
5、需要实现,往线程池添加任务。
我要实现的线程池,不光要管理任务 和 线程,还需要它们相互配合。
测试我们自己写的线程池是否有问题
总程序
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPoll{
//1.描述一个任务,直接使用Runnable,不需要重新创建类了
//2.使用数据结构来组织好任务
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
//3.利用一个静态内部类来描述一个线程,工作线程是用来从任务队列中取到任务并执行
static class Worker extends Thread{
BlockingQueue<Runnable> queue = null;
public Worker(BlockingQueue<Runnable> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
try {
//循环去取队列里面的任务
//如果队列为空了,此时就会引起阻塞。如果队列不为空,就会获取到度列的元素
Runnable runnable = queue.take();
//获取到任务之后就会执行任务
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//4.创建一个数据结构够来组织若干任务
private List<Thread> workers = new ArrayList<>();
public MyThreadPoll(int n){
//在构造方法中,创建出若干线程,放到上面的数组中
for (int i = 0; i < n; i++) {
//调用带参的构造方法,参数为阻塞队列的对象
Worker worker = new Worker(queue);
//启动线程
worker.start();
//将这个线程放到数组中
workers.add(worker);
}
}
//5.创建一个方法,能够允许程序员将任务放到线程池中
public void submit(Runnable runnable){
try {
//将任务放到阻塞队列里面
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Dome20 {
public static void main(String[] args) {
MyThreadPoll poll = new MyThreadPoll(10);
for (int i = 0; i < 100; i++) {
poll.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello MyThreadpoll");
}
});
}
}
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)