JavaEE 多线程初阶

线程是个啥?

如果把进程想象成是一个工厂,那么线程就是工厂里的生产线

写的代码最终目的就是要跑起来,最终都是要成为一些进程

对于Java代码来说,最终通过Java进程来跑起来的(此处进程就是平时说的JVM)

进程process  任务 task

操作系统如何管理进程

1.先描述一个进程(明确出一个进程上面的一些相关属性)

2.再组织若干个进程(使用一些数据结构,很多描述进程的信息给放到一起,方便进行增删改查)

        所谓的“创建进程”,就是先创建出PCB,然后把PCB加到双向链表中

        所谓的“销毁进程”,就是找到链表上的PCB,并且从链表上删除

        所谓的“查看任务管理器”就是遍历链表

内存指针~指明了这个进程要执行代码/指令在内存的哪里,以及这个进程执行以来的数据都在哪里

        当运行一个exe此时系统就会把exe加载到内存中去变成进程

文件描述表

        程序运行过程中,经常要和文件打交道(文件是在硬盘上的)

进程每次打开一个文件,就会在文件表述表上多增加一项。这个文件描述符表就可以视为是一个数组,里面的没每个元素又是一个结构体,就对应一个文件的相关信息。

        一个进程只要已启动,不管你代码中是否写了的打开/操作文件的代码

都会默认的打开三个文件(系统自动打开的)标准输入(System.out)标准错误(System.err)

上面的属性是一些基础的属性,下面的一组属性,主要是为了能够实现进程调度

状态

优先级

上下文

记账信息

并行和并发

并行:微观上,两个CPU核心,同时执行两个任务的代码

并发:微观上,一个CPU核心先执行一会儿任务 1再执行一会儿任务2,再执行一会儿任务

只要切换的足够快,宏观上看起来就好像这么多任务在同时执行一样。

如何解决这个问题?

1>

进程池~(数据库连接池,字符串常量池)

进程池虽然能解决上述问题,提高效率,同时也有问题,池子里的闲置进程,不使用的时候也在消耗资源系统资源,消耗的系统资源太多了

2>

使用线程来实现并发编程

线程比进程更轻量,每个进程可以执行一个任务,每个线程也能执行一个任务(执行一段代码),也能够并发编程。

创建线程,的成本比创建进程要低很多

销毁线程的成本也比销毁进程的成本低很多

调度线程的成本也比调度进程低很多

经典面试题(必考)

谈谈进程和线程的区别和联系

  1.线程包含进程,一个进程里可以有一个线程,也可以有多个线程

  2.进程和线程都是为了处理并发编程这样的的场景,但是进程有问题,频繁创建和释放的时候效率低,相比之下,线程更轻量,创建和释放效率更高(为啥更轻量,少了释放资源的过程)

  3.操作系统创建进程,要给进程分配资源,进程是操作系统分配资源的基本单位,操作系统创建的线程,是要在CPU上调度执行,线程是操作系统调度执行的基本单位

  4.进程具有独立性,每个进程有各自的虚拟地址,一个进程挂了,不会影响到其他进程

  同一个进程中的多个线程,共用同一个内存孔吉纳,一个线程挂了,可能影响到其他线程,甚至导致整个进程崩溃。

Thread类的基本用法

通过Thread 类创建线程,写法有很多种

其中最简单的做法,创建子类,继承自Thread,并且重写run方法

如果在一个循环中不加任何限制,这个循环的速度非常非常快,导致打印的东西太多,根本看不过来,就可以加上一个sleep操作,来强制让这个线程休眠

在一个进程中,至少会有一个线程

在一个Java进程中,也是至少会有一个调用

就绪状态

处于这个状态的线程,就是在就绪队列中

随时可以被调度到CPU上

如果代码中没有进行sleep,也没有进行其他的可能导致阻塞的操作

代码大概率是运行在Runnable状态的

代码中调用了sleep,就会进入到TIMED—WAITING

join

面试

线程安全问题

整个多线程中最重要最复杂的问题

造作系统,第哦啊读线程的时候,是随机的(抢占执行)

正是因为这样的随机性,就可能导致程序的执行及到线程安全问题

如果因为这样的调度随机性引入了bug,就认为代码是线程不安全的,如果是因为这样的调度随机性,也没有带来bug,就认为代码是线程安全的。

count++干了啥

站在CPU的角度看

1.把内存红的count的值 加载到cpu寄存器中

2.把寄存器中的值给+1

3.把寄存器的值写回到内存的count中

正因为前面说的“抢占先行”,这就导致两个线程同时执行这三个指令的时候,顺序上充满了随机性。

Java中加锁的方式

最常用的 synchronized 这样的关键字

产生线程不安全的原因:

1.线程是抢占式执行, 线程间调度充满随机性(线程不安全的主要原因)

2.多个线程对同一个变量进行修改操作(如果是多个线程针对不同的变量进行修改和多个线程针对同一个变量读,都没事。

3.争对变量的操作不是原子的,(讲数据库事务)

原子 (不可再拆分嘛,一步到位)

针对有些操作,比如读取变量的值,只是对于一条机器指令,此时这样的操作本身就可以视为是原子的,通过枷锁操作,也就是把好几个指令给打包成一个原子。

4.内存可见性,也会影响到线程安全

一个线程进行读操作(循环进行很多次)

一个线程进行修改操作(合适的时候执行一次)

5.指令重排序

解决方案

1使用synchronized关键字

(synchronized不光能保证指令的原子性,同时也能保证内存可见性)

被synchronized包裹起来的代码,编译器就不敢轻易做出上述的假设。

2.使用volatile关键字

volatile和原子性无关,但是能够保证内存可见性

禁止编译器作出上述优化,编译器每次执行判定相等,都会重新从内存读取isQuit的值

注:当循环中加上sleep,优化就消失了,也就没有内存可见性问题

while(isQuit == 0){
    try{
        Thread.sleep(millis:1000);
    }catch(InterruptedException e) {
        e.printStackTrace();
    }
}

5.指令重排序,也会影响到线程安全问题

指令重排化也是编译器优化的一种操作

synchronized

不光能保证原子性,同时还能保证内存可见性,同时还能禁止治理重排序。

sychrnoized (同步)的用法

同步不同语境不同含义,多线程中其实指的‘互斥’

1.直接修饰普通的方法

使用synchrnoized的时候,本质上是在针对某个“对象”进行加锁

此时锁对象就是this

2.修饰一个代码块

需要显示指定针对哪个对象加锁(Java中的任意对象都可以作为锁对象)

这种随手拿个对象都能作为锁对象的用法,是Java中非常有特色的设定

3.修饰一个静态方法

详单与针对当前类的对象加锁

Counter,class(反射)

线程中的一些基本情况

线程的状态

NEW:Thread 对象有了,内核中的线程还没有

TERMINATED:内核中的线程没了,Thread对象还在

RUNNABLE:就绪状态

TIMED_WAITING:因为sleep进入了阻塞状态

BLOCKED:是因为等待锁进入了阻塞状态

WAITING:因为wait进入了阻塞状态

线程安全

锁对象

如果要是针对某个代码块加锁,就需要手动指定,锁对象是啥

synchrnoized的本质操作,是修改了Object对象中的‘对象头’里面的一个标记~

当两个线程同时针对一个对象加锁,才会产生竞争

如果两个线程争对不同对象加锁,就不会有竞争。

把synchrnoized加到静态方法上

所谓的静态方法更严谨的叫法应该叫做类方法

普通的方法,更严谨的叫法叫做实列方法

分析一下,连续锁两次会咋样

外层加了锁,里层又对同一个对象再加一次锁

外层锁:进入方法。则开始加锁,这次能够加锁成功,当前锁是没有人占用的

里层锁:进入代码块,开始加锁,这次加锁不能加锁成功,因为外层锁占用着呢

得等外层锁释放了之后,里层锁才能加锁成功

外层锁要执行完整个方法,才能释放

但是要执行完整个方法,就得让里层加锁成功继续往下走

可重入锁的意义就是降低了程序猿的负担

但是也带来了代价,程序中需要更高的开销

死锁的其他场景

1.一个线程,一把锁

2,两个线程,两把锁

哲学家就餐问题,套娃问题

3,N个线程,M把锁

死锁的四个必要条件

1.互斥作用 一个所被一个线程占用了之后其他线程占用不了

2.不可抢占,一个锁被一个线程占用了之后,其他的线程不能把这个锁给抢走

3.请求和保持 当一个线程占据了多把锁之后,除非显式的是释放锁,否则这些锁始终都是被该线程持有的

4.环路等待,等待关系成立一个环

如何避免环路等待

只要约定好,针对多吧锁加锁的时候,有固定的顺序即可

所有的线程都遵守同样的规划顺序,就不会出现环路等待

标准库

Java有很多,现成的类,有些线程是安全的,有些是不安全的

在多线程环境下,如果使用线程不安全的类就需要谨慎

就是多线程修改时要注意

ArrayList

LinkList

HashMap

TreeMap

StringBuilder

vs

Vector

HashTable

ConcurrentHashMap

StringBuffer

String

synchronized使用的时候也是要付出代价的

代价就是医用就很容易阻塞,一旦线程阻塞,下次再回到CPU这个时间就不可控了,

如果调度不回来自然对应的任务执行时间也就拖慢了

volatile则不会引起线程阻塞

wait和notify

多线程调度随机性的问题

不喜欢随机性,需要人彼此之间有个固定的顺序

wait内部会做三件事

1 先释放锁

2 等待其他线程的通知

3 收到通知后,重新获取锁,并继续往下执行

package thread;


public class Demo17 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
       
            System.out.println("wait 前");

            System.out.println("wait 后");

            object.wait();

        

    }
}

报错

所以得和synchrnoized搭配

public class Demo17 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object){
            System.out.println("wait 前");

            System.out.println("wait 后");

            object.wait();

        }

    }
}

wait 前
wait 后

1.线程基本概念

2.Thread类

1)创建线程

2)终端线程

3)等待线程

4)获取线程实列

5)线程休眠

3.线程状态

4.线程安全问题

5.内存可见性

6.wait/notify

校招 设计模式

单列模式:要求代码中的某个类,只能有一个实列,不能有多个

JDBC-DataSource 这样的对象就应该是单列的

两种典型模式

class Singleton {}

1.饿汉模式 比较着急的去进行创建实例的

class Singleton {
    // 1.使用 static 创建一个实列 并且立即进行实例化
    //  这个instance 对应的实列,就是该类的为一实列
    private static Singleton instance = new Singleton();
    //2.防止程序员在其他地方不小心的new 这个 Singleton,就可以把后遭方法设为private
    private  Singleton(){}
    //3.提供一个方法,让外面能够拿到唯一实列
    public static Singleton getInstance(){
        return instance;
    }
}

针对这个唯一实列的初始化,比较着急。类加载阶段就会直接创建实列

2.懒汉模式 不太着急地去创建实例,只是在用的时候,才真正创建

package thread;
//实现单例模式,懒汉模式
class Singleton2{
    //1.就不是立即就初始化实列
    private static Singleton2 instance = null;
    //把构造方法设为 private
    private Singleton2(){}
    //3.提供一个方法来获取到上述单列的实列
    //  只有当真正需要用到这个实列 的时候,才会真正去创建这个实列
    public static Singleton2 getInstance(){
        if(instance == null){
            instance = new Singleton2();
        }
        return instance;
    }
}
public class Demo20 {
    public static void main(String[] args) {
        Singleton2 instancw = Singleton2.getInstance();
    }
}

其他场景中也会涉及懒汉饿汉模式,计算机更偏向懒汉模式。

static修饰的成员更准确的说,应该叫做‘类成员’=》‘类属性、类方法’

不加static修饰的叫做实列成员,属性,方法。

Java把c++的static给继承过来了

线程安不安全,具体指的是多线程环境下,并发的的调用getinstance方法,是否可能存在bug

不一定有synchronized就安全,还得看所在位置是否正确

public static Singleton2 getInstance(){
        synchronized (Singleton2.class) {
            if (instance == null) {
                instance = new Singleton2();
            }
        }
        return instance;
    }
}

虽然加锁后线程安全解决了,但又遇到了新的问题

对于刚才这个懒汉模式的代码来说,线程不安全,是发生在instance被初始化之前,未初始化的时候,多线程调用getinstance,就可以同时涉及到读和改,但是一旦instance被初始化之后,(一定不是null if条件一定不成立了)getinstance操作只剩下两个读操作

而按照上述的加锁方式,无论代码是初始化之后,还是初始化之前,每次用getinstance都会进行加锁,也就意味着即使初始化之后,已经线程安全了,但是仍然存在大量的锁竞争

开发效率更重要,但运行效率也很重要。

改进方式 让getinstance初始化之前,才进行加锁,初始化之后就不再进行加锁

在加锁这里再加上一层条件判定即可

条件就是当前是否已经初始化完成(instance == null)

    public static Singleton2 getInstance(){
        //如果这个条件成立,说明当前的单列未初始化过,存在线程安全风险,就需要加锁
        if(instance == null) {
            synchronized (Singleton2.class) {
                if (instance == null) {
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}

 1.线程安全

2.产生阻塞效果

1)如果队列为空,尝试出队列,就会出现阻塞,阻塞到队列不为空为止

2)如果队列为满,尝试入队列,也会出现阻塞,阻塞到队列不为满为止

假设,有两个服务器AB,A作为入口服务器直接接受用户的网络请求,B作为应用服务器,来给A提供一些数据

如果不适用生产者消费者模型,此时A和B的耦合性是比较强的

在开发A代码的时候就得充分了解到B提供的一些接口

开发B代码的时候也得充分了解到A是怎么调用的

一旦想把B换成C,A的代码就需要较大的波动

而且如果B挂了,也可能直接导致A也顺带挂了

先来了解一下Java标准库中的阻塞队列的用法

基于这个内置的阻塞队列,实现了一个简单的生产者消费者模型

再自己实现一个阻塞队列

package thread;


import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

public class Demo21 {
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque queue = new LinkedBlockingDeque<>();
        queue.put("hello");
        String s = queue.take();
    }
}

自己来实现一个阻塞队列

先实现一个普通队列

再加上线程安全

再加上阻塞

队列可以基于数组实现,也可以基于链表实现

此处基于数组实现阻塞队列更简单,就直接写数组版本

实现循环队列的时候,有一个重要的问题

如何区分是空队列还是满队列

如果不加额外限制,此时队列空或者满都是head和tail重合

1)浪费一个格子,head == tail 认为是空

                                head == tail+1认为是满

2)额外创建一个变量,size,记录元素的个数

        size == 0        空

        size == arr.length 满

 if(size == data.length){
        tail = 0;
}

tail = tail % data.length;

1.非常不直观,可读性比较差

2.对于计算机来说,开销要更大~~计算机出发的速度是不如比较操作的

定时器

join 指定超时时间

sleep 休眠指定时间

先介绍标准库的定时器用法

然后再看看如何实现一个定时器

java.util.Timer

核心方法就一个,schedule ,参数有两个

Timer内部都需要部署啥东西

1)管理很多任务

package thread;

import javafx.scene.layout.Priority;

import java.util.concurrent.PriorityBlockingQueue;

//
class MyTask{
  private Runnable runnable;
  //
    private long time;

   public MyTask(Runnable runnable,long after){
     this.runnable = runnable;
     this.time = System.currentTimeMillis() + after;
   }

   public void run(){
       runnable.run();
   }
   public long getTime(){
       return time;
   }
}

class MyTimer{
    //
    private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();
    public void schedule(Runnable runnable,long delay){
        MyTask task = new MyTask(runnable, delay);
        queue.put(task);
    }

    public MyTimer(){
        Thread t = new Thread(() -> {
            while (true){
                try {
                    MyTask task = queue.take();
                    //
                    long curTime = System.currentTimeMillis();
                    if(curTime < task.getTime()){
                        //
                        queue.put(task);
                    }else {
                        //
                        task.run();
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        t.start();

    }
}
public class Demo24 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello timer");
            }
        },3000);

        System.out.println("main");
    }

}

报错处理

Exception in thread "main" java.lang.ClassCastException: thread.MyTask cannot be cast to java

cast 转换 Comparable 讲优队列描述两个对象

像此时实现的MyTask这个类的比较规则,并不是默认存在的,这个需要手动指定,按照时间大小来比较的。

加入

@Override
public int compareTo(MyTask o) {
    return (int) (this.time - o.time);

第一个缺陷

MyTask没有指定比较规则

第二个缺陷

如果不加任何限制,这个循环就会执行的非常快

如果队列中的任务是空的,就还好,这里就阻塞了

就怕队列中的任务不空,并且任务时间还没到

上述操作,称为“忙等”

等确实是等了,但有没闲着

既没有实质性的工作产出,同时又没有进行休息

忙等非常浪费CPU

可以基于wait这样的机制来实现

那么既然是指定一个等待时间,为啥不直接用sleep,而是要在用一下wait呢

以为sleep不能被中途唤醒的

wait能够被中途唤醒

案例四:线程池

进程,比较重,频繁创建销毁,开销大,解决方案:进程池or 线程

线程,虽然比进程轻了,但是如果创建销毁的频率进一步增加,仍然会发现开销还是有的

解决方案:线程池or协程

把线程提前创建好,放到池子里

后面需要用线程,直接从池子里取,就不必从系统这边申请了

线程用完了,也不是还给系统,而是放回池子里,以备下次再用

这里的代码都称为“用户态”运行的代码

有些代码,需要调用操作系统的API,进一步的逻辑就会在内核中执行

列如,调用一个System.out.println

本质上要经过write系统调用,进入到内核中

内核执行一堆逻辑,控制显示器输出字符串

在内核中运行的代码,称为‘内核态’运行的代码

创建线程,本身就需要内核的支持

调用的Thread.start其实归根结底,也要进入内核来运行

而把创建好的线程放到“池子里”,由于池子就是用户态实现的,这个放到池子/从池子取,这个过程不需要涉及到内核态,就是纯粹的用户态代码就能完成

一般认为,纯用户态的操作,效率比经过内核态处理的操作,要效率高

 

Java标准库中的线程池

ThreadPoolExecutor

Java.util.concurrent

并发的意思

Java中很多和多线程相关的组件都在concurrent

重点看四个构造方法

以公司为例子

int corePoolSize 核心线程数        正式员工的数量

int maxmumPoolSize                最大现场(正式员工+临时工)

long keepAliveTime                允许临时摸鱼工作时间

TimeUnit unit                           时间的单位

BlockingQueue workQueue

任务队列 线程池会提供一个submit方法

让程序员把任务注册到线程池中

加到这个任务队列中

ThreadFactory threadFactory

线程工程,线程是怎么创建出来的 

RejectedExecutionHandler 

拒绝策略

当任务队列满了,怎么做

1.直接忽略最新的任务

2.阻塞等待

3.直接丢弃最老的任务

最重要的就是线程的个数

面试题

如果使用线程池的话,线程数设为多少合适?

不应该是具体的数字

通过性能测试的方法找到合适的值

列如写一个服务器程序,服务器里通过线程池,多线程的处理用户请求

就可以对这个服务器进行性能测试,比如构造一些请求,发送给服务器,要测试性能。这里的请求就需要构造很多,比如每秒发送500/1000/2000更具实际的业务场景,构造一个合适的值。

 更具这里不同的线程的线程数,来观察程序处理任务的速度,程序持有的CPU的占用率

当线程数多了,整体的速度是会变快,但是CPU占用率也会高

当线程数少,整体的速度是变慢,但是CPU占用率也会下降

需要找到一个让程序速度能接受并且CPU占用也合理这样的平衡点

不同类型的程序,因为单个任务,里面CPU上计算的时间和阻塞的时间

因此不可以直接通过具体数字来得出

搞了多线程,就是为了让程序跑得更快嘛?

为啥要考虑不让CPU占用率太高呢

对于线上服务器来说,要留有一定的冗余!!随时应对一些可能的突发情况!!

例如请求突然暴涨

如果本身已经把CPU快占用完了,这时候突然来一波请求的峰值,此时服务器可能就直接挂了

标准库中还提供了一个简化版本的线程池

Executors

本质是针对ThreadPoolExecutor进行了封装,提供了一些默认参数

通过Executors是咋用的,仿照这个实现一个线程池

package thread;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class Demo25 {
    public static void main(String[] args) {
        //创建一个固定线程数目的线程池,参数指定线程个数
        ExecutorService pool = Executors.newFixedThreadPool(10);
        //创建一个自动扩容的线程池,会根据任务量来自动进行扩容
        //Executors.newCachedThreadPool();
        //创建一个只有一个线程的线程池
        //Executors.newSingleThreadExecutor();
        //创建一个带有定时器功能的1线程池,类似于Timer
        //Executors.newScheduledThreadPool();

        for(int i = 0; i < 100; i++)

        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello threadpool");
            }
        });
    }
}

线程池里有啥

1.先能够描述任务 (直接用Runnable)

2.需要组织任务        (直接使用BlockQueue)

3.能够描述工作现场

4.还需要组织这些线程

5.需要实现,往线程池里添加任务

package thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
//import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool{
    //1.描述一个任务,直接使用Runnable,不需要额外创建类
    //2.使用一个数据结构来阻止若干个任务
    private BlockingQueue queue = new LinkedBlockingQueue<>();
    //3.描述一个线程
    static  class  Worker extends Thread{
        //当前线程池中有若干个 Worker线程 这些线程内部 都持有了上述的任务队列
        private BlockingQueue queue = null;

        public Worker(BlockingQueue 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 workers = new ArrayList<>();

    public MyThreadPool(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 Demo26 {

}
//测试代码
public class Demo26 {
    public static void main(String[] args) {
        MyThreadPool pool = new MyThreadPool(10);
        for (int i = 0; i < 100; i++){
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello threadpool");
                }
            });
        }

    }

}

多线程初阶

 


 

 

你可能感兴趣的:(java-ee,java,开发语言)