目录
一、常用锁(除Synchronized)
LongAddr
ReentrantLock
CountDownLatch
CyclicBarrier
Phaser
ReadWriteLock
Semaphore
Exchanger
LockSupport
二、AQS
三、ThreadLocal
首先声明LongAddr不为锁,他是一个原子操作类,类似于AtomicLong。通过上文我们可以知道count++这种操作它不是一个原子性操作,如果想让它变成原子性操作,我们可以通过加锁的方式事项。除了加锁以外我们也可以通过原子类实现,例如AtomicLong的increamentAndGet()方法或者LongAddr的increment()方法。
那么这两个原子类有什么区别的呢。AtomicLong和LongAddr底层是通过CAS自旋锁的方式实现,但是AtomicLong内部只有一个锁,而LongAddr中是分段锁,就好像它的内部会将数据分为一个数组,针对数组中的每个元素进行加锁,最后将数组整体相加。
通过下面的一段简单的程序,做一个比较:
static long count = 0L;
static AtomicLong countAtomic = new AtomicLong(0L);
static LongAdder countLongAddr = new LongAdder();
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[2000];
Object lock = new Object();
for (int i = 0; i < threads.length; i++){
threads[i] = new Thread(() -> {
for (int j = 0; j < 50000; j++){
synchronized (lock){
count++;
}
}
});
}
Long start = System.currentTimeMillis();
for (Thread thread : threads) thread.start();
for (Thread thread : threads) thread.join();
System.out.println("synchronized耗时:"+(System.currentTimeMillis()-start));
for (int i = 0; i < threads.length; i++){
threads[i] = new Thread(() -> {
for (int j = 0; j < 50000; j++){
countAtomic.incrementAndGet();
}
});
}
start = System.currentTimeMillis();
for (Thread thread : threads) thread.start();
for (Thread thread : threads) thread.join();
System.out.println("AtomicLong耗时:"+(System.currentTimeMillis()-start));
for (int i = 0; i < threads.length; i++){
threads[i] = new Thread(() -> {
for (int j = 0; j < 50000; j++){
countLongAddr.increment();
}
});
}
start = System.currentTimeMillis();
for (Thread thread : threads) thread.start();
for (Thread thread : threads) thread.join();
System.out.println("LongAddr耗时:"+(System.currentTimeMillis()-start));
}
下面是他们的耗时信息
synchronized耗时:4550
AtomicLong耗时:1636
LongAddr耗时:244
通过比较发现,他们的耗时Synchronized>Atomic>LongAddr,但是当线程数量比较少或者每个线程循环次数比较少时,Atomic还和LongAddr的效率并不一定谁高,需要进行进一步的比较。
先看下面一段代码
ReentrantLock lock = new ReentrantLock();
void first(){
try {
lock.lock();
for (int i = 0; i < 5; i++){
System.out.println(i);
if (i == 3){
second();
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
void second(){
try {
lock.lock();
System.out.println("second");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
D_Reentrant reentrant = new D_Reentrant();
reentrant.first();
}
看一下执行输出:
0
1
2
3
second
4
通过以上执行结果,可以看到,ReentrantLock为可重入锁。Synchronized也是可重入锁,那么ReentrantLock的优势在那里呢。因为它更加灵活,有一些更丰富的功能,看下面的代码片段:
// 创建公平锁
ReentrantLock reentrantLock = new ReentrantLock(true);
// 尝试获取锁
Boolean lockFlag = reentrantLock.tryLock();
// 在5秒内尝试获取锁
lockFlag = reentrantLock.tryLock(5, TimeUnit.SECONDS);
// 可以被打断的加锁,当其它线程调用interrupt方法的时候,抛出异常,并且释放锁
reentrantLock.lockInterruptibly();
它能对线程进行阻塞,在创建的时候传入一个值,当这个值不为0的时候,进行阻塞,当线程为零的时候放行,内部的线程安全通过CAS实现,具体看如下代码:
// 创建CountDownLatch,并且入参为10
CountDownLatch countDownLatch = new CountDownLatch(10);
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++){
threads[i] = new Thread(() -> {
System.out.println("thread start");
// 对countDownLatch中的值减1,通过cas来保证线程安全
countDownLatch.countDown();
});
}
for (Thread thread : threads){
thread.start();
}
// 进行阻塞,当countDownLatch中的值为0的时候放行
countDownLatch.await();
System.out.println("end");
输出结果:
thread start
thread start
thread start
thread start
thread start
thread start
thread start
thread start
thread start
thread start
end
它和CountDownLatch正好相反,CountDownLatch是计数--,当减到0以后,放行;而CyclicBarrier是计数++,当加到指定值以后,放行。CyclicBarrier是可以循环使用,不需要重新设置值的。具体使用看下面的代码:
// 创建CyclicBarrier,并且指定当阻拦的线程数量为25是执行指定的runnable
CyclicBarrier cyclicBarrier = new CyclicBarrier(25, new Runnable() {
@Override
public void run() {
System.out.println("桌满,开席");
}
});
for (int i = 0; i < 100; i++){
new Thread(()->{
try {
// 线程进行阻塞
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
按照上面的解释,没阻塞满25个线程执行一次打印,并且可以循环利用,那么上面的代码会打印4次,具体打印结果如下:
桌满,开席
桌满,开席
桌满,开席
桌满,开席
按照阶段的不同,来控制线程的执行。可以把它理解成一个带阶段的CyclicBarrier,每达到一个阶段去做不同的事情。在实际项目中使用的不是很多,它是在1.7以后才出现的类,通过下面的代码对它来做一个了解即可:
/**
* 自定义自己的Phaser,然后定义各个阶段,
* 需要注意阶段是从0开始
*/
static class MarryPhaser extends Phaser{
@Override
protected boolean onAdvance(int phase, int registeredParties) {
switch (phase) {
case 0:
System.out.println("所有人到齐了!"+registeredParties+"个人");
return false;
case 1:
System.out.println("所有人吃完了!"+registeredParties+"个人");
return false;
case 2:
System.out.println("所有人离开了!"+registeredParties+"个人");
return false;
case 3:
System.out.println("入洞房!"+registeredParties+"个人");
return true;
default:
return true;
}
}
}
static MarryPhaser marryPhaser = new MarryPhaser();
public static void waitSleep(int number) {
try {
Thread.sleep(number);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static Random random = new Random();
static class Person extends Thread {
private String name;
public Person(String name){
this.name = name;
}
public void arrive(){
waitSleep(random.nextInt(1000));
// 第一阶段,所有人必须都到达以后才能去进行下一步
marryPhaser.arriveAndAwaitAdvance();
}
public void eat(){
waitSleep(random.nextInt(1000));
// 第二阶段,所有人必读都吃完以后才能进行下一步
marryPhaser.arriveAndAwaitAdvance();
}
public void leave(){
waitSleep(random.nextInt(1000));
// 第三阶段,所有人必须都离开以后才能进行下一步
marryPhaser.arriveAndAwaitAdvance();
}
public void sleep(){
if ("新郎".equals(name) || "新娘".equals(name)){
waitSleep(random.nextInt(1000));
// 第四阶段,新郎和新娘睡觉就是入洞房,结束
marryPhaser.arriveAndAwaitAdvance();
}else {
// 第四阶段不需要除新郎和新娘以外的人参与
marryPhaser.arriveAndDeregister();
}
}
@Override
public void run(){
arrive();
eat();
leave();
sleep();
}
}
public static void main(String[] args) {
marryPhaser.bulkRegister(7);
for (int i = 0; i < 5; i++){
new Thread(new Person(i+"person")).start();
}
new Thread(new Person("新郎")).start();
new Thread(new Person("新娘")).start();
}
看一下输出结果:
所有人到齐了!7个人
所有人吃完了!7个人
所有人离开了!7个人
入洞房!2个人
读写锁,这种锁针对读和写进行不同锁处理方式,读锁时共享锁,写锁是排他锁。看下面一段代码:
public static void deal(Lock lock, String deal){
try {
lock.lock();
System.out.println(deal);
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
// 创建ReadWriteLock
ReadWriteLock lock = new ReentrantReadWriteLock();
// 获取读锁
Lock readLock = lock.readLock();
// 获取写锁
Lock writeLock = lock.writeLock();
Runnable readR = () -> deal(readLock, "read");
Runnable writeR = () -> deal(writeLock, "write");
for (int i = 0; i < 10; i++) new Thread(readR).start();
for (int i = 0; i < 2; i++) new Thread(writeR).start();
}
如果读锁和写锁都是排它锁,那么它会执行将近12s左右,但是运行完上述代码你回发现read几乎是同时完成,而write是一条一条完成。通过上述代码可以证明,读锁是共享锁,写锁是排它锁。
在创建Semaphore的时候会指定一个信号量,这个信号量代表的就是允许多少个线程同时执行,它可以起到一个限流的作用。看下面这段代码:
// 创建Semaphore对象
Semaphore semaphore = new Semaphore(2);
new Thread(() -> {
try {
// 获取信号量
semaphore.acquire();
System.out.println("first start");
Thread.sleep(1000);
System.out.println("first end");
// 释放信号量
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
// 获取信号量
semaphore.acquire();
System.out.println("second start");
Thread.sleep(1000);
System.out.println("second end");
// 释放信号量
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
// 获取信号量
semaphore.acquire();
System.out.println("third start");
Thread.sleep(1000);
System.out.println("third end");
// 释放信号量
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
}).start();
下面为输出结果:
first start
second start
first end
second end
third start
third end
从输出结果可以看出,由于信号量为2,因此两个线程同时运行,当其中一个线程释放信号量以后,另一个线程得到信号量,然后继续输出。
Exchanger能够让线程之间彼此通信,交换信息,需要注意的是线程数量要求为两个。看下面这段代码:
// 创建Exchanger对象
Exchanger exchanger = new Exchanger<>();
new Thread(() -> {
try {
// 与另外一个线程进行通信,将本线程的T1交换给另一个线程
String str = exchanger.exchange("T1");
System.out.println("T1线程"+str);
}catch (Exception e){
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
// 与另外一个线程进行通信,将本线程的T2交换给另一个线程
String str = exchanger.exchange("T2");
System.out.println("T2线程"+str);
}catch (Exception e){
e.printStackTrace();
}
}).start();
下面看一下输出结果:
T2线程T1
T1线程T2
通过上面的输出结果可以看到,两个线程发生了信息 交换,分别打印了在 自己线程中交换的信息。
它可以在不使用锁的前提之下,暂停线程。并且它可以随时阻塞线程以及唤醒线程,通过下面这段代码对它有一个更好的了解,下面这段代码实现了1A2B3C4D这样的交叉打印:
static Thread t1 = null;
static Thread t2 = null;
public static void main(String[] args) {
char[] letterArray = "ABCDEEF".toCharArray();
char[] numberArray = "1234567".toCharArray();
t1 = new Thread(() -> {
for (char number : numberArray){
System.out.println(number);
// 恢复t2线程
LockSupport.unpark(t2);
// 暂停t1线程
LockSupport.park();
}
});
t2 = new Thread(() -> {
for (char letter : letterArray){
// 暂停t2线程
LockSupport.park();
System.out.println(letter);
// 恢复t1线程
LockSupport.unpark(t1);
}
});
t1.start();
t2.start();
}
这种交叉打印也可以通过wait和notify实现,需要注意的是wait会释放锁,notify不会释放锁,看下面的代码:
char[] letterArray = "ABCDEF".toCharArray();
char[] numberArray = "123456".toCharArray();
Object object = new Object();
new Thread(() -> {
synchronized (object){
for (char number : numberArray){
try {
System.out.println(number);
// 通知另一个线程启动
object.notify();
// 当前线程阻塞,并释放锁
object.wait();
}catch (Exception e){
e.printStackTrace();
}
}
// 防止线程未结束
object.notify();
}
}).start();
new Thread(() -> {
synchronized (object){
for (char letter : letterArray){
try {
System.out.println(letter);
// 通知另一个线程启动
object.notify();
// 当前线程阻塞,并释放锁
object.wait();
}catch (Exception e){
e.printStackTrace();
}
}
// 防止线程未结束
object.notify();
}
}).start();
通过这种交叉打印的方式,补充一个ReentrantLock的知识点condition,看如下代码:
// 创建ReentrantLock锁
ReentrantLock reentrantLock = new ReentrantLock();
// 创建线程1的condition
Condition first = reentrantLock.newCondition();
// 创建线程2的condition
Condition second = reentrantLock.newCondition();
char[] letterArray = "ABCDEF".toCharArray();
char[] numberArray = "123456".toCharArray();
new Thread(() -> {
try {
reentrantLock.lock();
for(char number : numberArray){
System.out.println(number);
// 唤醒线程2
second.signal();
// 线程1等待并释放锁
first.await();
}
// 最终唤醒线程2,用于线程2运行结束
second.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}).start();
new Thread(() -> {
try {
reentrantLock.lock();
for(char letter : letterArray){
System.out.println(letter);
// 唤醒线程1
first.signal();
// 阻塞线程2并释放锁
second.await();
}
// 最终唤醒线程1,用于线程1运行结束
first.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}).start();
AQS(AbstractQueuedSynchronizer),很多锁的内部都是通过AQS来实现的,例如上面的CountDownLatch,ReentrantLock等。AQS主要有两部分组成,一个是代表锁状态的state,一个存放线程信息双向链表,这个集合的每个元素为Node,包含线程的相关信息。大致结构如下:
再介绍AQS之前,先看一下ReentrantLock的lock()方法的源码:
// ReentrantLock的lock方法,调用了sync的lock方法
public void lock() {
sync.lock();
}
// ReentrantLock的匿名内部类Sync继承了AbstractQueuedSynchronizer类,也就是AQS
abstract static class Sync extends AbstractQueuedSynchronizer{...}
// ReentrantLock的匿名内部类NonfairSync继承了Sync,根据继承的可传递性,也继承了AQS
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// 调用AQS的cas方法进行加锁,如果加锁成功,则进行加锁处理
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 如果调用AQS的cas方法加锁失败,则调用AQS的acquire方法
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// AQS的设置state的方法,也就是获取锁
protected final boolean compareAndSetState(int expect, int update) {
// 调用unsafe的cas方法
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
// AQS的acquire的方法
public final void acquire(int arg) {
// if条件里面第一步判断是否能获取到锁,因为使用的短路且,如果能获取到锁,则条件判断结束
// 如果没有获取到锁,则尝试将线程放入队列中
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// AQS的tryAcquire方法实现这里采用了模板的设计模式,由子类去进行具体的实现
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// AQS的addWaiter方法,通过CAS的方式往AQS的队列中的尾部添加新的线程节点
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
// AQS的acquireQueued方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 通过死循环的方式,让进来的线程去获取锁
for (;;) {
final Node p = node.predecessor();
// 当当前线程的节点的前一个节点为头结点的时候,尝试去获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// ReentrantLock的匿名内部类NonfairSync的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
// 调用ReentrantLock的匿名内部类Sync的nonfairTryAcquire方法
return nonfairTryAcquire(acquires);
}
// ReentrantLock的匿名内部类Sync的nonfairTryAcquire方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取AQS的当前锁状态
int c = getState();
// 如果当前锁没被使用
if (c == 0) {
// 调用AQS的cas方法上锁,如果上锁成功,则放回成功
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前锁被占用,并且占用线程未当前线程,则岁state进行+1处理
// 也是通过这种方式实现可重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
其实在上面已经对AQS的源码做了一部分的解读,接下来再看一下他的state和双向链表:
// 通过state来标识锁状态,0为无锁
private volatile int state;
// 双向链表的头结点
private transient volatile Node tail;
// 双向链表的尾结点
private transient volatile Node head;
// AQS的内部类Node
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
// 当前线程的等待状态
volatile int waitStatus;
// 当前节点的前节点
volatile Node prev;
// 当前节点的尾结点
volatile Node next;
// 当前节点的线程信息
volatile Thread thread;
/**
* Link to next node waiting on condition, or the special
* value SHARED. Because condition queues are accessed only
* when holding in exclusive mode, we just need a simple
* linked queue to hold nodes while they are waiting on
* conditions. They are then transferred to the queue to
* re-acquire. And because conditions can only be exclusive,
* we save a field by using special value to indicate shared
* mode.
*/
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
// 获取当前节点的前节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
在jdk1.9以后添加了Varhandle类型用来表示指向某个变量的引用,如下图:
这个类型的作用在于,他可以将普通对象进行原子操作,例如compareAndSet()方法和getAndAdd()方法。它是通过C实现的,可以理解为直接操作二进制码。
ThreadLocal它是线程私有,放入ThreadLocal的对象只对当前对象可见,通过下面这段代码进一步了解:
// 创建threadLocal对象
static ThreadLocal threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
try {
// 线程1沉睡两秒
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
// 然后输出threadLocal中的对象
System.out.println(threadLocal.get());
}).start();
new Thread(() -> {
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
// 线程2沉睡一秒以后往threadlocal中存放对象
threadLocal.set(new Product());
}).start();
}
static class Product{
private String name = "product";
}
如果ThreadLocal对象不是线程私有的话,那么线程1应该输出代用“product”值的对象,那么我们看一下输出结果:
null
输出结果为null,证明ThreadLocal是线程私有,看一下源码,可以对它有一个更好的了解:
// ThreadLocal的set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果map不为null,则将值存放到map中
map.set(this, value);
else
// 如果map为null,则创建map
createMap(t, value);
}
// ThreadLocal的getMap方法,获取的是当前线程的THreadLocalMap(它是Thread的内部类)
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// ThreadLocal的createMap方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
在前文的JVM垃圾回收中我们可以知道ThreadLocal中的ThreadLocalMap实现是通过弱引用来实现的,但是弱引用只针对entry中的key,但是value还存在,所以在使用结束以后,在finally中执行一下remove()方法,防止出现oom。
在我们的开发中它有一个很重要的作用,就是spring中的事物,如果同一个事务中有多个数据库操作,如果这些操作使用的数据库链接不同,那么是没有办法进行管理的。这时候就用到了ThreadLocal,将第一个获取到的数据库链接放入当前线程的ThreadLocal中,当前线程的其它数据库操作都从ThreadLocal中拿去数据库链接,这样就能保证事务的正常运行。