JavaEE_01_多线程基础

程序和进程?

程序:一系列有组织的文件、封装OS的各种API,实现不同的效果。

进程

程序在系统中的一次执行过程

进程是现代操作系统中分配资源(CPU,内存等关键资源)的最小单位,不同进程之间的是相互独立的。

IP地址

唯一在网络上标识一台主机的位置

通过IP地址找到该主机后,如何定位该主机上的一个进程?

端口号:在操作系统上能唯一地定位一个进程。

进程的端口号一般不变,进程的PCB(操作系统描述本次进程的信息结构)在每次启动进程时都会改变。

线程

线程就是进程中的一个子任务,线程是OS系统调度(任务执行)的基本单位

同一个进程的所有线程共享进程的资源

在这里插入图片描述

该CPU是一个四核八线程的处理器。每个核心(Core)能并行处理两个子任务。

进程线程的区别
  • 创建和销毁进程的开销远比创建和销毁线程大得多(操作进程比操作线程慢),线程更加轻量化
  • 调度一个线程比调度一个进程快得多
  • 进程包含线程,每个进程至少包含一个(主)线程
  • 进程之间相互独立,不同的进程不会共享内存空间,同一进程的线程共享内存空间。

java 主类名称 :就是启动一个java进程

JDK提供的线程库实际上就是利用操作系统中的线程库进行了二次封装


Thread

通过java.lang.Thread启动一个新的线程

public class Test {
    private class MyThread extends Thread {
	    public void run() {
        	//TODO
    	}
	}
    public class static void main() {
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();
        MyThread m3 = new MyThread();
        m1.start();
        m2.start();
        m3.start();
        //TODO
    }
}
//线程的开启是手动执行一个线程对象的start()方法,线程开启后,每个线程要做的事都在run()方法内,run()由JVM在线程开启后自动执行。

在这里插入图片描述

jconsole

在Terminal中输入jconsole命令,连接本地进程,使用java监视和管理控制台

创建线程
四种方法
  1. 继承Thread类,覆写run()

    //该类对象是一个线程实体
    

    线程启动后,JVM产生操作系统的线程,什么时候执行run(),由JVM决定,对用户不可见,无法控制。

  2. 实现Runnable((java.lang.Runnable))接口,覆写run()

    //该类实现了Runnable接口,只是一个任务对象
    class RunnableTest implements Runnable {
        public void run() {
            //TODO
        }
    }
    public class Main() {
        public class static void main() {
            RunnableTest runnableTest = new RunnableTest();
            Thread thread = new Thread(runnableTest);
            thread.start();
            //main.TODO
        }
    }
    

    推荐使用方式2,子类还能继承别的类,实现别的接口

    变形写法,使用匿名内部类、Lambda表达式简化上述写法。

    上述两种方法,启动该线程,都是通过Thread类的start()

  3. 实现Callable接口,覆写call()

  4. 使用线程池创建线程

多线程最大的应用场景:把一个大任务拆分为多个子任务,多个子线程并发执行,提高系统的处理效率。

创建线程的方法

Thread类的方法

每个线程都对应唯一的一个Thread对象

start()方法
线程对象调用start()方法后,再次调用start()方法:抛出异常:
IllegalThreadStateException
构造方法
Thread t1 = new Thread();

Thread t2 = new Thread(() -> {
    //new Thread(),括号里要传入一个实现了Runnable接口的类对象
    //这里用Lambda表达式简化写法,因为该接口内部只有一个抽象方法run(),可以用Lambda表达式。
    //()表示该类对象内部实现的run()的参数列表
    //->表示  实现
    //{}内部就是run()方法的具体实现
});

String name = "thread_name";  //为创建的线程起一个名字
Thread t3 = new Thread(name);

Thread t4 = new Thread(() -> {
    //TODO
},"这是线程的名字");

//线程可以被用来分组,分好的组就是线程组。
Thread t5 = new Thread(ThreadGroup group,Runnable target);//target是实现了Runnable接口的类对象
核心属性
//设有一个线程对象:thread,通过以下方法获取其常见属性
thread.getId();//ID:唯一标识线程,不同的线程,其ID不会重复
thread.getName();//名字
thread.getState();//状态
thread.getPriority();//优先级:建议JVM先执行优先级较高的线程,并非一定按照优先级从高到低执行线程。
thread.isDaemon();//是否是后台线程,JVM会在一个进程的所有非后台线程结束后,才会结束运行。
thread.isAlive();//是否存活,简单理解为:run()方法是否运行结束。
thread.isInterrupted();//该线程是否是中断状态。
中断线程

中断线程是线程间通信的一种方法

中断:在run()还未执行完的时候,中断其执行过程。

通过共享变量中断
volatile boolean isInterrupted;

代码

Thread类的所有静态方法,在哪个线程中使用就生效在哪个线程。

通过静态方法Thread.interrupted()或成员方法:Thread.currentThread().isInterrupted()

代码

分为两种情况:

  1. 当线程(thread)调用sleep(),wait(),join()等方法处在阻塞时,收到中断通知:thread.interrupt(),就会抛出中断异常:InterruptedException.当抛出异常后,当前线程的中断状态就会被清除。
  2. 当线程(thread)没有调用以上三种方法,处在正常运行时,收到中断通知:thread.interrupt(),
    1. 在该中断线程内,使用Thread.intrrrupted(),判断当前线程是否是中断状态,若状态为true,则清除中断状态,此后该线程就不是中断状态了。
    2. 在该中断线程内,使用Thread.currentThread().isInterrupted(),判断当前线程是否是中断状态,若状态为true,则不会清除中断状态,此后该线程还是处于中断状态。

Java中线程的启动、终止、中断,与程序员无关。一个线程的run()执行结束,该线程自动销毁。

Thread常用方法
线程对象A
线程对象B

在线程A中调用B.join():含义是,线程A进入等待状态,直到线程B执行结束,线程A恢复执行状态。

在这里插入图片描述

//Thread类的静态方法在哪个线程调用,就作用于哪个线程
Thread.currentThread();//获取当前线程对象的引用

Thread.sleep(m);//休眠m毫秒
//由于线程的调度不可控,只能保证实际休眠时间 >= 参数设置的休眠时间。
线程的状态
new:新建状态,还未开始执行start()方法
runnable:可执行状态(即将开始执行:ready和正在执行:running)
//等待状态,当前线程暂停执行,等待其他任务或资源
blocked:锁等待,需要等待其他线程释放锁对象。
waiting:执行wait()方法后,进入等待状态,等待其他线程唤醒(notify()方法)该线程。
timed_waiting:超时等待,等待的时间到了,就会被唤醒,切换为ready态。
//等待状态和ready不同,等待状态有一个等待条件,满足等待条件后就进入ready状态。而ready只是在任务队列中在排队,前面的任务执行完毕就会将当前任务置为running态。
terminated:当前线程已经执行结束(run()执行完毕)或者抛出异常进入终止状态,可以被销毁。
//一个线程除了new和terminate,其他状态都是存活状态(isAlive()判定为true)

Thread.yield();
//调用yield()的线程会主动让出CPU资源,从运行态转为ready态,等待被CPU再次调度。
//什么时候让出CPU,什么时候再次被CPU调度,都是由OS决定!
//有可能让出资源后,立马又被重新调度(执行)。
//也有可能让出资源后,很久不再被调度(执行)。

在这里插入图片描述

线程安全

多线程执行和将其转换为单线程执行得到的结果完全一致:线程安全;若结果不一致,则线程不安全。

JMM:

Java Memory Model

Java的内存模型,区别于JVM的内存区域划分(真实存在的六大区域).

JMM是描述多线程场景下,Java的线程内存(CPU的高速缓存和寄存器)和主内存(物理上存在)的关系

为什么需要设置CPU的高速缓存和寄存器?

速度快,但考虑到价格,所以容量有限。

每个线程都有自己的工作内存,每次读取共享变量(共享变量,不是线程的局部变量)都是先从主内存将变量加载到自己的工作内存,之后关于此变量的所有操作都是在自己的工作内存中进行,然后写回主内存。

共享变量:类中的成员变量,静态变量,常量,在堆和方法区中存储。

线程安全需要满足的条件
原子性

一个操作对应CPU的一条指令,这个操作不会被中断。这个操作要么全部执行,要么都不执行,只有这两种可能。

可见性

一个线程对共享变量的修改,能够及时被其他线程看到。

防止指令重排

代码的书写顺序不一定是JVM或CPU的执行顺序(编译器或CPU会在逻辑不改变的情况下,优化指令)。

在多线程场景下就有可能会出错:比如对象还未初始化,就被别的线程用了。

synchronized

使用synchronized可满足可见性和原子性。

synchronized就是监视器锁:monitor lock(对象锁)

mutex lock

某个线程获取到该对象的锁时,其他线程若也要获取同一个对象的锁,就会处在阻塞等待状态。

在Java内部,每个对象都有一块内存,描述当前对象的锁信息(当前对象被哪个线程持有,保存的是线程的id)。

在这里插入图片描述

刷新内存

synchronized关键字执行流程:

  1. 获取对象锁
  2. 从主内存拷贝共享变量值到持有当前对象锁的线程的工作内存
  3. 执行上锁的代码块
  4. 将更改后的值写回主内存
  5. 释放对象锁
可重入

Java中的线程安全锁都是可重入的

可重入:在该线程进入某个同步代码块以后,该对象上锁。在代码块内调用该对象的其他同步代码块时,不需要再重新获取该对象的锁

Java中每个对象有一个对象头,描述当前对象的锁信息(当前对象被哪个线程持有,保存线程ID)以及一个计数器(当前对象被上锁的次数)。

  • synchronized,加锁和解锁操作是隐式的,由JVM进行加锁和解锁。
  • 每当进入一次同步代码块,就会自动上锁,并将计数器+1;
  • 每当调用结束一次同步代码块,就会自动解锁,并将计数器-1;
  • 该对象可被外来线程获取的条件:锁信息为null(即:没有线程占有它)且计数器值为0.
synchronized是对象锁

必须有个具体的对象,执行被锁的动作。

  1. synchronized修饰类中的成员方法,锁住的就是当前调用该方法的对象

  2. synchronized修饰静态方法,锁的是当前这个类的class对象(该对象全局唯一),该类的所有对象都被上锁。(谨慎使用

    JVM加载类时,产生该类对应的唯一的一个class对象

  3. synchronized修饰代码块(锁的粒度更细,只有在需要同步的若干代码加上锁)

在这里插入图片描述

当锁的对象由 当前对象this变为一个全局唯一的对象Reentrant.class,就会变成多个线程竞争唯一一个对象,构成互斥。

Java标准库中的线程安全类
//线程不安全的常用类
ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder

//线程安全的类
Vector//不推荐用
HashTable//不推荐用
//以上两个上锁的粒度太粗,对于多线程操作,直接将涉及到的对象上锁,效率低。

ConcurrentHashMap//锁的是哈希桶(每个数组元素对应的链表)对象
StringBuffer
CopyOnWriteArrayList//线程安全的List集合,采用读写分离的机制:读的时候可以多线程并发,写的时候要获取锁。

java.util.concurrent:并发工具包
volatile
可见性

保证共享变量的可见性

当线程读取的内容由volatile修饰,线程直接从主内存中读取该值

当线程写的是由volatile修饰的变量,将当前工作内存修改后的变量立即刷新到主内存中(其他正在读取该变量的线程会等待,直到写回主内存的动作完成,保证读取到的是刷新后的内容)。

同一个volatile,其写操作一定发生在其读操作之前。

相当于强制读写主内存的变量值。

synchronized可同时保证原子性和可见性

volatile只能保证可见性,任何对共享变量的修改都能及时被其他线程看到。

不能保证原子性,如果一个操作需要由多个原子性操作组合来完成,volatile无法像synchronized那样,保证这一系列原子性操作的组合仍然是原子性的。

两者不能视为作用相等

内存屏障

在这里插入图片描述

当线程sleep或yield让出CPU资源,等到CPU重新调度该线程时,大概率会重新读一下主存,刷新工作内存。不一定百分百

这样一来,对于没有用volatile修饰的共享变量,在别的线程修改的值在本线程内可见了。

但是,这并不意味着没有用volatile修饰的共享变量在不同线程间就可以保证可见性了。

final修饰的常量也是可以保证可见性的

线程间等待与唤醒

wait()和notify()是Object类的方法,用于线程的等待与唤醒,须搭配synchronized来使用。

多线程并发的场景,有时需要某些线程先执行,这些线程执行结束后其他线程再继续执行。

wait();//死等,进入waitting状态,直到其他线程唤醒此线程。
wait(long timeout);//等待一段时间,若超过此段时间还未被唤醒,自动恢复执行。

notify();//随机唤醒一个处在等待状态的线程
notifyAll();//唤醒所有等待状态的线程

wait()和notify()如果不搭配synchronized使用,就会抛出illegalMonitorStateException.

一个线程内部,调用对象.wait(),就会让当前线程进入waitting状态,等待被其他线程唤醒:线程内部调用同一个对象.notify().

一个线程被唤醒之后,从等待处的下一行代码开始执行。

wait()方法会释放当前对象锁,sleep()不会释放锁。

wait()执行后,必须由其他线程唤醒 或者 wait(millis)后,到时间自动唤醒

  • 阻塞队列

    获取锁失败的线程会进入阻塞队列,当对象锁释放后,从阻塞队列中竞争对象锁。

  • 等待队列

    获取到对象锁的线程调用对象.wait(),就会释放该对象锁,此线程进入等待队列,阻塞队列中的线程重新竞争对象锁。处于等待队列中的线程等待别的线程执行:对象.notify(),来唤醒等待状态的线程。唤醒的规则:将等待队列中的线程随机唤醒一个(对象.notify()方法)或全部唤醒(对象.notifyAll()方法),唤醒后的线程全部放入阻塞队列中,与阻塞队列中原有的线程一起竞争对象锁。

//返回当前还未执行完的线程数量,包括主线程和一个后台线程
Thread.activeCount()

运动员裁判

设计模式——单例模式

保证某个类在程序中只有一个对象

构造方法私有化,在类内部唯一一次调用该类的构造方法创建一个外部可见的静态成员

饿汉式

类一经加载,就会产生该对象

天然的线程安全

懒汉式

只有调用对外可见的获取唯一对象的方法时,才会产生该对象

系统初始化时,外部不需要该对象就不产生;只有外部需要该单例对象才会实例化该对象–>懒加载lazy load

线程不安全

double check

用细粒度锁保证多线程场景下,懒汉式单例只产生一个对象。

懒汉式单例模式的实现

volatile修饰单例对象 防止初始化中断

线程1先进入同步代码块,当线程1的单例对象初始化还未完成,线程2此时判断该初始化了一部分的单例对象已经不为空,若没有在单例对象初始化的地方设置内存屏障,线程二就会直接返回一个不完整的对象!

所以,需要将单例对象用volatile修饰,其初始化的地方形成一个屏障:必须等该对象初始化结束后,才能执行其后的代码(对于所有线程而言都是这样)。

阻塞队列

在这里插入图片描述

生产者消费者模型

在这里插入图片描述

  • 阻塞队列相当于一个缓冲区,平衡了生产者和消费者的处理能力
  • 阻塞队列能使生产者和消费者之间解耦
JDK中的阻塞队列BlockingQueue

入队:put()

出队:take()

ArrayBlockingQueue

LinkedBlockingQueue

实现阻塞队列的基础上,再实现生产者消费者模型

定时器
import java.util.Timer
Timer timer = new Timer();
timer.schedule(new TimerTask() {
    public void run() {
        //要执行的任务
    }
},3000,1000);
timer.schedule()的参数:
第一个是要执行的任务对象;
第二个是delay,延迟3000ms开始执行任务;
第三个是period,开始执行后每隔1000ms循环执行一次任务

线程池

***池:存在的目的就是让某些对象被多次重复利用,减少频繁创建和销毁对象带来的开销。

例如数据库连接池,线程池,字符串常量池

线程池内部创建好了若干个线程,都是runnable态的。线程池最大的好处就是减少每次启动和销毁线程的损耗,提高时间和空间效率。

ExecutorService

线程池的核心父接口

最常用的子类实现:ThreadPoolExecutor

核心方法:

//提交一个任务(就是Callable接口的call()方法或Runnable接口的run()方法)到线程池,线程池派遣空闲的线程执行任务。(推荐使用)
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
//线程池接受一个Runnable对象并执行
public void execute(Runnable command) {...};

//尝试销毁线程池中所有线程,无论其是否处于工作状态
List<Runnable> shutdownNow();
//立即销毁当前处于空闲状态的线程,待工作状态的线程工作结束之后再销毁
void shutdown();

四大线程池的用法

都是基于ThreadPoolExecutor类来实现

在这里插入图片描述

涉及到的类和接口关系图

在这里插入图片描述

ThreadPoolExecutor的工作流程
//ThreadPoolExecutor类的构造函数
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }
//corePoolSize --> 相当于餐厅的正式员工数量
//maximumPoolSize --> 相当于餐厅的正式员工+临时工的数量
//keepAliveTime,unit --> 空闲的临时工,在经过keepAliveTime个unit单位的时间后就会自动解雇(对应线程的销毁)
//workQueue --> 阻塞队列,当线程池正式员工满负荷,新来的任务进入阻塞队列,阻塞队列有一个容量
//handler --> 拒绝策略:当该线程池整体满负荷以后,新来的任务该如何拒绝-->
1.AbortPolicy:满负荷后,对于新来的任务直接拒绝,抛出异常
2.CallerRunsPolicy:返回线程池的调用者处理
3.DiscardOldestPolicy:丢弃队列(workQueue)中最老的任务,也就是排队时间最长的任务。
4.DiscardPolicy:丢弃排队时间最短的任务。
  1. 当一个线程池创建好之后,新来任务后,先看corePoolSize是否达到最大数量,若未达到最大数量,无论其他核心线程(正式员工)是否空闲,都要创建一个核心线程,由此线程来执行任务。
  2. 若corePoolSize已达到最大值,则判断工作队列(BlockingQueue实现)是否已满。若队列未满,任务直接入队。
  3. 若队列已满,则判断maximumPoolSize(正式员工+临时工数量)是否已达最大值,若未达到最大值,则创建新的临时线程(招聘临时工)来执行该任务。
  4. 若maximumPoolSize已达最大值,则此线程池已满负荷,采取某种拒绝策略,对应参数handler

阿里编码规范:尽量不使用内置线程池,根据需求定制线程池。

乐观锁和悲观锁
乐观锁

每次读写数据都认为不会发生冲突,若线程冲突则当前线程不会阻塞,转而处理其他事情,过段时间再来尝试读写数据。当线程冲突不严重时,可以采用乐观锁来避免多次的加锁、解锁操作。

乐观锁的实现–通过版本号
  • 线程从主存读取数据和该数据对应的版本号到自己的工作内存
  • 对数据进行处理后,要写回主存时,检查主存数据的版本号与工作内存数据的版本号是否一致:
  • 若一致,则主存和工作内存的该数据的版本号都+1;
  • 若不一致,则采取两种策略:1.该线程报错,退出线程;2.CAS策略:从主存中读取最新的数据及其版本号,重新进行一轮操作,再次尝试写回主存,上述操作循环进行,直到版本号匹配为止。
悲观锁

每次读写数据都认为会产生线程冲突,会尝试加锁。当线程冲突严重时,采用乐观锁来避免线程频繁访问共享数据带来的CPU空转问题。

读写锁

并发读数据时,不会有线程安全问题,只有并发地增、删、改数据才会有线程安全问题。

  • 多个线程可并发访问读锁(读数据)
  • 多个线程访问写锁,同一时间只有一个线程能获取到写锁,进行写操作
  • 当线程既有读锁、又有写锁,则:只有写锁进行完毕,读锁才能开始。
重量级锁和轻量级锁
重量级锁

需要OS和硬件支持,线程获取重量级锁失败进入阻塞状态,OS从用户态切换到内核态(此过程开销非常大)。

轻量级锁

尽量在用户态执行操作,线程不阻塞,不会进行状态切换。

轻量级锁的常用实现

自旋锁:循环。当获取不到该锁时,定期循环访问对象,尝试获取该锁。线程不会入阻塞队列,不会进入Blocked态,不需要被唤醒(因此,当锁被释放时,可以较快地获取到该锁)。

公平锁和非公平锁

公平锁:当锁被释放时,最先在阻塞队列中的线程获取到锁;

非公平锁:当锁被释放,阻塞队列中的所有线程一起竞争锁。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AbyssPraise

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值