【3_多线程】

《并发编程之美》

什么是JUC

  • 源码+官方文档

JUC 就是 java.util .concurrent工具包的简称。这是一个处理线程的工具包。

在这里插入图片描述

并发与并行

  • 并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);

    ​ 单核CPU,同一时间段,多个程序都在执行。

    多线程并发编程。一个CPU,会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

    CPU分时轮询的执行线程。

  • 并行xing: 单位时间内(同一时刻),多个任务同时执行。

    ​ 多核CPU,同一时刻,多个程序同时执行。

    并行:多个物理处理器/ CPU同时执行,在同一时间点,任务一定是同时运行。

//CPU多核,多个线程可以同时执行。 我们可以使用线程池!
public class Test1 {
    public static void main(String[] args) {
        //获取cpu的核数
        System.out.println(Runtime.getRuntime().availableProcessors());
    }
}

并发编程的本质:充分利用CPU的资源!

1、并发的理解:

  • CPU同时处理线程的数量有限。
  • CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
    在这里插入图片描述

2、并行的理解:
在同一个时刻上,同时有多个线程在被CPU处理并执行。
在这里插入图片描述

线程和进程的区别

  • 先介绍线程和进程:

(1)、进程

进程是 程序的一次执行过程,是系统运行程序的基本单位

系统运行一个程序即是一个进程从创建,运行到消亡的过程。

(2)、线程

线程与进程相似,但线程是一个比进程更小的执行单位,是资源调度的基本单位

  • 再总说线程和进程的区别:

1、在 Java 中启动 main 函数时,其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程(主线程)。

2、一个进程中可以有多个线程,多个线程 共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的 程序计数器、虚拟机栈 和 本地方法栈。当所有的执行线程都结束了,那么进程就结束了。

3、各进程基本上是独立的,而同一进程中的各线程 极有可能会相互影响。

4、线程 执行开销小,但不利于资源的管理和保护;而进程正相反。

在这里插入图片描述

JAVA是没发开启线程的

public synchronized void start() {
   
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
          
        }
    }
}
//这是一个C++底层,Java是没有权限操作底层硬件的
private native void start0();
//用native修饰的方法就是本地方法,这是使用C来实现的**,然后一般这些方法都会**放到本地方法栈**中。

开启线程的start()方法调用了一个被native关键字修饰的本地方法——start0()方法,。是C、C++底层,Java是没有权限去操作硬件、开启线程的。

线程调度

从宏观角度上理解线程是 并行 运行的,但是从微观角度上分析却是 一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为 线程调度。

线程调度分为:

1、分时调度

​ 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

2、抢占式调度

​ 优先让优先级高的线程使用 CPU,如果线程的优先级相同,就随机选择(线程随机性),Java使用的为

抢占式调度。

CPU 使用抢占式调度模式在多个线程间进行着高速的切换。

对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的 运行速度,但能够提高程序 运行效率(提高了 CPU 的利用率)。

抢占式调度,每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定

线程优先级

如果希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。

Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统。

【没懂】

线程上下文切换

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。

  • 主动让出 CPU,比如调用了 sleep(), wait() 等。
  • 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死。
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
  • 被终止或结束运行

这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换

当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。

多线程

为什么要使用多线程

先从总体上来说:

  • 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销
  • 从当代互联网发展趋势来说: 现在的系统并发量大(百万级甚至千万级),多线程并发编程可以大大提高系统整体的 并发能力以及性能。

再深入到计算机底层来探讨:

  • 单核时代: 在单核时代多线程主要是为了 提高 CPU 和 IO 设备的综合利用率

    ​ 举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。假如有两个线程,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样就提高 CPU 和 IO 设备的综合利用率

  • 多核时代: 多核时代多线程主要是为了 提高 CPU 利用率。

    ​ 举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让 CPU的多个 核心被利用到,这样就提高了 CPU 的利用率

并发编程的本质:充分利用CPU的资源!

使用多线程可能带来的问题

多线程并发编程 的目的就是为了 提高程序的执行效率 和提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。

线程的生命周期和状态

public enum State { 	
	NEW,       // 初始状态, 线程被创建,但还没有调用start()方法
    
	RUNNABLE,  // 运行状态, Java线程 将操作系统中的 就绪和运行 两种状态笼统地称作 “运行中”
    	
	BLOCKED,   // 阻塞状态,  表示线程阻塞于锁

	WAITING,   // 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
	TIMED_WAITING,   // 超时等待状态,  该状态不同于WAITING,它是可以在指定的时间自行返回的

	TERMINATED;      // 终止状态,     表示当前线程已经执行完毕
}

线程的生命周期 是随着代码的执行而在不同状态之间切换:

在这里插入图片描述

在这里插入图片描述

1、线程创建之后它将处于 NEW(初始) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 **CPU 时间片(timeslice)**后就处于 RUNNING(运行) 状态。

分时调度,获得CPU时间片,

调用Thread.yield() 可以让出执行时间,给其他线程执行(只是让出一会时间并不是自己就 不执行了)

2、当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。

3、而 TIME_WAITING(超时等待) 状态相当于 在等待状态的基础上增加了超时限制。比如通过 sleep(long millis)方法或 wait(long millis)方法 可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE (运行)状态。

4、当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。

5、线程在执行完 Runnable 的**run()方法**之后将会进入到 TERMINATED(终止) 状态。

wait()方法 和 sleep()方法的区别

1、两者都可以暂停线程的执行。

2、wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。

3、两者最主要的区别在于:wait() 方法释放了锁 而 sleep() 方法没有释放锁

列:wait 会释放 lock 锁对象,notify/notifyAll 会唤醒其他正在等待获取 lock 锁对象的线程来抢占 lock 锁对象

4、wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

创建线程

  • Thread类
构造方法:
public Thread()                              //分配一个新的线程对象。 
public Thread(String name)                   //分配一个指定名字的新的线程对象。 
public Thread(Runnable target)               //分配一个带有指定目标新的线程对象。 
public Thread(Runnable target,String name)   //分配一个带有指定目标新的线程对象并指定名字(常用))
    
常用方法: 
public String getName()                 //获取当前线程名称。 
public void start()                     //线程开始执行; Java虚拟机调用此线程的run方法。 
public void run()                       //此线程要执行的任务在此处定义代码。 
public static void sleep(long millis)   //使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)
public static Thread currentThread()    //返回对当前正在执行的线程对象的引用。
    
System.out.println(Thread.currentThread().getName());

1、继承Thread类

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把 run()方法称为线程执行体。

  2. 创建Thread子类的实例,即创建了线程对象

  3. 调用线程对象的start()方法来启动该线程

java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时就不能通过 继承Thread类来创建线程了。

2、实现Runnable接口

//Runnable.java
@FunctionalInterface
public interface Runnable {
   /**
    * 被线程执行,没有返回值也无法抛出异常
    */
    public abstract void run();
}
  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正

的线程对象。

MyRunnable mr = new MyRunnable(); //创建线程对象 
Thread t = new Thread(mr, "线程名字");
  1. 调用线程对象的start()方法来启动线程。

Thread类实际上也是实现了Runnable接口的类

  • Thread和Runnable的区别

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。

  2. 可以避免java中的单继承的局限性。

  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

扩充:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用

java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进

程。

  • 通过匿名内部类的方式创建线程

整理完匿名内部类后,移除下面

  1. 匿名内部类,在【创建对象】的时候,只能使用唯一一次。

如果希望多次创建对象,而且类的内容一样的话,那么就需要使用单独定义的实现类了。

  1. 匿名对象,在【调用方法】的时候,只能调用唯一一次。

如果希望同一个对象,调用多次方法,那么必须给对象起个名字。

  1. 匿名内部类是省略了【实现类/子类名称】,但是匿名对象是省略了【对象名称】

强调:匿名内部类和匿名对象不是一回事!!!

//匿名内部类:
new 父类/接口(){
	重写父类/接口中的方法
}

/
Runnable r = new Runnable(){ 
    @Override
    public void run(){ 
        //线程任务...
    } 
};
new Thread(r).start();

/
new Thread(new Runnable(){
    @Override
    public void run(){
       //线程任务...
    }
}).start();

//
lambd表达式
//new Thread((参数)->{代码}).start();
Thread t1 = new Thread(()->{
	//线程任务...
},"线程名字");

t1.start();
  • 函数式编程

3、实现Callable接口

//Callable.java
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
//Calleable 泛型T就是 call方法的返回值类型

Thread源码,只能传入Runnable类型的参数;Callable怎么放入到Thread里面呢?

在这里插入图片描述

JDK api文档中可见:在Runnable里面有一个叫做FutureTask的实现类,它可以接受Callable参数;

在这里插入图片描述

在这里插入图片描述

这样我们就可以先把Callable 放入到FutureTask中, 再把FutureTask 放入到Thread就可以了。

  • 步骤:
  1. 定义Callable接口的实现类,并重写该接口的call()方法。call()方法有返回值,并且可以抛出异常。
  2. 创建Callable实现类的实例,
  3. 创建FutureTask类的实例对象,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正

的线程对象。

  1. 调用线程对象的start()方法来启动线程。
  2. 可以调用 FutureTask类的实例对象的.get()方法 获取call()方法的返回值
public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        for (int i = 1; i < 10; i++) {
//            new Thread(new Runnable()).start();
//            new Thread(new FutureTask<>( Callable)).start();
            
            MyThread t= new MyThread();
            //适配类:FutureTask
            FutureTask<String> futureTask = new FutureTask<>(t);
            //放入Thread使用
            new Thread(futureTask,String.valueOf(i)).start();
            
            //获取返回值
            //这个get方法可能会产生阻塞,通常把它放到最后面,挥或者使用异步通信来处理
            String s = futureTask.get();
            System.out.println("返回值:"+ s);
        }
    }
}

class MyThread implements Callable<String> {

    @Override
    public String call() throws Exception {  //可以有返回值,并且可以抛出异常
        System.out.println("Call:"+Thread.currentThread().getName());
        return "String"+Thread.currentThread().getName();
    }
}

4、线程池

调用 start() 方法会执行 run() 方法,为什么不能直接调用 run() 方法

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。

start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

如果,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

调用 start() 方法会执行线程的相应准备工作,会调用本地方法start0()方法 启动一个线程并使线程进入了就绪状态 ,直接执行 run() 方法的话不会以多线程的方式执行。

可以讲讲 java是没发开启线程的,是调用本地方法 start0()方法,由底层C、C++开启。

线程同步

线程安全问题都是由 全局变量及静态变量引起的。

若每个线程中对全局变量、静态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;

若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能出现线程安全问题

三种线程同步方式:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制。

同步代码块

//同步代码块: synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。 
synchronized(同步锁){ 
    需要同步操作的代码 
}

同步锁: 对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。
    1. 锁对象 可以是任意类型。   列:Object lock = new Object();  // 指定加锁对象
    2. 多个线程对象 要使用同一把锁。 
    //尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!
    
在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着 (BLOCKED)

同步方法

//同步方法: synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

public synchronized void method(){ 
     //线程任务...
}
//锁对象是: 谁调用这个方法就是谁。隐含锁对象就是 this 

同步锁是谁?

对于非static方法,同步锁就是 当前对象实例(this)

对于static方法,同步锁就是 当前方法所在类的字节码对象(类名.class)

//修饰实例方法:  作用于**当前对象实例**加锁,进入同步代码前要获得  **当前对象实例的锁**
synchronized void method() {
    //业务代码
}

//修饰静态方法: 就是给当前类加锁,会作用于类的**所有对象实例**,进入同步代码前要获得**当前class的锁**。 因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。    
synchronized static void method() {
    //业务代码
}

如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁

Lock锁

Lock锁也称同步锁

public void lock()     //加同步锁。 
public void unlock()   //释放同步锁。
    
//lock三部曲
//1、    Lock lock=new ReentrantLock(); 创建锁
//2、    lock.lock();   加锁
//3、    lock.unlock(); 解锁  
    
    锁【lock.lock】必须紧跟try代码块,且unlock要放到finally第一行。
public class Ticket implements Runnable{ 
    
    Lock lock = new ReentrantLock(); 
    @Override 
    public void run() { 
        while(true){  //窗口永远开启 
            lock.lock(); 
            //线程任务...
            lock.unlock(); 
        } 
    } 
}    

在这里插入图片描述

public class ReentrantLock implements Lock, java.io.Serializable {
    public ReentrantLock() {
        sync = new NonfairSync();  //非公平锁(默认)
    }

    //构造方法,ReentrantLock(true) ==> 公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

公平锁: 十分公平,必须先来后到~; (线程1需要1h,t2需要3s,t2必须要等t1执行完才能执行)

非公平锁: 十分不公平,可以插队; (默认为非公平锁)

Synchronized 和 Lock区别

  • 1、Synchronized 内置的Java关键字,Lock是一个Java接口

  • 2、Synchronized 无法判断获取锁的状态,Lock可以判断

  • 3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!

  • 4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);

    lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。

  • 5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;

  • 6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;

代码练习

//ctrl+f2终止程序运行
//Ctrl+alt+t 提示
public class SaleTicketDemo {
    public static void main(String[] args) {

        //并发:多线程操作同一个资源类,把资源类丢入线程
        Ticket2 ticket = new Ticket2();

        //new Thread((参数)->{代码}).start();
        Thread t1 = new Thread(()->{
            while(true){  //窗口永远开启,循环买票
                ticket.sale();
            }
        },"窗口1");
        t1.start();

        Thread t2 = new Thread(()->{ while(true){ticket.sale();} },"窗口2");
        t2.start();

        Thread t3 = new Thread(()->{ while(true){ticket.sale();} },"窗口3");
        t3.start();
    }

}

//资源类 OOP  :属性+方法
//synchronized (同步锁){}同步代码块
class Ticket1{
    private int number = 50;

    public void sale(){
        synchronized (this){
            if(number > 0){  //有票可以卖
                // 出票操作。使用sleep模拟一下出票时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+" 卖出了第"+number+" 张票,剩余:"+number+" 张票");
                number--;
            }
        }
    }
}

//资源类 OOP  :属性+方法
// synchronized修饰方法
class Ticket2{
    private int number = 50;

    public synchronized void sale(){

        if(number > 0){  //有票可以卖
            // 出票操作。使用sleep模拟一下出票时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" 卖出了第"+number+" 张票,剩余:"+number+" 张票");
            number--;
        }
    }
}

//资源类 OOP  :属性+方法
//lock锁
class Ticket3{
    private int number = 50;

    Lock lock = new ReentrantLock();

    public void sale(){

        lock.lock();
        try {
            if(number > 0){  //有票可以卖
                // 出票操作。使用sleep模拟一下出票时间
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+" 卖出了第"+number+" 张票,剩余:"+number +" 张票");
                number--;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

生产者和消费者

等待 业务 通知

Synchronized、wait、notify

public class A {
    public static void main(String[] args) {
        Data data = new Data();

        new Thread(()->{
            for(int i=0;i<10;i++) {
            	try {
                	data.increment();
            	} catch (InterruptedException e) {
                	e.printStackTrace();
            	}
        	}
        },"A").start();
        
        new Thread(()->{for(int i=0;i<10;i++) {
            try {
                data.decrement();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }},"B").start();
    }
}

//资源类 OOP  :属性+方法
class Data{

    private int number = 0;

    //+1
    public synchronized void increment() throws InterruptedException {
        if(number!=0){
			this.wait();   //等待
        }
        number++;          //业务
        System.out.println(Thread.currentThread().getName()+"=>"+number);       
        this.notifyAll();  //通知其他线程 我+1完毕了
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        if(number==0){            
            this.wait();   //等待操作
        }
        number--;          //业务
        System.out.println(Thread.currentThread().getName()+"=>"+number);
        this.notifyAll();  //通知其他线程  我-1完毕了
    }

}

问题存在,A线程B线程,现在如果我有四个线程A B C D!

this.notifyAll();唤醒所有,有的线程唤醒是没有必要的, 也就是被虚假唤醒了。

在这里插入图片描述

使用if判断,唤醒后线程会从wait之后的代码开始运行。不会重新判断if条件,直接继续运行if代码块之后的代码。使用while,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。

解决方案if 改为while即可,防止虚假唤醒

lock、wait、signal

public class B {
    public static void main(String[] args) {
        Data2 data = new Data2();

        new Thread(()->{
            for(int i=0;i<10;i++) {
            	data.increment();
      		}
        },"A").start();
        
        new Thread(()->{for(int i=0;i<10;i++) {
            data.decrement();}},"B").start();
        new Thread(()->{for(int i=0;i<10;i++) {
            data.increment();}},"C").start();
        new Thread(()->{for(int i=0;i<10;i++) {
            data.decrement();}},"D").start();
    }
}

//资源类 OOP  :属性+方法
class Data2{
    private int number = 0;

    //lock锁
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    //+1
    public void increment()  {
        lock.lock();
        try{
            //业务
            while (number!=0){          
                condition.await();  //等待操作
            }
            number++;
            System.out.println(Thread.currentThread().getName()+"=>"+number);  
            condition.signalAll();  //通知其他线程 我+1完毕了
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //-1
    public void decrement()  {
        lock.lock();
        try{
            //业务
            while (number==0){               
                condition.await();   //等待操作
            }
            number--;
            System.out.println(Thread.currentThread().getName()+"=>"+number);
            condition.signalAll();  //通知其他线程 我+1完毕了
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

Condition的优势:精准的通知和唤醒的线程!

指定通知的下一个进行顺序

/**A==>B==>C==>A
 * A 执行完 调用B
 * B 执行完 调用C
 * C 执行完 调用A
 */

public class C {

    public static void main(String[] args) {
        Data3 data3 = new Data3();
        new Thread(()->{
            for(int i=0;i<10;i++){
                data3.printA();
            }
        },"A").start();
        
        new Thread(()->{for(int i=0;i<10;i++){
                data3.printB();}},"B").start();
        new Thread(()->{for(int i=0;i<10;i++){
                data3.printC();}},"C").start();
    }
}

//资源类 OOP  :属性+方法
class Data3{
	//lock锁
    private Lock lock=new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    
    private int number = 1;  //标志位   //1A 2B 3C

    public void printA(){
        lock.lock();
        try {
            //业务 判断 -> 执行 -> 通知
            while(number!=1){               
                condition1.await();   //等待
            }            
            System.out.println(Thread.currentThread().getName()+",AAAAA");  //操作
            number=2;             //唤醒指定的线程
            condition2.signal();  // 唤醒2

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printB(){
        lock.lock();
        try {
            //业务 判断 -> 执行 -> 通知
            while (number!=2){
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName()+",BBBBB");      
            number=3;   //唤醒3
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            //业务 判断 -> 执行 -> 通知
            while(number!=3){
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName()+",CCCCC");           
            number=1;  //唤醒1
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
//应用场景
//流水线:下单-->支付-->交易-->物流

关于锁的8个问题

如何判断锁的是谁!锁到底锁的是谁?

锁会锁住:对象、Class

深刻理解我们的锁

/**
 * 1.标准情况下,两个线程谁先执行?(发短信和打电话谁先打印?)
 * 2.sendSms延迟一个线程情况下,两个线程谁先打印?
 * 答案:1和2都是:先发短信后打电话
 * 因为:两个方法使用的是同一个锁(一个对象phone),哪个先拿到哪个先执行。
 */
public class Lock_1To2 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();

        //发短信
        new Thread(()->{
            try {
                phone.sendSms();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"发短信").start();

        //延时
        TimeUnit.SECONDS.sleep(1);  // 这里保证 "发短信"线程 先拿到锁。即保证 "发短信"线程已经执行,先执行
        
        //打电话
        new Thread(()->{
            phone.call();
        },"打电话").start();
    }
}

class Phone{

    //synchronized 锁的对象是方法的调用者——phone对象
    //两个方法使用的是同一个锁,哪个先拿到哪个先执行。
    public synchronized void sendSms() throws InterruptedException {

/*        
		//2.延迟一个线程
        TimeUnit.SECONDS.sleep(4); 
*/        
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }
}
/**
 * 3.非静态方法,普通方法  发短信?hello?
 * 4.不同对象          发短信?打电话?
 * 答案:都是后发短信
 * 因为:3,不是同步方法,不受锁的影响。发短信线程有4秒延迟,所以就先打印 hello
 * 4,不同对象,不同的锁。发短信线程有4秒延迟,所以就先打印 打电话
 */
public class Lock_3To4 {
    public static void main(String[] args) throws InterruptedException {
        //4.不同对象
        Phone2 phone2 = new Phone2();
        Phone2 phone3 = new Phone2();

        //发短信
        new Thread(()->{
            try {
                phone2.sendSms();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"发短信").start();

        //延时
        TimeUnit.SECONDS.sleep(1);
/*        
        //3.普通方法
        new Thread(()->{
            phone3.hello();
        },"hello").start();
*/
        //打电话
        new Thread(()->{
            phone3.call();
        },"打电话").start();

    }
}

class Phone2{

    //synchronized 锁的对象是方法的调用者
    public synchronized void sendSms() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }

    //不是同步方法,不受锁的影响
    public void hello(){
        System.out.println("hello");
    }
}

同步锁是谁?

对于非static同步方法,同步锁就是 当前对象实例(this)

对于static同步方法,同步锁就是 当前方法所在类的字节码对象(类名.class)

/**
 * 5.静态同步方法,同一对象。  发短信? 打电话?
 * 6.静态同步方法,不同对象。  发短信? 打电话?
 * 答案:都是先发短信,后打电话
 * 因为:static同步方法,同步锁是 class。所以 两个方法使用的是同一个锁,哪个先拿到哪个先执行
 */
public class Lock_5To6 {
    public static void main(String[] args) throws InterruptedException {
        Phone3 phone1 = new Phone3();
        Phone3 phone2 = new Phone3();

        //发短信
        new Thread(()->{
            try {
                phone1.sendSms();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"发短信").start();

        //延时
        TimeUnit.SECONDS.sleep(1);

        //打电话
        new Thread(()->{
            phone2.call();
        },"打电话").start();

    }
}

class Phone3{

    //static 类一加载就有了!锁的是Class
    //synchronized 锁的对象是方法的调用者
    //两个方法使用的是同一个锁,哪个先拿到哪个先执行
    public static synchronized void sendSms() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }

    public static synchronized void call(){
        System.out.println("打电话");
    }

}
/**
 * 7.静态同步方法和同步方法 ,同一对象  发短信?打电话?
 * 8.静态同步方法和同步方法 ,不同对象  发短信?打电话?
 * 答案:都是打电话后发短信
 */

import java.util.concurrent.TimeUnit;


public class Lock_7To8 {
    public static void main(String[] args) throws InterruptedException {
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();

        //发短信
        new Thread(()->{
            try {
                phone1.sendSms();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"发短信").start();

        //延时
        TimeUnit.SECONDS.sleep(1);

        //打电话
        new Thread(()->{
            phone2.call();
        },"打电话").start();

    }
}

class Phone4{

    //synchronized 锁的对象是方法的调用者
    //两个方法使用的是同一个锁,哪个先拿到哪个先执行
    public static synchronized void sendSms() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }

    public synchronized void call(){
        System.out.println("打电话");
    }

}

总结:

非静态方法的锁 this, 静态方法的锁 对应的Class实例

某一个时刻内,只能由一个线程持有锁,无论几个方法

同一个锁,谁先拿到谁就先执行。(程序顺序,不管延时,但是…)

不同锁,看延时,时间少的先执行。(相同,就按程序顺序执行吧)

线程池(3+4=7)

https://www.cnblogs.com/yulinfeng/p/7021293.html

https://blog.csdn.net/suchenbin/article/details/102143890

线程池:三大方法、7大参数、4种拒绝策略

  • 池化技术

程序的运行本质:占用系统的资源!

为了减少每次获取资源的消耗,提高对资源的利用率 ===> 池化技术(线程池、JDBC的连接池、内存池…)

【资源的创建、销毁十分消耗资源】

池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率

  • 线程池的好处:

1、降低资源的消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

2、提高响应的速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行。

3、方便管理:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程复用、可以控制最大并发数、管理线程;

Java线程池的顶级接口是 java.util.concurrent.Executor ,它只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService

在这里插入图片描述

三大方法(Executors工具类创建线程池)

  • ExecutorService threadPool = Executors.newSingleThreadExecutor(); //单个线程
  • ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
  • ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的

线程池创建方式一:Executors 工具类

//工具类 Executors 三大方法;
public class Demo01 {
    public static void main(String[] args) {

        ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
        ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
        ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的

         
        try {

            for (int i = 1; i <=10 ; i++) {
                //threadPool.execute()获取线程池中的某一个线程对象,并执行任务
                threadPool2.execute(()->{
                    System.out.println(Thread.currentThread().getName()+ " ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool2.shutdown();  //线程池用完必须要关闭线程池 
        }
    }
}
/*
pool-2-thread-1 ok
pool-2-thread-1 ok
pool-2-thread-1 ok
pool-2-thread-1 ok
pool-2-thread-1 ok
pool-2-thread-1 ok
pool-2-thread-3 ok
pool-2-thread-2 ok
pool-2-thread-4 ok
pool-2-thread-5 ok

Process finished with exit code 0

在这里插入图片描述

7大参数

源码分析

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,//21亿
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

本质:三种方法都是开启的ThreadPoolExecutor

public ThreadPoolExecutor(
    int corePoolSize,      //线程池 核心的线程数量                        
    int maximumPoolSize,   //线程池 最大的线程数量
    long keepAliveTime,    //空闲线程存活时间(超过多少时间 没有人调用 就会释放线程)
    TimeUnit unit,         //超时单位
    BlockingQueue<Runnable> workQueue,  //指定 任务队列所使用的 阻塞队列
    ThreadFactory threadFactory,        //线程工厂 创建线程的 一般不用动
    //拒绝策略
    RejectedExecutionHandler handler) {
   
    
    
}

阿里巴巴的Java操作手册中明确说明:对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池。

在这里插入图片描述

线程最大数量队列长度 默认是Integer.MAX_VALUE,初始值较大,可能会有 OOM 的风险。

说白了就是:使用有界队列,控制线程创建数量。

补充:

除了避免 OOM 的原因之外,不推荐使用 Executors 提供的两种快捷的线程池的原因还有:

  1. 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。
  2. 我们应该显示地给我们的线程池命名,这样有助于我们定位问题。

线程池创建方式二::通过ThreadPoolExecutor构造方法

4种拒绝策略

在这里插入图片描述

// 如果当前同时运行的线程数量超出 最大承载(队列容量大小+maxPoolSize),就采用拒绝策略

1、不处理,并抛出异常: `RejectedExecutionException`
new ThreadPoolExecutor.AbortPolicy()  //源码默认

2、哪来的去哪里 main线程进行处理
new ThreadPoolExecutor.CallerRunsPolicy()

3、 不处理新任务,直接丢弃掉。不抛出异常
new ThreadPoolExecutor.DiscardPolicy()

4、尝试去和最早的进程竞争,丢弃最早的未处理的任务请求。不会抛出异常
new ThreadPoolExecutor.DiscardOldestPolicy()

ThreadPoolExecutor.CallerRunsPolicy 调用执行自己的线程(比如main线程)进行处理,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。

美团面试:栈和队列的应用场景

队列应用场景:比如有些任务很耗时但不得不做,那么你可能放在一个队列里,使用一个线程每一次从队列里取出一个并执行。

栈应用场景:浏览器的后退功能。如果你的系统需要支持撤消功能也可以考虑栈

ThreadPoolExecutor创建线程池

在这里插入图片描述

1、两个 核心的窗口 先工作

2、当两个 核心的窗口满了,多余的顾客就去 侯客区等候

3、候客区也满了,其余窗口也陆续打开。

4、6个窗口 和 候客区都满了,执行相应的拒绝策略。

public class Test {
    public static void main(String[] args) {

		ExecutorService threadPool = new ThreadPoolExecutor(
			2,
			6,
			3,
			TimeUnit.SECONDS,
			new LinkedBlockingDeque<>(3),
			Executors.defaultThreadFactory(),
			new ThreadPoolExecutor.AbortPolicy());
        
        try {
            for (int i = 1; i <=6 ; i++) {
                //threadPool.execute()获取线程池中的某一个线程对象,并执行任务
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+ " ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();  //线程池用完必须要关闭线程池
        }
    }
}

随着 任务数量 的增加

1、当 工作线程数量 小于等于corePoolSize时,那么直接调用addWoker(),来添加工作线程。

2、当大于corePoolSize时,会通过workQueue.offer()方法 试图将任务加入任务队列。(阻塞队列存储任务)

3、当任务队列(有界)满了,但运行的线程数小于maximumPoolSize最大线程池的数量时,会新建线程用于执行任务,直到到达maximumPoolSize最大线程池的数量。

4、当运行的线程数到达了maximumPoolSize最大线程池的数量,此时达到 最大承载(队列容量大小+maxPoolSize)。会拒绝继续执行任务 并执行相应的拒绝策略。

  • execute()方法源码:
// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

private static int workerCountOf(int c) {
    return c & CAPACITY;
}

private final BlockingQueue<Runnable> workQueue;

public void execute(Runnable command) {
    // 如果任务为null,则抛出异常。
    if (command == null)
        throw new NullPointerException();
    // ctl 中保存的线程池当前的一些状态信息
    int c = ctl.get();

    //  下面会涉及到 3 步 操作
    // 1.首先判断当前线程池中执行的任务数量是否小于 corePoolSize
    // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 2.如果当前执行的任务数量大于等于 corePoolSize 的时候就会走到这里
    // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
        if (!isRunning(recheck) && remove(command))
            reject(command);
            // 如果当前线程池为空就新创建一个线程并执行。
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
    //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
    else if (!addWorker(command, false))
        reject(command);
}

在这里插入图片描述

问题:如果你提交任务是,线程队列已满,这时会发生什么?

核心线程池数…最大线程池的数…

如何设置线程池的最大大小

CPU密集型 和 IO密集型

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。


1、CPU密集型: maximunPoolSize的大小设置为 Ncpu + 1

每一个CPU核心都参与计算,将CPU的性能充分利用起来。

对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是: 线程数= CPU核数+1

+1是 为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间

//获取CPU核数
Runtime.getRuntime().availableProcessors();

2、I/O密集型: maximunPoolSize的大小设置为 2Ncpu

我们现在做的开发大部分都是WEB应用,涉及到大量的网络传输,不仅如此,与数据库,与缓存间的交互也涉及到IO,一旦发生IO,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行。因此从这里可以发现,对于IO密集型的应用,我们可以多设置一些线程池中线程的数量,这样就能让在等待的这段时间内,线程可以去做其它事,提高并发处理效率。

I/O密集型就是 判断我们程序中十分耗I/O的线程数量。

系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程。

3、美图面试: IO密集=Ncpu*2是怎么计算出来

参考 面试问到,就说找到了两个不同的公式,自己做了一些对比。

  • 《java并发编程实践》

Nthreads =Ncpu * Ucpu * (1+w/c)

​ Ncpu:CPU核心数

​ Ucpu:cpu使用率,0~1

​ W/C:等待时间与计算时间的比率

​ 假设cpu100%运转,即撇开CPU使用率这个因素,线程数=Ncpu*(1+w/c)。

  • 《Java 虚拟机并发编程》

线程数 = Ncpu/(1-阻塞系数)

​ 阻塞系数 = 阻塞时间/(阻塞时间+计算时间)= w/(w+c) ,0~1

​ 计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近1。一个完全阻塞的任务是注定要挂掉的,所以我们无须担心阻塞系数会达到1。

IO密集型

​ 一般情况下,如果存在IO,那么肯定w/c>1(阻塞耗时一般都是计算耗时的很多倍)。

​ 但是需要考虑系统内存有限(每开启一个线程都需要内存空间),这里需要上服务器测试具体多少个线程数适合(CPU占比、线程数、总耗时、内存消耗)。

​ 如果不想去测试,保守点取1即,Nthreads = Ncpu*(1+1) = 2Ncpu。这样设置一般都OK。

计算密集型:假设没有等待 w=0,则 W/C=0。Nthreads=Ncpu。

execute()方法和 submit()方法的区别

  1. execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  2. submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

我们以** AbstractExecutorService **接口中的一个 submit 方法为例子来看看源代码:

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

上面方法调用的 newTaskFor 方法返回了一个 FutureTask 对象。

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
}

我们再来看看execute()方法:

public void execute(Runnable command) {
  ...
}

synchronized 关键字

程序员囧辉-知乎

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

另外,在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。

为什么呢?

因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。

JDK1.6 之后的 synchronized 关键字底层做了哪些优化

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

关于这几种优化的详细信息可以查看下面这篇文章:Java6 及以上版本对 synchronized 的优化

构造方法不能使用 synchronized 关键字修饰

构造方法本身就属于线程安全的,不存在同步构造方法一说。

synchronize 底层维护了三个双向链表存放被阻塞的线程

sleep()和wait() 两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁

列:wait 会释放 lock 锁对象,notify/notifyAll 会唤醒其他正在等待获取 lock 锁对象的线程来抢占 lock 锁对象

synchronized 底层使用了3个双向链表来存放被阻塞的线程:cxq(Contention queue竞争队列)、EntryList、WaitSet。

1、当线程获取锁失败进入阻塞后,首先会被加入到 cxq 链表,cxq 链表的节点会在某个时刻被进一步转移到 EntryList 链表。(排队策略)

2、当持有锁的线程释放锁后,EntryList 链表头结点的线程会被唤醒,该线程称为 successor(假定继承者),然后该线程会尝试抢占锁。

释放锁时被唤醒的线程称为“假定继承者”。因为被唤醒的线程并不是就一定获取到锁了,该线程仍然需要去竞争锁,而且可能会失败,所以该线程并不是就一定会成为锁的“继承者”,而只是有机会成为,所以我们称它为假定的。

1、当我们调用 wait() 时,线程会被放入 WaitSet,直到调用了 notify()/notifyAll() 后,线程才被重新放入 cxq 或 EntryList,默认放入 cxq 链表头部。

objectMonitor 的整体流程如下图:

在这里插入图片描述

  • synchronized 是非公平锁,非公平体现在哪些地方?

1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作:

  1. 先将锁的持有者 owner 属性赋值为 null
  2. 唤醒等待链表中的一个线程(假定继承者)。

在1和2之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。

2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒。

  • notifyAll 是怎么实现全唤起的?

nofity 是获取 WaitSet 的头结点,执行唤起操作。

nofityAll 是循环遍历 WaitSet 的所有节点,对每个节点执行 notify 操作。

synchronized 关键字的底层原理

synchronized 关键字底层原理属于 JVM 层面。

(1)synchronized 同步语句块

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class

在这里插入图片描述

从上面我们可以看出:

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令

其中 monitorenter 指令指向同步代码块的开始位置monitorexit 指令则指明同步代码块的结束位置

当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。

另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

(2)synchronized 修饰方法

public class SynchronizedDemo2 {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}
Copy to clipboardErrorCopied

在这里插入图片描述

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

(3)总结

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

不过两者的本质都是对对象监视器 monitor 的获取。

synchronized 和 ReentrantLock 的区别

(1)两者都是可重入锁

“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。

(2)synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

(3)ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronizedReentrantLock增加了一些高级功能。主要来说主要有三点:

  • 等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • 可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • 可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。

Condition是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。性能已不是选择标准

volatile 关键字

我们先要从 CPU 缓存模型 说起!

CPU 缓存模型

为什么要弄一个 CPU 高速缓存呢?

类比我们开发网站后台系统使用的缓存(比如 Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。 CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。

我们甚至可以把 内存可以看作外存的高速缓存,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。

总结:CPU缓存的是内存数据 用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据 用于解决硬盘访问速度过慢的问题。

为了更好地理解,我画了一个简单的 CPU Cache 示意图如下(实际上,现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache):

在这里插入图片描述

CPU Cache 的工作方式:

先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 内存缓存不一致性的问题 !比如我执行一个 i++操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。

CPU 为了解决内存缓存不一致性问题可以通过制定 缓存一致协议 或者其他手段来解决。

JMM

(1)JAVA内存模型:JMM

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。

本地内存 是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

在这里插入图片描述

多线程访问共享变量

​ 线程操作的是自己的工作内存,而不会直接操作主内存。如果线程对变量的操作没有刷写回主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。而如果另一个变量没有读取主内存中的新的值,而是使用旧的值的话,同样的也可以列为不可见。

​ 对于jvm来说,主内存是所有线程共享的java堆,而工作内存中的共享变量的副本是从主内存拷贝过去的,是线程私有的局部变量,位于java栈中。

(2)关于JMM的一些同步的约定:

1、线程解锁前,必须把共享变量立刻刷回主存;

2、线程加锁前,必须读取主存中的最新值到工作内存中;

3、加锁和解锁是同一把锁;

线程中分为 工作内存、主内存

(3)8种操作:

  • Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;

  • load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;

  • Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;

  • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;

  • store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;

  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;

  • lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;

  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

JMM对这8种操作给了相应的规定

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

在这里插入图片描述

(4)遇到问题:

一个线程在主存中修改了 变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致

在这里插入图片描述

public class Test {        
    private static Integer number = 0;    
    public static void main(String[] args) {   //main线程        
        //子线程1        
        new Thread(()->{  // 线程1对主内存的变化不知道            
            while (number==0){  
                
            }        
        }).start();  
        
        try {            
            TimeUnit.SECONDS.sleep(2);        
        } catch (InterruptedException e) {            
            e.printStackTrace();        
        }        
        number=1;        
        System.out.println(number);    
    }
}
/*打印1,程序不停止运行
//main线程修改number为1,但是子线程1的本地内存中的number还是为0。所以一直while循环

Volatile

Volatile 是 Java 虚拟机提供 轻量级的同步机制

1、保证可见性
2、不保证原子性
3、禁止指令重排 (加内存屏障)

面试官:那么你知道在哪里用这个内存屏障用得最多呢?单例模式

并发编程的三个重要特性:

  1. 可见性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
  2. 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原子性。
  3. 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排。

1、保证可见性

在这里插入图片描述

在这里插入图片描述

变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

public class JMMDemo01 {    
    //volatile    
    private volatile static Integer number = 0;    
    public static void main(String[] args) {   //main线程        
        //子线程1        
        new Thread(()->{  // 线程1对主内存的变化不这道            
            while (number==0){   
                
            }        
        }).start();        
        
        try {            
            TimeUnit.SECONDS.sleep(2);        
        } catch (InterruptedException e) {            
            e.printStackTrace();        }        
        number=1;        
        System.out.println(number);    
    }
}
/*打印1,程序停止运行

在这里插入图片描述

2、不保证原子性

原子性:不可分割;线程A在执行任务的时候,不能被打扰的,也不能被分割的,要么同时成功,要么同时失败。

public class VDemo02 {

    private static volatile int number = 0; // 加不加volatile 都不能保证原子性

    public static void add(){  //加锁 lock或synchronized 可以保证原子性
        number++; 
        // ++ 不是一个原子性操作,是两个~3个操作。可能被多个线程同时操作
    }

    public static void main(String[] args) {
        
        //理论上number == 20000
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000 ; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){ //存活的线程数量> 2  //main  gc           
            Thread.yield(); //让出计算资源并重新竞争资源
        }
        System.out.println(Thread.currentThread().getName()+",num="+number);
    }
}
//结果不为20000,说明在执行线程任务的时候被打扰了,不能保证原子性

number++; 实际上包含了三个独立的操作,不会作为一个不可分割的操作来执行。所以他并不是一个原子性操作。

number++; 是一个“读取-修改-写入”的操作序列,并且其结果依赖于之前的状态。可能被多个线程同时操作

在这里插入图片描述

要解决这个问题,可以加锁,或者使用原子类(如 AtomicInteger)。

如果不加lock和synchronized ,怎么样保证原子性?

使用原子类解决原子性问题。

public class VDemo02 {

    //原子类的Integer
    private static volatile AtomicInteger number = new AtomicInteger();

    public static void add(){
        // number++;
        number.incrementAndGet();  // AtomicInteger +1方法。底层是CAS保证的原子性
    }

    public static void main(String[] args) {
        
        //理论上number == 20000
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000 ; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount()>2){ //存活的线程数量> 2  //main  gc           
            Thread.yield(); //让出计算资源并重新竞争资源
        }
        System.out.println(Thread.currentThread().getName()+",num="+number);
    }
}
//输出 20000

这些类的底层都直接和操作系统挂钩!是在内存中修改值。Unsafe类是一个很特殊的存在;

3、禁止指令重排(加内存屏障)

什么是指令重排?

我们写的程序,计算机并不是按照我们自己写的顺序去执行的

源代码–->编译器优化重排-–>指令并行也可能会重排–->内存系统也会重排–->执行

处理器在进行指令重排的时候,会考虑数据之间的依赖性!

int x=1;    //1
int y=2;    //2
x = x+5;    //3
y = x*x;    //4

//我们期望的执行顺序是 1-->2-->3-->4  可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的

可能造成的影响结果:前提:a b x y这四个值 默认都是0

线程A线程B
x = ay = b
b = 1a = 2

正常的结果: x = 0; y =0;

线程A线程B
x=ay=b
b=1a=2

可能在线程A中会出现,先执行b=1,然后再执行x=a;

在B线程中可能会出现,先执行a=2,然后执行y=b;

那么就有可能结果如下:x=2; y=1.

volatile可以避免指令重排:

volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。

内存屏障:CPU指令。作用:

1、保证特定的操作的执行顺序;

2、可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)

image-20210804151856100

synchronized 关键字和 volatile 关键字的区别

synchronized其是一种加锁机制,会有性能损耗、 产生阻塞。

使用 volatile 和 synchronized 锁都可以保证共享变量的可见性。相比 synchronized 而言,volatile 可以看作是一个轻量级锁,所以使用 volatile 的成本更低,因为它不会引起线程上下文的切换和调度。但 volatile 无法像 synchronized 一样保证操作的原子性。

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

  • volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

Atomic 原子类

既能保证原子性也能保证线程安全

https://blog.csdn.net/weixin_38003389/article/details/88569336

更多见:JUC 中的 Atomic 原子类总结

原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原子性。

Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。所以,所谓原子类说简单点就是具有 原子/原子操作 特征的类。

为什么要有原子类
对多线程访问同一变量,我们需要加锁,而锁是比较消耗性能的。新增的原子类提供了一种简单、性能高效、线程安全地更新一个变量的方式。

JUC并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic

在这里插入图片描述

根据操作的数据类型,可将JUC包的原子类分为4类:

基本类型

使用原子的方式更新基本类型

AtomicInteger    //整形原子类
AtomicLong       //长整型原子类
AtomicBoolean    //布尔型原子类

数组类型

使用原子的方式更新数组里的某个元素

AtomicIntegerArray		//整形数组原子类
AtomicLongArray		    //长整形数组原子类
AtomicReferenceArray	//引用类型数组原子类

引用类型

AtomicReference             //引用类型原子类
AtomicMarkableReference     //原子更新带有标记位的引用类型
AtomicStampedReference      //原子更新带有版本号的引用类型。 该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

对象的属性修改类型

AtomicIntegerFieldUpdater   //原子更新整形字段的更新器
AtomicLongFieldUpdater      //原子更新长整形字段的更新器
AtomicReferenceFieldUpdater //原子更新引用类型字段的更新器

JDK8新增原子类

DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder

AtomicInteger

//AtomicInteger 类常用方法

public final int get()                   // 获取当前的值
public final int getAndSet(int newValue) // 以原子的方式设置为newValue,并返回旧值
public final int getAndIncrement()       // 获取当前的值,并自增         (i++)
public final int incrementAndGet()       // 获取 当前的值+1            (++i)
public final int getAndDecrement()       // 获取当前的值,并自减         (i--)
public final int getAndAdd(int delta)    // 获取当前的值,并加上预期的值  (a+=i)
    
//如果输入的 实际值等于expect期望值,则以原子方式将该值更新为update
boolean compareAndSet(int expect, int update) 
    
//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。   
public final void lazySet(int newValue)

AtomicInteger 类的使用示例

class AtomicIntegerTest {
    
    private AtomicInteger count = new AtomicInteger();
    
    public void increment() {
        count.incrementAndGet();  //底层是CAS保证的原子性
    }

    public int getCount() {
        return count.get();
    }
}

源码分析:

在这里插入图片描述

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe U = Unsafe.getUnsafe();
    /*
    java无法操作内存
    java可以调用C++  (native方法)
    C++可以造作内存
    可以通过 Unsafe类 操作内存
    */
    //...
    private static final long VALUE //获取内存地址偏移值
        = U.objectFieldOffset(AtomicInteger.class, "value");

    private volatile int value;  // volatile保证可见性
     
    //...
	public final int incrementAndGet() {
    	return U.getAndAddInt(this, VALUE, 1) + 1;
    }
    //...
}
    
} 

在这里插入图片描述

public final class Unsafe {
    //....Unsafe类中有很多 用native修饰的方法就是本地方法
    //...
    public long objectFieldOffset(Class<?> c, String name) {
        if (c == null || name == null) {
            throw new NullPointerException();
        }
        return objectFieldOffset1(c, name);
    }
    
    //...
    @HotSpotIntrinsicCandidate
    public final int getAndAddInt(Object o, long offset, int delta) {  //delta==1
        int v;
        do {
            v = getIntVolatile(o, offset);//获取内存中的地址值
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        //对象o的 内存地址偏移值offset 和 内存中的地址值v,比较交换,更新值为 v+1
        return v;
    }
    //....
}   

CAS:比较当前工作内存中的值 和 主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环,使用的是自旋锁。

在这里插入图片描述

总结:

AtomicInteger 类主要利用 CAS (compareAndSet) + volatile 和 native() 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

CAS

CAS 是CPU的并发原语,要依靠硬件的支持,CAS指令有三个参数(共享变量的地址A,用于比较的变量B, 更新的新值C),只有当A=B时候,将A处的值更新为C。作为一条CPU指令,CAS指令本身是可以保证原子性的。

CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。

CAS相对与互斥锁方案,最大的优点就是性能。互斥锁为了达到互斥的目的操作前后都要有加锁解锁。这样性能损耗很大,而CAS采用无锁的方式达到了互斥的效果,性能相对而言较高。

CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

悲观锁:很悲观,认为什么时候都会出现问题,无论做什么都会加锁

乐观锁:很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据

CAS:compareAndSet 比较并交换 如果实际值 和 我的期望值相同,那么就更新

public class casDemo {
    //CAS : compareAndSet 比较并交换
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        //boolean compareAndSet(int expect, int update)  //期望值、更新值
        //如果实际值 和 我的期望值相同,那么就更新
        //如果实际值 和 我的期望值不同,那么就不更新
        System.out.println(atomicInteger.compareAndSet(2020, 2021)); //true
        System.out.println(atomicInteger.get());  //2021
        
        // CAS 是CPU的并发原语
        // 实际值变成 2021
        System.out.println(atomicInteger.compareAndSet(2020, 2022));//false
        System.out.println(atomicInteger.get()); //2021
    }
}
//compareAndSet()底层是用的 == 比较

缺点:

  • 循环会耗时;(自旋锁 如果长时间不成功,会给CPU带来非常大的执行开销。)
  • 一次性只能保证一个共享变量的原子性;
  • 它会存在ABA问题

ABA问题

如果一个线程修某个变量值(假设原来是A),先修改成B,再修改回成A。当另一个线程的CAS操作无法分辨当前变量值是否发生过变化。

在这里插入图片描述

线程2:两个操作:

  • 1、期望值是1,变成3
  • 2、期望是3,变成1

结果 A还是 = 1

线程1:期望值是1,要变成2;

所以对于线程1来说,虽然 一开始A的值还是1,但是是被线程2修改过的。

public class casDemo {
    //CAS : compareAndSet 比较并交换
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        //boolean compareAndSet(int expect, int update)  //期望值、更新值       
        //如果实际值 和 我的期望值相同,那么就更新
        //如果实际值 和 我的期望值不同,那么就不更新
        
         //=============捣乱的线程=================
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get()); //2021

        System.out.println(atomicInteger.compareAndSet(2021, 2020));
        System.out.println(atomicInteger.get()); //2020
		//此时atomicInteger是更新后的2020

        //=============期望的线程=================
        System.out.println(atomicInteger.compareAndSet(2020, 6666));
        System.out.println(atomicInteger.get()); //6666
    }
}

原子引用

原子引用(带版本号的 原子操作)

对于ABA问题,比较有效的方案是 引入版本号,内存中的值每发生一次变化,版本号都+1;在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。

public class Test {

    //AtomicInteger atomicInteger = new AtomicInteger(2020);

    //正常的业务,对于initialRef,比较的都是一个个对象
    static AtomicStampedReference<Integer> atomicStampedReference =
            new AtomicStampedReference<>(1, 1);
    //AtomicStampedReference<>(initialRef,intialStamp时间戳/版本号)

    public static void main(String[] args) {


        //t1 线程
        // Ref:1-->2-->1
        //Stamp: 初始为1,修改2次,最后为3
        new Thread(()->{
            System.out.println("时间戳为:"+atomicStampedReference.getStamp()); //获得版本号 //1
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

//public boolean compareAndSet(V expectedReference,V newReference,
//                             int expectedStamp,int newStamp);
            atomicStampedReference.compareAndSet(1, 2,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp()+1);

            System.out.println("时间戳为:"+atomicStampedReference.getStamp()); //2

            atomicStampedReference.compareAndSet(2, 1,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp()+1);

            System.out.println("时间戳为:"+atomicStampedReference.getStamp()); //3

        },"t1").start();


        //和乐观锁的原理相同
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println("当前版本号为:"+stamp); //1

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("时间戳为:"+atomicStampedReference.getStamp()); //3
            System.out.println(atomicStampedReference.
                    compareAndSet(1, 6,stamp,stamp+1)); //false
            //expectedReference:1==1但是 stamp为1,atomicStampedReference.getStamp()为3
            
            System.out.println("时间戳为:"+atomicStampedReference.getStamp()); //3
        },"t2").start();
    }
/*
时间戳为:1
当前版本号为:1     //t1 t2顺序执行
时间戳为:2
时间戳为:3
时间戳为:3
false
时间戳为:3       //有延时,先执行时间少的线程
*/

}

这儿举例子出现的一个坑:

// 如果泛型是一个包装类,注意对象的引用问题。(大多数包装类都实现了 常量池技术)
static AtomicStampedReference<Integer> atomicStampedReference =
            new AtomicStampedReference<>(2020, 1);

此处initialRef是Integer类型,
int 包装类Integer 实现了常量池技术,做了缓存,-128127。
当不在[-128 127]范围内,就会重新new,创建新的对象存储在堆中,分配新的内存空间。
而 compareAndSet()底层判断相等否用的是 ==,对于引用类型比较的是地址值。
所以,initialRef设为 2020,不会得到正确的结果。
    
正常的业务,对于initialRef,比较的都是一个个对象 
static AtomicStampedReference<Person> atomicStampedReference =
            new AtomicStampedReference<>(person, 1);    

解决ABA问题,对应的思想:就是使用了乐观锁~

总结

原子类

对多线程访问同一变量,我们需要加锁,而锁是比较消耗性能的。新增的原子类提供了一种简单、性能高效、线程安全地更新一个变量的方式。

JUC包的原子类分为4类:基本类型、数组类型、引用类型、对象的属性修改类型,还有JDK8新增原子类。

AtomicInteger 类主要利用 CAS (compareAndSet) + volatile 和 native() 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。(源码要去看一下)

CAS(CompareAndSet比较并交换)

CAS 是CPU的并发原语,要依靠硬件的支持,他本身是可以保证原子性的。所以多线程并发使用CAS更新数据时,可以不使用锁。

CAS相对与互斥锁方案,最大的优点就是性能。互斥锁为了达到互斥的目的操作前后都要有加锁解锁。这样性能损耗很大,而CAS采用无锁的方式达到了互斥的效果,性能相对而言较高。

  • 基本类型 AtomicInteger类
AtomicInteger atomicInteger = new AtomicInteger( 实际值 );

atomicInteger.compareAndSet(int expect, int update)方法
  • 会出现ABA问题(如果一个线程修某个变量值(假设原来是A),先修改成B,再修改回成A。另一个线程的CAS操作无法分辨当前变量值是否发生过变化。)

  • 解决方法:原子引用(带版本号的 原子操作)

  • 引用类型 AtomicStampedReference类(原子更新带有版本号的引用类型)。 该类将整数值与引用关联起来,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

// 如果泛型是一个包装类,注意对象的引用问题。(大多数包装类都实现了 常量池缓存技术)
static AtomicStampedReference<Integer> atomicStampedReference =
            new AtomicStampedReference<>(2020, 1);
atomicStampedReference.compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
    
此处initialRef是Integer类型,
int 包装类Integer 实现了常量池技术,做了缓存,-128127。
当不在[-128 127]范围内,就会重新new,创建新的对象存储在堆中,分配新的内存空间。
而 compareAndSet()底层判断相等否用的是 ==,对于引用类型比较的是地址值。
所以,initialRef设为 2020,不会得到正确的结果。
    
正常的业务,对于initialRef,比较的都是一个个对象 
static AtomicStampedReference<Person> atomicStampedReference =
            new AtomicStampedReference<>(person, 1);       

ThreadLocal

参考

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

他们可以使用 get()set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。

threadlocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据,

static final ThreadLocal<T> threadLocal = new ThreadLocal<T>();
threadLocal.set()
threadLocal.get()
    
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue(){ }

//get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,
//set()用来设置当前线程中变量的副本,
//remove()用来移除当前线程中变量的副本,
//initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,下面会详细说明。

(1)ThreadLocal 示例

import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}

Output:

Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mmCopy to clipboardErrorCopied

从输出中可以看出,Thread-0 已经改变了 formatter 的值,但仍然是 thread-2 默认格式化程序与初始化值相同,其他线程也一样。

上面有一段代码用到了创建 ThreadLocal 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA 会提示你转换为 Java8 的格式(IDEA 真的不错!)。因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法withInitial(),将 Supplier 功能接口作为参数。

private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
    @Override
    protected SimpleDateFormat initialValue(){
        return new SimpleDateFormat("yyyyMMdd HHmm");
    }
};

(2)ThreadLocal 原理

Thread类源代码入手。

public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法。

ThreadLocal类的set()方法

public void set(T value) {
	//1、获取当前线程
    Thread t = Thread.currentThread();
    //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
    //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        // 初始化thradLocalMap 并赋值
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
/*
 `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。

通过上面这些内容,我们足以通过猜测得出结论:最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。

在这里插入图片描述

//每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为 key ,Object 对象为 value 的键值对。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //......
}

比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。

在这里插入图片描述

ThreadLocalMapThreadLocal的静态内部类。

在这里插入图片描述

(3)ThreadLocal 内存泄露问题

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

弱引用介绍:

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

(4)ThreadLocal与Synchronized的区别

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是

  • Synchronized是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

(5)ThreadLocal 常见使用场景

1、每个线程需要有自己单独的实例
2、实例需要在多个方法中共享,但不希望被多线程共享

场景一、存储用户Session

一个简单的用ThreadLocal来存储Session的例子:

private static final ThreadLocal threadSession = new ThreadLocal();
 
    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }

场景二、数据库连接,处理数据库事务

场景三、数据跨层传递(controller,service, dao)

​ 每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。

比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。这个例子和存储session有些像。

package com.kong.threadlocal;
 
 
public class ThreadLocalDemo05 {
    public static void main(String[] args) {
        User user = new User("jack");
        new Service1().service1(user);
    }
 
}
 
class Service1 {
    public void service1(User user){
        //给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
        UserContextHolder.holder.set(user);
        new Service2().service2();
    }
}
 
class Service2 {
    public void service2(){
        User user = UserContextHolder.holder.get();
        System.out.println("service2拿到的用户:"+user.name);
        new Service3().service3();
    }
}
 
class Service3 {
    public void service3(){
        User user = UserContextHolder.holder.get();
        System.out.println("service3拿到的用户:"+user.name);
        //在整个流程执行完毕后,一定要执行remove
        UserContextHolder.holder.remove();
    }
}
 
class UserContextHolder {
    //创建ThreadLocal保存User对象
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
 
class User {
    String name;
    public User(String name){
        this.name = name;
    }
}
 
执行的结果:
 
service2拿到的用户:jack
service3拿到的用户:jack

场景四、Spring使用ThreadLocal解决线程安全问题

​ 我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。

一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图所示。

在这里插入图片描述

这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。

下面的实例能够体现Spring对有状态Bean的改造思路:

代码清单9-5 TopicDao:非线程安全

 public class TopicDao {
   //①一个非线程安全的变量
   private Connection conn; 
   public void addTopic(){
        //②引用非线程安全变量
	   Statement stat = conn.createStatement();}

由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

代码清单9-6 TopicDao:线程安全

 
import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {
 
  //①使用ThreadLocal保存Connection变量
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
public static Connection getConnection(){
         
	    //②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
        //并将其保存到线程本地变量中。
if (connThreadLocal.get() == null) {
			Connection conn = ConnectionManager.getConnection();
			connThreadLocal.set(conn);
              return conn;
		}else{
              //③直接返回线程本地变量
			return connThreadLocal.get();
		}
	}
	public void addTopic() {
 
		//④从ThreadLocal中获取线程对应的
         Statement stat = getConnection().createStatement();
	}   

不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否为null,如果为null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其他线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在Dao只能做到本Dao的多个方法共享Connection时不发生线程安全问题,但无法和其他Dao共用同一个Connection,要做到同一事务多Dao共享同一个Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。但这个实例基本上说明了Spring对有状态类线程安全化的解决思路。在本章后面的内容中,我们将详细说明Spring如何通过ThreadLocal解决事务管理的问题。

总结

ThreadLocal 就是让 每一个线程都有自己的专属本地变量。

ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,而在方法或类间共享。

ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以通过get()方法或者set()方法访问自己内部的副本变量。

在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。

  1. 在每个线程Thread内部有一个ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的。
  2. ThreadLocalMap以ThreadLocal对象为key,线程变量副本为value。

在这里插入图片描述

AQS

AQS(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。

在这里插入图片描述

AQS 是一个用来 构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLockSemaphore,其他的诸如 ReentrantReadWriteLockSynchronousQueueFutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。

AQS 原理分析

面试题:请你说一下自己对于 AQS 原理的理解

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

AQS(AbstractQueuedSynchronizer)原理图:

在这里插入图片描述

AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

状态信息通过 protected 类型的 getState,setState,compareAndSetState 进行操作

//返回同步状态的当前值
protected final int getState() {
    return state;
}
//设置同步状态的值
protected final void setState(int newState) {
    state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS 对资源的共享方式

AQS 定义两种资源共享方式

  • Exclusive

    (独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如 CountDownLatchSemaphoreCyclicBarrierReadWriteLock 我们都会在后面讲到。

  • ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。

AQS 底层使用了模板方法模式

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

  1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)
  2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。

AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:

isHeldExclusively()   //该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)       //独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)       //独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int) //共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int) //共享方式。尝试释放资源,成功则返回true,失败则返回false。

默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。

以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。

再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown() 一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await() 函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

推荐两篇 AQS 原理和相关源码分析的文章:

  • https://www.cnblogs.com/waterystone/p/4920797.html
  • https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html

AQS 组件总结

  • Semaphore(信号量)——允许多个线程同时访问: synchronizedReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
  • CountDownLatch (倒计时器): CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
  • CyclicBarrier(循环栅栏): CyclicBarrierCountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

用过 CountDownLatch 么?什么场景下用的

CountDownLatch 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 CountDownLatch 。具体场景是下面这样的:

我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。

为此我们定义了一个线程池和 count 为 6 的CountDownLatch对象 。使用线程池处理读取任务,每一个线程处理完之后就将 count-1,调用CountDownLatch对象的 await()方法,直到所有文件读取完之后,才会接着执行后面的逻辑。

伪代码是下面这样的:

public class CountDownLatchExample1 {
    // 处理文件的数量
    private static final int threadCount = 6;

    public static void main(String[] args) throws InterruptedException {
        // 创建一个具有固定线程数量的线程池对象(推荐使用构造方法创建)
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        for (int i = 0; i < threadCount; i++) {
            final int threadnum = i;
            threadPool.execute(() -> {
                try {
                    //处理文件的业务操作
                    //......
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //表示一个文件已经被完成
                    countDownLatch.countDown();
                }

            });
        }
        countDownLatch.await();
        threadPool.shutdown();
        System.out.println("finish");
    }
}

有没有可以改进的地方呢?

可以使用 CompletableFuture 类来改进!Java8 的 CompletableFuture 提供了很多对多线程友好的方法,使用它可以很方便地为我们编写多线程程序,什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便。

CompletableFuture<Void> task1 =
    CompletableFuture.supplyAsync(()->{
        //自定义业务操作
    });
......
CompletableFuture<Void> task6 =
    CompletableFuture.supplyAsync(()->{
    //自定义业务操作
    });
......
CompletableFuture<Void> headerFuture=CompletableFuture.allOf(task1,.....,task6);

try {
    headerFuture.join();
} catch (Exception ex) {
    //......
}
System.out.println("all done. ");

上面的代码还可以接续优化,当任务过多的时候,把每一个 task 都列出来不太现实,可以考虑通过循环来添加任务。

//文件夹位置
List<String> filePaths = Arrays.asList(...)
// 异步处理所有文件
List<CompletableFuture<String>> fileFutures = filePaths.stream()
    .map(filePath -> doSomeThing(filePath))
    .collect(Collectors.toList());
// 将他们合并起来
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
    fileFutures.toArray(new CompletableFuture[fileFutures.size()])
);

各种锁


1、公平锁、非公平锁

公平锁: 十分公平,必须先来后到~; (线程1需要1h,t2需要3s,t2必须要等t1执行完才能执行)

非公平锁: 十分不公平,可以插队; (默认为非公平锁)

public class ReentrantLock implements Lock, java.io.Serializable {
    public ReentrantLock() {
        sync = new NonfairSync();  //非公平锁(默认)
    }

    //构造方法,ReentrantLock(true) ==> 公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

乐观锁和悲观锁

CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

悲观锁:

  • 很悲观,认为什么时候都会出现问题,无论做什么都会加锁

乐观锁:

  • 很乐观,认为什么时候都不会出现问题,所以不会上锁!更新数据的时候去判断一下,在此期间是否有人修改过这个数据
  • 获取version
  • 更新的时候比较version

同步锁

Synchronized是同步锁。( 当一个线程A 访问 资源的时候,其他线程将会阻塞)

2、可重入锁(好像讲得不对)

可重入锁(递归锁):拿到外面的锁之后,会自动获得里面的锁

可重入锁指:可重复递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class)

“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。

在这里插入图片描述

Synchronized锁 (可重入锁、非公平、 重量级锁)

//拿到外面的锁之后,会自动获得里面的锁
public class Demo01 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sms();
        },"A").start();
        new Thread(()->{
            phone.sms();
        },"B").start();
    }

}

class Phone{
    public synchronized void sms(){
        System.out.println(Thread.currentThread().getName()+"=> sms");
        call();//这里也有一把锁
    }
    public synchronized void call(){
        System.out.println(Thread.currentThread().getName()+"=> call");
    }
}
/*
A=> sms
A=> call
B=> sms
B=> call
*/

//A线程获得sms()方法的锁,执行完sms()方法后,释放该锁。
//然后自动获得call()方法的锁,又去执行call()方法。
//call()方法执行完并释放锁后,才会执行B线程

lock锁

//lock
public class Demo02 {

    public static void main(String[] args) {
        Phone2 phone = new Phone2();
        new Thread(()->{
            phone.sms();
        },"A").start();
        new Thread(()->{
            phone.sms();
        },"B").start();
    }

}
class Phone2{

    Lock lock=new ReentrantLock();

    public void sms(){
        lock.lock(); //细节:这个是两把锁,两个钥匙
        //lock锁必须配对,否则就会死锁在里面
        try {
            System.out.println(Thread.currentThread().getName()+"=> sms");
            call();//这里也有一把锁
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void call(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "=> call");
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            lock.unlock();
        }
    }
}
  • lock锁必须配对,相当于lock和 unlock 必须数量相同;
  • 在外面加的锁,也可以在里面解锁;在里面加的锁,在外面也可以解锁;

3、自旋锁

spinlock

在这里插入图片描述

自我设计自旋锁: 主要就是CAS操作

public class Test {

    /*
    用两个线程模拟,t1线程先获取锁,但不释放锁,t2线程阻塞;
    t1线程释放锁,然后t2线程释放锁。
    */
    public static void main(String[] args) throws InterruptedException {
//        ReentrantLock reentrantLock = new ReentrantLock();
//        reentrantLock.lock();
//        reentrantLock.unlock();


        //使用CAS实现自旋锁
        SpinlockDemo spinlockDemo = new SpinlockDemo();
        new Thread(()->{
            spinlockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinlockDemo.myunlock();
            }
        },"t1").start();

        TimeUnit.SECONDS.sleep(1);   // 这里保证线程t1先拿到锁 ????


        new Thread(()->{
            spinlockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                spinlockDemo.myunlock();
            }
        },"t2").start();
    }

}
class SpinlockDemo {

    //int 0
    //thread null
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    //加锁
    public void myLock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"===> 正在上锁");

        //自旋锁   //compareAndSet(expect期望值, update更新值)
        while (!atomicReference.compareAndSet(null,thread)){   //不为null,就要自旋
//            System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");
        }
    }


    //解锁
    public void myunlock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"===> 释放锁");
        atomicReference.compareAndSet(thread,null);
    }

}
/*
t1===> 正在上锁
t2===> 正在上锁
t1===> 释放锁
t2===> 释放锁



1、主线程main执行
2、SpinlockDemo spinlockDemo = new SpinlockDemo();
无参构造创建对象spinlockDemo,对象的属性atomicReference(实际值)为null
3、start()开启t1线程,执行run()方法
4、执行t1的 spinlockDemo.myLock();
打印:t1===> 正在上锁
atomicReference(实际值)为null,与期望值null相等,返回true,并atomicReference(实际值)= t1
while(false),不自旋,直接跳出spinlockDemo.myLock()方法
t1开始10s休眠(实际指t1执行业务)


这儿不等待t1线程执行完,再去执行下面代码,是因为:
(在程序运行时,主线程main已经启动并在运行中,而另外起一个线程start表示线程处于就绪状态,还要等JVM机制调用进入运行状态)



5、TimeUnit.SECONDS.sleep(1); 休眠1s
6、start()开启t2线程,执行run()方法
7、执行t2的 spinlockDemo.myLock();
打印:t2===> 正在上锁
atomicReference(实际值)为t1,与期望值null不相等,返回false,并atomicReference(实际值)仍= t1
while(true),t2自旋。一直在spinlockDemo.myLock()方法中

8、直到t1的10s休眠结束,t1 执行finally {spinlockDemo.myunlock();}
9、atomicReference.compareAndSet(thread,null);
atomicReference(实际值)仍= t1 与期望值t1相等,返回true,并atomicReference(实际值)= null
打印: t1===> 释放锁   
跳出myunlock()方法
此时run()方法执行完毕,线程t1终止状态

10、t2自旋锁发现atomicReference(实际值)= null,与期望值null相等,返回true,并atomicReference(实际值)= t2
while(false),不再自旋,直接跳出spinlockDemo.myLock()方法
t2开始3s休眠(实际指t2执行业务)
...


【先执行t1的run()方法 再执行t2的,是因为 同一个锁,谁先拿到谁就先执行。(程序顺序,不管延时)
TimeUnit.SECONDS.sleep(1);   // 这里保证线程t1先拿到锁 ???? 】

4、死锁

什么是死锁

参考

在这里插入图片描述尝试获取对方的锁

两个或两个以上的线程在执行过程中,由于竞争资源而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

一组相互竞争资源的线程因为互相等待,导致“永久”阻塞的现象

竞争资源。系统中的资源可以分为两类:

  1. 可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺性资源;
  2. 另一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。

产生死锁中的竞争资源之一指的是竞争不可剥夺资源

产生死锁的4个必要条件:

1、互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。

2、请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。

3、不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。

4、环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

死锁处理方法

  1. 通过协议来预防或避免死锁,确保系统不会进入死锁状态。
  2. 可以允许系统进入死锁状态,然后检测它,并加以恢复。

预防死锁:

死锁预防方法确保至少有一个必要条件不成立。这些方法通过限制如何申请资源的方法来预防死锁。

  • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)

  • 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)

  • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)

  • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

比如 超时放弃

超时放弃

当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,

然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。

避免死锁

预防死锁的几种策略,会严重地损害系统性能。

银行家算法

首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。

排查死锁

1、Jstack命令

2、JConsole工具

package com.ogj.lock;

import java.util.concurrent.TimeUnit;

public class Test {
    public static void main(String[] args) {
        String lockA= "lockA";
        String lockB= "lockB";

        new Thread(new MyThread(lockA,lockB),"t1").start();
        new Thread(new MyThread(lockB,lockA),"t2").start();
    }
}

class MyThread implements Runnable{

    private String lockA;
    private String lockB;

    public MyThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName()+" lock"+lockA+"===>get"+lockB);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);
            }
        }
    }
}
//没搞懂

解决问题

1、使用jps -l查看当前进程

在这里插入图片描述

2、使用jstack 进程号 找到死锁信息

jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

在这里插入图片描述

一般情况信息在最后:

在这里插入图片描述

排查死锁也可以用JDK自带的监控工具 —— JConsole

Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。

解除死锁:

当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:

1、剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;

2、撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

面试,工作中!排查问题!

1、日志

2、堆栈信息

单例模式

单例(Singleton)模式要求一个类有且仅有一个实例,并且提供了一个全局的访问点。

Singleton instance = Singleton.getInstance();  //单例模式:通过类获取一个实例

应该在什么时候下使用单例模式?

举一个小例子,在我们的windows桌面上,我们打开了一个回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口。,也就是说在整个系统运行的过程中,系统只维护一个回收站的实例。这就是一个典型的单例模式运用。

继续说回收站,我们在实际使用中并不存在需要同时打开两个回收站窗口的必要性。假如我每次创建回收站时都需要消耗大量的资源,而每个回收站之间资源是共享的,那么在没有必要多次重复创建该实例的情况下,创建了多个实例,这样做就会给系统造成不必要的负担,造成资源浪费。

再举一个例子,网站的计数器,一般也是采用单例模式实现,如果你存在多个计数器,每一个用户的访问都刷新计数器的值,这样的话你的实计数的值是难以同步的。但是如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。同样多线程的线程池的设计一般也是采用单例模式,这是由于线程池需要方便对池中的线程进行控制

同样,对于一些应用程序的日志应用,或者web开发中读取配置文件都适合使用单例模式,如HttpApplication 就是单例的典型应用。

从上述的例子中我们可以总结出适合使用单例模式的场景和优缺点:

适用场景: 1.需要生成唯一序列的环境

2.需要频繁实例化然后销毁的对象。

3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。

4.方便资源相互通信的环境

优点:1.实现了对唯一实例访问的可控

​ 2.对于一些需要频繁创建和销毁的对象来说可以提高系统的性能。

缺点:1. 不适用于变化频繁的对象
2.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出。

​ 3.如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失。

饿汉式

//饿汉式单例
public class Hungry {

    // 1、必须在该类中,自己先创建出一个对象
    private final static Hungry HUNGRY = new Hungry();

    // 2、私有化自身的构造器,防止外界通过构造器创建新的对象
    private Hungry(){

    }
    
    // 3、向外暴露一个公共的静态方法用于获取自身的对象
    public static Hungry getInstance(){
        return HUNGRY;
    }
    
    //饿汉式存在的问题:
    	//有可能会浪费内存空间
    
    //饿汉式一上来就创建对象,把这些全部加入内存空间。没有使用这个对象时,他也占用了空内存间
    private byte[] data1=new byte[1024*1024];
    private byte[] data2=new byte[1024*1024];
    private byte[] data3=new byte[1024*1024];
    private byte[] data4=new byte[1024*1024];
}

懒汉式

//懒汉式单例模式
public class LazyMan {
    
    //1、使用的时候才去创建对象
    private volatile static LazyMan lazyMan;
    
    // 2、私有化自身的构造器,防止外界通过构造器创建新的对象
    private LazyMan(){
        System.out.println(Thread.currentThread().getName());
    }   

    // 3、向外暴露一个公共的静态方法用于获取自身的对象
    // 双重检测锁模式 简称DCL懒汉式
    public static LazyMan getInstance(){      
        if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }

 	//单线程下 是ok的
    //并发情况下出现 线程安全问题
	public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()-> {
                LanzyMan.getInstance();
            }).start();
        }
    }
//结果是:执行多个线程,产生多个lazyMan实例,而单例模式要求一个类有且仅有一个实例。

}    

DCL懒汉式(双重检测锁模式)

//DCL懒汉式单例模式
public class Singleton {
    
    //1、使用的时候才去创建对象
    private volatile static Singleton singleton;
    
    // 2、私有化自身的构造器,防止外界通过构造器创建新的对象
    private Singleton(){
        
    }   

    // 3、向外暴露一个公共的静态方法用于获取自身的对象
    // 双重检测锁模式 简称DCL懒汉式
    public static Singleton getInstance(){  
        //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if(singleton == null){           //第1重判断
            synchronized (Singleton.class){
                if(singleton == null){   //第2重判断
                    singleton = new Singleton();    //不是原子性操作
                }
            }
        }
        return singleton;
    }
}    
  • 双重检测:

第一次判断是否为null:

​ 由于单例模式只会创建一个实例,所以如果已经创建了singleton对象,就不用进入同步代码块,不用竞争锁,直接返回前面创建的实例即可,这样大大提升效率。

第二次判断是否为null:

​ (1)假设:线程A已经经过第一次判断,判断singleton=null,准备进入同步代码块。
  (2)此时线程B获得时间片,由于线程A并没有创建实例,所以,仍然是singleton=null,所以线程B创建了实例singleton。
  (3)此时,线程A再次获得时间片,由于刚刚经过第一次判断singleton=null(不会重复判断),直接进入同步代码块,这个时候,如果不加第二次判断的话,那么线程A又会创造一个实例singleton,就不满足我们的单例模式的要求,所以第二次判断是很有必要的。

  • 声明变量时为什么要用volatile关键字?

volatile关键字:可以防止jvm指令重排优化,使用了volatile关键字可用来保证其线程间的可见性和有序性;

对象的创建需要分为3个步骤执行【singleton = new Singleton(); //不是原子性操作 】

​ 指令1:为singleton对象 分配内存空间
​ 指令2:初始化singleton对象
​ 指令3:将引用变量singleton 指向内存地址

由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例

例如,线程 T1 执行了 1 和 3,此时 T2 调用 getInstance() 后发现 singleton 不为空,因此直接返回 singleton,但此时 singleton 还未被初始化。

所以:使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

反射可以破坏单例

public class Singleton {

    private volatile static Singleton singleton;

    private Singleton(){
        //System.out.println(Thread.currentThread().getName());
    }
    public static Singleton getInstance(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    public static void main(String[] args) throws Exception {
        Singleton instance1 = Singleton.getInstance();  //单例模式:通过类获取一个实例
        //用反射去创建对象,破坏单例
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();  // 调用无参构造器
        declaredConstructor.setAccessible(true);   //暴力反射。忽略访问权限修饰符的安全检查
        Singleton instance2 = declaredConstructor.newInstance();

        System.out.println(instance1 == instance2);  //false

    }
}

优化1——升级为3重检测:

public class Singleton {
    
    private volatile static Singleton singleton;

    private Singleton(){
        synchronized (Singleton.class){
            if(singleton!= null){       //升级为3重检测
                throw new RuntimeException("不要通过反射破坏单例");
            }
        }
    }
    public static Singleton getInstance(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    public static void main(String[] args) throws Exception {
        Singleton instance1 = Singleton.getInstance();  //单例模式:通过类获取一个实例
        //用反射去创建对象,破坏单例
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();  // 调用无参构造器
        declaredConstructor.setAccessible(true);   //暴力反射。忽略访问权限修饰符的安全检查
        Singleton instance2 = declaredConstructor.newInstance();

        System.out.println(instance1 == instance2); 

    }
}
//只能创建一个实例对象instance1。
//通过反射去破坏单例的时候 报异常

在这里插入图片描述

优化2——增加标志位:

优化1存在问题:

升级为3重检测后,避免了某一种反射的破坏,但是

如果不用Singleton.getInstance()去创建对象。直接全部用反射创建对象

 //Singleton instance1 = Singleton.getInstance();  //单例模式:通过类获取一个实例
//用反射去创建对象,破坏单例
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(); 
declaredConstructor.setAccessible(true);   //暴力反射。忽略访问权限修饰符的安全检查
Singleton instance1 = declaredConstructor.newInstance();
Singleton instance2 = declaredConstructor.newInstance();

System.out.println(instance1 == instance2);  //false

通过一个非当前的对象,比如增加一个标志位。

public class Singleton {

    private volatile static Singleton singleton;
    private static boolean key = false;  //通过一个标志位(可以做一些加密处理)

    private Singleton(){
        synchronized (Singleton.class){
            if( key == false){
                key = true;
            }else{
                throw new RuntimeException("不要通过反射破坏单例");
            }
        }
    }
    public static Singleton getInstance(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    public static void main(String[] args) throws Exception {
        //Singleton instance1 = Singleton.getInstance();  //单例模式:通过类获取一个实例
        //用反射去创建对象,破坏单例
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();  // 调用无参构造器
        declaredConstructor.setAccessible(true);   //暴力反射。忽略访问权限修饰符的安全检查
        Singleton instance1 = declaredConstructor.newInstance();
        System.out.println(instance1);  
        
        Singleton instance2 = declaredConstructor.newInstance();
        System.out.println(instance2);

    }
}
//只能创建一个实例对象instance1。
//第二次 射去破坏单例的时候 报异常。

在这里插入图片描述

优化3——枚举:

优化2存在问题:

解密后,修改状态

public static void main(String[] args) throws Exception {

    Field key = Singleton.class.getDeclaredField("key");//获取指定名称成员变量,不考虑修饰符
    key.setAccessible(true);

    //Singleton instance1 = Singleton.getInstance();  //单例模式:通过类获取一个实例
    //用反射去创建对象,破坏单例
    Constructor<Singleton> declaredConstructor =	
        Singleton.class.getDeclaredConstructor();  // 调用无参构造器
    declaredConstructor.setAccessible(true);   //暴力反射。忽略访问权限修饰符的安全检查
    Singleton instance1 = declaredConstructor.newInstance();
    System.out.println(instance1);

    key.set(instance1,false);
    Singleton instance2 = declaredConstructor.newInstance();
    System.out.println(instance2);
}
//com.cn.liujp.test.Singleton@2e817b38
//com.cn.liujp.test.Singleton@1a6c5a9e
//有创建了2个对象,破坏了单例

newInstance源码中,采用枚举方式避免反射破坏单例

public T newInstance(Object ... initargs)
    throws InstantiationException, IllegalAccessException,
           IllegalArgumentException, InvocationTargetException
{
    Class<?> caller = override ? null : Reflection.getCallerClass();
    return newInstanceWithCaller(initargs, !override, caller);
}

/* package-private */
T newInstanceWithCaller(Object[] args, boolean checkAccess, Class<?> caller)
    throws InstantiationException, IllegalAccessException,
           InvocationTargetException
{
    if (checkAccess)
        checkAccess(caller, clazz, clazz, modifiers);
               
//
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");

    ConstructorAccessor ca = constructorAccessor;   // read volatile
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(args);
    return inst;
}
//enum 是什么? enum本身也是一个Class 类
public enum EnumSingle {
    INSTANCE;  //对象
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}


class Test{
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        
        EnumSingle instance1 = EnumSingle.INSTANCE; //单例模式:通过类获取一个实例
        Constructor<EnumSingle> declaredConstructor =        //有参构造
            EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
		System.out.println(instance1);

        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance2);
    }
}
//反射不能破坏枚举的单例

在这里插入图片描述

//枚举类型使用JAD反编译后源码:
public final class EnumSingle extends Enum
{

    public static EnumSingle[] values()
    {
        return (EnumSingle[])$VALUES.clone();
    }

    public static EnumSingle valueOf(String name)
    {
        return (EnumSingle)Enum.valueOf(com/ogj/single/EnumSingle, name);
    }

    //有参构造
    private EnumSingle(String s, int i)
    {
        super(s, i);
    }

    public EnumSingle getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingle INSTANCE;
    private static final EnumSingle $VALUES[];

    static 
    {
        INSTANCE = new EnumSingle("INSTANCE", 0);
        $VALUES = (new EnumSingle[] {
            INSTANCE
        });
    }
}

未命名1

lambda表达式

四大函数式接口

新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算

目的:简化编程模型

函数式接口:只有一个方法的接口

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
//超级多的@FunctionalInterface
//简化编程模型,在新版本的框架底层大量应用
//foreach()的参数也是一个函数式接口,消费者类的函数式接口

在这里插入图片描述

函数型接口可以使用lambda表达式;

Function函数型接口

在这里插入图片描述

/**
 * Function函数型接口
 */
public class Demo01 {
    public static void main(String[] args) {
        Function<String,String> function = new Function<String,String> (){
            @Override
            public String apply(String str){
                return str;
            }       
        };
        System.out.println(function.apply("liupi"));
    }
}

//lambda表达式简化
public class Demo01 {
    public static void main(String[] args) {
        //输出 输入字符串
        Function<String,String> function = (str) ->{return str;};
        System.out.println(function.apply("liupi"));
    }
}
Predicate断定型接口

在这里插入图片描述

/**
 * 断定型接口:有一个输入参数,返回值只能是 布尔值!
 */
public class Demo02 {
    public static void main(String[] args) {
        Predicate<String> predicate = new Predicate<String>(){
            @Override
            public boolean test(String str){
                return str.isEmpty();
            }       
        };
        System.out.println(function.apply("liupi"));
    }
}

//lambda表达式简化
public class Demo2 {
    public static void main(String[] args) {
        //判断字符串是否为空
        Predicate<String> predicate = (str)->{return str.isEmpty();};
        
        System.out.println(predicate.test("11"));
        System.out.println(predicate.test(""));
    }
}

Consummer 消费型接口

在这里插入图片描述

/**
 * 消费型接口:只有输入,没有返回值
 */
public class Demo3 {
    public static void main(String[] args) {
        Consumer<String> consumer = (str)->{
            System.out.println(str);
        };
        consumer.accept("abc");
    }
}
Supplier供给型接口

在这里插入图片描述

/**
 * 供给型接口:没有参数,只有返回值
 */
public class Demo4 {
    public static void main(String[] args) {
        Supplier<String> supplier = ()->{return "1024";};
        System.out.println(supplier.get());
    }
}

13、Stream流式计算

什么是Stream流式计算?

存储+计算

存储:集合、MySQL~

计算:流式计算~

// mysql,集合本质是来放东西 计算的东西应该交给流

链式编程
/**
 * 题目要求:一分钟内完成此题,只能用一行代码实现!
 * 现在有5个用户!筛选:
 * 1、密码必须是偶数
 * 2、年龄必须大于23岁
 * 3、用户名转为大写字母
 * 4、用户名字母倒着排序
 * 5、只输出一个用户!
 */
public class Test {
    public static void main(String[] args) {
        
        User user1 = new User(1,"a",21);  //(密码,用户名,年龄)
        User user2 = new User(2,"b",22);
        User user3 = new User(3,"c",23);
        User user4 = new User(4,"d",24);
        User user5 = new User(5,"e",25);
        User user6 = new User(6,"f",26);
        List<User> list = Arrays.asList(user1, user2, user3, user4, user5, user6);

        // 计算交给流
        // lambda表达式、链式编程、函数式接口、Stream流式计算
        list.stream()
                .filter((u)->{ return u.getId()%2==0; })  //filter过滤
                .filter((u)->{ return u.getAge()>23;})
                .map((u)->{ return u.getName().toUpperCase();})
                .sorted((uu1,uu2)->{ return uu2.compareTo(uu1);})
                .limit(1)
                .forEach(System.out::println);
    }
}

14、ForkJoin

什么是ForkJoin?

ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!

大数据中:MapReduce 核心思想:把大任务拆分为小任务!

在这里插入图片描述

ForkJoin 特点: 工作窃取!

实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!

在这里插入图片描述

如何使用ForkJoin?

1、通过ForkJoinPool来执行

2、计算任务 execute(ForkJoinTask<?> task)

在这里插入图片描述

3、计算类要去继承ForkJoinTask

ForkJoin的计算类!

public class ForkJoinDemo extends RecursiveTask<Long> {

    private long star;
    private long end;
    private long temp=100_0000L;  //临界值

    public ForkJoinDemo(long star, long end) {
        this.star = star;
        this.end = end;
    }


    //计算方法
    @Override
    protected Long compute() {
        if((end-star)<temp){
            Long sum = 0L;
            for (Long i = star; i < end; i++) {
                sum+=i;
            }
            //System.out.println(sum);
            return sum;
        }else {
            //使用forkJoin 分而治之 计算
            //计算平均值
            long middle = (star+ end)/2;
            ForkJoinDemo task1 = new ForkJoinDemo(star, middle);
            task1.fork();  //拆分任务,把线程任务压入线程队列
            ForkJoinDemo task2 = new ForkJoinDemo(middle, end);
            task2.fork();  //拆分任务,把线程任务压入线程队列
            long taskSum = task1.join() + task2.join();
            return taskSum;
        }
    }
}

测试类!

//计数求和任务
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        test1();
        test2();
        test3();
    }

    /**
     * 普通计算
     */
    public static void test1(){
        long star = System.currentTimeMillis();
        long sum = 0L;
        for (long i = 1; i < 20_0000_0000; i++) {
            sum+=i;
        }
        long end = System.currentTimeMillis();
        System.out.println("sum="+"时间:"+(end-star));
        System.out.println(sum);
    }

    /**
     * 使用ForkJoin
     */
    public static void test2() throws ExecutionException, InterruptedException {
        long star = System.currentTimeMillis();
        
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new ForkJoinDemo(0L, 20_0000_0000L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(task); //提交任务
        Long aLong = submit.get();
             
        long end = System.currentTimeMillis();
        System.out.println("sum="+"时间:"+(end-star));
        System.out.println(aLong);
    }


    /**
     * 使用Stream 并行流
     */
    public static void test3(){
        long star = System.currentTimeMillis();
       
        //Stream并行流()
        long sum = LongStream.range(0L, 20_0000_0000L).parallel().reduce(0, Long::sum);
        
        long end = System.currentTimeMillis();
        System.out.println("sum="+"时间:"+(end-star));
        System.out.println(sum);
    }
}

在这里插入图片描述

.parallel().reduce(0, Long::sum)使用一个并行流去计算整个计算,提高效率。

在这里插入图片描述

reduce方法的优点:

在这里插入图片描述

未命名2

6、集合类不安全

List不安全

我们来看一下List这个集合类:

//java.util.ConcurrentModificationException 并发修改异常!
public class ListTest {
    public static void main(String[] args) {

        List<Object> arrayList = new ArrayList<>();

        for(int i=1;i<=10;i++){
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));//随机
                System.out.println(arrayList);
            },String.valueOf(i)).start();
        }

    }
}

会造成:

在这里插入图片描述

ArrayList 在并发情况下是不安全的!

解决方案:

1、切换成Vector就是线程安全的啦!

在这里插入图片描述

2、使用Collections.synchronizedList(new ArrayList<>());

public class ListTest {
    public static void main(String[] args) {

        List<Object> arrayList = Collections.synchronizedList(new ArrayList<>());

        for(int i=1;i<=10;i++){
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(arrayList);
            },String.valueOf(i)).start();
        }

    }
}

3、使用JUC中的包:List arrayList = new CopyOnWriteArrayList<>();

public class ListTest {
    public static void main(String[] args) {

        List<Object> arrayList = new CopyOnWriteArrayList<>();

        for(int i=1;i<=10;i++){
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(arrayList);
            },String.valueOf(i)).start();
        }

    }
}

CopyOnWriteArrayList:写入时复制! COW 计算机程序设计领域的一种优化策略

多个线程调用的时候,资源list,读取的时候是固定的,写入(存在覆盖操作);在写入的时候避免覆盖,造成数据错乱的问题;

CopyOnWriteArrayListVector厉害在哪里?

Vector底层是使用synchronized关键字来实现的:效率特别低下。

在这里插入图片描述

CopyOnWriteArrayList使用的是Lock锁,效率会更加高效!

在这里插入图片描述

Set不安全

在这里插入图片描述

和List、Set同级的还有一个BlockingQueue 阻塞队列;

Set和List同理可得: 多线程情况下,普通的Set集合是线程不安全的;

解决方案还是两种:

  • 使用Collections工具类的synchronized包装的Set类
  • 使用CopyOnWriteArraySet 写入复制的JUC解决方案
//同理:java.util.ConcurrentModificationException
// 解决方案:
public class SetTest {
    public static void main(String[] args) {
        //Set<String> hashSet = Collections.synchronizedSet(new HashSet<>()); 
        Set<String> hashSet = new CopyOnWriteArraySet<>();//解决方案2
        for (int i = 1; i < 100; i++) {
            new Thread(()->{
                hashSet.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(hashSet);
            },String.valueOf(i)).start();
        }
    }
}

HashSet底层是什么?

hashSet底层就是一个HashMap

public HashSet() {
        map = new HashMap<>();
}

//add 本质其实就是一个map的key,map的key是无法重复的,所以使用的就是map存储
//hashSet就是使用了hashmap key不能重复的原理
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}
//PRESENT是什么? 是一个常量  不会改变的常量  无用的占位
private static final Object PRESENT = new Object();
Map不安全

回顾map的基本操作:

//map 是这样用的吗?  不是,工作中不使用这个
//默认等价什么? new HashMap<>(16,0.75);
Map<String, String> map = new HashMap<>();
//加载因子、初始化容量

默认加载因子是0.75,默认的初始容量是16

在这里插入图片描述

同样的HashMap基础类也存在并发修改异常

public static void main(String[] args) {
        //map 是这样用的吗?  不是,工作中不使用这个
        //默认等价什么? new HashMap<>(16,0.75);
        Map<String, String> map = new HashMap<>();
        //加载因子、初始化容量
        for (int i = 1; i < 100; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }

结果同样的出现了:异常java.util.ConcurrentModificationException 并发修改异常

解决方案:

  • 使用Collections.synchronizedMap(new HashMap<>());处理
  • 使用ConcurrentHashMap进行并发处理

8、常用的辅助类(必会!)

8.1 CountDownLatch

在这里插入图片描述

其实就是一个减法计数器,对于计数器归零之后再进行后面的操作,这是一个计数器!

//这是一个计数器  减法
public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
       
        CountDownLatch countDownLatch = new CountDownLatch(6);  //总数是6

        for (int i = 1; i <= 6 ; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" Go out");
                countDownLatch.countDown(); //每个线程都数量-1
            },String.valueOf(i)).start();
        }
        countDownLatch.await();  //等待计数器归零  然后向下执行

        System.out.println("close door");

    }

}

主要方法:

  • countDown 减一操作;
  • await 等待计数器归零。

await等待计数器为0,就唤醒,再继续向下运行。

8.2 CyclickBarrier

在这里插入图片描述

其实就是一个加法计数器;

public class CyclicBarrierDemo {
    public static void main(String[] args) {

        //主线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("召唤神龙~");
        });

        for (int i = 1; i <= 7; i++) {
            //子线程
            int finalI = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" 收集了第 {"+ finalI+"} 颗龙珠");
                try {
                    cyclicBarrier.await(); //加法计数 等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }

    }
}

8.3 Semaphore

Semaphore:信号量

抢车位:

3个车位 6辆车:

public class SemaphoreDemo {
    public static void main(String[] args) {
        
        Semaphore semaphore = new Semaphore(3);  //停车位为3个
        for (int i = 1; i <= 6; i++) {
            int finalI = i;
            new Thread(()->{
                try {
                    semaphore.acquire(); //得到
                    //抢到车位
                    System.out.println(Thread.currentThread().getName()+" 抢到了车位{"+ finalI +"}");
                    TimeUnit.SECONDS.sleep(2); //停车2s
                    System.out.println(Thread.currentThread().getName()+" 离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();//释放
                }
            },String.valueOf(i)).start();
        }
    }
}
/*
2 抢到了车位{2}
1 抢到了车位{1}
3 抢到了车位{3}
3 离开车位
2 离开车位
1 离开车位
4 抢到了车位{4}
5 抢到了车位{5}
6 抢到了车位{6}
6 离开车位
5 离开车位
4 离开车位

原理:

semaphore.acquire()获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!

semaphore.release()释放,会将当前的信号量释放+1,然后唤醒等待的线程!

作用: 多个共享资源互斥的使用! 并发限流,控制最大的线程数!

9、读写锁

先对于不加锁的情况:

如果我们做一个我们自己的cache缓存。分别有写入操作、读取操作;

我们采用五个线程去写入,使用十个线程去读取。

我们来看一下这个的效果,如果我们不加锁的情况!

package com.ogj.rw;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache_ReadWriteLock mycache = new MyCache_ReadWriteLock();
        
        //开启5个线程 写入数据
        for (int i = 1; i <=5 ; i++) {
            int finalI = i;  
            new Thread(()->{  //lambda表达式不能访问外部变量,用个中间表达式转换
                mycache.put(String.valueOf(finalI),String.valueOf(finalI));
            }).start();
        }
        //开启10个线程去读取数据
        for (int i = 1; i <=10 ; i++) {
            int finalI = i;
            new Thread(()->{
                String o = mycache.get(String.valueOf(finalI));
            }).start();
        }
    }
}

//自定义缓存
class MyCache_ReadWriteLock{
    
    private volatile Map<String,String> map=new HashMap<>();

    public void put(String key,String value){
        //写入
        System.out.println(Thread.currentThread().getName()+" 线程 开始写入");
        map.put(key, value);
        System.out.println(Thread.currentThread().getName()+" 线程 写入OK");
    }

    public String get(String key){
        //得到
        System.out.println(Thread.currentThread().getName()+" 线程 开始读取");
        String o = map.get(key);
        System.out.println(Thread.currentThread().getName()+" 线程 读取OK");
        return o;
    }
}

运行效果如下:

Thread-0 线程 开始写入
Thread-4 线程 开始写入  # 插入了其他的线程进行写入
Thread-4 线程 写入OK
Thread-3 线程 开始写入
Thread-1 线程 开始写入
Thread-2 线程 开始写入
Thread-1 线程 写入OK
Thread-3 线程 写入OK
Thread-0 线程 写入OK   # 对于这种情况会出现 数据不一致等情况
Thread-2 线程 写入OK
Thread-5 线程 开始读取
Thread-6 线程 开始读取
Thread-6 线程 读取OK
Thread-7 线程 开始读取
Thread-7 线程 读取OK
Thread-5 线程 读取OK
Thread-8 线程 开始读取
Thread-8 线程 读取OK
Thread-9 线程 开始读取
Thread-9 线程 读取OK
Thread-10 线程 开始读取
Thread-11 线程 开始读取
Thread-12 线程 开始读取
Thread-12 线程 读取OK
Thread-10 线程 读取OK
Thread-14 线程 开始读取
Thread-13 线程 开始读取
Thread-13 线程 读取OK
Thread-11 线程 读取OK
Thread-14 线程 读取OK

所以如果我们不加锁的情况,多线程的读写会造成数据不可靠的问题。

我们也可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。

但是这次我们采用更细粒度的锁:ReadWriteLock 读写锁来保证

在这里插入图片描述

/*
独占锁(写锁)   一次只能被一个线程占有
共享锁(读锁)   多个线程可以同时占有
*/
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache_ReadWriteLock mycache = new MyCache_ReadWriteLock();
        //开启5个线程 写入数据
        for (int i = 1; i <=5 ; i++) {
            int finalI = i; //lambda表达式不能访问外部变量,用个中间表达式转换
            new Thread(()->{
                mycache.put(String.valueOf(finalI),String.valueOf(finalI));
            }).start();
        }
        //开启10个线程去读取数据
        for (int i = 1; i <=10 ; i++) {
            int finalI = i;
            new Thread(()->{
                String o = mycache.get(String.valueOf(finalI));
            }).start();
        }
    }
}

//自定义缓存
class MyCache_ReadWriteLock{
    private volatile Map<String,String> map=new HashMap<>();

    //使用读写锁
    private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
    //普通锁
    private Lock lock=new ReentrantLock();

    //写入的时候,线程一个一个来
    public void put(String key,String value){
        //加锁
        readWriteLock.writeLock().lock();
        try {
            //写入
            //业务流程
            System.out.println(Thread.currentThread().getName()+" 线程 开始写入");
            map.put(key, value);
            System.out.println(Thread.currentThread().getName()+" 线程 写入OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock(); //解锁
        }
    }

    //读的时候,所有线程都可以读
    public String get(String key){
        //加锁
        String o="";
        readWriteLock.readLock().lock();
        try {
            //得到
            System.out.println(Thread.currentThread().getName()+" 线程 开始读取");
            o = map.get(key);
            System.out.println(Thread.currentThread().getName()+" 线程 读取OK");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
        return o;
    }
}

运行结果如下:

Thread-0 线程 开始写入
Thread-0 线程 写入OK
Thread-1 线程 开始写入
Thread-1 线程 写入OK
Thread-2 线程 开始写入
Thread-2 线程 写入OK
Thread-3 线程 开始写入
Thread-3 线程 写入OK
Thread-4 线程 开始写入
Thread-4 线程 写入OK

# 以上 写入整个过程没有再出现错乱的情况,
# 对于读取,可以运行多个线程同时读取。也要加锁,
# 因为这样不会造成数据不一致问题,也能在一定程度上提高效率
Thread-9 线程 开始读取
Thread-9 线程 读取OK
Thread-10 线程 开始读取
Thread-5 线程 开始读取
Thread-11 线程 开始读取
Thread-11 线程 读取OK
Thread-10 线程 读取OK
Thread-7 线程 开始读取
Thread-7 线程 读取OK
Thread-6 线程 开始读取
Thread-5 线程 读取OK
Thread-14 线程 开始读取
Thread-8 线程 开始读取
Thread-14 线程 读取OK
Thread-6 线程 读取OK
Thread-13 线程 开始读取
Thread-12 线程 开始读取
Thread-13 线程 读取OK
Thread-8 线程 读取OK
Thread-12 线程 读取OK

10、阻塞队列

阻塞

队列

在这里插入图片描述

阻塞队列jdk1.8文档解释:

在这里插入图片描述

BlockingQueue

blockingQueue 是Collection的一个子类;

在这里插入图片描述

整个阻塞队列的家族如下:Queue以下实现的有Deque、AbstaractQueue、BlockingQueue;

BlockingQueue以下有Link链表实现的阻塞队列、也有Array数组实现的阻塞队列。

什么情况我们会使用 阻塞队列呢?

多线程并发处理、线程池!

使用阻塞队列


操作:添加、移除

四组API

方式抛出异常不会抛出异常,有返回值阻塞 等待超时 等待
添加addofferputoffer(timenum,timeUnit)
移除removepolltakepoll(timenum,timeUnit)
判断队列首部elementpeek--
/**
* 抛出异常
*/
    public static void test1(){
        //需要初始化队列的大小
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        System.out.println(blockingQueue.add("c"));        
        // System.out.println(blockingQueue.add("d"));
        //再添加 抛出异常:java.lang.IllegalStateException: Queue full
        
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
            
        //System.out.println(blockingQueue.remove());
        //再移除 抛出异常 java.util.NoSuchElementException 
    }
=======================================================================================
/**
 * 不抛出异常,有返回值
*/
    public static void test2(){
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        //添加 一个不能添加的元素 使用offer只会返回false 不会抛出异常
        System.out.println(blockingQueue.offer("d"));

        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        //弹出 如果没有元素 只会返回null 不会抛出异常
        System.out.println(blockingQueue.poll());
    }
=======================================================================================
/**
* 等待 一直阻塞
*/
    public static void test3() throws InterruptedException {
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

        //一直阻塞 不会返回
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");

//如果队列已经满了,再进去一个元素,这种情况会一直等待这个队列,什么时候有了位置再进去,程序不会停止
       // blockingQueue.put("d");

        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        //如果我们再来一个  这种情况也会等待,程序会一直运行 阻塞
        //System.out.println(blockingQueue.take());
    }
=======================================================================================
/**
* 等待 超时阻塞
*  这种情况也会等待队列有位置 或者有产品 但是会超时结束
*/
    public static void test4() throws InterruptedException {
        ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
        blockingQueue.offer("a");
        blockingQueue.offer("b");
        blockingQueue.offer("c");
        System.out.println("开始等待");
        blockingQueue.offer("d",2, TimeUnit.SECONDS);  
        //超时时间2s 等待如果超过2s就结束等待
    
        System.out.println("结束等待");
        System.out.println("===========取值==================");
    
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println("开始等待");
        blockingQueue.poll(2,TimeUnit.SECONDS); //超过两秒 我们就不要等待了
        System.out.println("结束等待");
    }

SynchronousQueue同步队列

同步队列 没有容量,也可以视为容量为1的队列

进去一个元素,必须等待取出来之后,才能再往里面放入一个元素;

put方法 和 take方法;

Synchronized 和 其他的BlockingQueue 不一样 它不存储元素;

put了一个元素,就必须从里面先take出来,否则不能再put进去值!

并且SynchronousQueue 的take是使用了lock锁保证线程安全的。

/**
 * 同步队列
 */
public class SynchronousQueueDemo {
    public static void main(String[] args) {
        BlockingQueue<String> synchronousQueue = new SynchronousQueue<>();
        //研究一下 如果判断这是一个同步队列

        //使用两个进程
        // 一个进程 放进去
        // 一个进程 拿出来
        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+" Put 1");
                synchronousQueue.put("1");
                System.out.println(Thread.currentThread().getName()+" Put 2");
                synchronousQueue.put("2");
                System.out.println(Thread.currentThread().getName()+" Put 3");
                synchronousQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T1").start();

        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+" Take "+synchronousQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+" Take "+synchronousQueue.take());
                TimeUnit.SECONDS.sleep(3);
                System.out.println(Thread.currentThread().getName()+" Take "+synchronousQueue.take());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T2").start();
    }
}

在这里插入图片描述

15、异步回调

同步回调,即阻塞,单向

public class Test {
    public static void main(String[] args) {
        test();
        System.out.println("后续代码执行");

    }

    public static void test(){
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("我已执行完毕");
    }
}
/*
我已执行完毕
后续代码执行

当一个方法 test()被调用时,调用者main()需要等待该方法执行完毕并返回才能继续执行后续代码,我们称这个方法是同步方法;

test()方法执行的时间过长,程序会阻塞,并且无法继续执行其他的代码。

异步调用就不一样,在调用完方法后,不必等待该方法执行完,就可以执行其他的代码,直到该方法执行完,才把结果返回。

Future 设计的初衷:对将来的某个事件结果进行建模!

其实就是前端 --> 发送ajax异步请求给后端

在这里插入图片描述

但是我们平时都使用CompletableFuture

没有返回值的异步回调runAsync
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 发起一个异步任务
        CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+".....");
            System.out.println("我已执行完毕");
        });

        System.out.println("后续代码执行");
        //回调
        //future.get();  //获取执行结果
        System.out.println(future.get());
        
    }
}
/*
后续代码执行
ForkJoinPool.commonPool-worker-3.....
我已执行完毕
null     //null是因为 runAsync异步回调无返回值
有返回值的异步回调supplyAsync
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 发起一个异步任务
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(2);
                //int i=1/0;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 1024;
        });

        System.out.println("后续代码执行");

        //回调
        System.out.println(completableFuture.whenComplete((t, u) -> {  //success 回调
            System.out.println("t=>" + t);          //t 正常的返回结果
            System.out.println("u=>" + u);          //u 抛出的异常 错误信息
        }).exceptionally((e) -> {     //error 回调
            System.out.println(e.getMessage());
            return 404;
        }).get());


    }
}
/* 无错误时
后续代码执行
ForkJoinPool.commonPool-worker-3
t=>1024
u=>null
1024

/* 有错误时  int i=1/0;
后续代码执行
ForkJoinPool.commonPool-worker-3
t=>null
u=>java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
404

面试题

  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值