特性

  • 封装
    • 隐藏类的属性和方法,对外提供公共的数据访问接口
    • 解耦合、类的内部结构可以自由修改、可以对成员变量进行更精确的控制、隐藏信息实现细节
  • 继承
    • 子类继承父类的特征和行为
    • 只能单个继承,但是可以多级继承
    • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系紧密,代码独立性差)
  • 多态
    • 同一个接口,使用不同的实例而执行不同操作
    • 解耦合、可替换、可扩充、接口性、灵活性、简化性
  • 反射
    • 优点
      • 反射提高了程序的灵活性和拓展性,降低耦合性,提升自适应能力。它允许程序创建和控制任何类的对象,无需提前硬解码目标类
    • 缺点
      • 性能问题,使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。
      • 因此反射主要应用在灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。

        数据类型

        基本数据类型

        四类八种
数据类型 关键字 内存占用 取值范围
字节型 byte 1个字节 -128 至 127
短整型 short 2个字节 -32768 至 32767
整型 int(默认) 4个字节 -231 至 231-1
长整型 long 8个字节 -263 至 263-1 19位数字
单精度浮点数 float 4个字节 1.4013E-45 至 3.4028E+38
双精度浮点数 double(默认) 8个字节 4.9E-324 至 1.7977E+308
字符型 char 2个字节 0 至 216-1
布尔类型 boolean 1个字节 true,false

特殊数据类型

  • String
    • 由final修饰的, 所有操作都会创建新的String对象
    • 字符串拼接操作
      • str.intern
        • 判断常量池中是否有该字符串,有返回,没有则创建

  • StringBuffer
    • 由synchronized修饰的 它是一个线程安全的可修改字符串序列。因为保证率线程安全,所以会带来额外的性能消耗;
  • StringBuilder

    • 无修饰,效率最高;

      集合

      List

      有序的 可重复的
      单列集合

      ArrayList

  • 线程不安全 动态数组(可扩展的数组结构)

  • 初始容量: 10 扩容:1.5倍
    1. public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}

Vector

  • 速度慢 成员方法上添加了synchronized关键字
  • 线程安全、数组结构
  • 扩容增量:原容量的1倍
  • 适用场景:经常随机访问,且不经常对非尾节点进行插入删除
  1. public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}

CopyOnWriteArrayList

  • 底层与ArrayList大致相同
  • 线程安全
  • 读写分离
    1. public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

LinkedList

  • 查询慢,增删快
  • 线程不安全
  • 双向链表数据结构
  • 内部链表对象为Node 包含:

    • E item(当前元素的值)
    • Node next(下一个节点地址值)
    • Node prev(上一个节点地址值)
      1. public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{}

      Set

      HashSet

  • 存取速度快

  • 线程不安全 底层实现是一个HashMap(保存数据)实现Set接口
  • 底层与HashMap一致

LinkedHashSet

保证元素添加的顺序

Treeset

保证元素自然的顺序

Map

HashMap

JDK7
实例化后,底层创建了长度是16的一维数组Entry[] table。
map.put(k,v)(可能执行多次put…):
首先调用k所在类的hashCode()计算k哈希值,此哈希值经过某种算法计算后,等到Entry数组中存放的位置
如果此位置上的数据为空,此时k,v添加成功
如果此位置上的数据不为空,(意味此位置上存在一个或多个数据(以链表形式存在)),比较k和已经存在的一个或多个数据的很哈希值:
如果k的哈希值与已存在的数据的哈希值都不同,此时k,v添加成功
如果k的哈希值与已存在的某一个数据的哈希值相同,继续比较:调用k所在的equals()
如果equals()返回false:此时k-v添加成功
如果equals()返回true:替换k的v43
较于JDK8的不同

  1. new HashMap():底层没有创建长度为16的数组(首次调用put() 方法时,底层创建长度为16的数组)
  2. JDK8底层的数组是Node[] ,而非Entry[]
  3. JDK7底层结构只有:数组+链表。 JDK8中底层结构:数组+链表+红黑树
    • 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 && 当前数组的长度 > 64 时,改为使用红黑树存储
    • JDK 1.8 的优化目的主要是:减少 Hash冲突 & 提高哈希表的存、取效率

HashTable

  • Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。
  • Hashtable也是JDK1.0引入的类,线程安全,能用于多线程环境中。
  • Hashtable同样实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆。

ConcurrentHashMap

  • 分段锁
    • 不是指具体的一种锁,而是一种锁的设计。
    • 分段锁的设计目的是细化锁的操作,例如当操作不需要更新整个数组时,仅针对数组中的一个元素进行更新时,我们仅给该元素加锁即可。
    • ConcurrentHashMap就是利用分段锁的形式实现高效地并发操作:
      • ConcurrentHashMap和hashMap一样,有一个Entry数组,数组中的每个元素是一个链表,也是一个Segment(分段锁)。
      • 当需要put元素时,不是先对整个hashMap加锁(线程安全的hashTable是整个加锁),而是通过hashCode知道它要放在哪一个分段中,然后对这个分段进行加锁,所以当有多个线程put时,只要不是放在同一个分段中,就不会产生同步阻塞现象。
      • 在统计size时,即获取ConcurrentHashMap信息时,就需要获取所有分段锁才能统计。
        1. public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {

TreeMap

  • TreeMap是基于红黑树结构实现的一种Map,红黑树是一种自平衡二叉查找树。

多线程

实现方式

  1. 继承Thread类
  2. 实现Runnable接口
    1. 类和方法都可以实现
    2. 不带返回值
  3. 实现Callable接口
    1. 类和方法都可以实现
    2. 带返回值

线程状态

  1. 初始 new
  2. 就绪 runnable
  3. 阻塞 blocker
  4. 等待 waiting
  5. 计时等待 timed_waiting
  6. 终止 terminated

线程池

创建方式

  1. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  3. newScheduledThreadPool 创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。
  4. newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

线程池构造参数

四种线程池本质都是创建ThreadPoolExecutor类,ThreadPoolExecutor构造参数如下

  1. int corePoolSize, 核心线程数
    1. CPU密集型:核心线程数 = CPU核数 + 1
    2. IO密集型:核心线程数 = CPU核数 * 2
      1. 核心线程数 = CPU核数 / (1-阻塞系数)
      2. 例如阻塞系数 0.8,CPU核数为4 则核心线程数为20
  2. int maximumPoolSize,最大线程数
  3. long keepAliveTime, 保持活动时间 ( 销毁超过核心线程数的线程多久时间不活动 )
  4. TimeUnit unit, 时间单位
  5. BlockingQueue workQueue 任务队列
    1. ArrayBlockingQueue
      1. 基于数组的阻塞队列实现,在内部维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
      2. ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;
      3. 按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。
      4. ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。
      5. 在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
    2. LinkedBlockingQueue
      1. 基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。
      2. 而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
      3. 如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
        ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个类足以。
    3. DelayQueue
      1. DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
        使用场景:
        DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。
    4. PriorityBlockingQueue
      1. 基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
    5. SynchronousQueue
      1. 一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
      2. 声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:
        1. 如果采用公平模式:
          1. SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
        2. 但如果是非公平模式(SynchronousQueue默认):
          1. SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
  6. ThreadFactory threadFactory 线程池工厂
  7. RejectedExecutionHandler handler 拒绝策略

阻塞队列

  1. ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列
  2. LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列(常用)
  3. PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列
  4. DelayQueue: 一个使用优先级队列实现的无界阻塞队列
  5. SynchronousQueue: 一个不存储元素的阻塞队列(常用)
  6. LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列
  7. LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列

任务执行流程

  1. 当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
  2. 当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
  3. 当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
  4. 当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
  5. 当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,释放空闲线程
  6. 当设置allowCoreThreadTimeOut(true)时,该参数默认false,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭

lock

java提供的一个接口

  • 可中断性
    • 使用lock类中的tryLock方法(只有在调用时才可以获得锁)

      synchronized

      在 JDK 1.6 之前,
      synchronized 是重量级锁,效率低下。
      从 JDK 1.6 开始,
      synchronized 做了很多优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
      synchronized 同步锁一共包含四种状态:无锁、偏向锁、轻量级锁、重量级锁,它会随着竞争情况逐渐升级。
      synchronized 同步锁可以升级但是不可以降级,目的是为了提高获取锁和释放锁的效率。

对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是ReentrantLock 重新进入锁。
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

  • 可重入性

    • synchronized是可重入锁
      • 原理:synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁
      • 好处:
        • 避免死锁
        • 更好的封装代码

          volatile

  • 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

    • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
    • 禁止进行指令重排序。
      • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
      • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行
    • 无法保证对变量的任何操作都是原子性的

JAVA对CAS的支持:
在JDK1.5中新增java.util.concurrent包就是建立在CAS之上的。
相对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。
所以java.util.concurrent包中的AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解getAndIncrement方法,该方法的作用相当于++i操作。

  1. public class AtomicInteger extends Number implements java.io.Serializable{
  2.   private volatile int value;
  3.   public final int get(){
  4.     return value;
  5.   }
  6.    public final int getAndIncrement(){
  7.     for (;;){
  8.       int current = get();
  9.       int next = current + 1;
  10.       if (compareAndSet(current, next))
  11.       return current;
  12.     }
  13.   }
  14.   public final boolean compareAndSet(int expect, int update){
  15.     return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
  16.   }
  17. }

ThreadLocal

此类提供线程局部变量。这些变量不同于它们的普通对应变量,因为访问一个变量的每个线程(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是希望将状态与线程(例如,用户ID或事务ID)关联的类中的私有静态字段。
只要线程处于活动状态且ThreadLocal实例可访问,每个线程都持有对其线程局部变量副本的隐式引用;线程消失后,其线程本地实例的所有副本都将接受垃圾收集(除非存在对这些副本的其他引用)。

AtomicInteger 原子对象

  1. public class AtomicInteger extends Number implements Serializable
  2. //一个int可能原子更新的值。有关原子变量属性的描述,请参阅java.util.concurrent.atomic包规范。一个AtomicInteger用于诸如原子增量计数器的应用程序中,不能用作Integer的替代品 。但是,这个类确实扩展了Number以允许通过处理基于数字类的工具和实用程序的统一访问。
  3. //从1.5版本开始:

CAS 算法(乐观锁)

CAS(Compare and Swap 比较并交换)是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS是一种无锁算法,CAS有三个操作数:内存值V 旧的预期值A 拟写入的新值B
当且仅当预期值A和内存值V相同时 将内存值V修改为B 否则不做任何改变

sleep()与wait()的?

  1. 相同点:
    1. 一旦执行方法都会使当前线程进行阻塞状态
  2. 不同点:
    1. 声明的位置不同:
      1. Thread类中声明sleep()
      2. Object类中声明wait()
    2. 调用的要求不同
      1. sleep()可以在任何需要的场景下调用
      2. wait()必须使用在同步代码块中
    3. 关于同步监视器
      1. 如果两个方法都使用在同步代码块或同步方法中sleep()不会释放锁 wait()会释放锁

IO

BIO

同步并阻塞(传统阻塞型),相关类和接口在java.io包下
传输方式:字节流
服务器实现模式:一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的开销
应用场景:连接数目较小且固定的架构
JKD1.4之前的唯一选择

NIO

New IO/java non-blocking IO 同步非阻塞,
三大核心:Channel(通道) Buffer(缓冲区) Selector(选择器)
传输方式:基于缓冲区
服务器实现模式:一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就是进行处理
应用场景:连接数目多且连接比较短(轻操作)的架构 如:聊天系统,弹幕系统,服务器通信

Channel

类似于流,但是又有些不同,管道是可以读取数据和写入数据 而流通常是单项的只能读 或者写
通道可以非阻塞读取和写入通道 通道可以读取或写入缓冲区,也支持异步地读写

Buffer

一块可以读写数据的内存,这块内存被包装成NIO Buffer对象并提供了一系列方法来操作和管理

Selector

是Java NIO的组件 可以检查一个或多个NIO通道 并确定哪些通道已经准备好进行读写

AIO

(NIO2.0)异步非阻塞
服务器实现模式:一个有效请求一个线程,客户端I/O请求都是由OS先完成了再通知服务器应用去完成线程进行处理,
应用场景:适用于连接较多连接世界较长的应用 如:相册服务器,编程较为复杂
JDK1.7开始支持

BIO NIO AIO
Socket SocketChannel AsynchronousSocketChannel
ServerSocket ServerSocketChannel AsynchronoisServerSocketChannel

设计模式

https://www.runoob.com/design-pattern/observer-pattern.html

工厂模式

定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
使用场景:

  • 1、要求生产唯一序列号。
  • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

    建造者模式

    将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
    主要解决:主要解决在软件系统中,有时候面临着”一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
    使用场景:1、需要生成的对象具有复杂的内部结构。 2、需要生成的对象内部属性本身相互依赖。

    原型模式

    用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
    主要解决:在运行期建立和删除原型。
    使用场景: 1、资源优化场景。 2、类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。 3、性能和安全要求的场景。 4、通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。 5、一个对象多个修改者的场景。 6、一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。 7、在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。

    幂等性

    幂等性概念

    在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
    幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
    例如,“getUsername()和setTrue()”函数就是一个幂等函数. 更复杂的操作幂等保证是利用唯一交易号(流水号)实现.
    幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。

    幂等性场景

  1. 查询操作:查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作;
  2. 删除操作:删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个) ;
  3. 唯一索引:防止新增脏数据。比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可);
  4. token机制:防止页面重复提交。
    1. 原理上通过session token来实现的(也可以通过redis来实现)。当客户端请求页面时,服务器会生成一个随机数Token,并且将Token放置到session当中,然后将Token发给客户端(一般通过构造hidden表单)。
      下次客户端提交请求时,Token会随着表单一起提交到服务器端。
    2. 服务器端第一次验证相同过后,会将session中的Token值更新下,若用户重复提交,第二次的验证判断将失败,因为用户提交的表单中的Token没变,但服务器端session中Token已经改变了。
  5. 悲观锁
    获取数据的时候加锁获取。select * from table_xxx where id=’xxx’ for update; 注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的;悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用;
  6. 乐观锁——乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。乐观锁的实现方式多种多样可以通过version或者其他状态条件:
    1. 通过版本号实现update table_xxx set name=#name#,version=version+1 where version=#version#;
    2. 通过条件限制 update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高;
  7. 分布式锁
    1. 如果是分布式系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供);
  8. select + insert
    并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了。注意:核心高并发流程不要用这种方法;
  9. 状态机幂等
    在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助
  10. 对外提供接口的api如何保证幂等
    如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号;source+seq在数据库里面做唯一索引,防止多次付款(并发时,只能处理一个请求) 。
    重点:对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。注意,为了幂等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理了。