多线程(一):基础知识

多线程基础笔记

  • 学习视频地址:https://www.bilibili.com/video/BV1Z54y1j7JT?p=3&share_source=copy_web

一、创建线程的3种方式

  • 1.继承 Thread 类
    • image-20220708001747855
    • image-20220705100639289
  • 2.实现Runnable接口
    • image-20220708001834215
    • image-20220705100719416
  • 3.实现 Callable 接口
    • image-20220705100824300
    • image-20220705100904790

二、获取线程相关信息

1. 获取当前正在执行的线程

  • Thread 类的静态方法: currentThread( )
image-20220705102011913 image-20220705102023795 image-20220705102113462
  • 底层 toString 打印内容
image-20220705102131000

2. 获取和设置线程的名称

  • 获取名称—— getName
image-20220705102242917
  • 设置名称——setName
image-20220705102314602
  • image-20220705102335214

image-20220705102352948

3. 获取和设置线程优先级

线程优先级范围:1~10

  • 设置: setPriority
  • 获取:getPriority
image-20220705102440823 image-20220705102501916 image-20220705102532782 image-20220705102554899 image-20220705102612821

三、操作线程的基本方法

1. 使当前正在执行的线程进入休眠状态

  • Thread 的静态方法: sleep(long millis 毫秒数)
image-20220705102712349 image-20220705102743042 image-20220705102808976 image-20220705102820944

2. run 方法 和 start 方法的区别

  1. 位置

  2. 类型

  3. 作用

  4. 线程数量

  5. 调用次数:方法调用多次,是否会出现新的问题

    image-20220705103345025

3. 让线程优雅地停止

  • Thread 类的 interrupt 方法
image-20220705102936606
  • 1.用于停止正在运行的线程
  • 2.如果停止休眠中的线程,则会抛出线程中断异常

使用

image-20220705103020215 image-20220705103045672

问题:线程没有停下来,程序也并未结束

  • 该方法只是将线程标记为中断状态,实际并未中断
  • 需要在 run 方法中通过 isInterrupted 方法主动去判断线程是否被中断(是否有中断标记)

image-20220705103741324

image-20220705103810919 image-20220705104216996 image-20220705104504343

4. 使当前线程放弃执行权

Thread.yield( ) 方法

  • 用于一些需要一定条件才执行的任务
  • 未满足条件时,调度到了该线程就放弃执行权

1、编写打印任务和赋值任务

  • 打印线程

    • image-20220705104631353
  • 赋值线程

    • image-20220705104604048

2、 主线程

image-20220705104709399

3、比较代码段的不同

image-20220705104809687

  • 空循环较为浪费资源
  • yield 可以主动放弃执行权,而非被动地被调度

5. 等待另一条线程死亡( join 方法)

Thread.join( ) :等待调用该方法的线程死亡

用于使多个线程按顺序执行

例子

  • 线程1
    • image-20220705105216541
  • 线程2
    • image-20220705105243559
  • 线程3
    • image-20220705105251408
  • 执行
    • image-20220705105309599

6. 守护线程(Daemon)

使 A 线程随着 B 线程的结束而结束——设置 A 线程 为 B 线程的守护进程

1、设置后台线程

image-20220705105440519

测试

image-20220705105613262

2、判断线程是否为后台线程

image-20220705105641198

3、主线程

  • 由执行结果可知,守护线程确实随着主线程的结束而结束了,并未受死循环影响。
image-20220705105700173

7. 如何得知线程是否执行完

通过判断线程是否存活

image-20220705105731239

测试

image-20220705105753974 image-20220705105833213

8. 线程组

1、获取线程组

  • thread.getThreadGroup( )
image-20220705110127242 image-20220705110201790 image-20220705110306526

2、创建线程组

image-20220705110335720 image-20220705110343698

3、设置线程组

image-20220705110409523 image-20220705110511155

4、线程组常用方法

image-20220705110525097 image-20220705110542724

四、线程同步:synchronized

1. synchronized 关键字

  • 可用来给对象和方法或者代码块加锁
  • 被synchronized 修饰的代码,,同一时刻最多只有一个线程执行这个方法/这段代码。

同步代码块

image-20220708071033691

同步方法

image-20220708071041039

静态同步方法

image-20220708071046784

测试

测试1:无同步操作
image-20220705110919830 image-20220705110938670

出现问题:有重复票

image-20220705110843861

修改建议:

  • 需要添加同步操作
  • 但不要将整个 while 循环放入同步代码块中,否则会出现一个线程把票买完的情况
测试2:同步代码块_无双检
image-20220705111032126

问题:出现错票

image-20220705110803312

问题原因:只剩 1 个资源,但有多个线程同时判断到有资源可用

image-20220705111153295
测试3:同步代码块_有双检

修改:进入同步代码块后,再进行一次条件检测

image-20220705111325164

记住问题

  • 重复票:无同步操作
  • 错票:有同步操作,但没有同步操作内外双重判断

2. 同步锁

  • 同步锁是为了保证每个线程都能正常执行原子不可更改操作,【同步监听对象/同步锁/同步监听器/互斥锁】的一个标记锁。
  • 同一时刻,最多只有一个线程拿到同步锁,然后执行同步代码。

按锁的类型分类

  • 对象类型
  • 类类型
  • image-20220705160358365

同步类型对应的锁类型

image-20220705160511181

3. 多个线程争夺同一把锁和不同锁的场景

image-20220708072813039 image-20220705161811089

结果说明:当线程 sleep 时,是不会释放锁的

image-20220705161941809

修改测试:Task.class → Main.class

image-20220708073108968 image-20220705162026438

总结

  • 线程的 sleep 方法,不会释放锁
  • 争夺同一把锁,线程会阻塞

4. 死锁

1、什么是死锁

  • 死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
  • 此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的线程称为死锁线程。
  • 两个或两个以上的线程争夺彼此的锁,造成阻塞,程序永远处于阻塞状态。

2、死锁产生的条件

  • 资源互斥:一个资源只能同时被一个线程占有
  • 请求与保存:线程持有其资源的情况下,能申请获取另一个资源
  • 不剥夺:线程任务未完成时,不主动释放自己的资源
  • 循环等待:两个以上线程,构成环

3、死锁示例

  • 实现思路

    • 两个或两个以上的线程
    • 两个或两个以上的锁(资源)
    • 两个或两个以上的线程去持有不同的锁
    • 持有着不同锁的线程(即在锁住的代码内容中)去争夺对方的锁
  • image-20220705162335592

  • image-20220705162351336
  • 然后程序在输出 A、B之后,进入死锁。

五、线程间通讯

1. 等待(wait)、唤醒机制(notify)

  • 首先需要有一个锁对象:lock
  • 然后某个线程用 synchronized ( lock ) 获得该锁后,可调用 lock.wait( ) 方法,此时发生
    • 让出这个锁资源:lock
    • 同时线程开始无限期等待(如果没设置等待时间)
    • 等待 lock.notify() (如果只有1条与该锁相关的线程,则被唤醒)或 lock.notifyAll 被唤醒继续执行

1、等待 wait

image-20220705164018578 image-20220705164125135

2、唤醒单个线程 notify

image-20220705164158185 image-20220705164233135
  • 如果有多个线程执行 task ,则 notify 只唤醒1个线程

3、唤醒所有线程 notifyAll

  • image-20220706155754871
  • image-20220706155826159

4、总结

image-20220705164525645

2. wait 与 sleep 的区别

  1. 位置
  2. 是否需要当前线程拥有锁
  3. 是否支持手动唤醒?
  4. 是否支持自动唤醒?
  5. 是否支持中断?
  6. 是否释放锁?
  7. 线程状态

位置

  • sleep:是 Thread 类的静态方法
    • image-20220706155957498
  • wait:是 Object 类的方法
    • image-20220706160006559

总结

image-20220706160028754

thread.getState( ) :获取线程状态

3. 线程间通讯(wait & notify 应用)

1、数据类

/**
 * 数据
 */
public class Data {

    /**
     * 数据信息
     */
    private String message;

    /**
     * 设置数据信息
     *
     * @param message 数据信息
     */
    public void setMessage(String message){
        // 设置数据信息
        this.message = message;
    }

    /**
     * 获取数据信息
     *
     * @return 数据信息
     */
    public String getMessage() {
        return message;
    }
}

2、生产者类

package lab;

/**
 * 生产者任务
 */
public class Producer implements Runnable {

    /**
     * 数据
     */
    private Data data;

    /**
     * 计数器
     */
    private int count = 0;

    /**
     * 构造方法
     *
     * @param data 数据
     */
    public Producer(Data data) {
        // 设置数据
        this.data = data;
    }

    /**
     * 将需要线程执行的任务写在run()方法中
     */
    @Override
    public void run() {
        // 无限循环
        while (true) {

            try {
                // 让当前线程睡1秒钟
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 同步对象为data的同步代码块
            synchronized (data) {

                // 当数据信息不为空时,即还有数据信息未消费
                while (data.getMessage() != null) {
                    try {
                        // 使当前生产者线程等待
                        data.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 数据信息
                String message = "你好!" + count++;

                // 设置数据信息
                data.setMessage(message);

                // 唤醒正在此对象监视器上等待的所有线程
                data.notifyAll();
            }
        }
    }
}

3、消费者类

package lab;

/**
 * 消费者任务
 */
public class Consumer implements Runnable {

    /**
     * 数据
     */
    private Data data;

    /**
     * 构造方法
     *
     * @param data 数据
     */
    public Consumer(Data data) {
        // 设置数据
        this.data = data;
    }

    /**
     * 将需要线程执行的任务写在run()方法中
     */
    @Override
    public void run() {
        // 无限循环
        while (true){
            // 同步对象为data的同步代码块
            synchronized (data) {
                // 当数据信息为空时
                while (data.getMessage() == null) {
                    try {
                        // 等待
                        data.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 消费数据信息
                System.out.println("数据信息:" + data.getMessage());

                // 将数据对象里面的数据信息设置为null
                data.setMessage(null);

                // 唤醒正在此对象监视器上等待的所有线程
                data.notifyAll();
            }
        }
    }
}

4、主线程测试

package main;

import lab.Consumer;
import lab.Data;
import lab.Producer;

public class Main {
    /**
     * 主入口
     *
     * @param args 参数列表
     */
    public static void main(String[] args) {
        // 数据
        Data data = new Data();

        // 创建生产者任务和消费者任务
        Producer producer = new Producer(data);
        Consumer consumer = new Consumer(data);

        // 创建生产者线程和消费者线程
        Thread producerThread1 = new Thread(producer);
        Thread consumerThread1 = new Thread(consumer);

        // 启动线程
        producerThread1.start();
        consumerThread1.start();
    }
}

六、线程同步:Lock 锁

具备和synchronized一样的作用,可以实现线程同步,但比synchronized 更强大。

image-20220706160719817

Lock接口定义了 6 个方法

  • image-20220706160823372

常用实现类

  • image-20220706160852825

使用

  • package main;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Main {
    
        /**
         * 主入口
         *
         * @param args 参数列表
         */
        public static void main(String[] args) {
            // 创建显式锁对象
            Lock lock = new ReentrantLock();
    
            // 通过匿名内部类方式实现Runnable接口
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    // 获取锁
                    lock.lock();
    
                    try {
                        // 使当前线程睡1秒钟
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    // 输出语句
                    System.out.println(Thread.currentThread().getName() + " --- run");
                }
            };
    
            // 创建线程
            Thread thread1 = new Thread(runnable);
            Thread thread2 = new Thread(runnable);
            Thread thread3 = new Thread(runnable);
    
            // 启动线程
            thread1.start();
            thread2.start();
            thread3.start();
        }
    }
    

总结

  • image-20220706160953618

七、锁比较

1. 非阻塞式获取锁

1、阻塞式获取锁

  • 发现锁未释放,则一直等待

  • image-20220706162040957

2、非阻塞式获取锁

  • 发现锁未释放,则先去做其他事
  • image-20220706162108093

使用格式

  • image-20220706162147674

测试

package main;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Task implements Runnable {

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        // 非阻塞式获取锁
        try {
            if (lock.tryLock(3, TimeUnit.SECONDS)) {
                try {
                    // 使当前线程休眠1秒钟
                    Thread.sleep(1000);
                    // 输出当前线程名称
                    System.out.println(Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            } else {
                System.out.println("锁被占用,执行其他任务。");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

package main;

public class Main {

    public static void main(String[] args) {
        // 创建任务
        Task task = new Task();
        // 创建线程
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        // 启动线程
        thread1.start();
        thread2.start();
    }
}

3、尝试定时获取锁

image-20220706161728486

2. 中断等待锁的线程

image-20220706162233752

可中断测试

拿到锁持有 3 秒钟

image-20220706162338191

thread0持有锁时,还未释放,主线程此时主动中断 thread1

image-20220706162405941

发生异常

  • 注意:该异常是 thread1 获取到锁之后抛出的
  • 即 3 s 后,线程 thread1 已经拿到了锁,但发现自己已经被中断了,所以抛出异常

image-20220706162448623

lockInterruptibly

  • 当一个线程获取了锁之后,是不会被interrupt()方法中断的。
  • 因为调用 interrupt( ) 方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程,而进入阻塞状态前是需要判断获取锁的
  • 因此当通过 lockInterruptibly( ) 方法获取某个锁时,如果不能获取到,则可通过抛出异常来响应中断。
  • image-20220706162644131
  • 对比 lock 方法,该方法操作性更强
image-20220706162557809 image-20220706162612523

3. Lock 锁的等待唤醒机制

  • 通过 Condition 实现的

  • 每次返回的都是新的 Condition 实例

image-20220706162839904

1、Condition类的方法

image-20220706162906684

image-20220706162934060

方法对应

  • image-20220706163017327

常用方法

  • image-20220706163044589

2、测试

package main;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {
    public static void main(String[] args) {
        // 创建显式锁
        Lock lock = new ReentrantLock();
        // 获取Condition对象
        Condition condition = lock.newCondition();
        
        // 创建线程
        Thread thread = new Thread(){
            @Override
            public void run() {
                try {
                    // 获取锁
                    lock.lock();
                    // 输出语句
                    System.out.println(Thread.currentThread().getName() + " --- 等待");
                    // await()方法造成当前线程在等待,直到它被唤醒,通常由被通知或中断。
                    condition.await();
                    // 输出语句
                    System.out.println(Thread.currentThread().getName() + " --- 被唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            }
        };

        // 启动线程
        thread.start();
    }
}

测试唤醒

  • 主线程添加代码
  • 注:需要主线程先获取到 Lock 显式锁
  • image-20220706163835940

测试唤醒全部

  • 主线程添加代码
  • image-20220708112711883

3、生产者与消费者(Condition应用)

1、数据类

  • 定义两个不同的 Condition

    • image-20220706164007036
  • package lab;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * 数据
     */
    public class Data {
    
        /**
         * 数据信息
         */
        private String message;
    
        /**
         * 计数器
         */
        private int count = 0;
    
        /**
         * 当数据信息被消费时为true,当数据信息被生产时为false。
         */
        private boolean empty = true;
    
        /**
         * 显式锁
         */
        private Lock lock = new ReentrantLock(true);
    
        /**
         * 设置数据信息的Condition
         */
        private Condition setCondition = lock.newCondition();
    
        /**
         * 获取数据信息的Condition
         */
        private Condition getCondition = lock.newCondition();
    
        /**
         * 设置数据信息
         *
         * @param message 数据信息
         * @throws InterruptedException 当线程被中断时产生此异常
         */
        public void setMessage(String message) throws InterruptedException {
            // 获取锁
            lock.lock();
            try {
                // 当数据信息不为空时,即还有数据信息未消费
                while (!empty) {
                    // 使当前生产者线程等待
                    setCondition.await();
                }
                // 设置数据信息
                this.message = message + Thread.currentThread().getName() + " --- " + count++;
                // 数据信息没有被消费
                empty = false;
    
                try {
                    // 让当前线程睡1秒钟
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                // 唤醒正在此对象监视器上等待的单个线程
                getCondition.signal();
            } finally {
                // 释放锁
                lock.unlock();
            }
        }
    
        /**
         * 获取数据信息
         *
         * @return 数据信息
         * @throws InterruptedException 当线程被中断时产生此异常
         */
        public String getMessage() throws InterruptedException {
            // 获取锁
            lock.lock();
            try {
                // 当数据信息为空时
                while (empty) {
                    // 使当前生产者线程等待
                    getCondition.await();
                }
                // 数据信息已经被消费
                empty = true;
                // 唤醒正在此对象监视器上等待的单个线程
                setCondition.signal();
            } finally {
                // 释放锁
                lock.unlock();
            }
            // 返回数据信息
            return message;
        }
    }
    

2、生产者

package lab;

/**
 * 生产者任务
 */
public class Producer implements Runnable {

    /**
     * 数据
     */
    private Data data;

    /**
     * 构造方法
     *
     * @param data 数据
     */
    public Producer(Data data) {
        // 设置数据
        this.data = data;
    }

    /**
     * 将需要线程执行的任务写在run()方法中
     */
    @Override
    public void run() {
        try {
            // 无限循环
            while (true) {
                // 设置数据信息
                data.setMessage("你好!");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3、消费者

package lab;

/**
 * 消费者任务
 */
public class Consumer implements Runnable {

    /**
     * 数据
     */
    private Data data;

    /**
     * 构造方法
     *
     * @param data 数据
     */
    public Consumer(Data data) {
        // 设置数据
        this.data = data;
    }

    /**
     * 将需要线程执行的任务写在run()方法中
     */
    @Override
    public void run() {
        try {
            // 无限循环
            while (true){
                // 消费数据信息
                System.out.println("数据信息:" + data.getMessage());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试

package main;

import lab.Consumer;
import lab.Data;
import lab.Producer;

public class Main {

    /**
     * 主入口
     *
     * @param args 参数列表
     */
    public static void main(String[] args) {
        // 数据
        Data data = new Data();

        // 创建生产者任务和消费者任务
        Producer producer = new Producer(data);
        Consumer consumer = new Consumer(data);

        // 创建生产者线程和消费者线程
        Thread producerThread1 = new Thread(producer);
        Thread consumerThread1 = new Thread(consumer);

        // 启动线程
        producerThread1.start();
        consumerThread1.start();
    }
}

利用await、signal、signalAll等待唤醒机制,让线程间友好协作完成任务,更高效地使用资源。

4. 可重入锁和不可重入锁

  • (不)可:(不)可以
  • 重入:重复进入同步作用域(使用同步锁)
  • 锁:同步锁
image-20220706164514198 image-20220706164523778

测试 synchronized 可重入锁

image-20220706164717891

说明可重复使用持有的锁

image-20220706164731309

同步方法上的同步锁,也可重复使用(毕竟锁住了整个类)

image-20220706164833356

测试 lock 可重入锁

image-20220706164932891

测试不可重入锁

自定义不可重入锁的思路

  • 实现 Lock 接口——着重实现 lock 和 unlock 方法即可
  • 绑定已获取锁的线程
  • 实现获取锁的方法
  • 实现释放锁的方法
package lab;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 不可重入锁
 */
public class NotReentrantLock implements Lock {

    /**
     * 用于绑定线程的变量
     */
    private Thread thread;

    /**
     * 获取锁
     */
    @Override
    public void lock() {
        // 获取当前线程
        Thread currentThread = Thread.currentThread();
        // 当当前线程为已经拿到锁的线程时
        while (currentThread == thread) {
            // 同步对象为this的同步代码块
            synchronized (this) {
                try {
                    // 使当前线程等待
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        // 记录当前线程
        thread = currentThread;
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    /**
     * 释放锁
     */
    @Override
    public void unlock() {
        // 获取当前线程
        Thread currentThread = Thread.currentThread();
        // 当当前线程不为绑定线程时
        while (currentThread != thread) {
            // 同步对象为this的同步代码块
            synchronized (this) {
                try {
                    // 使当前线程等待
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        // 将绑定线程置空
        thread = null;
        // 同步对象为this的同步代码块
        synchronized (this){
            // 唤醒等待该锁的所有线程
            notifyAll();
        }
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

5. 公平锁与非公平锁

线程获取锁的机会平等与否

  1. 非公平锁(synchronized)
  2. 非公平锁(ReentrantLock)——默认是非公平的 ( false )
  3. 公平锁(ReentrantLock)
  4. 判断锁是否公平(isFair)——只能判断 ReentrantLock 和 ReentranReadWriteLock
    • reentrantLock.isFair( )
    • reentranReadWriteLock.isFair()
image-20220706165714999

6. 读(共享)锁与写(独占)锁

image-20220706170237671 image-20220706170309097

1、读锁

image-20220706170431521 image-20220706170513594

线程启动后,几乎可以同时拿到读锁

2、写锁

image-20220706170553240
  • 三条线程依次输出(独占的,一个用完再给下一个)

3、读锁(共享锁)的意义

  • 能保证支持多个线程同时读取的时候,禁止修改
  • 能保证同一时刻只有一个线程在修改

7. 读写锁实战高并发容器

image-20220706170846502

1、高并发容器

  • 键值对容器

    • 读锁——获取数据
    • 写锁——存储数据
  • 方法

    • 获取数据
    • 存储数据
    • 获取所有键
    • 清空容器
  • package lab;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    import java.util.TreeMap;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    /**
     * 高并发容器
     */
    public class ConcurrentDictionary {
    
        /**
         * 数据容器
         */
        private Map<String, String> m = new TreeMap<>();
    
        /**
         * 可重入读写锁
         */
        private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(true);
    
        /**
         * 读锁
         */
        private ReentrantReadWriteLock.ReadLock r = rwl.readLock();
    
        /**
         * 写锁
         */
        private ReentrantReadWriteLock.WriteLock w = rwl.writeLock();
    
        /**
         * 添加数据并将旧值返回给调用者
         *
         * @param key   键
         * @param value 值
         * @return 旧值
         */
        public String put(String key, String value) {
            // 获取写锁
            w.lock();
            try {
                // 添加数据并将旧值返回给调用者
                return m.put(key, value);
            } finally {
                // 释放写锁
                w.unlock();
            }
        }
    
        /**
         * 获取数据
         *
         * @param key 键
         * @return 键所对应的值
         */
        public String get(String key) {
            // 获取读锁
            r.lock();
            try {
                // 返回键所对应的值
                return m.get(key);
            } finally {
                // 释放读锁
                r.unlock();
            }
        }
    
        /**
         * 获取容器中所有的key值
         *
         * @return 容器中所有的key值
         */
        public List<String> allKeys() {
            // 获取读锁
            r.lock();
            try {
                // 返回容器中所有的key值
                return new ArrayList<>(m.keySet());
            } finally {
                // 释放读锁
                r.unlock();
            }
        }
    
        /**
         * 清空容器
         */
        public void clear() {
            // 获取写锁
            w.lock();
            try {
                // 清空容器
                m.clear();
            } finally {
                // 释放写锁
                w.unlock();
            }
        }
    }
    

2、写任务

image-20220706171325215

3、读任务

image-20220706171405415

4、测试

image-20220706171515802

8. synchronized 和 Lock 的区别

1、类型

  • synchronized 关键字
  • Lock 接口
    • image-20220706171652183

2、格式

image-20220706171713426

3、释放锁的方式

image-20220706171744996

image-20220706171754430 image-20220706171820530

4、别名

image-20220706171842616

5、连续释放锁的方式

  • 多个锁嵌套之后,释放的方式
  • image-20220706180809154
  • image-20220706180748067
  • image-20220706180831466
  • image-20220706180840803

6、(非)公平锁

image-20220706180852305
  • synchronized 只有非公平锁

  • image-20220706180939660

7、读写锁

image-20220706181035934
  • ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
    //写锁
    Lock writeLock = readWriteLock.writeLock();
    //读锁
    Lock  readLock = readWriteLock.readLock();
    

8、总结

image-20220706181050418

八、等待、唤醒工具类:LockSupport

1. LockSupport 引入

LockSupport

  • 功能

    • 使线程等待
    • 唤醒当前等待中的线程
  • 该工具类无法被实例化

    • image-20220706181452122
image-20220706181223204 image-20220706181249029

1、方法分类

  • 按功能分类
    • image-20220706181402284
  • 都是静态方法,可直接通过类名调用
    • image-20220706181429651

2、等待 park ( )

image-20220706181612472

3、唤醒 unpark ( )

image-20220706181709335

2. LockSupport 实现互斥锁

思路:

  • 锁标志——记录锁是否已经被其他线程使用
  • 等待队列
  • 处理线程中断——有线程不想等了,需要处理

实现:

  • 先进先出互斥锁:FIFOMutex
    • image-20220706182055482
    • 上锁方法的实现:lock( )
      • 互斥锁的实质:同一时刻只有1个线程能用该锁,所以需要一个等待队列辅助排队。
      • image-20220706182330962
    • image-20220706182353199
  • Task类
    • image-20220706182449329
  • Main
    • image-20220706182523165

九、读写锁扩展

1. 读写锁互斥情况

image-20220706182712416

2. 读写锁升级版:StampedLock

  • 普通的读写锁,存在“线程饥饿”问题

    • image-20220706235014631
  • 会导致写线程饥饿的情况:

    • 当线程 A 持有读锁读取数据时,线程 B 要获取写锁修改数据就只能到队列里排队。
    • 此时又来了线程 C 读取数据,那么线程 C 就可以获取到读锁,而要执行写操作线程 B 就要等线程 C 释放读锁。
    • 由于该场景下读操作的数量远远大于写操作的数量,此时可能会有很多线程来读取数据而获取到读锁,那么要获取写锁的线程 B 就只能一直等待下去,最终导致饥饿。
  • StampedLock是基于CLH锁原理实现的

    • CLH是一种基于排队思想实现的自旋锁,可以保证FIFO(先进先出)的服务顺序,所以会避免写线程饥饿问题
    • 其实就是其中实现了一个队列,每次不管是读锁也好写锁也好,未拿到锁就加入队列
    • 然后每次解锁后,队列头存储的线程节点获取锁,从而避免饥饿。
  • StampedLock 常用方法

    • image-20220706235114411

3. StampedLock 的可重入性

  • 可重入性和普通的读写锁有区别
    • image-20220706235913696

十、线程状态与线程周期

获取线程状态的方法

  • image-20220707000050345
  • 线程状态
    • image-20220707000643038
  • 线程周期
    • image-20220707000733473

十一、ThreadLocal

1. ThreadLocal 线程本地变量

image-20220707000906053 image-20220707001024450

本质

  • Thread内部有一个ThreadLocalMap的成员变量,里面存放着和这个线程绑定的数据。
  • 虽然ThreadLocalMap叫map,但是并没有实现map接口,只是提供了几个和map接口一样的方法
  • ThreadLocal作为ThreadLocalMap的key
  • 所以说ThreadLocal本身并不存放数据,数据都在Thread对象中的ThreadLocalMap里
  • 所以同一个 ThreadLocal,在不同线程Thread的ThreadLocalMap中作为 key ,其 value 可以不同,相对独立。

ThreadLocal的定位是什么?

  • Thread 类的 ThreadLocalMap 属性的 key 值

  • image-20220707001311975
  • image-20220707001337729
  • image-20220707001348681
  • image-20220707001430439

  • image-20220707001443972
  • key就是 ThreadLocal

  • ThreadLocal与Thread 的关系是什么?

    • ThreadLocal 与Thread是相互协作的关系。
    • Thread负责存储数据,ThreadLocal负责操作数据。

简单测试

image-20220707001707267
  • 输出为 null
image-20220707001809350
  • 修改

    image-20220707001837164
  • 删除—— remove 方法

    • image-20220707001906386
    • image-20220707001931431

2. 线程间如何共享 ThreadLocal

InheritableThreadLocal

  • 继承自 ThreadLocal,方法一样
  • 就当做是属性值一样来用
image-20220707002039530
  • 并不是说,线程 B extends 自线程 A
  • 而是,【线程 B】 需要在【线程 A】 的 run 方法中创建
  • 然后即可共享线程 A 中的本地变量
  • image-20220708154523695

测试

  • 线程B
    • image-20220707002333285
  • 线程A
    • image-20220707002349047
  • Main
    • image-20220707002401091

3. 线程池之间如何共享 ThreadLocal

TransmitableThreadLocal

  • 继承自 InheritableThreadLocal
  • 该类非 JDK 自带的,而是 :https://github.com/alibaba/transmittable-thread-local
  • image-20220707002542282

ThreadLocal 线程池测试

  • task

    • image-20220708154923057
  • 提交到线程池的任务,执行时无法获取到 ThreadLocal

  • InheritableThreadLocal 也一样

  • 说明: ThreadLocal 和 InheritableThreadLocal 无法再线程池间共享

  • image-20220707003034602

TransmitableThreadLocal 测试

发现仍然不一致;

需要按照规定使用:

  • 方法一:改变线程池任务提交方式

    • 借助 TtlRunnable 类,绑定任务 task
    • image-20220707003248711
  • 方法二:改变线程池

    • 创建线程池:
      • ExecutorService executorService = Executors.newFixedThreadPool( 1 );
      • TtlExcutors.getTtlExecutorService( executorService );
    • image-20220707003330130
    • image-20220707003345936
    • 正常提交任务即可

总结

image-20220707003611487

image-20220707003629419

十二、多线程基础扩展

1. 内存可见性——volatile

专门解决内存可见性问题的关键字

  • 内存可见性
    • image-20220707004037913

理解内存可见性

image-20220707004010493

发现, thread 线程并未停下来。

原因:

image-20220707004303552

volatile 解决该问题

image-20220707004324498

修改代码:

image-20220707004342544

实现内存可见性。

即使用 volatile 修饰的变量,会主动通知更新【对应线程的工作内存】

2. 原子性

一个或多个操作不可中断的,要么全部执行成功,要么全部执行失败。

对应的操作——原子操作——具备原子性的操作

i++ 是原子操作吗?

  • 不是

判断过程:让多个线程执行 i++ ,看最后的值是否为累加即可

image-20220707004628651

发现,i++不是原子操作,其本质步骤分为:

每一步都有可能被其他线程干扰

  • 1.从主内存读取变量i
  • 2.执行i++
  • 3.将变量i写入主内存

使用 volatile 无法解决原子性问题

image-20220707004800112

添加 同步代码 可以解决

image-20220707004837697

使用原子类解决

具有原子性的类

image-20220707004931349 image-20220707004950062 image-20220707005003787

总结

image-20220707005056842

3. CAS (比较并交换)技术

比较并交换技术(CAS技术):CAS:Compare And Swap

CAS操作包含三个操作数:内存值、预期原值、新值

如果内存位置的值与预期原值相匹配,那么将内存值更新为新值;否则什么也不做

  • 和谁比较
  • 和谁交换

CAS操作包含三个值

  • 内存中
  • 预期原值
  • 新值
  • image-20220707005254031

测试线程干扰

  • 线程干扰
  • 当线程未执行到变量值修改的语句时,对应的变量值先被其他线程修改

CAS 方法抗干扰

image-20220707005507581

image-20220707005525608

CAS的应用

  • 原子类里大多都是使用 CAS 方法来保证

总结

image-20220707005615347

4. 原子类

  • 分类

    • image-20220707005654557
  • image-20220707005846664

5. ABA 问题

CAS算法存在的ABA问题

  • 假如一个值原来是A,现在变成了B,
  • 后来又变成了A,
  • 那么CAS检查时会发现它的值没有发生变化,
  • 但是实际上却变化了

解决方法

  • 打标记(version等)
  • 原子类

image-20220707010432199


image-20220707010448236

修改

image-20220707010547573

image-20220707010641642

image-20220707010716278

总结

  • ABA问题:假如一个值原来是A,现在变成了B,后来又变成了A,那么CAS检查时会发现它的值没有发生变化,但是实际上却变化了
  • AtomicMarkableReference是一个带修改标记的引用类型原子类
  • AtomicStampedReference是一个带版本号的引用类型原子类
image-20220707004931349 image-20220707004950062 image-20220707005003787

总结

image-20220707005056842

3. CAS (比较并交换)技术

比较并交换技术(CAS技术):CAS:Compare And Swap

CAS操作包含三个操作数:内存值、预期原值、新值

如果内存位置的值与预期原值相匹配,那么将内存值更新为新值;否则什么也不做

  • 和谁比较
  • 和谁交换

CAS操作包含三个值

  • 内存中
  • 预期原值
  • 新值
  • [外链图片转存中…(img-LmiX1huB-1657340436259)]

测试线程干扰

  • 线程干扰
  • 当线程未执行到变量值修改的语句时,对应的变量值先被其他线程修改

CAS 方法抗干扰

[外链图片转存中…(img-cp5nN0uD-1657340436259)]

[外链图片转存中…(img-YTU9xp6Y-1657340436260)]

CAS的应用

  • 原子类里大多都是使用 CAS 方法来保证

总结

image-20220707005615347

4. 原子类

  • 分类

    • [外链图片转存中…(img-7fJfXpFz-1657340436261)]
  • [外链图片转存中…(img-O03n1ML3-1657340436261)]

5. ABA 问题

CAS算法存在的ABA问题

  • 假如一个值原来是A,现在变成了B,
  • 后来又变成了A,
  • 那么CAS检查时会发现它的值没有发生变化,
  • 但是实际上却变化了

解决方法

  • 打标记(version等)
  • 原子类

[外链图片转存中…(img-4aG1DCZM-1657340436262)]


[外链图片转存中…(img-VG9A8qWg-1657340436262)]

修改

[外链图片转存中…(img-JWrtHnOI-1657340436263)]

[外链图片转存中…(img-xRvNxT75-1657340436264)]

[外链图片转存中…(img-6DyzF1A5-1657340436264)]

总结

  • ABA问题:假如一个值原来是A,现在变成了B,后来又变成了A,那么CAS检查时会发现它的值没有发生变化,但是实际上却变化了
  • AtomicMarkableReference是一个带修改标记的引用类型原子类
  • AtomicStampedReference是一个带版本号的引用类型原子类
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值