文章目录
最近在看《Java并发编程的艺术》,看到书里的一些程序片段,发现了一些之前没有仔细思考过的东西,现在记录下来。
wait和notify
wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放),调用wait方法的一个或多个线程就会解除wait状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行。比如下面这个例子:
package xyz.dzh.ArtJavaConcurrency.chapter04;
import java.sql.Connection;
import java.util.LinkedList;
import lombok.extern.slf4j.Slf4j;
/**
* 6-16
*/
@Slf4j
public class ConnectionPool {
private final LinkedList<Connection> pool = new LinkedList<Connection>();
public ConnectionPool(int initialSize) {
if (initialSize > 0) {
for (int i = 0; i < initialSize; i++) {
pool.addLast(ConnectionDriver.createConnection());
}
}
}
public void releaseConnection(Connection connection) throws InterruptedException {
if (connection != null) {
synchronized (pool) {
pool.addLast(connection);
pool.notifyAll();
// Thread.sleep(1000);
log.info("notify之后不会立即释放锁");
}
}
}
// �ȴ�
public Connection fetchConnection(long mills) throws InterruptedException {
synchronized (pool) {
if (mills <= 0) {
while (pool.isEmpty()) {
// 执行锁所属对象的wait方法,此线程会释放锁,进入线程等待池中等待被唤醒
pool.wait();
}
return pool.removeFirst();
} else {
long future = System.currentTimeMillis() + mills;
long remaining = mills;
while (pool.isEmpty() && remaining > 0) {
pool.wait(remaining);
remaining = future - System.currentTimeMillis();
}
Connection result = null;
if (!pool.isEmpty()) {
result = pool.removeFirst();
}
return result;
}
}
}
}
注意:如果线程调用了wait方法,此线程会在这里等待,其他线程调用notify之后,该线程参与到锁的竞争中来,如果该线程获取了锁,则会从wait方法后面开始执行。
package xyz.dzh.ArtJavaConcurrency.chapter04;
import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 6-18
*/
@SuppressWarnings("checkstyle:VisibilityModifier")
public class ConnectionPoolTest {
static ConnectionPool pool = new ConnectionPool(10);
// 保证所有的ConnectionPool能够同时开始
static CountDownLatch start = new CountDownLatch(1);
// main线程将会等待所有ConnectionRunner结束之后才能继续操作
static CountDownLatch end;
@SuppressWarnings("checkstyle:MagicNumber")
public static void main(String[] args) throws Exception {
// 线程数量
int threadCount = 50;
end = new CountDownLatch(threadCount);
int count = 20;
AtomicInteger got = new AtomicInteger();
AtomicInteger notGot = new AtomicInteger();
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(new ConnetionRunner(count, got, notGot), "ConnectionRunnerThread");
thread.start();
}
start.countDown();
end.await(); // 主线程等待其他线程执行完毕。
System.out.println("total invoke: " + (threadCount * count));
System.out.println("got connection: " + got);
System.out.println("not got connection " + notGot);
}
static class ConnetionRunner implements Runnable {
int count;
AtomicInteger got;
AtomicInteger notGot;
public ConnetionRunner(int count, AtomicInteger got, AtomicInteger notGot) {
this.count = count;
this.got = got;
this.notGot = notGot;
}
public void run() {
try {
//表示在这个线程里面等待main方法执行完连接池的初始化之后再统一去竞争连接池。main初始化完了之后通过countDown通知这里继续往下执行。
start.await();
} catch (Exception ex) {
//
}
while (count > 0) {
try {
// 从线程池中获取连接,如果一秒内获取不到,将会返回null
// 分别统计连接获取的数量got和为获取到的数量notGot
Connection connection = pool.fetchConnection(1000);
if (connection != null) {
try {
connection.createStatement();
connection.commit();
} finally {
pool.releaseConnection(connection);
got.incrementAndGet();
}
} else {
notGot.incrementAndGet();
}
} catch (Exception ex) {
//
} finally {
count--;
}
}
end.countDown();
}
}
}
上面这段代码中除了实现了竞争连接池的功能以外,还有用到了CountDownLatch,可以看这篇文章:CountDownLatch
CountDownLatch
CDL通过一个计数器来实现了线程之间的同步。
其中几个主要方法:
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };
其中实现的功能和join有类似的地方。
CountDownLatch和join的区别
调用join方法需要等待thread执行完毕才能继续向下执行,而CountDownLatch只需要检查计数器的值为零就可以继续向下执行,相比之下,CountDownLatch更加灵活一些,可以实现一些更加复杂的业务场景。
区别示例
Semaphore
Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。
Semaphore实现的功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。
队列同步器AbstractQueuedSynchronizer
AbstractQueuedSynchronizer
https://juejin.im/post/5afb9ab3f265da0b736dd1e1
aqs设计的妙处:
1、自旋锁
当我们执行一个有确定结果的操作,同时又需要并发正确执行,通常可以采用自旋锁实现。在AQS中,自旋锁采用 死循环 + CAS 实现。
2、模板方法
在AQS中,模板方法设计模式体现在其acquire()、release()方法上,我们先来看下源码:
public final void acquire(int arg) {
// 首先尝试获取共享状态,如果获取成功,则tryAcquire()返回true
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
复制代码其中调用tryAcquire()方法的默认实现是抛出一个异常,也就是说tryAcquire()方法留给子类去实现,acquire()方法定义了一个模板,一套处理逻辑,相关具体执行方法留给子类去实现。
自己做一个锁
https://zhuanlan.zhihu.com/p/32883297 这个写的也很好
package xyz.dzh.ArtJavaConcurrency.chapter05;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 10-2
*/
public class Mutex implements Lock {
// 静态内部类,自定义同步器
// ReentrantLock里面也有类似的一个Sync
private static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -4387327721959839431L;
// 是否处于占用状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为0的时候获取锁
public boolean tryAcquire(int acquires) {
assert acquires == 1; // Otherwise unused
// 如果经过cas设置成功,则表示获取了同步状态。
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,将状态职位0
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise unused
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 返回一个condition每一个condition包含一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
// 仅需要将操作代理到Sync即可
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public boolean tryLock() {
return sync.tryAcquire(1);
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
用一个经典的对一个数字累加的程序测试一下这个锁。
package xyz.dzh.ArtJavaConcurrency.chapter05;
/**
* @author dongzhonghua <dongzhonghua03@kuaishou.com>
* Created on 2020-04-18
*/
@SuppressWarnings("checkstyle:MagicNumber")
public class MutexTest {
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
Mutex lock = new Mutex();
Runnable task = new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
try {
lock.lock();
num++;
} finally {
lock.unlock();
}
}
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(num);
}
}
可重入锁
上边这个锁是不可重入锁,因为我们每次只把state更新为1。如果要支持可重入也很简单,获取锁时检测锁是不是被当前线程占有着,如果是就把state的值加1,释放锁时每次减1即可,减为0时表示锁已释放。
Condition
条件变量Condition
条件变量很大一个程度上是为了解决Object.wait/notify/notifyAll难以使用的问题。
Condition