Java多线程与高并发基础
一.线程与进程的相关概念
1.1 进程
是操作系统进行资源分配和调度的基本单位,通俗一点讲就是正在运行的程序
1.2 线程
线程就是进程中的一个执行单元,进程的一个执行分支
举例 :运行的QQ音乐程序是一个进程,那么放歌和展示歌词这两个同时执行的操作,就可以看作是两个线程
1.3 进程和线程的关系
进程是线程的容器,一个进程可以有多个线程
二.主线程和子线程
2.1 主线程
在JVM启动时会创建一个主线程,这个主线程执行main方法,那么在main方法中创建的线程Thread就是在主线程中创建的子线程.
2.2 子线程
A线程中创建了B线程,那么B线程就是A的子线程,A就是B的父线程
2.3 子线程和主线程之间的关系
如果B是A的子线程(但B不是A的守护线程),那么A和B两个线程的周期互不影响,也就是A线程的结束不会造成B线程的结束,反之亦然.
三.串行,并发,并行
3.1 串行
线程任务都必须排队等待处理,可以理解为有多线程任务,但是只有一个CPU,并且每一个线程任务完成后,才能轮到下一个线程的执行
3.2 并发
和串行有点不同,可以理解为有多个线程任务,但是只有一个CPU,每一个线程都被CPU执行指定时长,不管该线程有没有执行完都必须让出CPU资源让下一个线程执行,表面上看,同一时间CPU在执行多个任务,实际上时间片很短,CPU在不断轮换线程执行
3.3 并行
最理想的并发状态,可以理解为,有多个线程任务,但也有多个CPU资源,可以真正意义上的完成同一时刻,执行多个线程任务
3.4 串行,并发,并行的理解图
可以点赞收藏哟,便于复习,理论知识到这里就告一段落了啦,小伙伴可以休息休息找找感觉,接下来是实操部分,
四.线程的创建和启动
4.1 线程的创建
4.1.1 方法一:实现Runnable接口
public static void main(String[] args) {
//MyRunnable实现了Runnable接口
new Thread(new MyRunnable()).start();
}
//实现了Runnable接口
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable.......");
}
}
4.1.2 方法2:继承Thread类
public static void main(String[] args) {
//MyThread继承Thread类
MyThread myThread = new MyThread();
myThread.start();
}
//继承Thread类
static class MyThread extends Thread{
@Override
public void run() {
System.out.println("MyThread");
}
}
4.2 线程的启动
调用start()方法启动线程,实质就是请求JVM运行相应的线程,这个线程具体什么时候运行,由线程调度器决定
start()和run()的区别
1.start()方法的作用:开启新的线程,并且自动执行线程中的run()方法,且run方法只执行一次
2.run()方法:相当于普通的方法,可以直接调用,但是不会创建新的线程,谁的线程调用run()就在谁的线程中运行run()
五.线程的常用方法
5.1 currentThread()方法获取当前线程
5.2 setName()/getName()方法设置和获取线程名称
5.3 isAlive()判断线程是否存活
5.4 yield()让正在运行的线程让出CPU资源
5.5 setPriority()设置线程的优先级
设置线程优先级一定要谨慎,避免造成饿死现象,所谓饿死现象就是,一些线程一直得不到CPU资源,从而一直处于等待状态
5.5 interrupt()中断线程
该方法仅仅只是给线程设置了一个中断标志告诉JVM此线程可以中断了,但并不意味这线程就立刻中断了,可能仍然在执行
5.6 setDaemon()设置守护线程
守护线程特点:
1.守护线程不能单独执行,必须依赖于普通非守护线程
2.当非守护线程结束时,守护线程也结束了,也即守护线程的生命周期依赖于它所守护的非守护线程
六.线程的生命周期
线程生命周期存在5个状态:新建,就绪,运行,阻塞,死亡
一.新建状态:刚new出来的线程,还未调用start()方法
二.就绪状态:1.调用start()方法等待获得CPU资源的线程 2.被唤醒后解除阻塞,等待CPU资源的线程
三.运行状态:获得CPU资源的线程
四.阻塞状态:要求等待进入阻塞状态的线程
五.死亡状态:1.线程正常执行完run方法后完成任务,结束线程的生命,进入死亡状态
2.被中断的线程进入死亡状态 3.出现异常JVM虚拟机退出,所有线程都进入死亡状态
理解图:
七.线程的优势和缺点
7.1 线程的优势
1.多线程可以提高吞吐量:也即单位时间内,程序内完成信息交互成功的数量,通俗的理解就是程序性能提高了
2.多线程可以提高响应性:同时时段可以执行多个线程的任务,不用像串行那样后面的线程一直要等待前面线程执行完才能执行,这样可以提高响应性,也即缩短反馈时间,带来更好的体验
3.多线程可以充分利用CPU资源:如果是多核CPU,那么采用多线程的并发执行,可以充分利用CPU资源,而不会造成某些CPU资源空闲的现象
7.2线程的缺点
7.2.1线程安全问题
1.进入数据处理的时候,如果不采用合适的同步处理机制,就会造成数据一致性问题,例如(脏读,幻读,不可重复读取,串行话读等问题)银行转账问题就是典型案例
7.2.2线程活性问题
由于程序本身有缺陷,有些线程一直得不到CPU资源处于非运行状态
常见的线程活性问题有如下几种:
7.2.2.1死锁
死锁:两个线程互相占用着对方需要的资源,谁也不肯先释放,造成死锁问题,可以理解为鹬蚌相争
7.2.2.2活锁
活锁:和死锁恰好相反,两个线程相互占用着对方的资源,两个线程都互相释放资源让对方先执行,谁也不肯先执行,这样就造成了活锁
7.2.2.3锁死
锁死:A线程需要被B线程唤醒,但是B线程提前结束了,那么A线程就一直处于阻塞状态,这就是锁死
7.2.2.4饿死
饿死:线程一直得不到CPU资源,一直处于非运行状态,导致线程的任务无法完成,这就是所谓的饿死现象
八.线程安全问题
多个线程同时对一个数据修改,会导致数据不同步的问题
线程安全问题主要表现为3个方面:原子性,可见性,有序性
8.1原子性
原子(Atomic)就是不可分割的意思,即使有多个线程同时访问同一个数据,但是当其中一个线程对数据进行修改时,其他线程不能干扰此修改过程,这就是原子性
例如:生活中的ATM取款机,要么存钱和取钱要么成功要么失败,不会出现,存钱成功了但是金额没有增加的情况,也不会出现取钱成功了但是金额没有减少的情况
Java中实现原子性的方法有两种
1.加锁:synchronized修饰为同步代码块,锁具有排他性,同一时刻只有一个线程可以获得同一把锁,保证同一时刻共享变量只能被一个线程访问操作
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
//修饰为同步代码块,this作为锁对象,this就是该线程对象
synchronized (this){
System.out.println("开始存钱...");
try {
Thread.sleep(1000);//模拟存钱时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("存钱成功!");
}
}
}).start();
}
2.利用处理器的CAS指令:采用了CAS原理(Compare and Swap)先比较再交换.
//CAS原理
public boolean compareAndSwap(int expectvalue,int newvalue){
//加锁保证同步性
synchronized (Test01.class) {
//再次读取数据,判断和第一次的读取是否一直
if (expectvalue == count) {
count = newvalue;
return true;
}
return false;
}
}
public void incrementCount(){
int expectvalue;
int newvalue;
do {
expectvalue = count;//第一次读取数据
newvalue = count+1;
}while (!compareAndSwap(expectvalue,newvalue));
}
}
CAS原理:就是当一个线程修改数据之前,会把此数据再读取一次,如果值没有改变就说明其他线程没有操作过该数据,此时立刻修改该数据,如果发现值不一样了,说明在读取到修改这段期间其他线程已经对数据操作过了,此时放弃修改操作
3.使用原子类:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包)
原子类的底层原理就是CAS
8.2可见性
从Java内存模型看,每一个线程都有自己的工作内存(线程私有),但是共享一个主内存.
可见性:多个线程同时访问一个数据,当数据被修改时直接更新到主存,让其他线程能够读取到被更新数据值,而不会出现读到旧数据(脏数据)的问题
保证可见性:可以使用volatile修饰变量,被volatile修饰的变量,在被线程修改时会立即更新到主存中,让其他线程从主内存中更新自己线程中工作内存的数据线程可见,避免其他线程读取到旧数据.volatile只保证可见性,但不保证原子性
public static void main(String[] args) throws InterruptedException {
printString printString = new printString();
new Thread(new Runnable() {
@Override
public void run() {
printString.printStringMethod();
}
}).start();
Thread.sleep(1000);
System.out.println("修改continueprint");
printString.setConotinueprint(false);
}
static class printString{
private Boolean conotinueprint = true;
// private volatile Boolean conotinueprint = true;
public void setConotinueprint(boolean conotinueprint){
this.conotinueprint = conotinueprint;
}
public void printStringMethod(){
while(conotinueprint){
System.out.println("print...");
}
}
}
8.3有序性
有序性是指:执行代码的顺序和源代码的顺序不一致,给人的感觉就是你写在后面的代码比写在前面的代码先执行.
对于单线程来说不会出现并发所以不会造成有序性问题,而对于多线程涉及到并发,就会造成有序性问题,下面展示给大家看
有序性造成的原因:处理器和编译器可能会对程序执行指令重排,导致执行顺序和程序顺序不一致.
可以使用 volatile 关键字, synchronized 关键字实现有序性
九.Java内存模型图解
十.线程同步
10.1.线程同步简介
线程同步时一套协调多线程之间的数据访问机制,用于保障多个线程同时访问数据时的安全性
Java提供的线程同步机制包括:synchronized,锁,volatile关键字,JUC等
10.2.锁概述
- 1.将多个线程并发访问数据转化为串行化,也就是多个线程中同一时刻只能有一个线程获得锁,获得锁的线程称为锁的持有线程,只有该线程在持有锁和释放锁这段时间内才能对数据进行操作,其他线程进入等待状态
- 2.锁又分为互斥锁和非互斥锁,读锁非互斥锁,写锁是互斥锁
- 3.互斥锁具有排他性,同一时刻只能由一个线程获得锁,这种锁称为排他锁或互斥锁
- 4.锁是保障线程安全的一个实现机制
- 5.Java中锁又分为显示锁和内部锁,内部锁是由synchronized实现,显示锁由JUC(java.util.concurrent.locks)包下的Lock接口的实现类(ReentrantLock , ReentrantReadWriteLock.ReadLock等)实现
10.3锁的相关概念
10.3.1锁的可重入性(Reentrancy)
也就是说,一个线程申请问完一个锁之后,还可以再次申请同一个锁对象
void methodA(){ 申请 a 锁 methodB(); 释放 a 锁 }
void methodB(){ 申请 a 锁 .... 释放 a 锁 }
10.3.2锁的争用与调度
Java中公平锁与非公平锁,锁的释放分配机制是不一样的
非公平锁:synchronized就是非公平锁,当锁释放时会随机分配给一个正在等待的线程,这样对于一些线程来说是不公平的,可能一直活不到锁对象,产生饿死状态
10.3.3锁的粒度
锁的粒度:一个锁所保护的共享线程数据量大,所的粒度大,反之所的力度小
锁的粒度过大线程在申请锁时会进行等待,锁的粒度小,调度锁时会增加CPU的开销
10.4.内部锁:synchronized关键字
- 1.synchronized关键字修饰方法(非static方法)时,默认的锁对象为this对象,this也就是调用该方法的对象
public synchronized void method(){}
- 2.synchronized关键字修饰static方法时,默认以类对象作为锁对象
public synchronized static void method(){}
- 3.自定义一个静态常量对象作为锁对象(这也是修饰同步代码块时最常用的方法)
public class Test022 {
public static final Object objLock = new Object();
public void main(){
synchronized (objLock) {//使用常量对象作为锁对象
//当同步代码块中的程序执行玩后,才会释放锁对象
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+"----->"+i);
}
}
}
}
10.5.volatile关键字
- 1.volatile关键字可以保证可见性,但不能保证原子性
可见性展示
/**
* volatile关键字保证数据的可见性
*/
public class Test04 {
public volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (flag){
System.out.println(Thread.currentThread().getName());
}
},"Input Thread Name").start();
Thread.sleep(1);//主线程睡眠1s,将flag置为false
flag = false;
}
}
不能保证原子性
证明:
/**
* 测试volatile具备可见性,但是不具备原子性
*/
public class Test02 {
public static void main(String[] args) throws InterruptedException {
for(int i=1;i<=20;i++){
new SubThread().start();
}
Thread.sleep(1000);//线程睡眠1s,让20个子线程执行完
System.out.println(SubThread.count);//最后结果不是20*1000=20000
}
static class SubThread extends Thread{
//volatile具备可见性,但是不具备原子性
private volatile static int count = 0;
public void addcount(){
for(int i=1;i<=1000;i++){
count++;
}
System.out.println(Thread.currentThread().getName()+" count="+count);
count++;
}
@Override
public void run() {
addcount();
}
}
}
为了保证原子性,可以使用原子类进行计数,也可以加锁
10.6 CAS原理
- 1.CAS(Compare and swap):也即先比较再修改,它可以将read-modify这类操作保证其原子性
- 2.原理:当线程需要修改某个共享数据时,会先去主存读取一边该数据,如果数据值和之前读取的不 一样,那么说明有线程修改过该数据,此时本线程修改数据的操作将终止,否则修改数据
- 3.举例:例如A线程一开始读取C数据的值是10,一段时间后A线程想把C数据的值修改为20,此时A线程会再次去主内存读取C数据的值,如果任然为10,说明之前没有线程对C数据进行修改,此时A线程不C数据修改为20,如果C数据值不为10,说明之前有线程对C数据进行过修改,此时A线程放弃修改C数据,不做处理
分析图解:
十一.原子类
- 原子变量类基于CAS实现的, 是基于volatile的加强,volatile只能保证共享数据的可见性,并不能保证原子性,而原子类在volatile的基础上加强了CAS原理,保证了共享数据的原子性
- CAS原理的代码实现,采用do-while当型循环结构
public void CAS(){
int expectvalue;
int newvalue;
do {
expectvalue = count;//第一次读取数据
newvalue = count+1;
}while (!compareAndSwap(expectvalue,newvalue));//再次读取数据count与第一次读取的expectvalue进行比较
}
- 这里我们来分析AtomicLong原子类的自增方法incrementAndGet的源码
十二.线程间的通信
12.1等待与唤醒机制概述
当A线程未到达执行条件时会先进入等待状态,当其他的B线程完成后更新条件使得A线程到达执行条件,此时B线程唤醒A线程进入等待队列,等待获得CPU执行权
12.1等待与唤醒机制实现
- 1.wait()让线程进入等待状态,nofity()随机唤醒等待线程中的一个,notifyAll()唤醒所有等待线程
- 2.wait()和notify(),notifyAll()方法只能由锁对象调用
//在调用 wait()方法前获得对象的内部锁
synchronized( 锁对象 ){ while( 条件不成立 ){
//通过锁对象调用 wait()方法暂停线程,会释放锁对象
锁对象.wait(); }
//线程的条件满足了继续向下执行,唤醒其他线程
锁对象.notify() }
- 3.生产者消费者问题
/**
* 模拟一生产者一消费者,生产蛋糕和消费蛋糕的问题,蛋糕最多生产一个不能多生产
* 此过程重复循环十次
*/
public class Test03 {
//volatile参数修饰变量保证其可见性
public static volatile int number = 0;//蛋糕的数量
public static final Object LOCK = new Object();//定义一个常量对象作为锁
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
//创建一个生产者线程
new Thread(() -> {
synchronized (LOCK) {
//当蛋糕不为0时,生产者进入等待
while (number != 0) {
try {
LOCK.wait();//生产者线程进入等待状态,释放锁资源
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//当生产者被唤醒时,生产蛋糕
number++;
System.out.println(Thread.currentThread().getName() + "生产了一个蛋糕,蛋糕数量为:" + number);
//唤醒消费者线程消费
LOCK.notify();
}
}, "Cake Producer").start();
//创建一个消费者线程
new Thread(() -> {
synchronized (LOCK) {
//当蛋糕为0时,消费者进入等待
while (number == 0) {
try {
LOCK.wait();//消费者线程进入等待状态,释放锁资源
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//当消费者被唤醒时,消费蛋糕
number--;
System.out.println(Thread.currentThread().getName() + "消费了一个蛋糕,蛋糕数量为:" + number);
//唤醒生产者线程生产
LOCK.notify();
}
}, "Cake Consumer").start();
}
}
}
十三.Lock显示锁
Lock是JUC报下的一个接口,它是显示锁,我们一般用它的实现类ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock
13.1 ReentrantLock(重点)
13.1.1 ReentrantLock的基本使用
/**
* ReentrantLock的基本用法
*Lock()方法获得锁对象
*unlock()方法释放锁对象
*Lock()和unlock()之间的代码等价于被synchronized修饰的同步代码块,是线程安全的
*/
public class Test5 {
//创建锁对象
public static ReentrantLock Lock = new ReentrantLock();
public static void main(String[] args) {
//获得锁
Lock.lock();
try {
new Thread(()->{
System.out.println("hello world");
},"Input Thread Name").start();
} catch (Exception e) {
e.printStackTrace();
}finally {
Lock.unlock();//释放锁
}
}
}
13.1.2 ReentrantLock的可重入性
ReentrantLock是可重入锁
/**
* ReentrantLock的可重入性
*/
public class Test {
//创建锁对象
public static ReentrantLock Lock = new ReentrantLock();
public static void main(String[] args) {
//2次获得锁
Lock.lock();
Lock.lock();
try {
new Thread(()->{
System.out.println("hello world");
},"Input Thread Name").start();
} catch (Exception e) {
e.printStackTrace();
}finally {
Lock.unlock();//2次释放锁
Lock.unlock();
}
}
}
13.1.3 lockInterruptibly()方法
lockInterruptibly()方法当线程没有被中断就获得锁,如果该线程被中断就出现异常,执行finally方法释放该线程已经获得的锁资源
这样可以解决多线程获得锁顺序不一致而导致的死锁问题
/**
* LOCK.lockInterruptibly()方法当线程没有被中断就获得锁,如果该线程被中断就出现异常,执行finally方法释放该线程已经获得的锁资源
* 这样可以解决多线程获得锁顺序不一致而导致的死锁问题
*/
public class Test {
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
SubThread t1 = new SubThread(11);
SubThread t2 = new SubThread(22);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
//主线程睡眠3s
Thread.sleep(3000);
//3s之后中断线程t2,避免死锁
if(t2.isAlive()) t2.interrupt();
}
static class SubThread extends Thread{
private int num = 0;
public SubThread(int num){
this.num = num;
}
@Override
public void run() {
//当num是奇数的时候,先获得lock1,在请求获得lock2
try {
if(num%2==1){
lock1.lockInterruptibly();//可以解决死锁问题
System.out.println(Thread.currentThread().getName()+"已经获得lock1,再获得lock2就无敌了");
Thread.sleep(100);
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+"已经获得了lock1和lock2");
}
//当num是偶数的时候,先获得lock2,再获得lock1
else{
lock2.lockInterruptibly();//可以解决死锁问题
System.out.println(Thread.currentThread().getName()+"已经获得lock2,再获得lock1就无敌了");
Thread.sleep(100);
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+"已经获得了lock1和lock2");
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()+"被异常中断");
}finally {
//如果该线程持有lock1,那么就释放lock1
if(lock1.isHeldByCurrentThread()) lock1.unlock();
//如果该线程持有lock2,那么就释放lock2
if(lock2.isHeldByCurrentThread()) lock2.unlock();
}
}
}
}
13.1.4 newCondition方法(重点)
- 个人感觉这个方法是ReentrantLock的灵魂和重点
- synchronized和ReentrantLock都是可重入锁,能由wait和notify方法让线程等待和唤醒线程,但是为什么要有ReentrantLock而不是用synchronized呢?除了ReentrantLock其他的优秀方法外,Condition对象可以唤醒指定的线程,这使得线程的等待与唤醒变得更加灵活
- Condition唤醒指定线程的案例实现,一定要仔细看哟~~,看完就掌握Condition对象的用法了
/**
* 案例实现:创建三个线程A,B,C,实现这三个线程的轮流打印,要求打印顺序为A->B->C
* 交替实现10次
*/
public class Test4 {
public static int flag = 1;//标记变量,1,2,3分别表示A,B,C线程打印条件
//创建一个锁对象
public static final ReentrantLock Lock = new ReentrantLock();
//获得锁对象的Condition对象,注意这里Condition是一个接口,Lock.newCondition()方法是创建了一个Condition接口的实现类对象
public static final Condition conditon1 = Lock.newCondition();//负责A线程的等待与唤醒
public static final Condition conditon2 = Lock.newCondition();//负责B线程的等待与唤醒
public static final Condition conditon3 = Lock.newCondition();//负责C线程的等待与唤醒
public static void main(String[] args) {
//创建3个线程A,B,C
new Thread(()->{
//打印10次
for(int i=1;i<=10;i++){
Lock.lock();//获得锁对象
//注意这里flag一定要用while循环不要用if,if会造成脏读的线程安全问题
while(flag!=1){//当flag!=1时,说明不该A线程打印,此时A线程进入等待状态
try {
conditon1.await();//线程A进入等待状态,切记调用condition对象的方法时,一定要先获得锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//此时flag==1,A线程开始打印,并唤醒B线程
System.out.println(Thread.currentThread().getName()+"打印了hello");
flag = 2;
conditon2.signal();//唤醒B线程
Lock.unlock();//释放锁对象
}
},"A").start();
new Thread(()->{
//打印10次
for(int i=1;i<=10;i++){
Lock.lock();//获得锁对象
//注意这里flag一定要用while循环不要用if,if会造成脏读的线程安全问题
while(flag!=2){//当flag!=2时,说明不该B线程打印,此时B线程进入等待状态
try {
conditon2.await();//线程B进入等待状态,切记调用condition对象的方法时,一定要先获得锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//此时flag==2,B线程开始打印,并唤醒B线程
System.out.println(Thread.currentThread().getName()+"打印了hello");
flag = 3;
conditon3.signal();//唤醒C线程
Lock.unlock();//释放锁对象
}
},"B").start();
new Thread(()->{
//打印10次
for(int i=1;i<=10;i++){
Lock.lock();//获得锁对象
//注意这里flag一定要用while循环不要用if,if会造成脏读的线程安全问题
while(flag!=3){//当flag!=3时,说明不该C线程打印,此时C线程进入等待状态
try {
conditon3.await();//线程C进入等待状态,切记调用condition对象的方法时,一定要先获得锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//此时flag==3,C线程开始打印,并唤醒A线程
System.out.println(Thread.currentThread().getName()+"打印了hello");
flag = 1;
conditon1.signal();//唤醒B线程
Lock.unlock();//释放锁对象
}
},"C").start();
}
}
13.2 ReentrantReadWriteLock读写锁(重点)
- 1.读写锁的由来:巨佬们发现并不是所有的锁都需要互斥,这样会影响程序性能,举个例子:一份数据,如果只有当一个开发人员读取结束后,另一个开发人员才能读取,设想如果由10000个开发人员需要读取同一份数据,那么第一个人读取之前其余9999个人都在等待,这样的话开发效率就会非常慢,对于程序来说也是如此.于是就诞生了读写锁
- 2.ReadLock 读锁:读锁是非互斥锁,也就是所同一时刻允许多个线程获得同一把读锁,多个线程可以同时访问共享数据,但不能修改
- 3.WriteLock写锁:写锁是互斥锁(排他锁),也就是说同一时刻只允许一个线程获得写锁,同一时刻只能有一个线程对共享数据进行修改
- 4.读写互斥:如果一个线程获得了读锁,那么其他任何线程就不能获得写锁
/**
* 测试读写锁的读写互斥
*/
public class Test3 {
//读写锁对象
private static ReadWriteLock rwlock = new ReentrantReadWriteLock();
//读数据的方法
static void read(){
try {
//获取读写锁的读锁
rwlock.readLock().lock();
System.out.println(Thread.currentThread().getName()+"已经获取读锁,开始读取数据:"+System.currentTimeMillis());
TimeUnit.SECONDS.sleep(3);//模拟读取数据需要3s
System.out.println(Thread.currentThread().getName()+"读取数据完毕:"+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放读锁
rwlock.readLock().unlock();
}
}
static void write(){
try {
rwlock.writeLock().lock();
System.out.println(Thread.currentThread()+"获取写锁的时间是:"+System.currentTimeMillis());
Thread.sleep(3000);//模拟写数据需要3s
System.out.println(Thread.currentThread()+"写数据完毕:"+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放写锁资源
rwlock.writeLock().unlock();
}
}
public static void main(String[] args) {
for(int i=0;i<2;i++)
new Thread(new Runnable() {
@Override
public void run() {
read();
}
}).start();
for(int i=0;i<2;i++)
new Thread(new Runnable() {
@Override
public void run() {
write();
}
}).start();
}
}
反之亦然,如果一个线程获得了写锁,那么其他线程就不能获得读锁
- 5.写写互斥:如果一个线程获得了写锁,那么其他线程就不能获得写锁
/**
* 测试ReadWriteLock读写锁的 写写互斥
*/
public class Test2 {
//定义读写锁对象
private static ReadWriteLock rwlock = new ReentrantReadWriteLock();
public static void main(String[] args) {
//模拟多个线程尝试获得写锁
for(int i=0;i<5;i++){
new Thread(new Runnable() {
@Override
public void run() {
write();
}
}).start();
}
//观察结果可以发现同一时间只有一个线程获得写锁,需要写数据的线程需要等待写锁的释放
}
static void write(){
try {
rwlock.writeLock().lock();
System.out.println(Thread.currentThread()+"获取写锁的时间是:"+System.currentTimeMillis());
Thread.sleep(3000);//模拟写数据需要3s
System.out.println(Thread.currentThread()+"写数据完毕:"+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放写锁资源
rwlock.writeLock().unlock();
}
}
}
- 6.读读共享:如果一个线程获得读锁,那么其他线程也可以获得读锁
/**
* ReadWriteLock,读写锁可以实现读读共享
*/
public class Test {
//读写锁对象
private static ReadWriteLock rwlock = new ReentrantReadWriteLock();
public static void main(String[] args) {
//模拟多个线程共享读锁
for(int i=0;i<5;i++){
new Thread(new Runnable() {
@Override
public void run() {
read();
}
}).start();
}
}
//读数据的方法
static void read(){
try {
//获取读写锁的读锁
rwlock.readLock().lock();
System.out.println(Thread.currentThread().getName()+"已经获取读锁,开始读取数据:"+System.currentTimeMillis());
TimeUnit.SECONDS.sleep(3);//模拟读取数据需要3s
System.out.println(Thread.currentThread().getName()+"读取数据完毕:"+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放读锁
rwlock.readLock().unlock();
}
}
}
十四.线程池(重点)
学习线程池可以看我下面这篇博客
ThreadPool线程池用法及源码剖析