JAVA并发编程知识

一、 并发编程的挑战

  1. 上下文切换

创建和上下文切换需要开销,减少上下文切换的方法有:无锁并发编程、CAS算法、使用最少线程、使用协程。

死锁

避免死锁的方法:避免一个线程同时获取多个锁

                        避免一个线程在锁内同时占有多个资源

                        使用定时锁,使用lock.tryLock()来替代使用内部锁机制

                        对于数据库锁,加锁和解锁在一个数据库连接里

资源限制

硬件:带宽的速度、硬盘读写速度、CPU处理速度(使用集群)

软件:数据库连接数、socket连接数(考虑资源复用)

·合理调整并发度

二、 Java并发机制的底层代码实现

Volatile

Java虚拟机提供的最轻量级的同步机制。

Volatile变量具备两种特性: 保证此变量对所有线程的可见性(不保证原子性)

禁止指令重排序优化(会干扰程序的并发执行)

·Volatile与锁的选择依据:volatile能否满足场景需求

·64位的long和double一般不需要声明为volatile(虚拟机实现为原子性操作)

·除了volatile,synchronized和final也能实现可见性

·volatile和synchronized可以保证线程之间的有序性

·并发中的3个重要特性:原子性、可见性、有序性

① Volatile的实现原理:

有volatile修饰共享变量进行读写操作时会生成lock前缀的指令,完成两件事:

1)将当前处理器缓存行的数据写回系统内存

2)使在其他CPU缓存了该内存地址的数据无效


追加字节性能优化:

使用追加到64字节的方式来填满高速缓存行,避免头节点和尾节点加载到同一行,使用时不会相互锁定。

(如果队列的头尾节点都不足64字节,处理器会将他们读到同一个高速缓存行,一个处理器修改头节点时会将整行锁定,其他处理器无法操作尾节点。)

Synchronized

synchronized关键字是Java语言中重量级的操作,synchronized同步块对同一线程可重入,同步块在已经进入的线程执行完成前会阻塞后面的线程(阻塞线程需要系统协助,从用户态切换到核心态,消耗处理器时间)。虚拟机优化:在阻塞前加入一段自旋等待过程。

Synchronized用的锁存在Java对象头中。Java中每个对象都可以作为锁,具体为:

·普通同步方法,锁是当前实例对象

·静态同步方法,锁是当前类的Class对象

·同步方法块,锁是Synchronized括号里配置的对象

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步使用monitorenter和monitorexit指令实现,方法同步使用另一种方法(可以使用方法修饰符上的ACC_SYNCHRONIZED,也可以使用这两个指令,本质都是monitor的获取)。

编译后,monitorenter指令插入到同步代码块的开始位置,monitorexit插入到方法结束处和异常处,每个monitorenter必须要对应的monitorexit与之匹配,任何对象有一个monitor与之关联,当且仅当monitor被持有后,它将处于锁定状态。线程执行monitorenter时将会尝试获取所对应的monitor所有权(锁)。

·Java SE1.6锁一共4个级别:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

(锁可以升级但是不能降级)

·三种锁的比较

① 轻量级锁

没有多线程竞争的情况下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。

加锁过程:对象头中有2bit用于存储锁标志位,代码进入同步块后对象没有被锁(01),虚拟机在线程的栈帧中创建一个Lock Record空间,存储对象头中Mark Word的拷贝。然后使用CAS尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功则拥有锁,标志位置位00,失败则检查对象的Mark Word是否指向当先线程的栈帧,是则说明当前线程已经拥有锁,否则说明这个锁对象已经被其他线程抢占。

如果有两个以上线程竞争同一个锁,则膨胀为重量级锁,标志位置10,Mark Word存储指向重量级锁的指针,后面的线程进入阻塞状态。

② 偏向锁

消除数据在无竞争情况下的同步原语(操作系统或计算机网络用语范畴,是由若干条指令组成的,用于完成一定功能的一个过程),把整个同步去除,进一步提高程序的运行性能。

锁偏向于第一个获得它的线程,如果该锁没有被其他线程获得,则持有偏向锁的线程永远不需要再进行同步。是一种带有效益权衡性质的优化,并不一定总对程序有利,如果总是被多个线程访问则偏向模式就是多余的。

原子操作

① 处理器实现

处理器可以保证一个字节(最新:一个缓存行进行16/32/64位)的操作是原子的。对于更复杂的操作,处理器提供总线锁和缓存锁来保证复杂操作的原子性。

总线锁:使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器被阻塞,此处理器独享共享内存

缓存锁:内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当他执行操作回写到内存时,处理器不在总线声明LOCK#信号,而是修改内部地址,使用缓存一致性机制来保证原子性。

(操作的数据不能被缓存在处理器内部或者跨多个缓存行时,使用总线锁)

② Java实现

Java通过锁和循环CAS的方式实现。

循环CAS:

JDK提供一些类支持原子操作,如AtomicInteger用原子的方式更新int值。

问题:

   ·ABA问题(1.5开始使用Atomic的AtomicStampedReference类来解决)

   ·循环时间长开销大

   ·只能保证一个共享变量的原子操作

三、 线程相关

线程简介

(1)Java线程调度(Java使用抢占式线程调度)

线程调度指系统为线程分配处理器使用权,主要有协同式线程调度、抢占式线程调度。

协同式线程调度,线程的执行时间由线程本身决定不可控,执行完成主动切换到另外的线程,没有线程同步问题,容易阻塞;抢占式线程调度,每个线程将由系统分配执行时间。

Java语言设置10个级别的线程优先级,当两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行。(线程优先级和系统优先级不能对应时,两个线程的优先级可能会变成相同;系统可能被系统自行改变,例如Windows的“优先级推进器”)

(2)状态转换

Java定义了5(6)种线程状态:

新建(new):创建后尚未启动

运行(Runable):包括系统线程状态的Running、Ready,即运行或者等待CPU分配执行时间

无限期等待(Waiting):等待被其他线程显示地唤醒

限期等待(Timed Waiting):在一定时间后自动唤醒

阻塞(Blocked):等待获取排它锁(另一个线程放弃这个锁是发生)

(结束(Terminated):已经终止的线程状态)

(3)Daemon线程(守护线程)

支持型线程,主要用于程序中后台调度以及支持性工作。比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

Thread.setDaemon(true);方法可以将线程设为Daemon线程。(一定要在start方法前)

启动和终止线程

(1) 构造线程

(2) 启动线程

当前线程告知虚拟机立即调用start()方法。

(3) 中断

线程通过isInterrupt()来判断是否被中断,通过Thread.interrupted()对当前中断标志位复位。(处于终结状态的线程标志位为false)

在抛出InterruptedException异常前会先将中断标志清除,然后再抛出异常。抛出InterruptedException的代表方法有:

java.lang.Object 类的 wait 方法

java.lang.Thread 类的 sleep 方法

sleep方法:interrupt方法是Thread类的实例方法,在执行的时候并不需要获取Thread实例的锁定,任何线程在任何时刻,都可以通过线程实例来调用其他线程的interrupt方法。 当在sleep中的线程被调用interrupt方法时,就会放弃暂停的状态,并抛出InterruptedException异常,这样一来,线程的控制权就交给了捕捉这个异常的catch块了。
wait方法:当线程调用wait方法后,线程在进入等待区时,会把锁定接触。当对wait中的线程调用interrupt方法时,会先重新获取锁定,再抛出InterruptedException异常,获取锁定之前,无法抛出InterruptedException异常。
join方法:当线程以join方法等待其他线程结束时,一样可以使用interrupt方法取消。因为join方法不需要获取锁定,故而与sleep一样,会马上跳到catch程序块

java.lang.Thread 类的 join 方法

(4) 过期的suspend()、 resume() 、stop()

暂停、恢复、停止

(5) 安全终止

利用一个Boolean变量来控制是否需要停止任务并终止该线程。

线程间的通信

(1) 等待/通知机制

线程A调用了对象O的wait()方法进入等待状态;另一个线程B调用了对象O的notify()方法或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。两个线程通过对象O完成交互。

wait()方法会释放锁;notify()使线程由等待变为阻塞,需要获取锁才能执行

(2) 管道输入/输出流

主要用于线程之间的数据传输,传输的媒介为内存。

主要包括:PipedOutputStream、PipedInputStream、PipedReader、PipedWriter,前两种面向字节,后两种面向字符。

对于Piped类型的流,需要将输入和输出绑定(调用connect()方法),否则抛出异常。

等待/通知经典范式:加锁、循环、处理

(3) Thread.join()的使用

当前线程A等待thread线程终止后才从thread.join()返回。(前驱线程结终止后从join()方法返回,也是一种等待/通知机制)

(4) ThreadLocal的使用

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。

ThreadLocal类提供的几个方法:

1
2
3
4

public T
get() { }
public void set(T
value) { }
public void
remove() { }
protected T initialValue() { }

工作过程:

首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

小结:

1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;

2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;

3)在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。

参考:http://www.cnblogs.com/dolphin0520/p/3920407.html

示例:

public class
Test {

ThreadLocal<Long> longLocal = new

ThreadLocal();

ThreadLocal<String> stringLocal = new

ThreadLocal();

public void set() {

longLocal.set(Thread.currentThread().getId());

stringLocal.set(Thread.currentThread().getName());

}   

public long getLong() {

    return longLocal.get();

}

public String getString() {

    return stringLocal.get();

} 

public static void main(String[] args)

throws InterruptedException {

    final Test test = new Test();

    test.set();

    System.out.println(test.getLong());

    System.out.println(test.getString());

    Thread thread1 = new Thread(){

        public void run() {

            test.set();

System.out.println(test.getLong());

System.out.println(test.getString());

        };

    };

    thread1.start();

    thread1.join(); 

    System.out.println(test.getLong());

    System.out.println(test.getString());

}

}

结果为:

四、 Java中的锁

Lock接口

JDK5开始提供了与synchronized类似的同步功能,在使用时需要显式的获取和释放锁。拥有锁获取与释放的可操作性。

同步特性:

尝试非阻塞的获取锁:如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁。

可中断的获取锁:能相应中断,抛出中断异常并释放锁

超时获取锁:指定一段时间无法获取到锁则返回

使用方式:

队列同步器(AQS)

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法(acquire和release)来管理同步状态。使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int
update))来对同步状态进行更改,因为它们能够保证状态的改变是安全的。可以同时实现排他锁和共享锁模式(独占、共享)

① 同步队列

同步器包含两个节点的引用,一个指向头节点,一个指向尾节点。

加入队列的过程(线程不安全)必须要保证线程安全,采用基于CAS的方法:compareAndSetTail(Node expect ,
Node update) ,它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后续节点,而后续节点将会在获取同步状态成功时将自己设置为首节点。设置首节点是由获取同步状态成功的线程来完成的,由于只有一个线程能够成功的获取到同步状态,线程安全,它只需要将首节点设置成为原首节点后继节点,并断开首节点的next引用即可。

重入锁(ReentrantLock)

ReentrantLock重入锁在基本用法上和synchronized类似,增加了高级功能:

等待可中断:正在等待的线程可以选择放弃等待

公平锁:通过构造函数实现,多个线程等待同一个锁时按申请时间顺序依次获得锁

锁可以绑定:ReentrantLock对象可以同时绑定多个Condition对象

实现重进入

1) 判断当前线程是否为获取锁的线程来决定获取操作是否成功;

2) 获取锁的线程再次请求时,则同步状态值进行增加;

3) 释放锁:同步状态时为0完全释放,将占有线程设置为null

公平锁与非公平锁

公平锁:锁的获取顺序符合请求的绝对时间顺序(FIFO)

(默认为非公平锁)

获取锁:非公平锁只要CAS设置同步状态成功则表示当前线程获取了锁;公平锁加入了同步队列中当前节点是否有前驱节点的判断。有则表示有线程比当前线程更早地请求了锁。需要等待前驱线程获取并释放锁。

非公平锁可能使线程“饥饿”,但其线程切换少,开销更小。(刚刚释放锁的线程再次获取锁的几率非常大)

读写锁

读写锁维护了一对锁(读锁和写锁),允许同一时刻有多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。

读写锁可以保证写操作对读操作的可见性以及提升并发性能,能够简化读写交互场景的编程方式。一般情况下性能优于排它锁。(大多数场景读多于写)

ReadWriteLock接口主要方法:

readLock():获取读锁

writeLock():获取写锁

ReentrantReadWriteLock实现类增加方法:

读写锁的自定义同步器需要在同步状态(整型变量)上维护多个读线程和一个写线程,“按位分割变量”,高16位表示读,低16位表示写。

LockSupport工具

LockSupport提供最基本的线程阻塞和唤醒功能。

Java 6以后的方法可以提供阻塞对象的信息。

Condition接口

等待/通知模式: Object的监视器+synchronized同步关键字

                        Condition接口+Lock

Condition对象有Lock对象创建出来,示例:

await()方法调用后,当前线程会释放锁并在此等待;其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。

五、 Java并发容器和框架

ConcurrentHashMap

线程安全且高效的HashMap。

HashMap:

多线程环境下使用HashMap进行put操作会引起死循环,多线程会导致HashMap的Entry链表形成环形数据结构,next节点永不为空,产生死循环获取Entry。

HashTable:

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。

ConcurrentHashMap:

使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

① ConcurrentHashMap的结构

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。

一个ConcurrentHashMap里包含一个Segment数组,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

② ConcurrentHashMap的初始化

·segmentShift是用来计算segments数组索引的位移量

·segmentMask则是用来计算索引的掩码值

I 初始化Segments数组

Segments数组的长度ssize通过concurrencyLevel计算得出,ssize为大于等于concurrencyLevel的2的最小次幂。(保证能通过按位与的散列算法定位segments数组索引)

II 初始化segmentShift和segmentMask

   segmentShift=32-sshift ,

sshift=ssize的2次幂数;segmentMask=ssize-1(转为二进制)

例如并发度为16时(即segments数组长度为16),segmentShift为32-4=28(因为2的4次幂为16),而segmentMask则为1111(二进制),索引的计算式如下:

int j = (hash

segmentShift) & segmentMask;

III 初始化每个segment

输入参数initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每个segment的负载因子,在构造方法里需要通过这两个参数来初始化数组中的每个segment。

if
(initialCapacity >
MAXIMUM_CAPACITY)

initialCapacity = MAXIMUM_CAPACITY;

int c = initialCapacity /
ssize;

if (c *
ssize < initialCapacity)

++c;

int cap = 1;

while (cap <
c)

cap <<= 1;

for (int i =
0;
i < this.segments.length;
++i)

this.segments[i]
= new Segment<K,
V>(cap,
loadFactor);

上面代码中的变量cap就是segment里HashEntry数组的长度,它等于initialCapacity除以ssize的倍数c,如果c大于1,就会取大于等于c的2的N次方值,所以cap不是1,就是2的N次方。 segment的容量threshold=(int)cap*loadFactor,默认initialCapacity等于16,loadfactor等于0.75,通过运算cap等于1,threshold等于零。

③ 定位Segment

ConcurrentHashMap使用分段锁Segment来保护不同段的数据,在插入和获取元素时,先通过散列算法定位到Segment。进行再散列,是为了减少散列冲突,使元素能够均匀地分布在不同的Segment上,从而提高容器的存取效率。
假如散列的质量差到极点,那么所有的元素都在一个Segment中,不仅存取元素缓慢,分段锁也会失去意义。

ConcurrentHashMap通过以下散列算法定位segment:

final Segment<K,V> segmentFor(int hash) {

  return segments[(hash >>> segmentShift) & segmentMask];

}

默认情况下segmentShift为28,segmentMask为15,再散列后的数最大是32位二进制数据,向右无符号移动28位,即让高4位参与到散列运算中,(hash>>>segmentShift) &segmentMask的运算结果分别是4、15、7和8,可以看到散列值没有发生冲突。

④ ConcurrentHashMap的操作

I get操作

Segment的get操作实现非常简单和高效:先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素。

public V get(Object key) {

int
h = hash(key);

return segmentFor(hash).get(key,hash); }

高效之处在于整个过程不需要加锁,除非读到的值是空才会加锁重。只需要计算两次hash值,然后遍历一个单向链表(此链表长度平均小于2),因此get性能很高。

II put操作

由于需要对共享变量进行写操作,所以为了线程安全,在操作共享变量时必须加锁。

put方法首先定位到Segment,然后在Segment里进行插入操作。需要经历两个步骤:

·判断是否需要对Segment里的HashEntry数组进行扩容

·定位添加元素的位置,然后将其放在HashEntry数组里

是否需要扩容

在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈值,则对数组进行扩容。

Segment的扩容判断比HashMap更恰当。HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。

如何扩容

在扩容的时候,首先会创建一个容量是原来两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。
为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment扩容。

III size操作

统计整个ConcurrentHashMap里元素的数量,就必须统计所有Segment里元素的数量后求和。最安全的做法是在统计size的时候把所有Segment的put、remove和clean方法全部锁住,把所有Segment的count相加,但是这种做法显然非常低效。

ConcurrentHashMap的做法是先尝试2次通过不锁Segment的方式来统计各个Segment大小,如果统计的过程中,count发生了变化,则再采用加锁的方式来统计所有Segment的大小。(使用modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。)

ConcurrentLinkedQuene

一个基于链接节点的无界线程安全队列。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许使用 null 元素。

构造方法:

ConcurrentLinkedQueue()
:创建一个最初为空的
ConcurrentLinkedQueue。

ConcurrentLinkedQueue(Collection<?
extends E> c) :创建一个最初包含给定
collection 元素的
ConcurrentLinkedQueue,按照此 collection 迭代器的遍历顺序来添加元素。

主要方法:

offer(E e) 或 add(E e) : 将指定元素插入此队列的尾部。

poll() :
获取并移除此队列的头,如果此队列为空,则返回 null。

peek() :
获取但不移除此队列的头;如果此队列为空,则返回 null

remove(Object o)
:从队列中移除指定元素的单个实例(如果存在)

size() :
返回此队列中的元素数量

contains(Object
o) :如果此队列包含指定元素,则返回 true

isEmpty() : 如果此队列不包含任何元素,则返回 true。

toArray() : 返回以恰当顺序包含此队列所有元素的数组

toArray(T[] a):
返回以恰当顺序包含此队列所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。

Fork/Join框架

Java7提供的一个用于并行执行任务的框架,把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果。

① 工作窃取算法:

指某个线程从其他队列里窃取任务来执行。当某个线程先完成队列里任务空闲时,从别的双端队列尾部拿任务来执行。

优点:可以充分利用线程进行并行计算,减少了线程间的竞争。

缺点:某些情况下仍然存在竞争,并且消耗更多的系统资源,如创建多个线程和多个双端队列。

② 框架设计和使用

   Fork/Join使用两个类:

          ForkJoinTask:创建Forkjoin任务,提供fork()和join()操作的同步机制,使用时一般继承其子类:RecursiveAction(无返回结果)、RecursiveTask(有返回结果)

   ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行。

ForkJoinTask需要实现compute方法,在这个方法里首先需要判断任务是否足够小,足够小时直接执行任务,否则需要分割子任务,子任务调用fork()时又会再去判断。使用join()方法会等待子任务执行完成并得到其结果。

ForkJoinTask需要通过ForkJoinPool来执行:ForkJoinPool.submit(ForkJoinTask);

六、 并发工具类

CountDownLatch工具类

CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

构造函数:

public void CountDownLatch(int count) {…}

构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。

相关方法:

①与CountDownLatch的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。

②其他N 个线程必须引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的任务。这种通知机制是通过
CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的count值就减1。所以当N个线程都调 用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。

Semaphore工具类

Semaphore又称信号量,是操作系统中的一个概念,在Java并发编程中,信号量控制的是线程并发的数量。

构造方法:

   Public Semaphore(int permits)

Public Semaphore (int permits, boolen fair)

//当fair等于true时,创建具有给定许可数的计数信号量并设置为公平信号量

主要方法:

void acquire():从此信号量获取一个许可前线程将一直阻塞。相当于一辆车占了一个车位。

void acquire(int
n):从此信号量获取给定数目许可,在提供这些许可前一直将线程阻塞。比如n=2,就相当于一辆车占了两个车位。

void release():释放一个许可,将其返回给信号量。就如同车开走返回一个车位。

void release(int
n):释放n个许可。

int
availablePermits():当前可用的许可数。

Semaphore主要用于控制当前活动线程数目,就如同停车场系统一般,而Semaphore则相当于看守的人,用于控制总共允许停车的停车位的个数,而对于每辆车来说就如同一个线程,线程需要通过acquire()方法获取许可,而release()释放许可。如果许可数达到最大活动数,那么调用acquire()之后,便进入等待队列,等待已获得许可的线程释放许可,从而使得多线程能够合理的运行。

CyclicBarrier工具类

CyclicBarrier让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行。与CountDownLatch不同的是该屏障(barrier)在释放等待线程后可以重用,所以称它为循环(Cyclic)的屏障。

构造方法:

public
CyclicBarrier(int parties)

public CyclicBarrier(int
parties, Runnable barrierAction)

//parties表示屏障拦截的线程数量,当线程到达屏障时先执行barrierAction然后在释放所有线程

主要方法:

getParties():获取CyclicBarrier打开屏障的线程数量,也成为方数。

getNumberWaiting():获取正在CyclicBarrier上等待的线程数量。

await():在CyclicBarrier上进行阻塞等待,直到发生以下情形之一:

·在CyclicBarrier上等待的线程数量达到parties,则所有线程被释放,继续执行。

·当前线程被中断,则抛出InterruptedException异常,并停止等待,继续执行。

·其他等待的线程被中断,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。

·其他等待的线程超时,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。

·其他线程调用CyclicBarrier.reset()方法,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。

await(timeout,TimeUnit):在CyclicBarrier进行限时阻塞等待,直到发生以下情形之一:

·在CyclicBarrier上等待的线程数量达到parties,则所有线程被释放,继续执行。

·当前线程被中断,则抛出InterruptedException异常,并停止等待,继续执行。

·当前线程等待超时,则抛出TimeoutException异常,并停止等待,继续执行。

·其他等待的线程被中断,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。

·其他等待的线程超时,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。

·其他线程调用CyclicBarrier.reset()方法,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。

isBroken():获取是否破损标志位broken的值,此值有以下几种情况:

·CyclicBarrier初始化时,broken=false,表示屏障未破损。

·如果正在等待的线程被中断,则broken=true,表示屏障破损。

·如果正在等待的线程超时,则broken=true,表示屏障破损。

·如果有线程调用CyclicBarrier.reset()方法,则broken=false,表示屏障回到未破损状态。

reset():使得CyclicBarrier回归初始状态,直观来看它做了两件事:

·如果有正在等待的线程,则会抛出BrokenBarrierException异常,且这些线程停止等待,继续执行。

·将是否破损标志位broken置为false。

CyclicBarrier与CountDownLatch比较

1)CountDownLatch:一个线程(或者多个),等待另外N个线程完成某个事情之后才能执行;CyclicBarrier:N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

2)CountDownLatch:一次性的;CyclicBarrier:可以重复使用。

3)CountDownLatch基于AQS;CyclicBarrier基于锁和Condition。本质上都是依赖于

volatile和CAS实现的。

七、 Java中的13个原子操作类

JDK1.5开始的java.util.concurrent.atomic包,提供了一种用法简单、性能高效、线程安全的更新一个变量的方式。

原子更新基本类型类

AtomicBoolean:原子更新布尔类型

AtomicInteger:原子更新整型

AtomicLong:原子更新长整型

以AtomicInteger为例,常用方法如下

int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果

boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值

int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值

void lazySet(int newValue):JDK6所增加,最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值

int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值

原子更新数组

AtomicIntegerArray:原子更新整型数组里的元素

AtomicLongArray:原子更新长整型数组里的元素

AtomicReferenceArray:原子更新引用类型数组里的元素

以AtomicIntegerArray类常用方法如下:

int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加

boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值.原子更新引用类型

原子更新字段类

AtomicReference:原子更新引用类型

AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)。

原子更新字段类

AtomicIntegerFieldUpdater:原子更新整型的字段的更新器

AtomicLongFieldUpdater:原子更新长整型字段的更新器。

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

要想原子地更新字段类需要两步

第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并设置想要更新的类和属性

第二步,更新类的字段(属性)必须使用public volatile修饰符,不能是 static的。

八、 线程池

Java中的线程池时运用场景最多的并发框架,几乎所有需要异步或者并发执行任务的程序都可以使用线程池。好处:降低资源消耗;提高响应速度;提高线程的可管理性。

线程池的实现原理

① 相关参数

· corePoolSize : 线程池的核心池大小,在创建线程池之后,线程池默认没有任何线程。

· maximumPoolSize : 线程池允许的最大线程数,他表示最大能创建多少个线程。maximumPoolSize肯定是大于等于corePoolSize。

· workQueue :一个阻塞队列,用来存储等待执行的任务,当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能

· keepAliveTime : 表示线程没有任务时最多保持多久然后停止。默认情况下,只有线程池中线程数大于corePoolSize 时,keepAliveTime 才会起作用。换句话说,当线程池中的线程数大于corePoolSize,并且一个线程空闲时间达到了keepAliveTime,那么就是shutdown。

· Unit: keepAliveTime 的单位。

· threadFactory :线程工厂,用来创建线程。

· handler : 表示当拒绝处理任务时的策略。

② 处理过程

· 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;

· 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;

· 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

· 如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

线程池的使用

new ThreadPoolExecutor (corePoolSize,maximumPoolSize,keepAlivetime,milliseconds,runnableTaskQueue,handler)

① 线程池的创建

runnableTaskQueue的类型为BlockingQueue,通常可以取下面三种类型:

1)有界任务队列ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;

2)无界任务队列LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;

3)直接提交队列SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。每个插入操作必须等待另一个线程调用移除操作,否则插入操作一直阻塞。

RejectedExecutionHandler拒绝(饱和)策略

当队列和线程池都满了说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。JDK1.5提供以下4种:

AbortPolicy:(默认)丢弃任务并抛出RejectedExecutionException

CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。

DiscardOldestPolicy:丢弃队列中最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

DiscardPolicy:丢弃任务,不做任何处理。

② 向线程池提交任务

execute():方法用于提交不需要返回值的任务,无法判断任务是否被线程池执行成功。

Submit():方法用于提交需要返回值的任务。通过线程池返回的future对象可以判断任务是否执行成功,且可以通过它的get()方法获取返回值。

③ 线程池的关闭

ThreadPoolExecutor提供了两个方法用于线程池的关闭,他们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法停止。

shutdown():将线程池状态设为SHOTDOWN,不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务

shutdownNow():立即终止线程池,将线程池的状态设为STOP,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务列表。

④ 合理配置线程池

一般需要根据任务的类型来配置线程池大小:

·如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1

·如果是IO密集型任务,参考值可以设置为2*NCPU

当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。

欢迎使用Markdown编辑器

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

新的改变

我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:

  1. 全新的界面设计 ,将会带来全新的写作体验;
  2. 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
  3. 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
  4. 全新的 KaTeX数学公式 语法;
  5. 增加了支持甘特图的mermaid语法1 功能;
  6. 增加了 多屏幕编辑 Markdown文章功能;
  7. 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
  8. 增加了 检查列表 功能。

功能快捷键

撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G

合理的创建标题,有助于目录的生成

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

如何改变文本的样式

强调文本 强调文本

加粗文本 加粗文本

标记文本

删除文本

引用文本

H2O is是液体。

210 运算结果是 1024.

插入链接与图片

链接: link.

图片: Alt

带尺寸的图片: Alt

居中的图片: Alt

居中并且带尺寸的图片: Alt

当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。

如何插入一段漂亮的代码片

博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.

// An highlighted block
var foo = 'bar';

生成一个适合你的列表

  • 项目
    • 项目
      • 项目
  1. 项目1
  2. 项目2
  3. 项目3
  • 计划任务
  • 完成任务

创建一个表格

一个简单的表格是这么创建的:

项目Value
电脑$1600
手机$12
导管$1

设定内容居中、居左、居右

使用:---------:居中
使用:----------居左
使用----------:居右

第一列第二列第三列
第一列文本居中第二列文本居右第三列文本居左

SmartyPants

SmartyPants将ASCII标点字符转换为“智能”印刷标点HTML实体。例如:

TYPEASCIIHTML
Single backticks'Isn't this fun?'‘Isn’t this fun?’
Quotes"Isn't this fun?"“Isn’t this fun?”
Dashes-- is en-dash, --- is em-dash– is en-dash, — is em-dash

创建一个自定义列表

Markdown
Text-to- HTML conversion tool
Authors
John
Luke

如何创建一个注脚

一个具有注脚的文本。2

注释也是必不可少的

Markdown将文本转换为 HTML

KaTeX数学公式

您可以使用渲染LaTeX数学表达式 KaTeX:

Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n1)!nN 是通过欧拉积分

Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t &ThinSpace; . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=0tz1etdt.

你可以找到更多关于的信息 LaTeX 数学表达式here.

新的甘特图功能,丰富你的文章

Mon 06 Mon 13 Mon 20 已完成 进行中 计划一 计划二 现有任务 Adding GANTT diagram functionality to mermaid
  • 关于 甘特图 语法,参考 这儿,

UML 图表

可以使用UML图表进行渲染。 Mermaid. 例如下面产生的一个序列图::

张三 李四 王五 你好!李四, 最近怎么样? 你最近怎么样,王五? 我很好,谢谢! 我很好,谢谢! 李四想了很长时间, 文字太长了 不适合放在一行. 打量着王五... 很好... 王五, 你怎么样? 张三 李四 王五

这将产生一个流程图。:

链接
长方形
圆角长方形
菱形
  • 关于 Mermaid 语法,参考 这儿,

FLowchart流程图

我们依旧会支持flowchart的流程图:

Created with Raphaël 2.2.0 开始 我的操作 确认? 结束 yes no
  • 关于 Flowchart流程图 语法,参考 这儿.

导出与导入

导出

如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。

导入

如果你想加载一篇你写过的.md文件或者.html文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。


  1. mermaid语法说明 ↩︎

  2. 注脚的解释 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值