多线程 (同步锁,乐观锁,悲观锁,公平锁,可重入锁,死锁,自旋锁,读写锁)

多线程

进程

进程是正在运行的程序

  • 是系统进行资源分配和调用的独立单位地方
  • 是每个进程都有它自己的内存空间和系统资源

线程

线程:是进程中的单个顺序控制流,是一条执行路径

  • 单线程:一个进程如果只有一条执行路径,则称为单线程程序
  • 多线程:一个进程如果有多条执行路径,则称为多线程程序

多线程常用方法

  • run方法是用来封装被线程执行的代码

  • run():封装线程执行的代码,直接调用,相当于普通方法的调用

  • start():启动线程;然后由JVM调用此线程的run()方法

  • Thread.currentThread(): 获取当前线程 java任何一段代码都是执行在某个线程当中,方法的 返回值是在代码实际运行时候的线程对象。

  • isAlive (): 判断当前线程是否还活着

  • Thread.sleep(millis): 让当前线程休眠指定的毫秒数 当前线程是指Thread.currentThread()返回的线程

  • Thread.yield() 方法的作用是 礼让 不一定礼让成功 看cpu心情

  • thread.setPriority(num) :设置线程的优先级 num取值范围1到10 默认是5 num如果超出范围会抛出异常,优先级高的线程获取cpu时间片的概率大 谁先执行还是看cpu心情。设置不当可能导致某些线程永远无法得到运行,所以线程的优先级不是设置的越高越好,一般情况使用普通的优先级即可。线程的优先级具有继承性,在A线程中创建了B线程,则B线程的优先级和A线程是一样的。

  • interrupt() : 中断线程 仅仅在当前线程打上一个停止标志,并不是真正的停止线程 外部调用这个方法 a.interrupt() 这个方法 a里面的 this.isInterropted()该方法可以返回线程的中断标志 返回a的值是true。当线程处于 wait()等待状态时, 调用线程对象的 interrupt()方法会 中断线程的等待状态, 会产生 InterruptedException 异常。

  • setDaemon(true) : 设置守护线程 java中的线程分为用户线程和守护线程 守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程,当用户线程都执行完毕,守护线程会自动销毁

  • notify()与 notifyAll() :notify()一次只能唤醒一个线程,如果有多个等待的线程,只能随机 唤醒其中的某一个; 想要唤醒所有等待线程,需要调用 notifyAll().

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

  • getState() : 该方法可以获取线程的状态。

    • NEW, 新建状态,创建了线程对象,在调用start方法启动之前的状态
    • RUNNABLE,可运行状态,它是一个复合状态,包含:READY和RUNNING两个状态,READY状态该线程可以被线程调度器进行调度使它处于RUNING状态,RUNNING状态表示线程正在执行。Thread.yield()方法把线程由RUNNING状态转换为READY状态。
    • BLOCKED 阻塞状态,线程发起阻塞的IO操作,或者申请由其它线程占用的独占资源,线程会转换为BLOCKED 阻塞状态 ,处于阻塞状态的线程不会占用CPU资源,当阻塞IO操作执行完,或者线程获得其申请的资源,线程可以转换为RUNNABLE
    • WAITING 等待状态,线程执行了 object.wait(),thread.join()方法会把线程转换为WAITING等待状态,执行object.notify()方法,或者加入的线程执行完毕,当前线程会转换为RUNNABLE状态
    • TIMED_WAITING 等待状态,与WAITING 状态相似,区别在与处于该状态的线程不会无限等待,如果线程没有在规定的时间完成期望的操作,该线程会自动转换为RUNNABLE
    • TERMINATED 终止状态,线程结束。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qGzasbVm-1630281947495)(https://i.loli.net/2021/08/26/FhYPW85CExAMTcO.png)]

线程安全

表现为三个方面:原子性,可见性和有序性

原子性

原子(Atomic)就是不可分割的意思

  1. 访问(读,写)某个共享变量的操作从其他线程来看,该操作要 么已经执行完毕,要么尚未发生, 即其他线程年示到当前操作的中 间结果

  2. 访问同一组共享变量的原子操作是不能够交错的

Java 有两种方式实现原子性: 一种是使用锁; 另一种利用处理器 的 CAS(Compare and Swap)指令.

原子变量类基于CAS实现的, 当对共享变量进行read-modify-write 更新操作时,通过原子变量类可以保障操作的原子性与可见性.对变量 的 read-modify-write 更新操作是指当前操作不是一个简单的赋值,而 是变量的新值依赖变量的旧值,如自增操作i++. 由于volatile只能保证 可见性,无法保障原子性, 原子变量类内部就是借助一个 Volatile 变量, 并且保障了该变量的 read-modify-write 操作的原子性, 有时把原子变 量类看作增强的 volatile 变量. 原子变量类有 12 个,如:

可见性

​ 在多线程环境中, 一个线程对某个共享变量进行更新之后 , 后续 其他的线程可能无法立即读到这个更新的结果, 这就是线程安全问 题的另外一种形式: 可见性(visibility).

​ 如果一个线程对共享变量更新后, 后续访问该变量的其他线程可 以读到更新的结果, 称这个线程对共享变量的更新对其他线程可见, 否则称这个线程对共享变量的更新对其他线程不可见. 多线程程序因为可见性问题可能会导致其他线程读取到了旧数据 (脏数据)

有序性

编写的代码顺序 不一定是执行的顺序;会受到编译器和cpu的影响,java编译器为了优化代码的执行速率会改变代码的执行顺序,当然这种是在保证单线程的前提下。所以多线程就存在了线程安全问题。

java虚拟机内存模型:理解线程不安全发生的原因

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sjoQrMIy-1630281947497)(https://i.loli.net/2021/08/26/muxcfijR96oL1FE.png)]

​ 线程工作内存是 JMM抽象 抽象出的一种模型。首先变量都是存储在主内存中,每个线程都有自己独立的工作内存,里面保存的是该线程使用变量的副本。线程修改的值是修改的是该线程的工作内存的副本,然后再将修改后的值刷新到主内存。

​ JMM规定 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接在主内存中读写。不同线程之间不能直接访问其它线程工作内存的变量,线程之间变量值传递只能通过内存来传递。更新不及时导致线程安全问题。

​ 线程安全问题的产生前提是多个线程并发访问共享数据. 将多个线程对共享数据的并发访问转换为串行访问,即一个共享 数据一次只能被一个线程访问.锁就是利用这种思路来保障线程安全 的

可重入锁

​ 可重入性(Reentrancy)描述这样一个问题: 一个线程持有该锁的时 候能再次(多次)申请该锁。如果一个线程持有一个锁的时候还能够继续成功申请该锁,称该 锁是可重入的, 否则就称该锁为不可重入的。

void methodA(){
      申请 a 锁
      methodB();
      释放 a 锁
}
void methodB(){
      申请 a 锁
....  释放 a 锁
}

锁的征用与调度

Java 平台中内部锁属于非公平锁, 显示 Lock 锁既支持公平锁又支 持非公平锁

锁的粒度

一个锁可以保护的共享数据的数量大小称为锁的粒度. 锁保护共享数据量大,称该锁的粒度粗,为重量级锁 否则就称该锁的粒度细为轻量级锁. 锁的粒度过粗会导致线程在申请锁时会进行不必要的等待.锁的 粒度过细会增加锁调度的开销.

内部锁

​ Java 中的每个对象都有一个与之关联的内部锁(Intrinsic lock). 这 种锁也称为监视器(Monitor), 这种内部锁是一种排他锁,可以保障原 子性,可见性与有序性. 内部锁是通过 synchronized 关键字实现的.synchronized 关键字修 饰代码块,修饰该方法

修饰代码块的语法:

​ synchronized( 对象锁 ) {

​ 同步代码块,可以在同步代码块中访问共享数据 }

修饰实例方法就称为同步实例方法 (this)

修饰静态方法称称为同步静态方法 (类.clss)

死锁

public class T3 {
    final Object locka = new Object();
    final Object lockb = new Object();

    public void m1() {
        String tn = Thread.currentThread().getName();
        System.out.printf("%s启动等待%n", tn);
        synchronized (locka) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            synchronized (lockb) {
            }
        }
        System.out.printf("%s结束%n", tn);
    }

    public void m2() {
        String tn = Thread.currentThread().getName();
        System.out.printf("%s启动等待%n", tn);
        synchronized (lockb) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                e.printStackTrace();
            }
            synchronized (locka) {
            }
        }
        System.out.printf("%s结束%n", tn);
    }

    public static void main(String[] args) {
        var t = new T3();
        new Thread(t::m1, "A").start();
        new Thread(t::m2, "B").start();
    }
}

volatile 与 synchronized 比较

  1. volatile 关键字是线程同步的轻量级实现,所以volatile性能肯定 比 synchronized 要好; volatile 只能修饰变量,而 synchronized 可以修 饰方法,代码块. 随着 JDK 新版本的发布,synchronized 的执行效率也 有较大的提升,在开发中使用 sychronized 的比率还是很大的.
  2. 多线程访问 volatile 变量不会发生阻塞,而 synchronized 可能会 阻塞
  3. volatile 能保证数据的可见性,但是不能保证原子性; 而 synchronized 可以保证原子性,也可以保证可见性
  4. 关键字 volatile 解决的是变量在多个线程之间的可见性;

原子性理解

​ 假如线程run方法的代码是下面 a是一个共享变量 两个线程执行共享变量需要三部操作,原子性就是这三部操作为一体不能分割,运行的时候不会打扰,加入volatile只能让第一部刷新副本,所以不能保证原子性。

  1. 获取副本a=0; //当这个地方t2抢到线程了 最后t1抢回来了 直接进行第二部 会覆盖 导致a变量不是++ 倒回来了
  2. 接着a=0;
  3. 同步主内存
while(true){
    a++;
}

原子类可以解决这个问题 atomic

int -AtomicInteger 自旋锁

long-AtomicLong

boolean-AtomicBollean

AtomicReferance 解决原子类的引用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gYaJRUYw-1630281947499)(https://i.loli.net/2021/08/27/8ojVXWz6M32uUTJ.png)]

乐观锁:只针对 值的修改,只在修改处加校验,具体大段代码逻辑,它不管

悲观锁:针对大段的逻辑,上下文关联的,要把代码变成原子性。针对 多改,少查。

CAS

​ CAS (Compare And Swap)是由硬件实现的. CAS 可以将 read- modify - write 这类的操作转换为原子操作

i++自增操作包括三个子操作:

  • 从主内存读取 i 变量值

  • 对 i 的值加 1

  • 再把加 1 之后 的值保存到主内存

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

线程间的通信

等待通知机制的实现

​ Object 类中的 wait()方法可以使执行当前代码的线程等待,暂停执 行,直到接到通知或被中断为止. 注意: 1) wait()方法只能 在同步代码块中由锁对象调用 2) 调用 wait()方法,当前线程会释放

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

Lock显示锁

​ 在 JDK5 中 增 加 了 Lock 锁 接 口 , 有 ReentrantLock 实 现 类,ReentrantLock 锁称为可重入锁, 它功能比 synchronized 多

可重入锁 ReentrantLock

假如一个方法a里面需要this锁 a里面有方法b b也需要this锁 ,如果当前线程能再次获得锁,这就是锁的可重入性,假设不可重入的话,可能造成死锁

  • 调用 lock()方法获得锁,

  • 调用 unlock()释放锁

  • lockInterruptibly() 方法的作用:如果当前线程未被中断则获得锁, 如果当前线程被中断则出现异常。对于 synchronized 内部锁来说,如果一个线程在等待锁,只有两个结 果:要么该线程获得锁继续执行;要么就保持等待. 对于 ReentrantLock 可重入锁来说,提供另外一种可能,在等待锁的过程中,程序可以根据该方法需要取消对锁的请求。

  • tryLock(long time, TimeUnit unit) 的作用在给定等待时长内锁没有 被另外的线程持有,并且当前线程也没有被中断,则获得该锁.通过该 方法可以实现锁对象的限时等待。lock.tryLock()只尝试一下,被其它线程占用,返回false,不等待。

tryLock解决死锁问题

package com.ysh;

import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @className: com.ysh.ThreadDemoabc
 * @description: TODO
 * @author: YSH
 * @create: 2021-08-27 11:38
 */
public class ThreadDemoabc {

    static class IntLock implements Runnable {
        private static ReentrantLock lock1 = new ReentrantLock();
        private static ReentrantLock lock2 = new ReentrantLock();
        private int lockNum; //用于控制锁的顺序

        public IntLock(int lockNum) {
            this.lockNum = lockNum;
        }

        @Override
        public void run() {
            if (lockNum % 2 == 0) { //偶数先锁 1,再锁 2
                while (true) {
                    try {
                        if (lock1.tryLock()) {
                            System.out.println(Thread.currentThread().getName() + "获得 锁 1, 还想获得锁 2");
                            Thread.sleep(new Random().nextInt(100));
                            try {
                                if (lock2.tryLock()) {
                                    System.out.println(Thread.currentThread().getName() + "同时获得锁 1 与锁 2 ----完成任务了");
                                    return; //结束 run()方法执行,即当前线程
                                }
                            } finally {
                                if (lock2.isHeldByCurrentThread()) {
                                    lock2.unlock();
                                }
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        if (lock1.isHeldByCurrentThread()) {
                            lock1.unlock();
                        }
                    }
                }
            } else { //奇数就先锁 2,再锁 1
                while (true) {
                    try {
                        if (lock2.tryLock()) {
                            System.out.println(Thread.currentThread().getName() + "获得 锁 2, 还想获得锁 1");
                            //Thread.sleep(new Random().nextInt(100))
                            try {
                                if (lock1.tryLock()) {
                                    System.out.println(Thread.currentThread().getName() + "同时获得锁 1 与锁 2 ----完成任务了");
                                    return; //结束 run()方法执行,即当前线程

                                }
                            } finally {
                                if (lock1.isHeldByCurrentThread()) {
                                    lock1.unlock();
                                }
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        if (lock2.isHeldByCurrentThread()) {
                            lock2.unlock();
                        }
                    }
                }
            }
        }


        public static void main(String[] args) {
            IntLock intLock1 = new IntLock(11);
            IntLock intLock2 = new IntLock(22);
            Thread t1 = new Thread(intLock1);
            Thread t2 = new Thread(intLock2);
            t1.start();
            t2.start();
//运行后,使用 tryLock()尝试获得锁,不会傻傻的等待,通过循环不停的再次尝试,如果
            // 等待的时间足够长, 线程总是会获得想要的资源
        }
    }
}

Condition

​ 关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实 现等待/通知模式. Lock 锁的 newContition()方法返回 Condition 对 象,Condition 类也可以实现等待/通知模式. 使用 notify()通知时, JVM 会随机唤醒某个等待的线程. 使用 Condition 类可以进行选择性通知. Condition 比较常用的两个方法: await()会使当前线程等待,同时会释放锁,当其他线程调用 signal() 时,线程会重新获得锁并继续执行. signal()用于唤醒一个等待的线程 注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相 关的 Lock 锁. 调用 await()后线程会释放这个锁,在 singal()调用后会从 当前 Condition 对象的等待队列中,唤醒 一个线程,唤醒 的线程尝试 获得锁, 一旦获得锁成功就继续执行.

private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
lock.lock();
conditionA.await();
lock.unlock();
//-----------
lock.lock();
conditionA.await();
lock.unlock();
//相当与给每个等待lock锁的线程都赋一个名字 到时候可以直接选择想要唤醒的线程
//再main方法唤醒想要唤醒的线程
lock.lock();
conditionA.signal();
lock.unlock();
//------------
lock.lock();
conditionA.signal();
lock.unlock();

公平锁与非公平锁

​ 大多数情况下,锁的申请都是非公平的. 如果线程1与线程2都在请求 锁 A, 当锁 A 可用时, 系统只是会从阻塞队列中随机的选择一个线程, 不能保证其公平性. 公平的锁会按照时间先后顺序,保证先到先得, 公平锁的这一特点不 会出现线程饥饿现象. 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() 判断当前线程是否持有该锁
  • boolean isLocked() 查询当前锁是否被线程持有

ReentrantReadWriteLock 读写锁

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

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

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

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

读读共享 读写互斥 写写互斥

//定义读写锁
ReadWriteLock rwLock = new ReentrantReadWriteLock();
//获得读锁
Lock readLock = rwLock.readLock();
//获得写锁
Lock writeLock = rwLock.writeLock();
//读数据
readLock.lock(); //申请读锁  rwLock.readLock.lock()
try{
读取共享数据
}finally{
readLock.unlock(); //总是在 finally 子句中释放锁
}
//写数据
writeLock.lock(); //申请写锁
try{
更新修改共享数据
}finally{
writeLock.unlock(); //总是在 finally 子句中释放锁
}

线程池

什么是线程池

​ 可以以 new Thread( () -> { 线程执行的任务 }).start(); 这种形式开 启一个线程. 当 run()方法运行结束,线程对象会被 GC 释放.

在真实的生产环境中,可能需要很多线程来支撑整个应用,当线数量非常多时 ,反而会耗尽 CPU 资源. 如果不对线程进行控制与管理, 反而会影响程序的性能. 线程开销主要包括:

​ 创建与启动线程的开销; 线程销毁开销; 线程调度的开销; 线程数量受限 CPU 处理器数量. 线程池就是有效使用线程的一种常用方式. 线程池内部可以预先 创建一定数量的工作线程,客户端代码直接将任务作为一个对象提交 给线程池, 线程池将这些任务缓存在工作队列中, 线程池中的工作线 程不断地从队列中取出任务并执行

//创建有 5 个线程大小的线程池,
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
//向线程池中提交 18 个任务,这 18 个任务存储到线程池的阻塞队列中, 线程池中这
5 个线程就从阻塞队列中取任务执行
for (int i = 0; i < 18; i++) {
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + " 编号的任务在执
行任务,开始时间: " + System.currentTimeMillis());
try {
          Thread.sleep(3000); //模拟任务执行时长
      } catch (InterruptedException e) {
               e.printStackTrace();
                                       }
            }
          });
   }
  }
}
/**
* 线程池的计划任务
*/
public class Test02 {
public static void main(String[] args) {
//创建一个有调度功能的线程池
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(10);
//在延迟 2 秒后执行任务, schedule( Runnable 任务, 延迟时长, 时间单位)
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
       System.out.println(Thread.currentThread().getId() + " -- " +
        System.currentTimeMillis() );
        }
}, 2, TimeUnit.SECONDS);
//以固定的频率执行任务,开启任务的时间是固定的, 在 3 秒后执行任务,以后每隔 5秒重新执行一次
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
         public void run() {
               System.out.println(Thread.currentThread().getId() + "----在固定频率开启任务---" + System.currentTimeMillis());
                   TimeUnit.SECONDS.sleep(3); 
//睡眠模拟任务执行时间 ,如果任务执行时长超过了时间间隔,则任务完成后立即开启下个任务
                    }
}, 3, 2, TimeUnit.SECONDS);*/
//在上次任务结束后,在固定延迟后再次执行该任务,不管执行任务耗时多长,总是在任务结束后的 2 秒再次开启新的任务
scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
           @Override
          public void run() {
               System.out.println(Thread.currentThread().getId() + "----在固定频率开启任务---" + System.currentTimeMillis());
TimeUnit.SECONDS.sleep(3); //睡眠模拟任务执行时间 ,如果任务执
行时长超过了时间间隔,则任务完成后立即开启下个任务
} 
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值