Java Thread 多线程
程序:是指令和数据的有序集合,本身没有任何运行的含义,是一个静态的概念
进程:是执行程序的一次执行过程,是一个动态的概念,是系统资源分配的单位
线程:一个进程可以包含若干个线程,一个进程至少有一个线程,不然没有存在的意义,线程是CPU调度和执行的单位。
线程创建
- 创建自定义线程类继承Thread类
重写run() 方法,编写线程执行体。创建线程对象,调用start() 方法启动线程
-
主线程如果调用run() 方法,则会先执行run() 方法
-
多条执行路径,主线程调用start() 方法,子线程就会调用run() 方法,主线程和子线程<u>交替</u>执行(即同时进行,但在同一时间只能做一件事情)
注意:
线程开启不一定立即执行,由CPU调度执行
<u>不建议使用:避免OOP单继承局限性</u>
- 创建一个线程声明实现Runnable接口
重写run() 方法,编写线程执行体。创建线程对象,调用start() 方法启动线程
在主线程里面创建Runnable接口的实现类对象
TestThread testThread = new TestThread(); //然后丢入创建的Threa类对象里
/*
Thread thread = new Thread(testThread); //创建线程对象,通过线程对象开启线程
thread.start(); */
//注释里两行代码相当于下行代码
new Thread(testThread).start();
<u>推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个对象使用。</u>
- 实现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(); }; 前提是:必须是函数式接口
单个或多个参数也可以去掉参数类型,要去就都要去掉,多个参数就需要加括号包裹
多线程的优势和存储的风险
多线程编程具备以下优势:
-
提高系统的吞吐率(Throughout),多线程编程可以使一个进程有多个并发的操作
-
提高响应性(Responsiveness),Web服务器会采用一些专门的线程负责用户的请求处理,缩短用户的等待时间
-
充分利用多核处理器资源,通过多线程可以充分的利用CPU资源
多线程编程存在的风险:
- 线程安全问题,多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生一些数据一致性问题,如读取脏数据(过期的数据),如丢失数据更新。
- 线程活性问题,由于程序自身的缺陷或者资源稀缺性导致线程一直处于非RUNNABLE状态,这就是线程活性问题,常见的活性故障有以下几种:
- 死锁(DeadLock)
- 锁死(Lockout)
- 活锁(LiveLock)
- 饥饿(Starvation)
- 上下文切换(Context Switch),处理器从执行一个线程切换到执行另外一个线程
- 可靠性,可能会由一个线程导致JVM意外终止,其他线程也无法执行
线程状态
创建状态、就绪状态、阻塞状态、运行状态、死亡状态
<u>创建状态</u>( new ) -> 调用start( ) 方法进入 <u>就绪状态</u>,等待CPU的调度,调度完后 -> 进入<u>运行状态</u>,运行状态中调用sleep、wait方法等可以使线程进入<u>阻塞状态</u> -> 阻塞状态解除后使线程又进入就绪状态 -> 如果线程正常执行完,就进入了<u>死亡状态</u>
Thread.getState();
//获取线程状态
Thread.State
- NEW :尚未启动的线程处于此状态
- RUNNABLE :在Java虚拟机中执行的线程处于此状态
- BLOCKED :被阻塞等待监视器锁定的线程处于此状态
- WAITING :正在等待另一个线程执行待定动作的线程处于此状态
- TIMED_WAITING :正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
- TERMINATED :已退出的线程处于此状态
一个线程可以在给定时间点处于一个状态,这些状态不反映任何操作系统线程状态的虚拟机状态。
thread.getState() 方法可以获取线程当前状态
线程中断或结束,一旦进入死亡状态,就不能再次启动,线程只能启动一次
线程方法
方法 | 说明 |
---|---|
setPriority(int newPriority) | 更改线程优先级 |
static void sleep(long millis) | 让当前线程休眠指定毫秒数 |
void join( ) | 等待该线程终止 |
static void yield( ) 礼让线程 | 暂停当前正在执行的线程对象,并执行其他线程 |
void interrupt( ) 不建议使用 | 中断线程,别用这个方式 |
boolean isAlive( ) | 测试线程是否处于活跃状态 |
不推荐jdk推荐的停止线程的方法(stop、destroy)
推荐线程自己停止下来,建议使用一个标志位进行终止变量,当flag=false,则终止线程运行
线程休眠:
- sleep 指定当前线程阻塞的毫秒数;
- sleep 存在异常InterruptException;
- sleep 时间达到后线程进入就绪状态;
- sleep 可以模拟网络延时,倒计时等;
- 每一个对象都有一个锁,sleep 不会释放锁;
线程礼让 yield:
- 礼让线程,让当前正在执行的线程暂停,但不阻塞
- 将线程从运行状态转为就绪状态
- 让CPU重新调度,<u>礼让不一定成功,取决于CPU</u>
合并线程 join:
- Join 合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞(插队)
线程优先级
- Java 提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
- 线程的优先级用数字表示,范围从 1~10
- Thread.MIN_PRIOROTY = 1 最小优先级
- Thread.MAX_PRIOROTY = 10 最大优先级
- Thread.NORM_PRIOROTY = 5 默认优先级(main)
注意:线程优先级大不一定先执行,真是权重更大了而已,获得的资源更多,还是看CPU的调度
- 使用以下方式该百年或获取优先级
- getPriority( )
- setPriority( int xxx )
守护线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
如:后台记录操作日志、监控内存、垃圾回收等待机制..
//设置守护线程
thread.setDaemon(true); //默认是false表示是用户线程,正常线程都是用户线程
中断线程
只是给线程打上一个中断的标志,但是线程并不会中断,需要判断过后起标志再去操作。
Thread.interrupt(); //中断
t.isInterrupted(); //判断是否中断(是否打上标志)
线程安全问题
非线程安全主要是指多个线程对同一个对象的实例变量进行操作时,会出现值被更改,值不同步的情况。
线程安全问题表现为三个方面:原子性、可见性和有序性。
-
原子性
原子(Atomic)就是不可分割的意思,原子操作的不可分割有两层含义:
- 访问(读/写)某个共享变量的操作从其他线程来看,该操作要么已经执行完毕,要么尚未发生,即其他线程看不到当前操作的中间结果
- 访问同一组共享变量的原子操作是不能交错的
Java有两种方式实现原子性:1)锁;2)处理器的CAS指令
- 锁具有排他性,保证共享变量在某一时刻只能被一个线程访问。
- CAS指令直接在硬件(处理器和内存)层次上实现,看作硬件锁
-
可见性
在多线程环境中,一个线程对某个共享变量进行更新后,后续的其他线程可能无法立即读到这个更新的结果
-
有序性
有序性(Ordering)是指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另外一个处理器运行的其他线程看来是乱序的。
乱序是指内存访问操作的顺序看起来发生了变化
在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:
- 编译器可能会改变两个操作的先后顺序;
- 处理器也可能不会按照目标代码的顺序执行;
- 这种一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码指定的顺序可能不一样,这种现象就叫重排序
- 重排序是对内存访问有序操作的一种优化,可以不影响单线程程序正确的情况下提升程序的性能,但是可能对多线程程序的正确性产生影响,即可能导致线程安全问题
可以把重排序分为指令重排序和存储子系统重排序两种:
- 指令重排序主要是由JIT编译器,处理器引起的,指程序顺序和执行顺序不一样
- 存储子系统重排序是由高速缓存,写缓冲器引起的,感知顺序与执行顺序不一致
指令重排序
在源码顺序与程序顺序不一致或者程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序(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平台中,锁的获得隐含着刷新处理器缓存的动作,而锁的释放隐含着冲刷处理器缓存的动作。
锁能够保障有序性,写线程在临界区所执行的操作,在读线程所执行的临界区看来像是完全按照源码顺序执行的。
注意:
使用锁保障线程的安全性,必须满足以下条件:
- 这些线程在访问共享数据时必须使用同一个锁
- 即使是读取共享数据的线程也需要使用同步锁
锁相关概念
-
可重入性(Reentrancy)
一个线程持有该锁的时候能再次(多次)申请该锁。即如果一个线程持有的一个锁的时候还能够继续成功申请该锁,称该锁是可重入的,否则就称该锁不可重入的。
-
锁的争用与调度
Java平台中内部锁属于非公平锁,显式锁lock既支持公平锁又支持非公平锁
-
锁的粒度
一个锁可以保护的共享数据的数量大小称为锁的粒度。
锁保护的共享数据量大,称该锁的粒度粗,否则就称该锁粒度细
锁的粒度过粗会导致线程在申请锁时会进行不必要的等待,锁的粒度过细会增加锁调度的开销。
内部锁:synchronized关键字
Java中的每个对象都有一个与之关联的内部锁,这种锁也称为监视器(Monitor),这种锁是一种排他锁,可以保障原子性、可见性和有序性
内部锁是通过synchronized关键字实现的,synchronized关键字可以修饰代码块,修饰该方法。
线程同步实现条件:队列+锁
自我理解:公共厕所的例子!线程在线程池排队访问对象,形成队列,<u>
每个对象都有一个锁
</u>一个线程访问对象后获得对象的排他锁,独占资源,使其他线程必须等待,使用完后释放锁即可
存在的问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程的竞争下,加锁会导致比较多的上下文切换 和 调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,引起优先级倒置,引起性能问题
同步方法:
synchronized 方法
public synchronized void method(int args){} //同步方法
- synchronized 方法控制对“对象”的访问,每个对象应有一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行
缺陷:若将一个大的方法申明为synchronized,将会影响效率
注意:
- 方法里面需要修改内容才需要锁,锁的太多浪费资源,只读内容不需要加锁
同步块:
synchronized (Obj) {//同步代码块,Obj:被锁的对象,方法丢在块里
同步代码块,访问共享数据
}
- Obj 称之为 同步监视器
- Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class
- 同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现没锁,然后锁定访问
注意:如果需要锁的对象就是this,那就可以直接使用同步方法,就是在方法前加synchronized,如果需要加锁的对象不是this,而是其他对象,就写同步块,锁定指定的对象,然后将方法写在块中。(哪个对象的属性进行增删改等修改了,就是需要锁的对象)
同步方法锁的粒度粗,并发效率低
同步代码块锁的粒度细,并发效率高
脏读
出现读取属性值出现一些意外,读取的是中间值,而不是修改之后的值。
出现脏读的原因是:对共享数据的修改 与对共享数据的读取不同步
解决方法:对修改数据的代码块进行同步,还要对读取数据的代码块进行同步
线程出现异常会自动释放锁
死锁:
多个线程各自占用一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能发生<u>死锁</u>问题
- 产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成形成一种头尾相接的循环等待资源关系
只需要破坏以上四个必要条件中的任意一个或多个,就能避免死锁发生。
轻量级同步机制:volatile关键字
volatile的作用
volatile关键的作用使变量在多个线程之间可见。可以强制线程从公共内存中读取变量的值,而不是从工作内存中读取。
volatile与synchronized比较
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好;volatile只能修饰变量,而synchronized还可以修饰方法,代码块。
- 多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞
- volatile能保证数据的可见性,但是不能保证原子性;而synchronized可以保证原子性,也可以保证可见性
- 关键字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锁
- 从JDK5.0开始,Java提供了强大的线程同步机制--通过显示定义同步锁对象老师先同步。同步锁使用Lock对象充当
- 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock 类实现了Lock,它拥有与synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。(ReentrantLock 可重入锁,它的功能比synchronized多)
锁的可重入性
锁的可重入性是指,当一个线程获得一个对象锁后,再次请求该对象锁时是可以获得该对象锁的。
//定义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比较常用的两个方法:
- await()会使当前线程等待,同时会释放锁。 当其他线程调用signal()时,线程会重新获得锁并继续执行。
- signal()用于唤醒一个等待线程。
注意:
在调用Condition的await()/signal()方法前,也需要线程持有相关的Lock锁。调用await()方法后线程会释放这个锁,调用signal()方法后会从当前的Condition对象的等待队列中,唤醒一个线程,唤醒的线程尝试获得锁,一旦获得锁成功后就继续执行。
公平锁和非公平锁
大多数情况下,锁的申请都是非公平的,系统只会从阻塞队列中随机选择一个线程,无法保证公平性。
公平的锁会按照时间的先后顺序,保证先到先得,公平锁这一特点不会出现线程饥饿的现象。多个线程不会发生同一个线程连续多次获得锁的可能,保证锁的公平性。公平锁看起来公平,但是要实现公平锁必须要求系统维护一个有序队列,所以公平锁的实现成本较高,性能也较低,因此默认情况下锁是非公平的。
- synchronized内部锁就是非公平的,ReentrantLock重入锁提供了一个构造方法:ReentrantLock(boolean fair),当在创建锁对象时实参传递true就可以把该锁设置为公平锁
ReentrantLock常用方法
- int getHoldCount():返回当前线程调用lock()方法的次数
- int getQueueLength():返回正等待获得锁的线程预估数
- int getWaitQueueLength(Condition condition):返回与Condition条件相关的等待的线程的预估数
- boolean hasQueuedThread(Thread thread):查询参数指定的线程是否在等待获得锁
- boolean hasQueuedThreads():查询是否还有线程在等待获得该锁
- boolean hasWaiters(Condition condition):查询是否还有线程正在等待指定的Condition条件
- boolean isFair():判断是否为公平锁
- boolean isHeldByCurrentThread():判断当前线程是否持有该锁
synchronized与Lock的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 使用优先顺序
- 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()方法可以使执行当前代码的线程等待,暂停执行,直到接到通知或被中断为止。(会释放锁)
注意:
-
wait()方法只能在同步代码块中由锁对象调用
-
调用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条件发生了变化,也可能会造成逻辑的混乱
线程协作
- 在生产者消费者问题中,仅有synchronized是不够的
- synchronized 可阻止并发更新同一个共享资源,实现了同步
- synchronized 不能用来实现不同线程之间的消息传递(通信)
- java提供了几个方法解决线程之间的通信问题
方法名 | 作用 |
---|---|
wait( ) | 表示线程会一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify( ) | 唤醒一个处于扽等该状态的线程 |
notifyAll( ) | 唤醒同一个对象上所有调用wait( )方法的线程,优先级别高的线程优先调度 |
注意:
均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IIIegaMonitorStateException
解决方式1
并发协作模式“生产者/消费者”--> <u>管程法</u>
- 生产者:负责生成数据的模块
- 消费者:负责处理数据的模块
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
生产者将生产好的数据放入缓冲区中,消费者从缓冲区拿出数据
解决方式2
并发协作模式“生产者/消费者模式”--> 信号灯法
生产者消费者模式
在java中,负责产生数据的模块是生产者,负责使用数据的模块是消费者,生产者消费者解决数据的平衡问题,即先有数据然后才能使用,没有数据时消费者需要等待。
- 生产-消费:操作数据
- 多生产-多消费:notify()不能保证是生产者唤醒消费者,如果生产者唤醒的还是生产者可能会出现假死的情况(所以唤醒操作要用notifyAll)
- 操作栈
通过管道实现线程间的通信
在java.io包中的PipeStream管道流用于在线程之间传送数据,一个线程发送数据到输出管道,另外一个线程从输入管道中读取数据。相关的类包括:PipedInputStream和PipedOutputStream,PipedReader和PipedWriter字符流。
ThreadLocal的使用
除了控制资源的访问外,还可以通过增加资源来保证线程安全。ThreadLocal主要解决为每个线程绑定自己的值
ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
线程管理
线程组
Thread类有几个构造方法允许在创建线程时指定线程组,如果在创建线程时没有制定线程组,则该线程属于父线程所在的线程组。JVM在创建main线程时会为他指定一个线程组,因此每个Java线程都有一个线程组与之关联,可以调用线程的getThreadGroup()方法返回线程组。
捕获线程的执行异常
在线程的run方法中,如果有受检异常必须进行捕获处理,如果想要获得run()方法中出现的运行时异常信息,可以通过回调UncaughtExceptionhandler接口获得哪个线程出现了运行时异常。在Thread类中有关处理运行时异常的方法有:
- getDefaultUncaughtExceptionhandler():获得全局的(默认的)UncaughtExceptionhandler
- getUncaughtExceptionhandler():获得当前线程的UncaughtExceptionhandler
- set
设置线程异常回调接口
注入Hook勾子线程
很多软件包括mysql、zookeeper、Kafka等都存在Hook线程的校验机制,目的是校验进程是否已启动,防止重复启动程序。
当JVM退出时会执行Hook线程,经常在程序启动时创建一个.lock文件,用.lock文件校验程序是否启动,在程序退出(JVM退出)时删除该.lock文件,在Hook线程中除了防止重新启动进程外,还可以做资源释放,尽量避免在Hook线程中进行复杂的操作。
线程池
-
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁,实现重复利用。
-
好处:
- 提供响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolsize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
//创建服务,创建线程池
ExecutorService service = Executor.newFixedThreadPool(x); //x:线程数
//通过线程池执行线程也可以,通过start也可以
service.execute(new MyThread());
//关闭链接
service.shutdown();
网友评论