多线程(基础)

多线程(基础),第1张

一、进程

为啥要有进程?因为 *** 作系统支持多任务执行,程序员也需要“并发编程”
通过多进程,是完全可以实现并发编程的,但是是有点问题的!!!
如果需要频繁的创建进程/销毁进程,这个事情的成本是比较高的如果需要频繁的调度进程,这个事情的成本也是比较高的

那要如何去解决这样的问题呢???

为啥线程要比进程更轻量???

1.1、面试题(进程和线程的区别和联系)

谈谈进程和线程的区别和联系?
1.进程包含线程.一个进程里可以有一个线程,也可以有多个线程.
2.进程和线程都是为了处理并发编程这样的场景.
但是进程有问题,频繁创建和释放的时候效率低.相比之下,线程更轻量,创建和释放效率更高.(为啥更轻量?少了申请释放 资源的过程)
3. *** 作系统创建进程,要给进程分配资源.进程是 *** 作系统分配资源的基本单位.
*** 作系统创建的线程,是要在CPU上调度执行.线程是 *** 作系统调度执行的基本单位
4.进程具有独立性.每个进程有各自的虚拟地址空间.一个进程挂了,不会影响到其他进程.
同一个进程中的多个线程,共用同一个内存空间,一个线程挂了,可能影响到其他线程的,甚至导致整个进程崩溃

二、多线程

什么是多线程???
java标准库中,就提供了一个Thread类,来表示/ *** 作线程Thread这个类也可以视为一组java标准库中提供的API
实例的Thread其实是和 *** 作系统中的线程是一 一对应的关系
创建一个Thread实例 就是创建了一个线程
*** 作系统提供了一组有关线程的API(C语言风格),java对这组API进一步封装,就成了Thread类

2.1、Thread类创建线程的方法 2.1.1、创建子类继承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方法里面执行的代码就是线程执行的内容


2.1.2、创建一个类,实现Runnable接口,在创建Runnable实例传递给Thread实例

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类的常见方法

Thread常见的构造方法

4.1,Thread(String name)

4.2,、Thread一些重要方法 4.2.1、start方法和run方法的区别(启动线程)

4.2.2、中断线程的几个方法

中断线程,让一个线程停下来
线程停下来的关键,是要让线程对应的run方法执行完
(还有一个特殊的;是main这个线程.对于main来说,得是 main方法执行完,线程就完了)


4.2.3、线程等待

多个线程之间,调度顺序是不确定的
线程之间的执行是按照调度器来安排的.这个过程可以视为是"无序,随机"这样不太好.有些时候,我们需要能够控制线程之间的顺序.
线程等待,就是其中一种,控制线程执行顺序的手段
此处的线程等待,主要是控制线程结束的先后顺序

调用join的时候,哪个线程调用的join,哪个线程就会阻塞等待~~得到对应的线程执行完毕为止(对应线程的run执行完)

4.2.4、获取线程的引用

4.2.5、线程休眠

所谓的休眠到底是在干啥?
进程,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次

为什么会导致这种情况发生???

通过加锁的方式就可以避免发生这种问题

6.1、加锁的原理(synchronized关键字)


给方法直接加上synchronized关键字.此时进入方法,就会自动加锁.
离开方法,就会自动解锁~
当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待.(此时对应的线程,就处在BLOCKED状态)
阻塞会一直持续到,占用锁的线程把锁释放为止~~

6.2、线程不安全发生的原因

线程安全:多线程并发执行某个代码,没有逻辑上的错误,就是“线程安全”。
线程不安全:多线程并发执行某个代码,产生逻辑上的错误,就是“线程不安全”。
1、线程是抢占式执行,线程间的调度充满随机性.
线程之间的调度完全由内核负责,用户代码中感知不到,也无法控制。
线程之间谁先执行,谁后执行,谁执行到哪里从CPU上下来,这样的过程都是用户无法控制也无法感知的。

2、针对变量的 *** 作不是原子的~~
自增 *** 作不是原子的
有时也把这个现象叫做同步互斥,表示 *** 作是互相排斥的。
一条 java 语句不一定是原子的,也不一定只是一条指令
比如刚才我们看到的 cont++,其实是由三步 *** 作组成的:
1. 从内存把数据读到 CPU
2. 进行数据更新
3. 把数据写回到 CPU

3.三个线程尝试修改同一个变量
a) 如果是一个线程修改一个变量,线程安全。
b) 如果多个线程读取同一个变量,线程安全。但是如果有多个线程,一个读取的变量,一个修改变量,则最后线程还是不安全的。(注意体会)
c) 如果多个线程修改不同的变量,线程安全。

4.内存可见性导致的线程安全问题。
一个具体的栗子:针对同一个变量~
一个线程(t1)进行读 *** 作(循环进行很多次),一个线程(t2)进行修改 *** 作(合适的时候执行一次)
当t1在循环读变量的时候(读内存的速度相对于读寄存器是很慢的,因为现代的编译器是会优化代码的(这是在保证逻辑不变的情况下,但是在单线程就不会出现线程不安全问题,多线程中如果进行优化可能就会翻车)),因此t1就会在寄存器中读取这个变量,这是t2在进行修改 *** 作的时候就会将这个变量改变,这是t1在去读变量的时候就会出错(原来的变量已经改变了),当发生这种情况的时候就会很危险

5.指令重排序(Java的编译器在编译代码时,会针对指令进行优化,调整指令的先后顺序,保证原有逻辑不变的情况下,提高程序的运行效率)
现代的编译器优化能力都非常强,优化后的代码,会比优化前快很多。
但是多线程的优化是不容易实现的,可能会导致一些问题。

6.3、synchronized关键字–监视器锁 monitor lock 6.3.1互斥

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. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁
6.3.2synchronized的用法

1)直接修饰普通方法:锁的 SynchronizedDemo 对象

2) 修饰静态方法: 锁的 SynchronizedDemo 类的对象

  1. 修饰代码块: 明确指定锁哪个对象.
    锁当前对象

    锁类对象

    我们重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待.
    两个线程分别尝试获取两把不同的锁, 不会产生竞争
6.3.3、可重入

6.3.4、死锁的场景

什么情况下会导致死锁
所谓死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进
1.一个线程,一把锁

2.两个线程,两把锁

3.n个线程m把锁

如何解决这种死锁问题:

6.3.4.1、死锁的四个必要条件

1.互斥使用~一个锁被一个线程占用了之后,其他线程占用不了(锁的本质,保证原子性)
2.不可抢占一个锁被一个线程占用了之后,其他的线程不能把这个锁给抢走.
3.请求和保持当一个线程占据了多把锁之后,除非显式的释放锁,否则这些锁始终都是被该线程持有的
4.环路等待,等待关系成环了~~A等B,B等C,C又等A

6.3.5、Java 标准库中的线程安全类

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder

但是还有一些是线程安全的. 使用了一些锁机制来控制.
Vector (不推荐使用)
HashTable (不推荐使用)
ConcurrentHashMap
StringBuffer

6.4、volatile 关键字

volatile关键字能保证内存可见性也就相当于禁止编译器的优化了,但是不能保证原子性
之所以存在 “内存可见性”问题,是因为我们的计算机的硬件所决定的
提到内存可见性,在Java中,经常往往会提到另外一词:JMM– Java Memory Model (Java内存模型/Java存储模型)JMM就是把上诉将的硬件结构,在Java中用专门的术语又重新抽象封装了一遍。
简单来说:就是换汤不换药,还是上述的硬件结构,只不过名称发生了改变。

6.4.1、volatile关键字能保证内存可见性,但是不能保证原子性

代码示例
在这个代码中
创建两个线程 t1 和 t2
t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
预期当用户输入非 0 的值的时候, t1 线程结束.

如果不用volatile修饰flag,t1 读的是自己工作内存中的内容.
当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化.

6.4.2、volatile 不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile
保证的是内存可见性.

代码示例

这个是最初的演示线程安全的代码. 给 increase 方法去掉 synchronized 给 count 加上 volatile 关键字.

6.4.3、volatile 和 synchronized 的区别

这两个本来就没有什么联系。
只是在 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一样,下一个模板,自己去填充)

从客观角度出发,可能很多的程序水平一般。 因此,如何才能保证 即使程序员水平一般,也能代码写好? 那就需要通过 设计模式 来去做出规范。

另外,关于“设计模式”的书,有不少。 我推荐是不着急去学习更多关于“设计模式” 的东西。 我们当前需要解决的问题是从无到有。 直白点说 就是
从不会写,到能写出来。   这是因为 “设计模式” 这个东西,它从 有 到 优。
就是说:我们本来就会写,只是写得不是很好。现在就可以通过设计模式,来进行进一步的优化代码。  
所以话说回来,我们目前的重点:是从无到有,从不会写到会写。 不过也别着急,等我们工作了,有了一定工作经验,这些东西你都会遇到的。
而且只要代码敲得多了,问题也就不存在了。

虽然 “设计模式” 不着急学。 但是!我们不能完全不会! 至少校招中,有两种“设计模式”是常提问的。
1、单例模式 2、工厂模式
这个我后面都会讲,这里先关注于 “单例模式”

7.1、单例模式

要求我们代码中的某个类,只能有一个实例,不能有多个实例。
实例就是对象。 就是说某个类只能new 一个对象,不能new多个对象。  
这种单例模式,在实际开发中是非常常见的,也是非常有用的。 开发中的很多“概念”,天然就是单例的。
比如说:我前面写的MySQL 的JDBC编程 里面,有一个DataSource(数据源)类,像数据源这样的对象就应该是单例的。 毕竟作为一个程序,数据源应该只有一个。有一个源就可以了,我们只要描述这些数据只来自于 一个 数据源, 就行了。 像这种,就应该像是一个单例。

我在讲 JDBC并没讲那么多,现在我来说一下:在真实情况下,像这种数据库的数据源都会被设计成单例模式的。
大部分跟数据有关的东西,服务器里面只存一份。那么,就都可以使用“单例模式”来进行表示。

单例模式的两种典型实现

单例模式中有两个典型实现:
1、饿汉模式
2、懒汉模式
我们来通过一个生活上的例子来给大家讲讲什么是饿汉模式,什么是懒汉模式。

洗碗,这件事不陌生把?
第一种情况:
假设我们中午吃饭的时候,一家人用了4个碗。然后吃完之后,马上就把碗给洗了。
这种情况,就是饿汉模式。
注意!饿汉模式的 “饿” 指的是着急的意思,不是肚子饿。

第二种情况
中午吃饭的时候,一家人用了4个碗。然后吃完之后,碗先放着,不着急洗。
等待晚上吃饭的时候,发现只需要2个碗。
那么就将 4个没洗的碗 中,洗出2个碗,拿来用。吃完之后,碗先放着,不着急洗。
如果下一顿只用一个玩,就洗出1个碗。
简单来说:就是用多少,拿多少。少的不够,多的不要。
这就是懒汉模式

饿汉的单例模式,是比较着急的去进行创建实例的.
懒汉的单例模式,是不太着急的去创建实例,只是在用的时候,才真正创建

懒汉模式不推荐现实生活中使用

7.1.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 挡住了. 也就不会继续创建其他实例

7.3、阻塞队列

先进先出
阻塞队列同样也是一个符合先进先出规则的队列
相比于普通队列,阻塞队列又有一些其他方面的功能!!
1.线程安全
2产生阻塞效果.
1)如果队列为空,尝试出队列,就会出现阻塞.阻塞到队列不为空为止.
2)如果队列为满,尝试入队列,也会出现阻塞.阻塞到队列不为满为止.
基于上述特性,就可以实现"生产者消费者模型"

生产者消费者模型 是日常开发中,处理多线程问题的一个典型方式。

举个例子:过年,吃饺子 既然吃饺子,就需要包饺子这件事。 而包出一个完美的饺子这件事很麻烦。
【和面,擀饺子皮,包饺子,煮/蒸。大概是这么一个流程,其中细节是非常多的】 如果数量非常多,就需要多人分工进行协作。

其中 和面 和煮饺子 不太好进行分工。【一般和面是一个人负责,煮饺子也是一个人】

毕竟和面这件事,一坨面一起和。没有说拆成两个部分来和面的。那样口感就不一样了。
煮饺子,那就更简单了,一个人拿着勺子不停的搅拌锅里的饺子,等到煮熟了,直接捞起来就行了。
擀饺子皮 和 包饺子 就比较好分工了。
毕竟面皮是一张一张擀出来了,饺子也是一个一个包的。

假设现在ABC三个人一起来擀饺子皮+包,就会有两种情况:

1)ABC分别每个人都是先斡一个皮,然后包一个饺子.(存在一定的问题,锁冲突比较激烈,(擀面杖只有一个,就会竞争))
待这个人使用完,让出来。然后,另外两个人就会出现竞争。 所以这个时候就会出现一系列的阻塞等待。擀起面皮就很难受了,要等。

2)A专门负责饺子皮,B和C专门负责包~~~[常见情况]
A就是饺子皮的生产者,要不断的生成一些饺子皮,BC就是饺子皮的消费者,要不断的使用/消耗饺子皮(A就擀饺子皮,BC就负责包饺子)
对于包饺子来说用来放饺子皮的那个"盖帘(存放饺子的地方)“就是"交易场所”

这种就是生产者消费者模型。
在这个模型中,既有生产者负责生产数据,消费者负责使用数据。
那么,生产者 和 消费者之间,需要有一个“桥梁” 来去进行沟通交互。我们将 “桥梁” 称其为 “交易场所”。放在 饺子 事件中,“交易场所” 就相当于 用来放饺子的那个“盖帘”。

A将生产出来的饺子皮放在盖帘上,B、C消耗的饺子皮,要从盖帘上面拿。
得有这样的一个空间来存放饺子皮,得有这样的一个空间来存储需要使用的数据。 这就是“交易场所”。

阻塞队列 就可以作为 生产者消费者模型 中的 “交易场所”。
生产者消费者模型,是实际开发中非常有用的一种多线程开发手段!!!
尤其是在服务器开发的场景中。

71、生产者消费者模型 优点1:能够让多个服务器程序之间更充分的解耦合

优点2:能够对于请求进行"削峰填谷"


实际开发中使用到的“阻塞队列”并不是一个简单的数据结构了,而是一个/一组﹐专门的服务器程序

7.2、使用java库中的阻塞队列

这里我们只是说一个 Java中内置阻塞队列是哪一个,顺带将一个常用的入队和出队方法

7.2.1、自己实现阻塞队列

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、定时器

类似于一个闹钟,可以进行定时,在一定时间之后,被唤醒并执行某个之间设定好的任务。

举个例子 在我们打开浏览器,随便打开一个网页 如果顺利,马上就能进入网站

如果不能进去的话就会执行,那就回出现一些提示,比如说这个页面就会一直转或者出现一个

这样的页面
此时我们很快就能想到 join,有一个用法,在括号里添加指定的 “超时时间” join(10000)。
sleep也可以达到这个效果,sleep(指定休眠时间) 。
join 和 sleep 都是基于系统内部的定时器,来实现的。

7.3.1、标准库中的定时器用法

java. uti1.Timer
核心方法就一个, schedule(安排),参数有两个:任务是啥,多长时间之后执行

7.3.2、自己如何实现定时器

首先,思考一个问题:Timer 类 的内部需要什么东西? 从Timer 的 工作内容入手
1、管理很多的任务
2、执行时间到了的任务

管理任务又可以细分为 2个:
1、描述任务(创建一个专门的类来表示一个定时器中的任务【Timer Task】)
2、组织任务(使用一定的数据及结构进行组织数据,把一些任务放到一起。)
  具体任务顺序为
1、描述任务(创建一个专门的类来表示一个定时器中的任务【Timer Task】)
2、组织任务(使用一定的数据及结构进行组织数据,把一些任务放到一起。)
3、执行时间到了的任务

7.3.2.1、描述任务

创建—个专门的类来表示—个定时器中的任务.(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文档

面试问题–有一个程序,这个程序要并发的/多线程的来完成一些任务~~,如果使用线程池的话,这里的线程数设为多少合适??

标准库中还提供了一个简化版本的线程池 Executors
本质是针对 ThreadPoolExecutor 进行了封装,提供了一些默认参数
看一下Executors是咋用的,仿照这个实现一个线程池

接下来我们使用 java标准库中的 Executors.newFixedThreadPool() 来创建一个任务

7.4.2、模拟实现一个简化版本的线程池 Executors

线程池里面都有什么?
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");
                }
            });
        }
    }
}

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/884258.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-14
下一篇 2022-05-14

发表评论

登录后才能评论

评论列表(0条)

保存