Java多线程与高并发基础篇,看这篇就够了(全是Gan货,Bao肝2天2夜终于写完啦)

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线程池用法及源码剖析

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值