一些收藏

Java Thread 多线程

2022-06-17  本文已影响0人  yohim

Java Thread 多线程

程序:是指令和数据的有序集合,本身没有任何运行的含义,是一个静态的概念

进程:是执行程序的一次执行过程,是一个动态的概念,是系统资源分配的单位

线程:一个进程可以包含若干个线程,一个进程至少有一个线程,不然没有存在的意义,线程是CPU调度和执行的单位。

线程创建

  1. 创建自定义线程类继承Thread类

重写run() 方法,编写线程执行体。创建线程对象,调用start() 方法启动线程

注意:

线程开启不一定立即执行,由CPU调度执行

<u>不建议使用:避免OOP单继承局限性</u>

  1. 创建一个线程声明实现Runnable接口

重写run() 方法,编写线程执行体。创建线程对象,调用start() 方法启动线程

在主线程里面创建Runnable接口的实现类对象

TestThread testThread = new TestThread(); //然后丢入创建的Threa类对象里
/*
Thread thread = new Thread(testThread); //创建线程对象,通过线程对象开启线程
thread.start(); */
//注释里两行代码相当于下行代码
new Thread(testThread).start();

<u>推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个对象使用。</u>

  1. 实现Callable 接口(了解)

实现Callable接口,需要返回值类型,重写call() 方法,需要抛出异常,创建目标对象,<u>创建执行服务</u> ExecutorService,通过服务去提交方法,最后关闭服务

好处:可以定义返回值,可以抛出异常

补充

用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体中的代码执行完毕而直接继续执行后续的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里的run()方法 称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

静态代理

静态代理模式:

好处:

Lambda表达式

new Thread( ()-> sout("nihao!") ).start();

函数式接口:

任何接口,如果只包含<u>唯一一个抽象方法</u>,那么他就是一个函数式接口

对于函数式接口,我们可以通过lambda表达式来创建该接口的对象

() -> sout() ; 语句中,前面的( ) 就是new 的接口和其包含的抽象方法

注意:

接口的实现类如果放在主类里,要加static 关键字,即作为 内部实现类

放在方法里,就是作为局部内部类

作为匿名内部类的话,new的是接口,而不是实现类

lambda表达式就是在匿名内部类的基础上省略了new 接口和重写的抽象方法,只需要留下参入的参数 -> ...

lambda表达式只能有一行代码的情况下才能简化为一行,如果有多行就必须用代码块包裹,即 -> 后加{ sout(); sout(); }; 前提是:必须是函数式接口

单个或多个参数也可以去掉参数类型,要去就都要去掉,多个参数就需要加括号包裹

多线程的优势和存储的风险

多线程编程具备以下优势:

多线程编程存在的风险:

线程状态

创建状态、就绪状态、阻塞状态、运行状态、死亡状态

<u>创建状态</u>( new ) -> 调用start( ) 方法进入 <u>就绪状态</u>,等待CPU的调度,调度完后 -> 进入<u>运行状态</u>,运行状态中调用sleep、wait方法等可以使线程进入<u>阻塞状态</u> -> 阻塞状态解除后使线程又进入就绪状态 -> 如果线程正常执行完,就进入了<u>死亡状态</u>

Thread.getState(); //获取线程状态

Thread.State

一个线程可以在给定时间点处于一个状态,这些状态不反映任何操作系统线程状态的虚拟机状态。

thread.getState() 方法可以获取线程当前状态

线程中断或结束,一旦进入死亡状态,就不能再次启动,线程只能启动一次

线程方法

方法 说明
setPriority(int newPriority) 更改线程优先级
static void sleep(long millis) 让当前线程休眠指定毫秒数
void join( ) 等待该线程终止
static void yield( ) 礼让线程 暂停当前正在执行的线程对象,并执行其他线程
void interrupt( ) 不建议使用 中断线程,别用这个方式
boolean isAlive( ) 测试线程是否处于活跃状态

不推荐jdk推荐的停止线程的方法(stop、destroy)

推荐线程自己停止下来,建议使用一个标志位进行终止变量,当flag=false,则终止线程运行

线程休眠:

线程礼让 yield:

合并线程 join:

线程优先级

注意:线程优先级大不一定先执行,真是权重更大了而已,获得的资源更多,还是看CPU的调度

守护线程

如:后台记录操作日志、监控内存、垃圾回收等待机制..

//设置守护线程
thread.setDaemon(true);     //默认是false表示是用户线程,正常线程都是用户线程

中断线程

只是给线程打上一个中断的标志,但是线程并不会中断,需要判断过后起标志再去操作。

Thread.interrupt(); //中断

t.isInterrupted(); //判断是否中断(是否打上标志)


线程安全问题

    非线程安全主要是指多个线程对同一个对象的实例变量进行操作时,会出现值被更改,值不同步的情况。

    线程安全问题表现为三个方面:原子性、可见性和有序性。

指令重排序

    在源码顺序与程序顺序不一致或者程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序(Instruction Reorder)

    指令重排是一种动作,确实对指令的顺序做出了调整,重排序的对象指令

    javac编译器一般不会执行指令重排序,而 JIT编译器可能执行指令重排序,处理器也可能执行指令重排序,使得执行顺序和程序顺序不一致。

    指令重排不会对单线程程序的结果正确性产生影响,可能导致对多线程程序出现非预期结果。

存储子系统重排序

    存储子系统是指写缓冲器与高速缓存。

    高速缓存(Cache)是CPU中为了匹配与主内存处理速度不匹配而设计的一个高速缓存。

    写缓冲器(Store buffer,Write buffer)用来提高写高速缓存操作的效率

    即使处理器严格按照程序顺序执行两个内存访问操作,在存储子系统的作用下,其他处理器对这两个操作的感知顺序与程序顺序不一致,即这两个操作的执行顺序看起来像是发生了变化,这种现象称为 存储子系统重排序。

    存储子系统重排序并没有真正的对指令执行顺序进行调整,而是造成一种指令执行顺序被调整的假象。

    存储子系统重排序对象是内存操作的结果。

保证内存访问的顺序性

    实质上就是怎么解决重排序导致的线程安全问题。

    可以使用volatile关键字,synchronized关键字实现有序性。

线程同步

    线程同步机制是用于协调线程之间的数据访问的机制,该机制可以保障线程安全。

    Java平台提供的线程同步机制包括:锁Lock,volatile关键字,final关键字,static关键字,以及相关的API,如Object.wait()/Object.notify()等

<u>同一个对象被多个线程同时操作,</u>这时候就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕,下一个线程再使用

锁的概述

    锁具有排他性(Exclusive),即一个锁一次只能被一个线程持有,这种锁称为排他锁或互斥锁(Mutex)

    **JVM把锁分为内部锁和显式锁,内部锁通过synchronized关键字实现;显式锁通过java.concurrent.locks.lock接口的实现类实现的。**

锁的作用

    锁可以实现对共享数据的安全访问,保障线程的原子性,可见性与有序性。锁是通过互斥保障原子性,一个锁只能被一个线程持有,这就保证临界区的代码一次只能被一个线程执行。使得临界区代码所执行的操作自然而然的具有不可分割的特性(原子性)

    可见性的保障是用过写线程冲刷处理器的缓存和读线程刷新处理器缓存这两个动作实现的。在java平台中,锁的获得隐含着刷新处理器缓存的动作,而锁的释放隐含着冲刷处理器缓存的动作。

    锁能够保障有序性,写线程在临界区所执行的操作,在读线程所执行的临界区看来像是完全按照源码顺序执行的。

注意:

    使用锁保障线程的安全性,必须满足以下条件:

锁相关概念

内部锁:synchronized关键字

    Java中的每个对象都有一个与之关联的内部锁,这种锁也称为监视器(Monitor),这种锁是一种排他锁,可以保障原子性、可见性和有序性

    内部锁是通过synchronized关键字实现的,synchronized关键字可以修饰代码块,修饰该方法。

线程同步实现条件:队列+锁

自我理解:公共厕所的例子!线程在线程池排队访问对象,形成队列,<u>每个对象都有一个锁</u>一个线程访问对象后获得对象的排他锁,独占资源,使其他线程必须等待,使用完后释放锁即可

存在的问题:


同步方法:

synchronized 方法

public synchronized void method(int args){}     //同步方法

缺陷:若将一个大的方法申明为synchronized,将会影响效率

注意:

  • 方法里面需要修改内容才需要锁,锁的太多浪费资源,只读内容不需要加锁

同步块:

synchronized (Obj) {//同步代码块,Obj:被锁的对象,方法丢在块里
    同步代码块,访问共享数据
}   

注意:如果需要锁的对象就是this,那就可以直接使用同步方法,就是在方法前加synchronized,如果需要加锁的对象不是this,而是其他对象,就写同步块,锁定指定的对象,然后将方法写在块中。(哪个对象的属性进行增删改等修改了,就是需要锁的对象)

    同步方法锁的粒度粗,并发效率低

    同步代码块锁的粒度细,并发效率高

脏读

出现读取属性值出现一些意外,读取的是中间值,而不是修改之后的值。

出现脏读的原因是:对共享数据的修改 与对共享数据的读取不同步

解决方法:对修改数据的代码块进行同步,还要对读取数据的代码块进行同步

线程出现异常会自动释放锁


死锁:

多个线程各自占用一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能发生<u>死锁</u>问题

只需要破坏以上四个必要条件中的任意一个或多个,就能避免死锁发生。


轻量级同步机制:volatile关键字

volatile的作用

    volatile关键的作用使变量在多个线程之间可见。可以强制线程从公共内存中读取变量的值,而不是从工作内存中读取。

volatile与synchronized比较

volatile非原子性

    volatile关键字增加了实例变量在多个线程之间的可见性,但是它不具备原子性

常用的原子类进行自增自减操作

    i++操作不是原子操作,所以不能保证线程安全,除了使用synchronized进行同步外,也可以使用AtomicInteger/AtomicLong原子类进行实现。

CAS(Compare And Swap)

    CAS是由硬件实现的。

    CAS可以将read- modify - write这类的操作转换为原子操作

CAS原理:

    在把数据更新到主内存时,再次读取主内存变量的值,如果现在变量的值与期望的值(操作起始时读取的值)一样就更新。

    CAS实现原子操作背后有一个假设:共享变量的当前值与当前线程提供的期望值相同,就认为这个变量没有被其他线程修改过。

    但是,实际上这种假设不一定总是成立。CAS会有ABA问题发生

    如果想要规避ABA问题,可以为共享变量引入一个修订号(时间戳),每次修改共享变量时,相应的修订号就会增加1,每次对共享变量的修改都会导致修订号的增加,通过修订号依然可以准确判断是否被其他线程修改过。AtomicStampedReference类就是基于这种思想产生的。

原子变量类

    原子变量类是基于CAS实现的,当对共享变量进行read- modify - write更新操作时,通过原子变量类可以保障操作的原子性和可见性。对变量的read- modify - write更新操作是指当前操作不是一个简单的赋值,而是变量的新值依赖变量的旧值。由于volatile只能保障变量的可见性,无法保障原子性,原子变量类内部就是借助一个volatile变量,并且保障了该变量的read- modify - write操作的原子性,有时把原子变量类看作增强的volatile变量,原子变量类有12个:
分组 原子变量类
基础数据型 AtomicInteger,AtomicLong,AtomicBoolean
数组型 AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
字段更新器 AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater
引用型 AtomicReference,AtomicStampedReference,AtomicMarkableReference

ReentrantLock锁

锁的可重入性

锁的可重入性是指,当一个线程获得一个对象锁后,再次请求该对象锁时是可以获得该对象锁的。

//定义lock锁
ReentrantLock lock = new ReentrantLock();
lock.lock()     //加锁
lock.unlock()       //解锁

lockInterruptibly()

lockInterruptibly()方法的作用:如果当前线程未被中断则获得锁,如果当前线程被中断则出现异常。<u>可以解决死锁问题</u>

lock.lock();                //获得锁定,即使调用了线程的interrupt()方法,线程也不会真正中断
lock.lockInterruptibly();           //如果线程中断了,不会获得锁,会发生异常

tryLock()方法

tryLock(long time,TimeUnit unit)的作用在给定等待时长内锁没有被另外的线程持有,并且当前线程也没有中断,则获得该锁,通过该方法可以实现锁对象的限时等待。

tryLock()无参方法仅在调用时锁定未被其他线程持有的锁,如果调用方法时,锁对象被其他线程持有,则放弃。

<u>使用tryLock()可以避免死锁问题</u>

newCondition()方法

关键字synchronized与wait()/notify()这两个方法一起使用可以实现等待/通知模式,Lock锁的newCondition()方法返回Condition对象,Condition类也可以实现等待/通知模式。

使用notify()通知时,JVM会随机唤醒某个等待的线程,使用Condition类则可以<u>进行选择性通知</u>。

Condition比较常用的两个方法:

注意:

在调用Condition的await()/signal()方法前,也需要线程持有相关的Lock锁。调用await()方法后线程会释放这个锁,调用signal()方法后会从当前的Condition对象的等待队列中,唤醒一个线程,唤醒的线程尝试获得锁,一旦获得锁成功后就继续执行。

公平锁和非公平锁

大多数情况下,锁的申请都是非公平的,系统只会从阻塞队列中随机选择一个线程,无法保证公平性。

公平的锁会按照时间的先后顺序,保证先到先得,公平锁这一特点不会出现线程饥饿的现象。多个线程不会发生同一个线程连续多次获得锁的可能,保证锁的公平性。公平锁看起来公平,但是要实现公平锁必须要求系统维护一个有序队列,所以公平锁的实现成本较高,性能也较低,因此默认情况下锁是非公平的。

ReentrantLock常用方法

synchronized与Lock的对比

对于synchronized内部锁来说,如果一个线程在等待锁,只有两种结果:要么该线程获得锁继续执行,要么就保持等待

对于ReentrantLock可重入锁来说,提供另外一种可能,在等待锁的过程中,程序可以根据需要取消对锁的请求。

ReentrantReadWriteLock读写锁

synchronized内部锁和ReentrantLock锁都是独占锁(排他锁),同一时间只允许一个线程执行同步代码块,可以保证线程的安全性,但是执行效率低。

ReentrantReadWriteLock读写锁是一种改进的排他锁,也可以称作共享/排他锁。允许多个线程同时读取共享数据,但是一次只允许一个线程对共享数据进行更新。

读写锁通过读锁和写锁来完成读写操作。线程在读取共享数据前必须先持有读锁,该读锁可以同时被多个线程持有,即它是共享的。写锁是排他的,线程在更新共享数据前必须先持有写锁, 一个线程持有写锁时其他线程线程无法获得相应的锁。

读锁只是在读线程之间共享,任何一个线程持有读锁时,其他线程都无法持有写锁,保证线程在读取数据期间没有其他线程对数据进行更新,使得读线程能够读取数据的最新值,保证读数据期间共享变量不被修改

//定义读写锁
ReadWriteLock rwlock = new ReentrantReadWriteLock();
//获得读锁
Lock readLock = rwlock.readLock();
//获得写锁
Lock writeLock = rwlock.writeLock();
//读线程方法
readLock.lock();
try{
        读取数据;
}finally{
        readLock.unlock();
}
//写线程方法
writeLock.lock();
try{
        更新数据;
}finally{
        writeLock.unlock(); 
}

等待/通知机制

Object类中的wait()方法可以使执行当前代码的线程等待,暂停执行,直到接到通知或被中断为止。(会释放锁)

注意:

  1. wait()方法只能在同步代码块中由锁对象调用

  2. 调用wait()方法,当前线程会释放锁

    Object类中的notify()可以唤醒线程,该方法也必须在同步代码块中由锁对象调用,没有使用锁对象调用wait()/notify()方法会抛出llegalMonitorStateException异常,如果有多个等待的线程,notify()方法只能唤醒其中一个。在同步代码块中调用notify()方法后,并不会立即释放锁对象,需要等当前同步代码块执行完后才会释放锁对象,一般将notify()放在同步代码块的最后。

interrupt()方法会中断wait()等待

interrupt方法会中断wait方法,会释放锁。

wait(long)的使用

wait(long)带有long类型参数的wait()等待,如果在参数指定的时间内没有被唤醒,超时后会自动唤醒

通知过早问题

线程wait()等待后,可以调用notify()唤醒线程,如果notify()唤醒的过早,在等待之前就调用了,notify()可能会打乱程序正常的运行逻辑(就是唤醒线程在等待线程之前先执行了,导致等待线程无法被唤醒,可以定义一个静态变量static boolean flag = true;作为线程运行的标志,在等待线程里加一个条件while(flag)判断线程状态,在唤醒线程中,将flag改为false,这样如果先执行唤醒线程,那等待线程也不会执行)

wait等待条件发生了变化

在使用wait/notify模式时,如果wait条件发生了变化,也可能会造成逻辑的混乱

线程协作

方法名 作用
wait( ) 表示线程会一直等待,直到其他线程通知,与sleep不同,会释放锁
wait(long timeout) 指定等待的毫秒数
notify( ) 唤醒一个处于扽等该状态的线程
notifyAll( ) 唤醒同一个对象上所有调用wait( )方法的线程,优先级别高的线程优先调度

注意:

均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegaMonitorStateException

解决方式1

并发协作模式“生产者/消费者”--> <u>管程法</u>

生产者将生产好的数据放入缓冲区中,消费者从缓冲区拿出数据

解决方式2

并发协作模式“生产者/消费者模式”--> 信号灯法

生产者消费者模式

在java中,负责产生数据的模块是生产者,负责使用数据的模块是消费者,生产者消费者解决数据的平衡问题,即先有数据然后才能使用,没有数据时消费者需要等待。

  1. 生产-消费:操作数据
  2. 多生产-多消费:notify()不能保证是生产者唤醒消费者,如果生产者唤醒的还是生产者可能会出现假死的情况(所以唤醒操作要用notifyAll)
  3. 操作栈

通过管道实现线程间的通信

在java.io包中的PipeStream管道流用于在线程之间传送数据,一个线程发送数据到输出管道,另外一个线程从输入管道中读取数据。相关的类包括:PipedInputStream和PipedOutputStream,PipedReader和PipedWriter字符流。

ThreadLocal的使用

除了控制资源的访问外,还可以通过增加资源来保证线程安全。ThreadLocal主要解决为每个线程绑定自己的值

ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。

线程管理

线程组

Thread类有几个构造方法允许在创建线程时指定线程组,如果在创建线程时没有制定线程组,则该线程属于父线程所在的线程组。JVM在创建main线程时会为他指定一个线程组,因此每个Java线程都有一个线程组与之关联,可以调用线程的getThreadGroup()方法返回线程组。

捕获线程的执行异常

在线程的run方法中,如果有受检异常必须进行捕获处理,如果想要获得run()方法中出现的运行时异常信息,可以通过回调UncaughtExceptionhandler接口获得哪个线程出现了运行时异常。在Thread类中有关处理运行时异常的方法有:

设置线程异常回调接口

注入Hook勾子线程

很多软件包括mysql、zookeeper、Kafka等都存在Hook线程的校验机制,目的是校验进程是否已启动,防止重复启动程序。

当JVM退出时会执行Hook线程,经常在程序启动时创建一个.lock文件,用.lock文件校验程序是否启动,在程序退出(JVM退出)时删除该.lock文件,在Hook线程中除了防止重新启动进程外,还可以做资源释放,尽量避免在Hook线程中进行复杂的操作。

线程池

//创建服务,创建线程池
ExecutorService service = Executor.newFixedThreadPool(x); //x:线程数
//通过线程池执行线程也可以,通过start也可以
service.execute(new MyThread());
//关闭链接
service.shutdown();
上一篇 下一篇

猜你喜欢

热点阅读