精华内容
下载资源
问答
  • Java实现推送通知的发送

    千次阅读 2019-08-09 16:17:20
    Java实现消息推送 1、在

    上一个文章【MUI+个推实现安卓和iOS移动消息推送】
    https://blog.csdn.net/weixin_44268320/article/details/98962314

    在上一个文章里我们在个推官网新建了应用,在这里我们就不重复了。
    在个推的开发者中心右上角有【文档】,我们点击进去
    在这里插入图片描述
    根据每个人的开发语言选择相对应的文档(这里我们选择Java)
    在这里插入图片描述
    先安装jdk(可以maven安装也可以拉包,一般选择meven安装,听说直接拉包会比较low)
    在这里插入图片描述
    在项目里新建一个类,把底下的代码复制到类里,再修改相应的代码
    在这里插入图片描述
    右键【Run As】运行,跟上一个文章的效果一样哦!
    在这里插入图片描述

    ------------升级版----------------
    在官网的文档介绍中有个【推送模板】,把代码同样放到类里面。
    在这里插入图片描述

    CTRL+SHIFT+O 把没有引入的包可以自动引入进去
    注释掉main方法里面的 LinkTemplate

    在这里插入图片描述

    再次右键运行看到有
    在这里插入图片描述
    就可以再次看到自己的信息了。

    展开全文
  • 一天搞定Java并发编程

    2020-04-25 14:11:33
    、并发编程的挑战 1.1 如何减少上下文切换 无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的...

    Java并发编程

    一、并发编程的挑战

    1.1 如何减少上下文切换

    • 无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据

    • CAS算法:Java的Atomic包使用的CAS算法来更新数据,而不需要加锁

    • 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态

    • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

    1.2 避免死锁的几个常见方法

    • 避免一个线程同时获取多个锁
    • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
    • 尝试使用定时锁,使用lock.tryLock(timeout)来代替使用内部锁机制
    • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

    二、Java并发机制的底层实现原理

    2.1 volatile的应用

    2.1.1 volatile实现可见性

    • 深入说明:通过加入内存屏障和禁止重排序优化来实现
      • 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
      • 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令
    • 通俗说明:volatile变量在每次被线程访问时,都强迫从主内存中重读改变量的值;而当该变量发生变化时,有会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到改变量的最新值

    2.1.2 volatile不能实现原子性

    2.1.3 volatile使用场合

    • 对变量的写入操作不依赖其当前值
      • 不满足:number++、count = count*5等
      • 满足:boolean变量、记录温度变化的变量等
    • 该变量没有包含在具有其他变量的不变式中
      • 不满足:不变式low < up

    2.2 synchronized的实现原理与应用

    2.2.1 synchronized和对象头

    ​ synchronized关键字锁住的是对象,而不是代码块,具体锁住的是对象的对象头。对象的对象头中有3个bit来表是对象被锁的状态:

    • 001:无锁
    • 101:偏向锁
    • 010:轻量锁
    • 010:重量锁
    • 011:GC

    2.2.2 synchronized的可见性

    JMM关于synchronized的两条规定:

    • 线程解锁前,必须把共享变量的最新值刷新到主内存中;
    • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值

    2.2.3 锁的升级与对比

    ​ 在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量锁和重量锁。

    这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级,意味着偏向锁升级成轻量锁后不能降级成偏向锁。

    这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

    2.2.3.1 偏向锁
    • 偏向锁的初始化:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁们只需简单地测试一下对象头的Mark Word里是否存储这只想当前线程的偏向锁。如果测试成功,表示已经获得了锁;如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1:如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁只想当前线程。

    • 偏向锁的撤销:偏向锁使用了一种等到竞争者出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

    • 关闭偏向锁:偏向锁在Java中是默认启动的,但是但是它在应用程序启动几秒钟之后才激活,如有必要,可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果确定应用程序所有的锁通常情况处于竞争状态,可以通过JVM参数关闭偏向锁:-XX-UseBiasedLocking=false,那么程序默认会进入轻量锁状态。

    2.2.3.2 轻量锁
    • 轻量锁加锁:线程在执行同步块之前,JVM会现在当前线程的栈帧中创建用于存储锁记录的空间,并将对象中的Mark Word复制到锁记录中,官方成为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为只想锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自选来获取锁。
    • 轻量锁解锁:会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。因为自旋会消耗CPU,为了避免无用的自旋,一旦升级为重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其它线程视图获取锁时,都会被阻塞,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。
    2.2.3.3 锁的优缺点对比
    优点缺点适用场景
    偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁的竞争,会带来额外的锁撤销的消耗只有一个线程访问同步块的场景
    轻量锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,使用自旋会消耗CPU追求响应时间,同步块执行速度非常快
    重量锁线程竞争不适用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行时间较长

    2.2.4 synchronized的用法

    1. 锁住某个对象;
    2. 锁住this关键字,相当于锁住当前类的对象;
    3. 锁住方法,同样是锁住当前类的对象,只有获取到当前类对象之后才能执行此方法
    4. 锁住静态方法,锁住的是类,因为静态方法是由类直接调用的;
    5. 锁住静态方法里面的class对象,同样是锁住类
    package com.demo.study.concurrent.synchronize.demo01;
    
    /**
     * <p>Descriptions...
     *
     * @author Johnson
     * @date 2019/10/3.
     */
    public class Demo {
        private int count = 10;
        private static int sCount = 10;
        private Object object = new Object();
        //锁住object对象
        public void test1(){
            synchronized (object){
                count--;
                System.out.println(Thread.currentThread().getName());
                System.out.println("count:"+count);
            }
        }
        //锁住当前对象,也就是想要执行这个方法,必须先拿到Demo对象的锁
        public void test2(){
            synchronized (this){
                count--;
                System.out.println(Thread.currentThread().getName());
                System.out.println("count:"+count);
            }
        }
        //test2的变形,特别注意这里锁住的是Demo的对象,而不是test3方法
        public synchronized void test3(){
                count--;
            System.out.println(Thread.currentThread().getName());
            System.out.println("count:"+count);
        }
        //这里的区别是,锁住的不是Demo对象,而是Demo这个类
        public synchronized static void test4(){
            sCount--;
            System.out.println(Thread.currentThread().getName());
            System.out.println("count:"+sCount);
        }
        //test4的变形
        public static void test5(){
            synchronized (Demo.class){
                sCount--;
                System.out.println(Thread.currentThread().getName());
                System.out.println("count:"+sCount);
            }
        }
    }
    

    2.3 原子操作的实现原理

    Java内存模型(JMM)

    Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。

    • 所有的变量都存储在主内存中
    • 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本

    两条规定:

    • 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存够中读写
    • 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成

    重排序

    代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化

    无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(Java编译器、运行时和处理器都会保证Java在单线程下遵循as-if-serial语义)

    重排序不会给单线程带来内存可见性的问题,多线程中程序交错执行时,重排序可能会造成内存可见性的问题

    • 编译器优化的重排序(编译器优化)
    • 指令级并行重排序(处理器优化)
    • 内存系统的重排序(处理器优化)

    可见性分析

    不可见的原因synchronized解决方案volatile解决方案
    线程的交叉执行原子性不支持
    重排序结合线程交叉执行原子性不支持
    共享变量未及时更新可见性可见性

    synchronized和volatile比较

    • volatile不需要加锁,比synchronized更轻量级,不会阻塞线程;
    • 从内存可见性角度讲,volatile读相当于加锁,volatile写相当于解锁;
    • synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性

    四、Java并发编程基础

    4.1 线程简介

    4.1.1 什么是线程

    ​ 现代操作系统在运行一个程序时,会为其创建一个进程。现在操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。

    4.1.2 为什么使用多线程

    • 更多的处理核心

    • 更快的响应时间

    • 更好的编程模型

    4.1.3 线程优先级

    ​ 现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性

    ​ 在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多余优先级低的线程。

    ​ 设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要涩会较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。

    ​ 在不同的JVM以及操作系统上,线程规划存在差异,有些操作系统甚至会忽略对线程优先级的设定。

    4.1.4 线程的状态

    状态码状态名称说明
    NEW初始状态线程被构建,但是还没有调用start()方法
    RUNNABLE运行状态Java线程将操作系统中的就绪和隐形两种状态统称为“运行中”
    BLOCKED阻塞状态标识线程阻塞于锁
    WAITING等待状态进入该状态表示当前线程需要其他线程做出一些特定动作(通知或中断)
    TIME_WAITING超时等待状态不同于WAITING,它是可以在指定的时间自行返回的
    TERMINATED终止状态表示当前线程已经执行完毕

    1570615031832

    • 线程创建后,调用start()方法开始执行,当线程执行wait()方法之后,线程进入等待状态;

    • 进入等待状态的线程需要依靠其它线程的通知才能够返回到运行状态;

    • 超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态;

    • 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态;

    • 线程在执行Runnable的run()方法之后会进入到终止状态;

    • 阻塞状态是线程进入synchronized关键字修饰的方法或代码块时的状态,但是阻塞在java.concurrent包中Lock接口中的线程状态确实等待状态,因为java.concurrent包中的接口对于阻塞的实现均使用了LockSupport类中的相关方法

    4.1.5 Daemon线程

    • Daemon线程是一种支持线程,因为它主要被用作程序中后台调度以及支持性工作
    • 当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出
    • 可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程
    • 注意:Daemon线程属性需要在启动线程之前设置,不能在启动线程之后设置
    • 注意:Java虚拟机在退出时,Daemon线程中的finally块并不一定会执行,因为当不存在非Daemon线程时,虚拟机会立即终止,所以不能依赖Daemon线程中的finally块来确保执行关闭或清理资源

    4.2 启动和终止线程

    4.2.1 构造线程

    ​ 在运行线程之前,首先要构造一个线程,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否为Daemon线程等信息。

    4.2.2 启动线程

    ​ 通过start()方法启动线程

    4.2.3 中断线程

    • 定义:中断可以理解为线程的一个标志位属性,它表示一个运行中的线程是否被其它线程进行了中断;

    • 实现:其它线程通过调用该线程的interrupt()方法对其进行中断操作;

    • 检查:线程通过方法isInterrupted()来进行判断是否被中断;

    • 复位:静态方法Thread.interrupted()对当前线程的中断标识进行复位;

    • 终结状态的线程,中断状态始终是false;

      注意:从Java API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException异常之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。

    4.2.4 过期的suspend()、resume()和stop()

    ​ 分别代表 暂停、恢复、终止 线程,但是已经过期了,不建议使用;

    ​ 原因:以suspend()为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题;stop()方法在终结一个线程时,不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。

    ​ suspend和resume方法可以用后面提到的等待/通知机制来替代

    4.2.5 安全地终止线程

    ​ 可以通过interrupt(中断)或cancel(取消)方法来终止线程,这样操作有机会去清理资源,而不是武断的将线程停止,因此这种终止线程的做法显得更加安全和优雅。

    4.2.26 sleep、wait、yield、suspend方法对比

    https://blog.csdn.net/Crazypokerk_/article/details/87171229

    方法调用者运行位置释放锁说明
    sleepThread对象/sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。
    waitObject对象同步块或方法wait()方法需要和notify()及notifyAll()两个方法一起介绍,这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized语句块内使用,也就是说,调用wait(),notify()和notifyAll()的任务在调用这些方法前必须拥有对象的锁;同时使用wait方法后会释放锁。注意,它们都是Object类的方法,而不是Thread类的方法
    yieldThread对象/yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同
    suspendThread对象/不会释放资源,需要和resume方法配合使用,已经不建议使用

    4.3 线程间通信

    4.3.1 volatile和synchronized关键字

    • volatile:用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新会共享内存,它能保证所有线程对变量访问的可见性;

    • synchronized:可以修饰方法或者以同步块的形式来使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程变量访问的可见性和排他性

    下面对synchronized详细讲解:

    ​ 对于同步块的实现使用了monitorentermonitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

    ​ 每个对象都有自己的监视器,当这个对象有同步块或者同步方法调用时执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入Blocked状态。

    下图描述了对象、对象的监视器、同步队列和执行线程之间的关系

    1570630803324

    4.3.2 等待/通知机制

    方法描述
    notify()通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁
    notifyAll()通知所有等待在该对象上的线程
    wait()调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()方法后,会释放对象的锁
    wait(long)超时等待一段时间,这里的参数时间是毫秒,也就是等待n毫秒,如果没有通知就超时返回
    wait(long, int)对于超时时间更细粒度的控制,可以达到纳秒级

    等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B 调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作

    细节说明:

    • 使用wait()、notify()和notifyAll()时需要先对调用对象加锁
    • 在同步块中调用对象的wait()方法的同时会释放锁
    • 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程防止到对象的等待队列
    • notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回;
    • notify()方法将等待队列中的一个等待线程从等待队列中移动到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移动到同步队列,被移动的线程状态由WAITING变为BLOCKED;
    • 从wait()方法返回的前提是获得了调用对象的锁。

    从上述细节中可以看到:等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改

    1570630829361

    4.3.3 等待/通知的经典范式

    等待方遵循如下原则

    • 获取对象的锁;

    • 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件;

    • 条件满足则执行对应的逻辑。

      伪代码:

      synchronized(对象){

      ​ while(条件不满足){

      ​ 对象.wait();

      ​ }

      ​ 对应的处理逻辑

      }

    通知方遵循如下原则

    • 获取对象的锁;

    • 改变条件;

    • 通知所有等待在对象上的线程。

      伪代码:

      synchronized(对象){

      ​ 改变条件

      ​ 对象.notifyAll();

      }

    4.3.4 管道输入/输出流

    ​ 管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。

    ​ 主要有4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter

    ​ 对于Piped类型的流,必须先要进行绑定,也就是调用connect()方法,如果没有将输出/输出流绑定起来,对于该流的访问将会抛出异常

    4.3.5 Thread.join()的使用

    • 如果一个线程A执行了thread.join()方法,其语义是:当前线程A等待thread线程终止之后才从thread.join()返回;

    • 当线程终止时,会调用自身的notifyAll()方法,通知所有等待在该线程对象上的线程;

    • 所以,join方法其实使用的是等待/通知机制:synchronized的对象是thread,等待的线程是A,notifyAll()方法是在线程thread终止时自动调的

    • 通过阅读join方法源码也能看出使用的是等待/通知机制:

      //加锁当前线程对象,即thread
      public final synchronized void join() throws InterrupterException {
          //条件不满足,继续等待
          while(isAlive()){
              wait(0);
          }
          //条件符合,方法返回
      }
      

    4.3.6 ThreadLocal深度解析

    https://www.jianshu.com/p/98b68c97df9b

    • Thread对象维护着一个ThreadLocalMap属性,但是需要通过ThreadLocal来操作这个属性;
    • ThreadLocalMap没有继承Map,而是自己实现的Map存储,而且key限定死了必须是ThreadLocal对象;
    • ThreadLocalMap的key是弱引用,而value是强引用,所以key容易发生GC,而value一直存在,所以会造成内存溢出,所以使用时,使用完get和set方法,需要remove来解除引用关系,这样就能被GC了
    • ThreadLocalMap结构非常简单,没有next引用,也就是没有使用链表的方式来解决hash冲突,ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低,所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能

    4.4 线程应用实例

    4.4.1 等待超时模式

    ​ 在等待/通知的经典范式,即加锁、条件循环和处理逻辑3个步骤的基础上,增加超时条件,可参考join()方法源码:

    //加锁当前线程对象,即thread
    public final synchronized void join(long millis)
        throws InterruptedException {
            long base = System.currentTimeMillis();
            long now = 0;
    
            if (millis < 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }
    
            if (millis == 0) {//当isAlive返回值不满足要求,继续等待
                while (isAlive()) {
                    wait(0);
                }
            } else {//当超时大于0并且isAlive返回值不满足要求,继续等待
                while (isAlive()) {
                    long delay = millis - now;
                    if (delay <= 0) {
                        break;
                    }
                    wait(delay);//等待时间一直在变化,都是 总等待时间-已经等待过的时间
                    now = System.currentTimeMillis() - base;
                }
            }
        	//条件满足需求,返回
        }
    

    ​ 开始觉得这个可以直接用wait(long mills)方法代替,但是细看之后发现是有区别的,直接调用wait(long mills)方法会一直超时等待mills时间,超时后继续下一个超时等待,这样一直不会被唤醒,所以用如上范例,在wait(long mills)方法外面增加一个总超时时间的限制,而且wait的时间一直是动态变化的,一直是future-now,也就是一定会在mills时间内返回的,经典的例子可以参考join(long mills)方法

    五、Java中的锁

    5.1 Lock接口

    ​ Lock缺少了synchronized提供的隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断性以及超时获取锁等多种synchronized关键字所不具备的同步特性:

    特性描述
    尝试非阻塞地获取锁当前线程尝试获取锁,如果这一时刻没有被其它线程获取到,则成功获取并持有锁
    能被中断地获取锁与synchronized不同,获取到锁的线程能响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
    超时获取锁在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回、

    ​ Lock的API:

    方法名称描述
    void lock()获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回
    void lockInterruptibly() throws InterruptedException可中断地获取锁,和lock()方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
    boolean tryLock()尝试非阻塞的获取锁,调用该方法后立即返回,如果能够获取则返回true,否则返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException超时的获取锁,当前线程在以下3中情况下会返回:
    当前线程在超时时间内获得了锁
    当前线程在超时时间内被中断
    超时时间结束,返回false
    void unlock()释放锁
    Condition newCondition()获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将会释放锁

    5.2 AbstractQueuedSynchronizer(AQS)

    • 队列同步器(AQS)是用来构建锁或者其他同步组件的基础框架

    • 它使用了一个int成员变量表示同步状态

    • 通过内置的FIFO队列来完成资源获取线程的排队工作

    • 同步器主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态

    • AQS提供了三个方法来操作同步状态

      • getState():获取当前同步状态

      • setState(int newState):设置当前状态(获取锁时使用

      • compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性(释放锁时使用

    • 子类推荐被定义为自定义同步组件的静态内部类

    • AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用

    • AQS既可以支持独占式获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)

    5.2.0锁和同步器的关系:

    • 同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器的实现锁的语义;
    • 锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;
    • 同步器面向的是锁的实现这,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作

    5.2.1 AQS的接口与实例

    AQS可重写的方法:

    方法名称描述
    protected boolean tryAcquire(int arg)独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态
    protected boolean tryRelease(int arg)独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
    protected int tryAcquireShared(int arg)共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败
    protected boolean tryReleaseShared(int arg)共享式释放同步状态
    protected boolean isHeldExclusively()当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所
    AQS提供的模板方法:
    
    • 独占式获取释放同步状态
    • 共享式获取与释放同步状态
    • 查询同步队列中等待线程的情况
    方法名称描述
    void acquire(int arg)独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)方法
    void acquireInterruptibly(int arg)与acquire(int arg)相同,但是该方法响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,则该方法会抛出InterruptException并返回
    boolean tryAcquireNanos(int arg, long nanos)void acquireInterruptibly(int arg)基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,如果获取到了返回true
    void acquireShared(int arg)共享式的获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态
    void acquireSharedInterruptibly(int arg)与acquireShared(int arg)相同,该方法响应中断
    blooean acquireSharedNanos(int arg, long nanos)在acquireSharedInterruptibly(int arg)基础上增加了超时限制
    boolean release(int arg)独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
    boolean releaseShared(int arg)共享式的释放同步状态
    Collection getQueuedThreads()获取等待在同步队列上的线程集合

    5.2.2 AQS的实现分析

    1. 同步队列

    ​ AQS依赖内部的同步队列(一个FIFO双向队列)来完成同步状态管理,当前线程获取同步状态失败时,同步器会将当前线程以及登台状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

    ​ 同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称以及描述如下:

    属性类型与名称描述
    int waitStatus等待状态
    包含如下状态:
    1.CANCELLED,值为1,由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,节点进入该状态将不会变化;
    2.SIGNAL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或被取消,将会通知后继节点,使后继节点的线程得以运行;
    3.CONDITION,值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中;
    4.PROPAGATE,值为-3,表示下一次共享式同步状态获取将会无条件地被传播下去;
    5.INITIAL,值为0,初始状态
    Node prev前驱节点,当节点加入同步队列时被设置(尾部添加)
    Node next后继节点
    Node nextWaiter等待队列中的后继节点,如果当前节点是共享的,那么这个字段将是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用一个字段
    Thread thread获取同步状态的线程
    • 节点是构成同步队列的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部;
    • 设置尾节点需要用到同步器基于CAS的方法:compareAndSetTail(Node expect, Node update);
    • 首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点;
    • 设置首节点是通过获取同步状态成功的线程来完成的,是线程安全的,不需要CAS操作,它只需要将首节点设置成为原首节点的后继节点并断开原节点的next引用即可;
    2.独占式同步状态获取与释放
    • 在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;

    • 移出队列的条件是前驱节点为头节点且成功获取了同步状态;

    • 在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

      preview

    1570802145984

    ​3.共享式同步状态获取与释放
    • 和独占式类似,共享式同步状态也是维护一个同步队列,只是共享式可以允许多个线程同时获取到锁
    • 退出自旋的条件是当前节点的前驱节点是head,并且**tryAcquireShared()**方法返回值 >= 0
    • 获取到同步状态后调用方法setHeadAndPropagate(),目的是除了要设置head为当前节点,同时需要唤醒其后继节点,这是与独占锁差别最大的地方
    • 释放锁的时候调用doReleaseShared()方法,不过要注意这里可能有多个线程来释放锁,所以需要CAS操作
    • 共享锁释放方法**doReleaseShared()有两个入口,一个是releaseShared()方法(此时是线程主动释放锁);另一个就是doAcquireShared()**方法中(当线程获取到锁之后释放后继的共享节点)

    AQS共享锁机制图解.jpg

    4.独占式超时获取同步状态
    • 通过调用同步器的doAcquireNanos(int arg, long nanosTimeout)方法可以超时获取同步状态,此功能是synchronize关键字所不具备的特性;
    • Java 5中,同步器提供了acquireInterruptibly(int arg)防范,这个方法在等待获取同步状态时,如果当前线程被中断,会立即返回,并抛出InterruptedException
    • 超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”,也就是**acquireNanos(int arg, long nanosTimeout)**方法不仅支持超时返回,同时支持响应中断返回
    • 自旋有一个最小时间spinForTimeoutThreshold限制,默认值为1000L纳秒,如果线程剩余的等待时间小于或等于这个时间限制,则不会进行等待,而是直接接着自旋。因为非常短的超时等待无法做到十分精确,所以,在超时非常短的场景下,同步器会进入无条件的快速自旋
      AQS独占式超时获取同步状态机制图解.png

    5.3 重入锁(ReentrantLock)

    Lock接口ReentrantLock实现
    lock()sync.acquire()
    lockInterruptibly()sync.acquireInterrunptibly(1)
    tryLock()sync.nonfairTryAcquire(1)
    tryLock(long time, TimeUnit unit)sync.tryAcquireNanos(1, unit.toNanos(timeout))
    unlock()sync.release(1)
    newCondition()sync.newCondition()

    ReantrantLock有三个重要特性:

    • ReentrantLock是独占锁,所以内部的Sync没有重写AQS中的shared()相关方法
    • ReentrantLock是重入锁,也就是一个线程可以重复获取同一个锁
    • ReentrantLock有公平锁和非公平锁两种实现,默认非公平锁(因为性能比较好)

    可以根据以上三点来理解ReentrantLock:

    1.ReentrantLock是独占锁

    ​ ReentrantLock中的同步器重写了AQS的tryAcquire()和tryRelease()方法,没有重写shared相关方法,所以是一个地地道道的独占锁,不支持共享锁机制

    2.ReentrantLock是重入锁

    ​ 重入的意思是一个线程获取了锁,然后再来获取锁可以直接得到锁,而不需要进入同步队列进行排队,实现原理就是tryAcquire()方法中的一段:

    			//如果尝试获取锁的线程就是当前获取到锁的线程则直接返回true
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0)
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);	//注意这里设置了增加了state,后续释放锁的时候需要用到
                    return true;
                }
    
    • 在公平锁和非公平锁中都有这个一段,这里实现了锁重入的机制。

    • 这里不但返回了true,而且state值增加了,这里就是重入锁的关键,也就是state记录了锁被重复获取的次数,在释放锁的时候,需要释放同样多次才完成锁的释放

    3.公平锁和非公平锁

    公平锁是尝试获取锁的时候,如果有同步队列,则自动加到队尾,等待前面的获取完成后再来获取锁,保证按顺序获取到锁,即先到先得;

    非公平锁则是在尝试获取锁的时候,先直接获取一下锁,如果获取到了直接返回true,没有获取到再去队尾排队,这样造成的结果就是可能有的线程一直在等待而获取不到锁的情况发生

    公平锁尝试获取锁:

    		//ReentrantLock公平锁尝试获取锁
    		protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    //这里是和非公平锁唯一区别的地方
                    //公平锁会先判断一下是否存在同步队列,存在就直接去队尾排队
                    //而非公平锁不会先判断是否有同步队列,而是直接尝试获取锁
                    if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                //如果尝试获取锁的线程就是当前获取到锁的线程则直接返回true
                else if (current == getExclusiveOwnerThread()) {
                    //每重复获取一次 state 值就增加一次
                    //对应着释放锁的时候,每释放一次就需要减一次
                    int nextc = c + acquires;
                    if (nextc < 0)
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
    

    非公平锁尝试获取锁:

            //ReentrantLock非公平锁尝试获取锁		
            final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    //直接CAS,不需判断是否有同步队列
                    if (compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {
                    //每重复获取一次 state 值就增加一次
                    //对应着释放锁的时候,每释放一次就需要减一次
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
    

    可以看到唯一的区别就是公平锁在尝试获取锁的时候先执行**hasQueuedPredecessors()**这个方法

    4.ReentrantLock锁的释放

    		//ReentrantLock尝试释放锁
    		protected final boolean tryRelease(int releases) {
                //对应之前加锁时 state 值做了加法,这里需要做减法
                int c = getState() - releases;
                if (Thread.currentThread() != getExclusiveOwnerThread())
                    throw new IllegalMonitorStateException();
                boolean free = false;
                //必须要将 state 减到0才算完成锁的释放
                if (c == 0) {
                    free = true;
                    setExclusiveOwnerThread(null);
                }
                setState(c);
                return free;
            }
    
    • 由于ReentrantLock是可重入锁,所以在同一个线程重入的时候需要对state做加法,释放的时候需要对state做减法,也就是重入多少次就需要释放多少次

    • 由于ReentrantLock是独占锁,同一时间只会有一个线程来释放锁

    • 所以重入锁需要保证加锁多少次就需要释放多少次,也就是lock()多少次,就需要unlock()多少次

    ReentrantLock重入锁流程

    5.公平锁和非公平锁对比

    公平锁保证了锁的获取按照FIFO的原则,而代价是进行大量的线程切换,影响性能;

    非公平锁虽然可能造成“饥饿”,但极少的线程切换,保证了其更大的吞吐量

    5.4 读写锁(ReentrantReadWriteLock)

    读写锁中既有独占锁(写锁)也有共享锁(读锁)

    AQS中state的高16位作为读锁标识,低16位作为写锁标识

    5.4.1读锁的获取

    • 获取读锁时,如果有写锁并且写锁的持有者不是本线程,会被阻塞,其它情况都可以获取
    • 当读锁被获取时,所有线程都可以再次获取读锁(共享锁),但是所有线程都不能来获取写锁

    5.4.2写锁的获取

    • 当写锁被获取到时,非当前线程读写操作都会被阻塞当前线程可以获取读锁也可以再次获取写锁(重入)
    • 获取写锁时,只要存在读锁,就阻塞;如果存在的写锁不是本线程,也会被阻塞

    5.4.3锁降级

    • 锁降级是指,同一个线程获取到写锁不释放,获取到读锁,再释放写锁的过程

    • 必须要在写锁释放前获取到读锁,不然不能称为降级锁

      			//获取写锁
      			writeLock.lock();
                  try {
                      if (!update) {
                          // 准备数据的流程(略)
                          update = true;
                      }
                      //释放写锁之前获取读锁
                      readLock.lock();
                  } finally {
                      //释放写锁
                      writeLock.unlock();
                  }
                  // 锁降级完成,写锁降级为读锁
      

      读写锁的获取关系总结如下:

    被获取的锁当前线程tryReadLock当前线程tryWriteLock其它线程tryReadLock其它线程tryWriteLock
    ReadLockY(锁重入)N(数据可见性)Y(共享锁)N(数据可见性)
    WriteLockY(锁降级)Y(锁重入)N(数据可见性)N(数据可见性)

    ReentrantReadWriteLock读写锁获取流程.png

    5.5 LockSupport工具

    方法名称描述
    void park()阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程被中断,才能从park()方法返回
    void parkNanos(long nanos)阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回
    void parkUnit(long deadline)阻塞当前线程,直到deadline时间(从1970年到deadline时间的毫秒数)
    void unpark(Thread thread)唤醒处于阻塞状态的线程thread

    ​ 除此之外,在Java6中,增加了**park(Object blocker)、park(Object blocker, long nanos)和park(Object blocker, long deadline)**3个方法,其中的参数blocker用来标识当前线程在等待的对象,该对象主要用于问题排查和系统监控。

    ​ 比如通过 jstack pid 命令查看线程状态:

    "线程2" #12 prio=5 os_prio=0 tid=0x000000001c639000 nid=0xfd24 waiting on condition [0x000000001d0ee000]
       java.lang.Thread.State: TIMED_WAITING (parking)
            at sun.misc.Unsafe.park(Native Method)
            //这里比线程1多显示了当前等待的对象,即传入的 blocker
            //这里比线程1多显示了当前等待的对象,即传入的 blocker
            //这里比线程1多显示了当前等待的对象,即传入的 blocker
            - parking to wait for  <0x0000000780990348> (a book.chapter05.ProcessData)
            at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
            at book.chapter05.ProcessData.lambda$main$1(ProcessData.java:48)
            at book.chapter05.ProcessData$$Lambda$2/999966131.run(Unknown Source)
            at java.lang.Thread.run(Thread.java:748)
    
    "线程1" #11 prio=5 os_prio=0 tid=0x000000001c634800 nid=0x13444 waiting on condition [0x000000001cfee000]
       java.lang.Thread.State: TIMED_WAITING (parking)
            at sun.misc.Unsafe.park(Native Method)
            at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:338)
            at book.chapter05.ProcessData.lambda$main$0(ProcessData.java:45)
            at book.chapter05.ProcessData$$Lambda$1/1149319664.run(Unknown Source)
            at java.lang.Thread.run(Thread.java:748)
    

    线程2用的是park(Object blocker, long nanos),线程1用的是void parkNanos(long nanos)

    可以看出来:线程2比线程1多显示了当前等待的对象,即传入的 blocker,有益于问题的排查和系统监控

    5.6 Condition接口

    ​ 任意一个Java对象都拥有一组监视器方法(定义在java.lang.Object上),主要包括:wait()、wait(long timeout)、notify()、notifyAll(),这些方法和synchronized关键字结合使用,可以实现等待/通知模式。

    ​ Condition接口也提供了类似Object的监视器方法,与Lock接口配合使用实现等待/通知模式。对比Object的监视器方法和Condition接口,可以更加详细的了解Condition 的特性:

    Object的监视器方法与Condition接口对比

    5.6.1 Condition接口与示例

    ​ Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。

    获取一个Condition必须通过Lock的newCondition()方法

    ​ Condition的等待/通知模型:

    			//等待
    			lock.lock();
                try {
                    while (条件不成立) {
                        condition.await();
                    }
                    //线程继续执行
                } finally {
                    lock.unlock();
                }
    
    			//通知
    			lock.lock();
                try {
                    改变条件
                    condition.signal();//唤醒等待线程
                } finally {
                    lock.unlock();
                }
    

    ​ Condition定义的(部分)方法以及描述:

    Condition部分方法以及描述

    5.6.2 Condition的实现分析

    • ConditionObject是同步器AQS的内部列,应为Condition的操作需要获取相关的锁,所以作为同步器的内部类是比较合理的。
    • 每个Condition对象都包含着一个队列(一下简称等待队列),该队列是Condition对象实现 等待/通知 功能的关键
    • 下面说的Condition没有特殊说明的话都是指的ConditionObject
    1.等待队列
    • 等待队列是一个FIFO双向队列,在队列的每个节点中都包含了一个线程的引用,该线程就是在Condition对象上等待的线程

    • 如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造节点加入等待队列(类似Object.wait())

    • 在AQS中,等待队列用的也是AQS中的Node,和同步队列一样

    • 一个Condition包含一个等待队列

    • Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)

    • 当前线程调用Condition.await()方法将会以当前线程构造节点,并将节点从尾部加入等待队列

      等待队列的基本结构

      ​ 如上图所示,Condition拥有首尾节点的引用,而新增节点只需将原有的尾节点 nextWaiter 指向它,并且更新尾节点(lastWaiter)即可。

      上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证

      Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切的说应该是同步器AQS)拥有一个同步队列和多个等待队列,其对应关系如图所示:

      同步队列和等待队列

      Condition的实现是同步器的内部类,隐刺每个Condition实例都能够访问同步器提供的方法,相当于每个Condition都拥有所属同步器的引用。

    2.等待
    调用Condition的await()方法(或者await开头的方法),会使当前线程进入等待队列,**并释放锁**,同时线程变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁
    
    • 注意等待节点不是将head节点移动到nextWaiter,而是通过addConditionWaiter()方法,把当前线程构造成一个新的节点并将其加入到等待队列中
    • 释放锁时head的移除在acquireQueue(Node node)中,通过下一个获取到锁的线程实现

    当前线程加入等待队列

    源码:

    		/**
    		 *Condition.await()
    		 */
    		public final void await() throws InterruptedException {
                if (Thread.interrupted())
                    throw new InterruptedException();
                //当前线程加入等待队列,需要构造新的节点
                Node node = addConditionWaiter();
                //释放同步队列,也就是释放锁
                int savedState = fullyRelease(node);
                int interruptMode = 0;
                //这里就是等待唤醒的条件(节点是否进入同步队列)
                while (!isOnSyncQueue(node)) {
                    LockSupport.park(this);
                    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                        break;
                }
                if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                    interruptMode = REINTERRUPT;
                if (node.nextWaiter != null) // clean up if cancelled
                    unlinkCancelledWaiters();
                if (interruptMode != 0)
                    reportInterruptAfterWait(interruptMode);
            }
    
    		/**
    		 *  构造新的节点加入等待队列尾部
    		 *  @return 构造的新节点
    		 */
    		private Node addConditionWaiter() {
                Node t = lastWaiter;
                // If lastWaiter is cancelled, clean out.
                if (t != null && t.waitStatus != Node.CONDITION) {
                    unlinkCancelledWaiters();
                    t = lastWaiter;
                }
                Node node = new Node(Thread.currentThread(), Node.CONDITION);
                if (t == null)
                    firstWaiter = node;
                else
                    t.nextWaiter = node;
                lastWaiter = node;
                return node;
            }
    
    		/**
             * 完全释放锁
             * @param 根据当前线程构造的等待节点
             * @return 前驱节点的 state
             */
            final int fullyRelease(Node node) {
                boolean failed = true;
                try {
                    int savedState = getState();
                    if (release(savedState)) {
                        failed = false;
                        return savedState;
                    } else {
                        throw new IllegalMonitorStateException();
                    }
                } finally {
                    if (failed)
                        node.waitStatus = Node.CANCELLED;
                }
            }
    
    3.通知

    ​ 调用Condition的signal()方法,降火唤醒在等待队列中等待时间最长的节点(即首节点),在节点在被唤醒之前,会被移动到同步队列中:

    节点从等待队列移动到同步队列

    1. 调用Signal()方法的前置条件是当前线程必须获取了锁,在signal()方法中进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程
    2. 接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程
    3. 通过调用同步器的enq(Node node)方法,等待队列的投节点线程安全的移动到同步队列
    4. 当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程
    5. 被唤醒的线程,将从await()方法中的while循环中退出(isOnSyncQueued())方法加入到获取同步状态的竞争中

    源码:

    			public final void signal() {
                    //这里判断调用signal的线程必须是获取了锁的线程
                    if (!isHeldExclusively())
                        throw new IllegalMonitorStateException();
                    Node first = firstWaiter;
                    if (first != null)
                        //唤醒操作
                        doSignal(first);
                }
    

    Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程

    展开全文
  • 前言:前几有粉丝问我,网上java面试题总是参差不齐,希望我为他整理一套全面的java面试题,并且这套java面试题有个要求,必须能够让他20就能看完这些面试题,他说现在很多面试题我看都得看3个月才能看完,然后...

    前言:前几天有粉丝问我,网上java面试题总是参差不齐,希望我为他整理一套全面的java面试题,并且这套java面试题有个要求,必须能够让他20天就能看完这些面试题,他说现在很多面试题我看都得看3个月才能看完,然后我才能去找工作,但是自己得时间有限所以想让我帮他总结一套万能面试java求职笔记。接下来我将会至少分为12个章节进行阐述这套Java求职笔记,感兴趣的可以坚持看完!觉得不错的可以点个赞。

    1为什么写这套Java求职笔记

    很早的时候我就想写一些Java求职方向的一些文章,由于很长时间耽搁了加上,有粉丝后台问我,可不可以帮他总结一些java求职笔记,他看了很多Java技术有很多技术,很迷茫不知道学哪些,我笑着说,不要着急,无论你到什么时候你都会迷茫,因为技术一直在更新,谢谢你信任我,解救你的迷茫唯一办法就是多看LRyab博客,就是这样我带着粉丝的问题,开始编写了这套Java求职笔记,看完这套求职笔记,我相信很多人都会找到属于自己的心仪工作。

    2一套知识大纲技术图

    思来想去,本来不想放这套思维大纲技术图,但是很多大佬都喜欢放思维导图,那么我也放一张思维大脑图,方便大家以后学习,在java求职的时候可以带上这张脑图。

    3那些年不能忘记的JAVA基础

    3.1面向对象(一切皆对象)

    • ✅ 面向对象的三大特征是什么?

    封装:封装就是把对象的属性和实现细节隐藏起来,仅对外提供公共的访问方法。

    将个对象的属性和行为代码封装到一个模块中,也就是一个类中,属性用变量定义,行为用方法定义,方法可以直接访问同一个对象中的属性。

    将对象的属性和方法结合一个独立的整体,隐藏其细节,并提供对外访问的接口。

    封装优点:1隐藏实现细节

    2安全性

    3增加代码的复用性:比如在工具类中封装的各种方法,可以任意调用,而不用每处去实现细节。 4模块化:分模块去封装属性、方法等等。有利于代码调试,相互配合。

    继承:多个类中存在相同的属性和行为时,将这些相同的内容抽取到一个单独类中,那么多类无需再定义这些属性和行为,只需要继承这个类即可。继承这个类的为新类,新类称之为原始类的派生类(子类)而原始类称之为新类的基类(父类),派生类可以从它的那里继承方法和实例变量,并且类可以修改或者增加新的方法使之更适合特殊的需求。可通过extends关键字实现继承。

    从已知的一个类中派生出来的一个新类叫子类。子类实现了父类所有非私有化的属性和方法,并且根据实际需求扩展出新的行为。

    继优点:1、继承是传递的,易于在其基础上构造和扩充。

    2、简化对事物的描绘,使得层次更加清晰。

    3、减少代码冗余。

    4、提高可维护性。

    多态:多态性是指允许不同类的对象对同一消息作响应,多态性语言具有灵活、抽象、行为共享,代码共享的优势,很好的解决了应用程序函数同名的问题。

    多个不同对象对同一消息作出响应。同一个消息根据不同的对象而采用各种不同的方法。

    多态的好处:主要是利于代码扩展。

    多态的表现形式:

    方法重载和方法重写:方法重载通常指一个类中,相同的方法名对应着不同的方法实现,这些方法名相同的方法其区别在于他们需要的参数不同。

    方法重写主要用于父类和子类间,子类重写父类的方法,只是对应的方法实现不同,其方法名和参数都相同。

    • ✅什么是抽象类和接口

    抽象类:在java语言中,一个类中的方法只给出类的标准。而没有给出具体实现的方法,这样的类就是抽象类。

    接口:在多态机制中接口比抽象类使用起来更加方便。而抽象类组成的集合就是接口。

    • ✅接口和抽象类的区别?

    抽象类是为了代码的复用,接口是为了实现多态性。

    接口可以进行多实现类似于多继承,抽象类无法实现多继承。

    抽象类特征:

    抽象类必须有abstract来修饰

    抽象类可以不含有抽象方法

    如果一个类包含抽象方法,那该类就必须是抽象类。

    3.2面试中常见的IO流

    IO分为字节流和字符流 字节流和字符流有入输出

    字节流分为:inputStream和OutputStream

    InputStrean 常用输入流:FileInputStream ByteArrayInputStream

    OutputStream 常用输出流:FiLe0utPutStrean ByteArrayOutputstrean

    字符流分为:Reader和Writer

    Reader常用入流:BufferReader、InputStreamReader(FileReader)

    Writer常用输出点:Bufferaríter·OutputStreaniriter(F1LeWriter) PrintWriter

    输入输出流是相对于内存而言的。

    • ✅ 文件复制都是通过输入流再到输出流进行一个读写操作?

    以文件上传为例?

    首先创建一个目标文件路径FiLe

    构建一个BufferedOutputStream输出流

    获取上传文件的输入流BufferedInputStrean输入流

    使用whiLe循环只要输入流的字节长度不为-1,就向输出流内写入字符。

    • ✅java几种常见的几种类型的流?

    字节流和字符流· 字节流继承inputstrean和outputstrean,字符流继承InputstreamReader和outputStreamWriter

    • ✅ 谈谈java Io里面常见的类,字节流、接口、实现类

    输入流就是从外部文件输入到内存,输出流就是从内存输出到文件

    字节流有抽象类inputstream和outputstream他们的子类有,FileoutPutStrean,BufferedoutPutstrean等字符流有BufferReader和Writer。

    它们都实现了CLoseabLe,FLushabLe、Appendeble这些接口·程序中输入输出全是以流的形式保存。流中实际保存的实际上全是字节文件。

    • ✅ IO流中的阻塞

    Java中阻塞方法是指程序调用改方法时,必须等待输入数据可用或者检测到输入结束或者抛出异常,否则程序会一直停留在改语句上,不会执下面的语句比如read()和readL.ine()方法

    • ✅ 字节流与字符流有什么区别?

    计算机中的一切都是二进制的字节形式存在,底层设备永远只按受字节数据,有时候写字符到底层设备,需要将人眼看的懂得字符转换为字节再进行写入字符流时,字节流得包装,字符流则是直接接收字符串,它内部将传转化为字节,再写入属出设备。

    • ✅讲讲NIO

    传统IO流是阻塞式的,会一直监听一个ServerScoket,再调用reader方法时,他会一直等待缓存区满才返回。

    NIO是非阻塞式核心类:

    Buffer为所有原始类型提供Buffer缓存支持

    Charset字符集编码解码解决方案

    Channel一个新的原始I/0抽象,用于读写Buffer类型。

    • ✅Java IO总结

    数据需要读取使用InputStream、Reader

    数据需要写入使用OutPutStream,Writer

    操作数据如果是纯文本:使用Reader和Writer

    不是则使用Inputstreem和outputstream

    操作案例:递归读取文件夹下的文件

    文件的上传下载(看我其他博文)

    3.3你应该知道的多线程

    线程和进程:

    当一个程序被运行,就开启了一个进程, 比如启动了qq,word

    程序由指令和数据组成,指令要运行,数据要加载,指令被cpu加载运行,数据被加载到内存,指令运行时可由cpu调度硬盘、网络等设备。

    线程:一个进程内可分为多个线程

    一个线程就是一个指令流,cpu调度的最小单位,由cpu一条一条执行指令

    并行和并发

    并发:单核cpu运行多线程时,时间片进行很快的切换。线程轮流执行cpu

    并行:多核cpu运行 多线程时,真正的在同一时刻运行

    为什么要用多线程?

    多线程能实现的都可以用单线程来完成,那单线程运行的好好的,为什么java要引入多线程的概念呢?

    多线程的好处:

    程序运行的更快!快!快!

    充分利用cpu资源,目前几乎没有线上的cpu是单核的,发挥多核cpu强大的能力

    多线程难在哪里?

    单线程只有一条执行线,过程容易理解,可以在大脑中清晰的勾勒出代码的执行流程

    多线程却是多条线,而且一般多条线之间有交互,多条线之间需要通信,一般难点有以下几点

    多线程的执行结果不确定,受到cpu调度的影响

    多线程的安全问题

    线程资源宝贵,依赖线程池操作线程,线程池的参数设置问题

    多线程执行是动态的,同时的,难以追踪过程

    多线程的底层是操作系统层面的,源码难度大

    Java多线程的基本使用:定义任务,创建和运行线程

    定义任务

    继承Thread类 (可以说是 将任务和线程合并在一起)

    实现Runnable接口 (可以说是 将任务和线程分开了)

    实现Callable接口 (利用FutureTask执行任务)

    Thread实现任务的局限性

    任务逻辑写在Thread类的run方法中,有单继承的局限性

    创建多线程时,每个任务有成员变量时不共享,必须加static才能做到共享

    Runnable和Callable解决了Thread的局限性

    但是Runbale相比Callable有以下的局限性

    任务没有返回值

    任务无法抛异常给调用方

    创建线程的方式

    通过Thread类直接创建线程

    利用线程池内部创建线程

    启动线程的方式

    调用线程的start()方法

    守护线程:

    默认情况下,java进程需要等待所有线程都运行结束,才会结束,有一种特殊线程叫守护线程,当所有的非守护线程都结束后,即使它没有执行完,也会强制结束。默认的线程都是非守护线程。垃圾回收线程就是典型的守护线程。

    线程阻塞:

    线程的阻塞可以分为好多种。

    BIO阻塞,即使用了阻塞式的io流

    sleep让线程休眠进入阻塞状态

    a.join() 调用该方法的线程进入阻塞

    同步锁阻塞

    同步锁等待产生阻塞

    sleep()

    使线程休眠,会将运行中的线程进入阻塞状态。当休眠时间结束后,重新争抢cpu的时间片继续运行

    join()

    join是指调用该方法的线程进入阻塞状态,等待某线程执行完成后恢复运行

    interrupt()

    中断线程

    线程分为哪几种状态?

    初始状态、可运行状态、运行状态、阻塞状态、终止状态。

    Thread类中的核心方法?

    Object类中与线程有关的方法?

    wait() notify()方法 随机唤醒被wait的一个线程。

    notifyAll()

    native 装饰的方法为本地方法,调用非java语言编写的ddl接口。称之为本地方法。

    线程安全:

    多线程调用同一个对象的临界区的方法时,对象的属性值一定不会发生错误,这就是保证了线程安全。

    synchronized

    同步锁也叫对象锁,是锁在对象上的,不同的对象就是不同的锁,保证线程安全的,是阻塞式的解决方案。

    当一个线程执行完synchronized的代码块后 会唤醒正在等待的线程

    加锁是加在对象上,一定要保证是同一对象,加锁才能生效

    线程通信:

    线程间通信可以通过共享变量+wait()&notify()来实现

    wait()将线程进入阻塞状态,notify()将线程唤醒

    wait()和sleep()的区别

    二者都会让线程进入阻塞状态,有以下区别

    wait是Object的方法 sleep是Thread的方法

    wait会立即释放锁 sleep不会释放锁

    wait后线程的状态是Watting sleep后线程的状态为 Time_Waiting

    生产者消费者模型案例

    生产者来生产数据,消费者来消费数据,生产者生产满了就不生产了,通知消费者取,等消费了再进行生产。

    同步锁生活银行取钱案例

    死锁:死锁会导致程序无法运行下去

    线程池:

    java开发中经常有池子的思想,如数据库连接池、Redis连接池。

    线程池的好处:降低资源消耗,通过池化思想,减少创建线程和销毁线程的消耗,控制资源

    提高响应速度,任务到达时,无需创建线程即可运行

    提供更多更强大的功能,可扩展性高

    3.4常见的底层性能调优方案

    • ✅你怎样给tomcat调优

    • ✅如何加大comcat连接数

    • ✅怎样加大tomcat的内存

    • ✅tomcat中如何禁止列目录下的文件

    • ✅Tomcat有几种部署方式

    • ✅Tomcat的优化经验

    • ✅描述一下JVM 加载CIass文件的原理机制?

    • ✅GC 是什么? 为什么要有 GC?

    • ✅简述 Java 垃圾回收机制。

    • ✅如何判断一个对象是否存活?(或者 GC 对象的判定方法)

    • ✅垃圾回收的优点和原理。

    • ✅有什么办法主动通知虚拟机进行垃圾回收?

    • ✅Java 中会存在内存泄漏吗, 请简单描述。

    • ✅java深拷贝和浅拷贝。

    • ✅什么是类加载器, 类加载器有哪些?

    • ✅类加载器双亲委派模型机制?

    • ✅Mysql数据库常见性能优化

    3.5常见设计模式案例

    java设计模式:

    顾名思义java中一些代码被固定写法形成了一种模式给这个模式起了个名字。所以叫设计模式

    java中的设计模式分为23种,但是你至少会这三种并且能举例说明哪些地方用到了。

    (1)单例模式、代理模式、工厂模式、观察者模式、装饰者模式、适配器模式

    单例模式又分为:懒汉式和饿汉式 单例保证一个对象在JVM种只能有一个实例

    struts2是多例的每次属性渲染都得重新生成新得对象。

    spring bean id是唯一生成得所以是单例得

    常用:获取数据库得conn链接可以使用单例设计模式。

    (2)代理模式

    静态代理和动态代理

    jdk自带得动态代理和cglib,javaaassist(字节码操作库)

    动态代理是基于反射原理。需要使用反射机制实现匿名代理类

    spring Aop切面就是使用CGLIB动态代理,实现日志切面。asm、cglib jar包

    spring 会自动在jdk动态代理和CGLIB之间切换

    (3)工厂模式

    工厂模式创建对象大大增加了创建对象封装得层次

    mybaties中的事务模块和数据源模块都使用了工厂模式

    (4)观察者模式

    定义了对象之间的一对多,这样一来,一个对象的改变所有的依赖对象都会接收到通知并且自动更新。

    (5)装饰者模式和适配器模式

    java IO到处都是装饰者模式:bufferReader和BufferedWriter增强了Reader和Writer对象,增强了单个对象的能力。

    3.6java中你不知道的容器集合技术

    • ✅什么是集合?

    集合就是一个放数据的容器,准确的说是放数据对象引用的容器

    集合类存放的都是对象的引用,而不是对象的本身

    集合类型主要有3种:set(集)、list(列表)和map(映射)。

    • ✅集合的特点

    集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。

    和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小

    • ✅集合和数组的区别

    数组是固定长度的;集合可变长度的。

    数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。

    数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

    • ✅常见的集合类有哪些?

    Collection接口的子接口包括:Set接口和List接口

    Map接口的实现类主要有:HashMap、TreeMap、 Hashtable、ConcurrentHashMap以及Properties等

    Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等

    List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

    • ✅List、Set、Map三者的区别?

    Collection集合的子接口有Set、List、Queue三种子接口。

    List:一个有序的容器,元素可以重复,可以插入多个null元素,元素都有索引。ArrayList、LinkedList。

    Set:一个无序,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。

    • ✅集合框架的底层数据结构

    List子类: ArrayList Object数组 LinkedList双向循环链表

    Set子类: HashSet 基于HashMap实现的 TreeSet红黑树 自平衡的排序二叉树

    Map子类: HashMap数组+链表 解决哈希冲突。LinkedHashMap 数组、链表或者红黑树

    HashTable 数组和链表组成的。treeMap红黑树。

    • ✅哪些集合类的线程是安全的

    Vector、hashTable、ConcurrentHashMap因为内部含有synchronized所以线程安全。

    • ✅java集合的快速失败机制

    假设2个线程,线程1通过Iterator在遍历A集合的元素,在某一个时刻,线程2修改了集合A的结构,那么这个时候就会抛出:ConcurrentModifyicationException异常从而产生fail-fast机制。

    • ✅怎么确保一个集合不被修改

    可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何 操作都会抛出 Java. lang. UnsupportedOperationException 异常。、

    • ✅迭代器 Iterator 是什么?

    Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例,迭代器允许调用者在迭代过程中移除元素。

    • ✅Iterator 怎么使用?有什么特点?

    List<String> list = new ArrayList<>();
    Iterator<String> it = list. iterator();
    while(it. hasNext()){
    	String obj = it. next();
    	System. out. println(obj);
    }

    Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改
    的时候,就会抛出 ConcurrentModificationException 异常。

    • ✅如何一边遍历一边移除Collection中的元素呢?

    一边遍历一边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。

    • ✅遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List遍历的最佳实践是什么?

    遍历方式有以下几种 for循环遍历、迭代器遍历、foreach循环遍历

    最佳实践:ArrayList使用for循环遍历

    • ✅说一下ArrayList的优缺点

    ArrayList 底层以数组实现,是一种随机访问模式。

    ArrayList 在顺序添加一个元素的时候非常方便。

    ArrayList 比较适合顺序添加、随机访问的场景。

    • ✅如何实现数组和List之间的转换

    数组转 List:使用 Arrays. asList(array) 进行转换。

    List 转数组:使用 List 自带的 toArray() 方法。

    • ✅ArrayList和LinkedList的区别是什么?

    数据结构:一个数组,一个双向链表

    随机访问:ArrayList比LinkedList在随机访问的时候效率高,增加删除元素效率linkedlist链表效率要高于数组

    内存占用:Linkedlist比arrayList要占用内存

    2者都不能保证线程安全

    • ✅ ArrayList和vector的区别是什么?

    vector线程安全一些

    性能:ArrayList在性能方便优于Vector

    扩容:ArrayList每次扩容只会增加50%,而Vector每次会增加1倍

    • ✅ 多线程下如何使用ArrayList?

    ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。

    • ✅ 为什么ArrayList的elementData加上transient修饰?

    transient关键词修饰的变量不参与序列化,每次之序列化ArrayList中的非transient元素,然后遍历Element打他,只序列化已存入的元素,这样既加快了序列化的速度,又减少了序列化之后的文件大小。

    • ✅List和Set的区别

    List支持for循环,也就是通过下标来遍历,也可以使用迭代器,但是set只能用迭代,因为它是无序,无法用下标来取得想要的值。

    • ✅简单说一下HashSet 的实现原理

    HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,基本上都是直接调用底层HashMap 的相关方法来完成,HashSet 不允许重复的值。

    • ✅HashSet与HashMap的区别
    • ✅说一下链表数据结构

    链表是可以将物理地址上不连续的数据连接起来,通过指针来对物理地址进行操作,实现增删改查等功能。

    链表大致分为单链表和双向链表。

    单链表:每个节点包含两部分,一部分存放数据变量的data,另一部分是指向下一节点的next指针

    双向链表:除了包含单链表的部分,还增加的pre前一个节点的指针

    链表的优点

    插入删除速度快、内存利用率高,不会浪费内存(可以使用内存中细小的不连续空间),大小没有固定,拓展很灵活。

    链表的缺点

    不能随机查找,必须从第一个开始遍历,查找效率低

    • ✅说一下HashMap的实现原理?

    HashMap的数据结构:在java编程语言中,最基本的结构就2种,一个数组,另外一个是模拟指针(引用),所有的数据结构都可以使用者2个基本结构构造的。

    HashMap实际上是一个链表散列的数据结构,即数组和链表的结合体。

    HashMap是基于Hash算法实现的

    放值和取值:过程,Hash冲突,将冲突的放入链表做进一步的比较,拉链法解决了hash冲突。

    • ✅ HashMap1.8之后发生了哪些变化?

    resize扩容优化

    引了红黑树链表过长影响查询效率

    多线程数据丢失问题

    • ✅红黑树

    平衡二叉树(一种特殊的二叉树)

    红黑树添加和删除,最常用到旋转和变色,因为可以使这棵树重新变成红黑树

    • ✅HashMap的put方法的具体流程?
    • ✅HashMap扩容操作怎么实现的?
    • ✅HashMap是怎么解决哈希冲突的?

    当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

    我们将数组和链表结合在一起,发挥两者各自的优势,就可以使用俩种方式:链地址法和开放地址法可以解决哈希冲突。

    • ✅能否使用任何类作为Map的key?
    • ✅为什么HashMap中的String、Integer这样的包装类更适合为key?

    能够有效的减少Hash碰撞的几率

    • ✅HashMap和HashTable的区别?

    HashTable已经淘汰

    • ✅TreeMap介绍?

    TreeMap是有序的基于红黑树实现

    • ✅如何决定使用HashMap还是TreeMap

    TreeMap适合遍历,HashMap适合集合元素添加和删除

    • ✅HashMap 和 ConcurrentHashMap 的区别

    最大的区别是线程安全

    • ✅ConcurrentHashMap 和 Hashtable 的区别?

    HashTable全表锁,ConcurrentHashMap分段锁,效率高

    • ✅说一说ConcurrentHashMap底层的具体实现以及实现原理
    • ✅comparable 和 comparator的区别?

    4结束语

    混社会这么久,越来越觉得思考的重要性,没有思考就没有成就,现在社会内卷严重,大多数人都知道去努力,但是大多数的人没有养成思考的习惯,如果你学会思考,你整个人就会很爽,就好比你在沙漠里面找到了一瓶水,现在社会谎言、潜规则、利益太多,如果你不思考,只能跟着别人的思想走。

    5个人说明

    我是LRyab博客,专注电商项目实战开发,擅长网站搭建与技术问题指导,经验是由一点一点积累的,思维也是由一天一天训练出来的。谢谢大家的阅读,原创不易,如果你认为文章对你有所帮助,就点个赞感谢大家支持,你的点赞是我持续写作的动力!

    展开全文
  • Java实现邮件自动发送

    千次阅读 2019-06-14 18:05:16
    有时候我们会遇到自动发送邮件通知、邮件验证码、节假日祝福邮件发送、邮件自动发送广告功能,今天我们就谈谈邮件自动发送是怎么实现的。案例使用Java语言来编写,使用springBoot来搭建项目。 步骤:添加邮件依赖...

    有时候我们会遇到自动发送邮件通知、邮件验证码、节假日祝福邮件发送、邮件自动发送广告功能,今天我们就谈谈邮件自动发送是怎么实现的。案例使用Java语言来编写,使用springBoot来搭建项目。

    步骤一:添加邮件依赖:spring-boot-starter-mail,如下图:

    如上图所示,添加项目使用到的依赖。

    步骤二:在配置文件编写邮箱信息,我这里使用yml文件格式,如下图:

    步骤三:在邮箱获取授权码,这里使用qq邮箱,如下图:

    1. 获取授权码入口:

    2. 开启POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务

    步骤三:代码的编写,这里只写一个简单的示例,大家可以自行扩展,如下图:

    1. 邮件发送工具类编写

    代码全:

    package com.common.common.utils;
    
    import org.springframework.mail.SimpleMailMessage;
    
    /**
     * <Description> 右键发送工具
     * @author WEIQI
     * @version V1.0
     * @createDate 2019-06-14 16:06
     * @see com.common.common.utils
     */
    public class SendMailUtil {
    
        /**
        * Description 邮件发送自定义方法
        * @author WEIQI
        * @param subject 邮件标题
        * @param text  邮件内容
        * @createDate 2019/6/14 17:03
        **/
        public SimpleMailMessage sendIdentifyingCode(String subject, String text) {
            
            SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
            
            simpleMailMessage.setSubject(subject);
            simpleMailMessage.setText(text);
    
            simpleMailMessage.setFrom("627501768@qq.com");
            simpleMailMessage.setTo("WEI_QI@vip.163.com");
    
            return simpleMailMessage;
        }
    }
    


    2. 自动发送邮件定时任务

    代码全:

    package com.common.common.utils;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.mail.javamail.JavaMailSenderImpl;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    /**
     * <Description> 邮件定时发送
     * @author WEIQI
     * @version V1.0
     * @createDate 2019-06-14 17:06
     * @see com.common.common.utils
     */
    @Component
    @EnableScheduling
    public class SendMailTask {
    
        private static Log log = LogFactory.getLog(SendMailTask.class);
    
        @Autowired
        JavaMailSenderImpl javaMailSender;
    
        @Value("${mail.subject}")
        String subject;
        @Value("${mail.sendText}")
        String sendText;
    
        /**
        * Description 邮件发送定时任务,示例中每隔五秒执行一次
        * @author WEIQI
        * @param
        * @createDate 2019/6/14 17:38
        **/
        @Scheduled(cron = "*/5 * * * * ?")
        public void timingSendMail() {
            log.info("*** 邮件发送定时任务 ***");
    
            javaMailSender.send(new SendMailUtil().sendIdentifyingCode(subject, sendText));
        }
    }
    

     

    3. 配置文件信息

    spring:
      application:
        name: common
      mail:
        username: 627501768@qq.com
        password: "你的授权码"
        host: smtp.qq.com
        properties:
          smtp:
            ssl:
              enable: true
    server:
      port: 8083
    eureka:
      client:
        service-url:
          defaultZone: http://weiqi:weiqi123456@localhost:8761/eureka
    
    # 项目常量配置
    mail:
      subject: "国庆节快乐"
      sendText: "∴★*°☆∴ 提前祝您国庆节快乐 ∴☆ *★∴"
    

    步骤四:测试

    启动项目查看设定的邮箱,收到我们自定义的邮件,如图所示:

    按照上面操作,你就可以自己实现一个简单的邮件自动发送系统,如果想要更复杂的业务逻辑,可以自己做扩展。

    消息可以设置的参数如下图,可以自行设置:

    你也可以根据自己的使用场景,添加更多工具类,比如:邮件验证码、节假日祝福邮件定时发送、推销广告等等。

    展开全文
  • 的,加个vip什么的,故而以下是博主整理的有关java面试题的以下内容,我想以种幽默风趣????的风格来给大家分享和探讨。另外附加有一些公司的面试真题(内部分享)下面我就开启????耳机模式。并且⭐是重要部分,要...
  • 我写个日程任务提醒系统,然后任务时间是由用户选择的,到了指定时间点,会在后台打印任务日志出来。目前遇到的问题是,怎么定时提醒,有考虑过当用户登录的时候从数据库查询,然后将任务ID和时间存在HashMap中,...
  • Java初级面试题(详细)

    千次阅读 2019-09-23 22:58:50
    1、个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 可以有多个类,但只能有个public的类,并且public的类名必须与文件名相一致。 2、Java有没有goto? java中的保留字,现在没有在java中使用....
  • Java实现多线程需要注意的细节 Thread线程类API 设置线程名 守护线程 优先级线程 线程生命周期 使用多线程需要注意的问题 线程安全问题 性能问题 安全发布对象 简述解决线程安全性的办法 原子性和可见性 线程...
  • 、什么是ZooKeeper ZooKeeper是个分布式服务协调框架,提供了分布式数据一致性的解决方案,基于ZooKeeper的数据结构,Watcher,选举机制等特点,可以实现数据的发布/订阅,软负载均衡,命名服务,统一配置管理,...
  • 次面试Java实习生经历

    千次阅读 2020-08-02 11:06:00
    原文链接:http://www.haitaoseo.com/178549.html前言分享网友第次面试的经历,写得很详细,希望对于即将面试的童鞋会有帮助~!1昨天去面试了,面试的是ja...
  • Java

    千次阅读 2020-08-01 09:56:23
    Java 1、Java 简介 Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表...
  • Modern Java():函数式编程 学习笔记

    千次阅读 2021-04-22 21:33:03
    、重新认识JAVA 二、Lambda表达式 2-1 匿名内部类的问题 三、常用函数式接口 四、组合异步编程与新API 五、总结 前言 因有感直播中接收知识点过于密集,单纯听听吸收下来的并不多,遂综合直播回放和文字...
  • 前段时间遇到一个很神奇且费解的问题,程序将用户的生日从日期类型转为string类型时,竟然莫名其妙的少了一天,具体表现为存在数据库的19900916这个日期,取出到程序后做转换为字符串的操作,然后神奇的变为了...
  • 【秋招】2019秋招总结Java转行之路

    千次阅读 多人点赞 2019-09-02 19:34:05
    博主秋招提前批已拿百度、字节跳动、拼多多、顺丰等公司的offer,可加微信:pcwl_Java 一起交流秋招面试经验,可获得博主的秋招...第篇:Java 转行之路 第二篇:Java 面试必备知识点 第三篇:秋招面经集合 01个...
  • Java高频面试题题解-持续更新中

    千次阅读 2021-03-09 01:15:14
    篇文章我发布了Java后端的100道高频面试题(实际是90道)。大家如果看过这90道题,会发现一般常规的面试问题可以说都覆盖到了,看完应该还有个感觉,每一道题都不是二句话能简单讲完的,如果能把这些面试题...
  • java程序员的非全日制软工硕士之路

    千次阅读 多人点赞 2020-02-01 12:18:41
    念头起 全日制和非全介绍 辞职考研 调剂非全 研 研二
  • 有位男子在职业论坛留言,分享了自己的经历,男子为过试用期每晚加班到11点,结果就因一天有事早走,隔天看通知蒙了,到底怎么回事?请往下看。 据男子描述:自己刚加入的新公司,一直很强调要...
  • java面试突击

    千次阅读 2020-02-23 11:51:27
    2019-3-2 v 2.0 对于第版进行了大幅度更新,除了修改了一些小错误之外,还增加了一些内容。 2019-4-18 v3.0 修复错误,完善内容,增加了少部分内容。 必看 本文档由 SnailClimb 整理,文章大部分内容来源于本人的...
  • 钉钉通知消息

    2021-09-23 10:06:27
    参考官方文档: 发送工作通知 - 钉钉开放平台 注意 当需要发送一条任务类的通知提醒给员工,比如审批任务等,这类情况... 同一个应用相同内容的消息,同一个用户一天只能接收一次。 同一个企业内部应用在一
  •  Java 基础部分 101、个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? 102、Java 有没有goto? 103、说说&amp;和&amp;&amp;的区别。 104、在 JAVA 中如何跳出当前的...
  • Java基础知识面试题

    万次阅读 多人点赞 2020-04-13 23:56:09
    什么是JVM、JDK、JRE为什么Java是跨平台的Java怎么实现平台无关JVM还支持哪些语言值传递、引用传递的区别访问修饰符public、private、protected,以及不写(默认)时的区别?抽象类与抽象接口重写与重载的区别基本...
  • Java面试】第二章:P6级面试

    万次阅读 多人点赞 2021-03-23 18:50:51
    HashMap底层原理,扩容机制,jdk8以后会使用红黑树优化?红黑树和二叉平衡树的区别,红黑树和B树,B+树的区别,Mysql二大引擎索引底层实现,HashMap在多线程环境中为何出错?...当我们向HashMap中存放个元素(k1,v1)
  • 从初级java开发到中级java开发,我用了三年

    千次阅读 多人点赞 2020-04-28 14:06:44
    第二个面试邀请是一间外包公司,是工行的外包,也是在科韵路的,这次面试很顺利,首先笔试,笔试没有什么悬念,因为外包公司为了卖人进去,提前给了笔试题,我在前一天已经做过了。面试也没问很难的问题,问了一下...
  • Java面试八股文整理

    千次阅读 多人点赞 2021-03-08 10:56:13
    小红书Java后端面试 1. 二叉树的特性 时间复杂度计算过程写一下 二叉树是棵树,且每个节点都不能有多于两个的儿子,且二叉树的子树有左右之分,次序不能颠倒。 二叉树的性质 在二叉树中的第i层上至多有2^(i-1)个...
  • 第二早上10点第轮电话面试,我们大约聊了半个小时,关于学历工作经验这些都没有问到,对方关注的是一些基本的知识,具体记得的几个问题: Spring或者数据库的事物隔离级别和传播特性是怎样的 JDBC的目的是什么...
  • Java基础

    2020-12-24 18:25:15
    部分:编程语言核心结构 主要知识点:变量、基本语法、分支、循环、数组、… 第二部分:Java面向对象的核心逻辑 主要知识点:OOP、封装、继承、多态、接口、… 第三部分:开发Java SE高级应用程序 主要知识点:...
  • java基础 第章 对象入门

    千次阅读 2015-04-21 21:54:29
    第1章 对象入门   “为什么面向对象的编程会在软件开发领域造成如此震憾的影响?...此外,面向对象工具以及库的巨大威力使编程成为项更使人愉悦的任务。每个人都可从中获益,至少表面如此。
  • 、自我介绍先来个自我介绍吧,本硕都是材料专业,还依稀记得当初考研就是为了有个更好的平台,以后想要读博的,可当自己进入研究生阶段后,发现事实却并非像自己想象的那种剧情发展...
  • java

    千次阅读 2020-05-21 07:40:47
    1、个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制? …14 2、Java 有没有 goto?..14 3、说说&和&&的区别。 …14 4、switch 语句能否作用在 byte 上,能否作用在 long
  • java个定时任务,其实不难,代码很简单,只要你知道方法,2分钟就搞掂了。 定时任务控制类:  package com.comit.sc.job.dms; import java.util.Calendar; import javax.servlet.ServletContextEvent;...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 10,185
精华内容 4,074
关键字:

java提前一天通知

java 订阅