海量数据处理:从并发编程到分布式系统

简介:

本系列文章主要围绕高并发这一话题展开,分享笔者在并发处理上的学习思路以及踩过的坑。具体思路大体分为三部分:

d47e62d2b349aca45e42305ed6714efbe5ed61d9Java多线程编程;
d47e62d2b349aca45e42305ed6714efbe5ed61d9高并发的解决思路;
d47e62d2b349aca45e42305ed6714efbe5ed61d9分布式架构中Redis、Zookeeper分布式锁的应用。

本文将重点讲解第一部分——Java多线程编程。

一、Java内存模型与线程

并发编程主要讨论以下几点:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 多个线程操作相同资源
d47e62d2b349aca45e42305ed6714efbe5ed61d9 保证线程安全
d47e62d2b349aca45e42305ed6714efbe5ed61d9 合理使用资源

通常我们可以将物理计算机中出现的并发问题类比到JVM中的并发。

物理计算机处理器、高速缓存、主内存间交互关系如图:

d9905aab152284e60203e6787741273063c1705b

处理器和内存的运行速度存在几个数量级别的差距,因此为解决此矛盾引入了“高速缓存”这一概念。当多个处理器的运行任务都涉及到同一块主内存区域时,就可能导致各自缓存数据的不一致问题。为解决一致性问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。(MSI、MESI、MOSI、Synapse、Firefly及Dragon Protocol等)

处理器为提高性能,会对输入代码乱序执行(Out-Of-Order Execution) 优化。

类比Java内存模型,线程、主内存、工作内存交互关系如图:

6c3b06dc5c6462bd8231daf1520ba3477509e036

JMM定义了程序中各个变量访问规则,即在虚拟机中将内存取出和存储的底层细节。

ee18f71083dd84f0c43a2c5cbd543e0afb712d56

线程A如果要跟线程B要通信的话,必须经历以下两个步骤:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 线程A把本地内存A中更新过的共享变量的值刷新到主内存中;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 线程B去主内存中读取A更新过的共享变量的值。

线程的工作内存中保存了该线程使用到变量的主内存副本拷贝(也可理解为此线程的私有拷贝),线程对变量的操作(读取、赋值等)都在工作内存中进行,而不能直接读写主内存中变量。不同线程之间的通信也需要通过主内存来完成。主内存对应Java堆中对象实例数据部分,而工作内存则对应虚拟机栈中部分区域。

在此还有非常重要的点需要提及!

指令重排序

执行程序时,为提高性能,编译器和处理器常常会对指令做出重排序。分三种:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 编译器优化的重排序;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 指令并行重排序;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 内存系统重排序。

JMM的编译器会禁止特定类型的编译器重排序,对于处理器重排序(后两者),则要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。

d9631a0b4f12b1804efb228b7b3108d5fb04f490

内存之间的交互操作

JMM中定义了8种操作来描述工作内存与主内存之间的实现细节:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占状态;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 read(读取):作用于主内存的变量,它把一个变量从主内存传输到线程工作内存中,以便后边的load操作;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 load(载入):作用于主内存的变量,它把read操作从主内存中得到的变量值放到工作内存副本中;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时,将会执行这个操作;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 assign(赋值):作用于工作内存的变量,它把从执行引擎接收到的值赋给工作内存,每当虚拟机遇到一个给变量赋值的字节码指令时执行此操作;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 store(存储):作用于工作内存的变量,它把工作内存的变量的值传送到主内存中,以便以后的write操作使用;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 write(写入):作用于主内存的变量,它把store操纵从工作内存中得到的变量值放入到主内存的变量中。

JMM规定了执行上述八种操作时必须满足的规则(与happens-before原则是等效的,即先行发生原则):

d47e62d2b349aca45e42305ed6714efbe5ed61d9 不允许read和load、store和write操作之一单独出现;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

相关JVM补充内容请查阅:JVM-攻城掠地

https://juejin.im/post/5b066aa9f265da0dbf004dde

测试工具

PostMan、Apache Bench、JMeter、LoadRunner。

二、线程安全性

原子性:提供了互斥访问,同一时刻只能由一个线程来对它进行操作。

可见性:一个线程对主内存的修改可以及时被其他线程观察到。

有序性:一个线程观察其它线程中指令执行顺序,由于指令重排序的存在,观察的结果一般为杂乱无章的。Java程序的天然有序性可以总结为——如果本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。前者指的是线程内的串行语义,后者指的是指令重排序和工作内存和主内存同步延迟现象。

1、原子性-Atomic包

AtomicXXX:

CAS、Unsafe.compareAndSwapInt

343889e66cbb4306788508197f0fcf421e6fe906

通过CAS来保证原子性,即Compare And Swap比较交换:

CAS利用处理器提供的CMPXCHG指令实现,自旋CAS实现的基本思路就是循环进行CAS直到成功为止。比较内存的值与预期的值,若相同则修改预期的值。

CAS虽然可以进行高效的进行原子操作,但是CAS仍在存在三大问题:

  • ABA问题。在Java1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。大部分情况下ABA问题并不影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更加高效;

  • 循环时间长开销大;

  • 只能保证一个共享变量进行的原子操作。

测试:

 

AtomicInteger

源码实现

 

AtomicLong与LongAdder

d47e62d2b349aca45e42305ed6714efbe5ed61d9 Java内存模型要求lock、unlock、read、load、assign、use、store、write这8个操作都是具有原子性,但是对于64位的数据类型(long、double),允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这四个原子操作,但是可以视为原子性操作。
d47e62d2b349aca45e42305ed6714efbe5ed61d9 AtomicLong CAS中如果并发量大,则会不断进行循环调用,效率会比较低。

d47e62d2b349aca45e42305ed6714efbe5ed61d9LongAdder实现热点数据的分离、更快,如果有并发更新可能会出现误差。底层用数组实现,其结果为数组的求和累加。

public void add(long x) {

Cell[] as; long b, v; int m; Cell a;

if ((as = cells) != null || !casBase(b = base, b + x)) {

boolean uncontended = true;

if (as == null || (m = as.length - 1) < 0 ||

(a = as[getProbe() & m]) == null ||

!(uncontended = a.cas(v = a.value, v + x)))

longAccumulate(x, null, uncontended);

}

}

/**

* Equivalent to {@code add(1)}.

*/

public void increment() {

add(1L);

}

AtomicBoolean

希望某件事情只执行一次。

 

AtomicIntegerFieldUpdater

以原子性更新类中某一个属性,这属性需要用volatile进行修饰。

 

AtomicStampedReference

作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子的方式将该引用和该标志的值设置为给定的更新值。

 

AtomicLongArray

维护数组

2、原子性-锁及对比

d47e62d2b349aca45e42305ed6714efbe5ed61d9 synchronized:依赖JVM,不可中断锁,适合锁竞争不激烈情况下(并发相对较小),代码的可读性好。
d47e62d2b349aca45e42305ed6714efbe5ed61d9 Lock:依赖特殊的CPU指令,代码实现,ReentrantLock。可中断锁,多样化同步,竞争激烈的时候能维持常态。

d47e62d2b349aca45e42305ed6714efbe5ed61d9Atomic:竞争激烈的时候能维持常态,比Lock性能更好,只能同步一个值。

3、线程安全-可见性

导致共享变量在线程间不可见的原因:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 线程交叉执行;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 重排序结合线程交叉执行;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 共享变量更新后的值没有在工作内存与主内存及时更新。

JMM关于synchronizd的两条规定:

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

volatile-可见性通过加入内存屏障和禁止重排序优化实现:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存共享变量的值刷新到主内存;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。

必须符合以下场景才可使用:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 运算结果并不依赖变量当前值,或者能够确保只有单一线程修改变量的值;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 变量不需要与其他状态变量共同参与不变约束。

原因:volatile变量在各个线程工作内存中不存在一致性问题,但是Java里面的运算并非原子性操作,导致volatile变量运算在并发下一样是不安全的。(可以通过反编译来验证)

 

volatile通常用来作为状态标记量

 

三、安全发布对象

发布对象:使一个对象能够被当前范围之外代码所使用。
对象逸出:一种错误的发布。当一个对象还没有构造完成,就能被其它线程所见。

安全发布对象:

  • d47e62d2b349aca45e42305ed6714efbe5ed61d9在静态初始化函数中初始化一个对象的引用;

  • d47e62d2b349aca45e42305ed6714efbe5ed61d9将对象的引用保存到volatile类型域或者AtomicReference对象中;

  • d47e62d2b349aca45e42305ed6714efbe5ed61d9对象引用保存到某个正确构造对象final类型域中;

  • d47e62d2b349aca45e42305ed6714efbe5ed61d9将对象的引用保存到一个由锁保护的域中。

对此,单例模式是个很好的学习例子:

 

通过枚举实现单例模式

 

四、线程安全策略

1、不可变对象

满足条件:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 对象创建以后其状态就不能修改;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 对象对所有域都是final类型;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 对象是正确创建的(对象在创建期间,this没有逸出);
d47e62d2b349aca45e42305ed6714efbe5ed61d9 Collections.unmodifiableXXX:Collection、List、Set、Map……

d47e62d2b349aca45e42305ed6714efbe5ed61d9Guava:ImmutableXXX:Collection、List、Set、Map……

a727b7b1945bde3db8f486297fef4ead1af4e1e0

2、线程封闭

d47e62d2b349aca45e42305ed6714efbe5ed61d9 Ad-hoc线程封闭:程序控制实现,最糟糕,忽略;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 堆栈封闭:局部变量,无并发问题;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 ThreadLocal线程封闭:特别好的封闭方法(实现权限管理)。
3、线程不安全写法
d47e62d2b349aca45e42305ed6714efbe5ed61d9 StringBuilder → StringBuffer
d47e62d2b349aca45e42305ed6714efbe5ed61d9 SimpleDateFormat → JodaTime(推荐)
d47e62d2b349aca45e42305ed6714efbe5ed61d9 ArrayList、HashSet、HashMap等Collections

d47e62d2b349aca45e42305ed6714efbe5ed61d9先检查再执行:if(condition(a)){handle(a);} →非原子操作

4、同步容器

d47e62d2b349aca45e42305ed6714efbe5ed61d9 ArrayList → Vector,Stack
d47e62d2b349aca45e42305ed6714efbe5ed61d9 HashMap → HashTable (key、value不能为null)
d47e62d2b349aca45e42305ed6714efbe5ed61d9 Collections.synchronizedXXX(List、Set、Map)

注意:同步容器在某些场合并不一定可以做到线程安全。

c5fdb695233c965179375954d5042bfbd06d3c51

5、线程安全-并发容器-J.U.C e01ced04968a65d1c090fbcb9e9a701aea694935

ArrayList → CopyOnWriteArrayList

d47e62d2b349aca45e42305ed6714efbe5ed61d9 拷贝数组过大,容易造成 young GC FUll GC;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 不适用于实时读的场景,适合读取多写少的场景;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 实现读写分离,满足最终一致性,使用的时候另外开辟空间;

d47e62d2b349aca45e42305ed6714efbe5ed61d9读取未加锁,写加锁。

public void add(int index, E element) {

final ReentrantLock lock = this.lock;

lock.lock();

try {

Object[] elements = getArray();

int len = elements.length;

if (index > len || index < 0)

throw new IndexOutOfBoundsException("Index: "+index+

", Size: "+len);

Object[] newElements;

int numMoved = len - index;

if (numMoved == 0)

newElements = Arrays.copyOf(elements, len + 1);

else {

newElements = new Object[len + 1];

System.arraycopy(elements, 0, newElements, 0, index);

System.arraycopy(elements, index, newElements, index + 1,

numMoved);

}

newElements[index] = element;

setArray(newElements);

} finally {

lock.unlock();

}

}

public E get(int index) {

return get(getArray(), index);

}

983e1af35ae2c0577acde27336800c675031d759

↑ ConcurrentSkipListSet对批量操作不能保证原子性。

参考:JDK1.8源码分析之ConcurrentSkipListSet(八)

https://www.cnblogs.com/leesf456/p/5549820.html

c2592af74efbdc9748d26a23ced1840d40415f6b

↑ ConcurrentHashMap 效率相对而言要比 ConcurrentSkipListMap高,而同时 ConcurrentSkipListMap 则有些其不具有的特性:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 ConcurrentSkipListMap 的key有序
d47e62d2b349aca45e42305ed6714efbe5ed61d9 支持更高的并发

ConcurrentHashMap

相比于HashTable采取更为高效的分段锁。ConcurrentHashMap里包含了一个Segment数组,一个Segment里包含了一个HashEntry数组。

Segment是一种可重入锁,扮演锁的角色;HashEntry则用于存储键值对数据。加锁/解锁是在Segment上。

ConcurrentLinkedQueu

非阻塞算法实现线程安全的队列。由head节点和tail节点组成,每个节点Node由节点元素item和指向下一个节点的next的引用组成,节点与节点之间同个这个next关联起来,组成链表结构的队列。

参考:

Java多线程(四)之ConcurrentSkipListMap深入分析

https://blog.csdn.net/guangcigeyun/article/details/8278349

探索 ConcurrentHashMap 高并发性的实现机制

https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/index.html

五、J.U.C之AQS

3316273bde8967233a3a27d550d94234e5b561db

d47e62d2b349aca45e42305ed6714efbe5ed61d9 使用Node实现FIFO队列,可以用于构建锁或者其它同步装置的基础框架;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 利用了一个int类型表示状态;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 使用方法是继承;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 子类通过继承并通过实现它的方法管理锁的状态,对应AQS中acquire和release的方法操纵锁状态;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 可以同步实现排它锁和共享锁模式(独占、共享)。

AQS同步组件

1、等待多线程完成的CountDownLatch(JDK1.5)

99252d2acd9e232aecbb2be7e78379d407d042bf

允许一个或多个线程等待其他线程完成操作。

其构造函数接收一个int类型的参数作为计数器,调用CountDown方法的时候,计数器的值会减1,CountDownLatch的await方法会阻塞当前线程,直到N变为零。

应用:并行计算,解析Excel中多个Sheet的数据。

2、控制并发线程数的Semaphore

用来控制同时访问特定资源线程的数量。

应用:流量控制,特别是公共资源有限的场景,如数据库连接。

 

3、同步屏障CyclicBarrier

423840db78a565437586b0d7c6028941d5b6fe0a

让一组线程达到一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,才会开门,所有被屏障拦截的线程才会继续执行。

应用:多线程计算数据,最后合并计算结果的场景。

CyclicBarrier和CountDownLatch的区别

d47e62d2b349aca45e42305ed6714efbe5ed61d9CountDownLatch计数器只能使用一次,CyclicBarrier可以调用reset()方法重置。所以CyclicBarrier可以支持更加复杂的场景,如发生错误后重置计数器,并让线程重新执行。

//屏障拦截的线程数量

CyclicBarrier(int permits)

//已经到达屏障

await()

//CyclicBarrier阻塞线程的数量

getNumberWaiting()

4、重入锁ReentrantLock (排他锁:同时允许单个线程访问。)

支持重进入的锁,表示该锁能够支持一个线程对资源的重复加锁,即实现重进入:任意线程获取到锁之后能够再次获取该锁而不会被锁阻塞。

d47e62d2b349aca45e42305ed6714efbe5ed61d9该锁支持获取锁时的公平和非公平性选择

public ReentrantLock(boolean fair) {

sync = fair ? new FairSync() : new NonfairSync();

}

公平锁就是等待时间最长的线程最优先获取锁,也就是说获取锁的是顺序的(FIFO),而非公平则允许插队。

非公平因为不保障顺序,则效率相对较高,而公平锁则可以减少饥饿发生的概率:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 提供了一个Condition类,可以分组唤醒需要唤醒的线程
d47e62d2b349aca45e42305ed6714efbe5ed61d9 提供能够中断等待锁的线程机制,lock.lockInterruptibly()

5、ReentrantReadWriteLock (读写锁,实现悲观读取,同时允许多个线程访问

在写线程访问时,所有读线程和其他写线程均被堵塞。其维护了一对锁,通过分离读锁、写锁,使得并发性比排他锁有很大提升。

适用于读多写少的环境,能够提供比排他锁更好的并发与吞吐量。

不足:ReentrantReadWriteLock是读写锁,在多线程环境下,大多数情况是读的情况远远大于写的操作,因此可能导致写的饥饿问题。

StampedLock

是ReentrantReadWriteLock 的增强版,是为了解决ReentrantReadWriteLock的一些不足。

StampedLock读锁并不会阻塞写锁,设计思路也比较简单,就是在读的时候发现有写操作,再去读多一次。StampedLock有两种锁,一种是悲观锁,另外一种是乐观锁,如果线程拿到乐观锁就读和写不互斥,如果拿到悲观锁就读和写互斥。

参考: Java8对读写锁的改进:StampedLock

https://blog.csdn.net/sunfeizhi/article/details/52135136

6、Condition

Condition提供了类似Object的监视器方法,依赖Lock实现等待/通知模式。

d47e62d2b349aca45e42305ed6714efbe5ed61d9 await():当前线程进入等待状态直到被通知或中断,当前线程进入运行状态且从await()方法返回;
d47e62d2b349aca45e42305ed6714efbe5ed61d9 signal():唤醒一个在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁。

参考:Java线程(九):Condition-线程通信更高效的方式

https://blog.csdn.net/ghsau/article/details/7481142

7、FutureTask

用于异步获取执行结果或取消执行任务的场景。(实现基于AQS)

参考:
Java并发编程:Callable、Future和FutureTask

https://www.cnblogs.com/dolphin0520/p/3949310.html

FutureTask的用法及两种常用的使用场景

https://blog.csdn.net/linchunquan/article/details/22382487

8、Fork/Join

并行执行任务,即把大任务分割成若干小任务并行执行,最后汇总成大任务结果的框架。

d47e62d2b349aca45e42305ed6714efbe5ed61d9 工作窃取算法:指的是某个线程从其他队列里窃取任务来执行。即这个队列先干完活,再去帮别人干点。

参考:Fork/Join 模式高级特性

https://www.ibm.com/developerworks/cn/java/j-lo-forkjoin/index.html

9、BlocklingQueu

阻塞队列是一个支持两个附加操作的队列:

d47e62d2b349aca45e42305ed6714efbe5ed61d9 阻塞插入:当队列满的时候,队列会阻塞插入元素的线程,直到队列不满。
d47e62d2b349aca45e42305ed6714efbe5ed61d9 阻塞移除:当队列为空时,获取元素的线程就会等待队列变为非空。

通常用于生产者和消费者场景。生产者是向队列里添加元素的线程,消费者是从列里获取元素的线程。阻塞队列就是生产者放元素,消费者获取元素的容器。(FIFO)

d47e62d2b349aca45e42305ed6714efbe5ed61d9 ArrayBlockingQueue
d47e62d2b349aca45e42305ed6714efbe5ed61d9 LinkedBlockingQueue
d47e62d2b349aca45e42305ed6714efbe5ed61d9 PriorityBlockingQueue
d47e62d2b349aca45e42305ed6714efbe5ed61d9 DelayQueue
d47e62d2b349aca45e42305ed6714efbe5ed61d9 SynchronousQueue
d47e62d2b349aca45e42305ed6714efbe5ed61d9 LinkedTransferQueue
d47e62d2b349aca45e42305ed6714efbe5ed61d9 LinkedBlockingDeque

参考:Java中的阻塞队列

http://www.infoq.com/cn/articles/java-blocking-queue

六、线程与线程池

最后我们聊一下线程创建的相关问题。

d47e62d2b349aca45e42305ed6714efbe5ed61d9 线程:程序中一条独立执行的线索,可以理解成进程中独立运行的子任务。
d47e62d2b349aca45e42305ed6714efbe5ed61d9 进程:一旦程序运行起来就变成了操作系统当中的一个进程。

线程的创建:

1、继承Thread类

public class TestThread extends Thread{

@Override

public void run(){

}

}

局限性:Java单根继承,不易于扩展。

2、实现Runnabl接口

public class TestThread implements Runnable{

@Override

public void run(){

}

}

3、实现Callable

运行Callable任务可以拿到一个Future对象,进行异步计算。

 

状态:

beb16f6b0d6be4bc4d3fbbbcd340c1a3fda4479e

为了保证共享数据的完整性,Java中引入了互斥锁的概念,即每个对象对应一个“互斥锁”的标记(monitor),用来保证任何时刻只能有一个线程访问该对象。利用Java中每个对象都拥有唯一的一个监视锁(monitor),当线程拥有这个标记时才会允许访问这个资源,而未拿到标记则进入阻塞,进入锁池。每个对象都有自己的一个锁池的空间,用于存放等待线程。由系统决定哪个线程拿到锁标记并运行。

方法:

  • currentThread():当前调用线程的相关信息;

  • isAlive():判断当前线程是否处于活动状态;

  • getId():线程的唯一标识;

  • interrupted():测试当前线程是否已经中断;

    注:线程终端状态由该方法清除,意味着连续两次执行此方法,第二次将返回false。

  • isInterrupted():测试线程是否已经中断;

    注:不清楚状态标志。

  • run(): 线程执行的具体方法,执行完成的会进入消亡状态;

  • start():使县城出局就绪状态,等待调用线程的对象执行run()方法;

  • sleep():让当前线程放弃CPU时间片直接返回就绪状态。

  • yield():让当前线程放弃CPU时间片直接返回就绪状态。但放弃的时间片不确定,可能刚刚放弃,便立即获取。

线程通信

  • join(): 让当前线程邀请调用方法的线程优先执行,在被邀请的线程执行结束之前,邀请别人的线程不再执行,处于阻塞状态,直到被邀请的线程执行结束之后,进入就绪状态;

  • interrupt(): 中断、打断线程的阻塞状态。直接让阻塞状态的线程返回就绪,由sleep()、join()导致的阻塞立刻解除;

  • wait():使当前执行代码的线程放弃monitor并进入等待状态,直到接收到通知或被中断为止(notify)。即此时线程将释放自己的所有锁标记和CPU占用,同时进入这个对象的等待池(阻塞状态)。只能在同步代码块中调用(synchronized);

  • notify():在等待池中随机唤醒一个线程,放入锁池,对象处于等待状态,直到获取对象的锁标记为止。 只能在同步代码块中调用(synchronized)。

4、线程池
  • d47e62d2b349aca45e42305ed6714efbe5ed61d9newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。潜在问题线程如果创建过多可能内存溢出。

d47e62d2b349aca45e42305ed6714efbe5ed61d9newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

d47e62d2b349aca45e42305ed6714efbe5ed61d9newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。

d47e62d2b349aca45e42305ed6714efbe5ed61d9newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO、LIFO、优先级)执行。

参考:Java 四种线程池的用法分析

https://blog.csdn.net/u011974987/article/details/51027795

暂且总结到这里。本文意在给大家提供学习的大体思路,其中有很多要点笔者并未深入剖析,比如ConcurrentHashMap,这块网络上有很多例子,可以参详。笔者也给出了很多不错的参考,大家可以根据个人需要点击阅读。如需详细了解,需具体阅读源码,多实践。


原文发布时间为:2018-06-13

本文作者: Mark

本文来自云栖社区合作伙伴“DBAplus社群”,了解相关信息可以关注“DBAplus社群”。

相关文章
|
6月前
|
存储 数据采集 缓存
海量数据去重的Hash、bitmap、BloomFilter、分布式一致性hash
海量数据去重的Hash、bitmap、BloomFilter、分布式一致性hash
98 1
|
存储 大数据 Apache
海量数据分布式存储--Apache HDFS之最新进展
本文PPT来自Intel研发经理、Hadoop committee成员郑锴于10月16日在2016年杭州云栖大会上发表的《海量数据分布式存储--Apache HDFS》。
3033 0
|
13天前
|
NoSQL Java 关系型数据库
【Redis系列笔记】分布式锁
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的核心思想就是让大家都使用同一把锁,只要大家使用的是同一把锁,那么我们就能锁住线程,不让线程进行,让程序串行执行,这就是分布式锁的核心思路
111 2
|
1月前
|
NoSQL Java Redis
redis分布式锁
redis分布式锁
|
2月前
|
缓存 NoSQL Java
分布式项目中锁的应用(本地锁-_redis【setnx】-_redisson-_springcache)-fen-bu-shi-xiang-mu-zhong-suo-de-ying-yong--ben-de-suo--redissetnx-springcache-redisson(一)
分布式项目中锁的应用(本地锁-_redis【setnx】-_redisson-_springcache)-fen-bu-shi-xiang-mu-zhong-suo-de-ying-yong--ben-de-suo--redissetnx-springcache-redisson
60 0
|
8天前
|
监控 NoSQL 算法
探秘Redis分布式锁:实战与注意事项
本文介绍了Redis分区容错中的分布式锁概念,包括利用Watch实现乐观锁和使用setnx防止库存超卖。乐观锁通过Watch命令监控键值变化,在事务中执行修改,若键值被改变则事务失败。Java代码示例展示了具体实现。setnx命令用于库存操作,确保无超卖,通过设置锁并检查库存来更新。文章还讨论了分布式锁存在的问题,如客户端阻塞、时钟漂移和单点故障,并提出了RedLock算法来提高可靠性。Redisson作为生产环境的分布式锁实现,提供了可重入锁、读写锁等高级功能。最后,文章对比了Redis、Zookeeper和etcd的分布式锁特性。
108 16
探秘Redis分布式锁:实战与注意事项
|
10天前
|
NoSQL Java 大数据
介绍redis分布式锁
分布式锁是解决多进程在分布式环境中争夺资源的问题,与本地锁相似但适用于不同进程。以Redis为例,通过`setIfAbsent`实现占锁,加锁同时设置过期时间避免死锁。然而,获取锁与设置过期时间非原子性可能导致并发问题,解决方案是使用`setIfAbsent`的超时参数。此外,释放锁前需验证归属,防止误删他人锁,可借助Lua脚本确保原子性。实际应用中还有锁续期、重试机制等复杂问题,现成解决方案如RedisLockRegistry和Redisson。
|
10天前
|
缓存 NoSQL Java
【亮剑】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护,如何使用注解来实现 Redis 分布式锁的功能?
【4月更文挑战第30天】分布式锁是保证多服务实例同步的关键机制,常用于互斥访问共享资源、控制访问顺序和系统保护。基于 Redis 的分布式锁利用 SETNX 或 SET 命令实现,并考虑自动过期、可重入及原子性以确保可靠性。在 Java Spring Boot 中,可通过 `@EnableCaching`、`@Cacheable` 和 `@CacheEvict` 注解轻松实现 Redis 分布式锁功能。
|
12天前
|
NoSQL Redis 微服务
分布式锁_redis实现
分布式锁_redis实现
|
15天前
|
NoSQL Java Redis
Redis入门到通关之分布式锁Rediision
Redis入门到通关之分布式锁Rediision
15 0