import com.google.common.collect.Lists;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* 自旋锁。
* 以原子性的设置当前线程作为临界条件,控制多个线程的访问,达到每次只有一个线程获取临界对象,其他线程等待
* 无法获取锁时,不停的循环直到设置成功
* Created by 张三丰 on 2017-07-15.
*/
public class SpinLock {
/**
* 原子性的操作
*/
private AtomicReference lockProvider = new AtomicReference<>();
/**
* 设置当前线程,占用lockProvider,设置成功表示获取锁
*/
public void lock() {
Thread thread = Thread.currentThread();
//进入自旋的临界条件:lockProvider已经被其他线程设置过
while (!lockProvider.compareAndSet(null, thread)) {
//不停自旋,直到compareAndSet成功
//当前线程一直在占用cpu,线程状态未改变
}
}
/**
* 不再占用lockProvider
*/
public void unlock() {
Thread thread = Thread.currentThread();
//持有锁的线程调用才能成功
boolean compareAndSet = lockProvider.compareAndSet(thread, null);
}
}
class SpinTester {
public static void main(String[] args) throws InterruptedException {
//测试锁,循环1000次,要求多线程共享的值能按照顺序+1,最终得到正确的结果 1000
List sharedValue = Lists.newArrayList(new Integer(0));
ExecutorService executorService = Executors.newFixedThreadPool(10);
SpinLock lock = new SpinLock();
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
lock.lock();
//把数据+1
sharedValue.add(0, sharedValue.get(0) + 1);
//输出的结构必然是按顺序的
System.out.println(sharedValue.get(0));
lock.unlock();
});
}
// executorService.awaitTermination(5, TimeUnit.SECONDS);
// //所有线程执行完毕后,查看最终结果
// System.out.println("===========" + sharedValue.get(0));
executorService.shutdown();
//优缺点:
//线程状态不改变,没有线程上下文的切换,响应速度快
//一直占用CPU,所以自旋线程比较多时(竞争比较激烈),性能下降明显
//适合竞争不太激烈,持有锁的时间很短的场景
}
}
/**
* 解决SpinLock的公平性问题。TicketLock按照FIFO顺序处理线程
*/
class TicketLock {
//服务的号码,从0开始递增
private AtomicInteger serveNumber = new AtomicInteger();
//号牌号码,从0开始,每个线程递增获取
private AtomicInteger ticketNumber = new AtomicInteger();
//以上2个数字一致,则表示正在服务某个号牌持有者的线程
//每个线程获取ticket之后保存在这里
private static final ThreadLocal ticketHolder = new ThreadLocal<>();
/**
* 根据号码自旋直到服务自己
*/
public void lock() {
int ticket = ticketNumber.getAndIncrement();
ticketHolder.set(ticket);
//进入自旋的临界条件:当前线程拿到的号码与服务号码不匹配
while (serveNumber.get() != ticket) {
//自旋
}
}
/**
* 把服务号码递增,解除下一个自旋
*/
public void unlock() {
if (serveNumber.get() == ticketHolder.get()) {
//正在服务的线程的调用才有效
//服务号码递增
serveNumber.incrementAndGet();
}
}
}
class TicketTester {
public static void main(String[] args) throws InterruptedException {
//测试锁,循环1000次,要求多线程共享的值能按照顺序+1,最终得到正确的结果 1000
List sharedValue = Lists.newArrayList(new Integer(0));
ExecutorService executorService = Executors.newFixedThreadPool(10);
TicketLock lock = new TicketLock();
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
lock.lock();
//把数据+1
sharedValue.add(0, sharedValue.get(0) + 1);
//输出的结构必然是按顺序的
System.out.println(sharedValue.get(0));
lock.unlock();
});
}
// executorService.awaitTermination(5, TimeUnit.SECONDS);
// //所有线程执行完毕后,查看最终结果
// System.out.println("===========" + sharedValue.get(0));
executorService.shutdown();
//优缺点:
//线程状态不改变,没有线程上下文的切换,响应速度快
//一直占用CPU,所以自旋线程比较多时(竞争比较激烈),性能下降明显
//适合竞争不太激烈,持有锁的时间很短的场景
//解决了SpinLock的公平性问题,但是有属性被所有线程访问,对改属性的更改需要频繁刷新JMM中公共内存
}
}
/**
* 解决TicketLock的serveNumber属性被所有线程访问的问题.
* 通过链表的形式保证顺序(公平性),同时每个节点只关注前一节点的isLocked属性
* 临界条件:节点属性isLocked
*/
class CLHLock {
static class Node {
/**
* 节点的锁定状态
*/
volatile boolean isLocked = true;
Node next;
}
private volatile Node tailNode;
private static final ThreadLocal NODE_HOLDER = new ThreadLocal<>();
private static final AtomicReferenceFieldUpdater FIELD_UPDATER =
AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, Node.class, "tailNode");
/**
* 建立当前节点与前一个节点的关联,并监听前一个节点的锁定状态
*/
public void lock() {
Node node = new Node();
NODE_HOLDER.set(node);
Node preNode = FIELD_UPDATER.getAndSet(this, node);
if (preNode != null) {
//临界条件:前一个节点被锁定
while (preNode.isLocked) {
//自旋
}
}
}
/**
* 把当前节点的锁定状态置为false,解除自旋的监听当前节点锁定状态的线程
*/
public void unlock() {
Node node = NODE_HOLDER.get();
node.isLocked = false;
}
}
class CLHTester {
public static void main(String[] args) throws InterruptedException {
//测试锁,循环1000次,要求多线程共享的值能按照顺序+1,最终得到正确的结果 1000
List sharedValue = Lists.newArrayList(new Integer(0));
ExecutorService executorService = Executors.newFixedThreadPool(10);
CLHLock lock = new CLHLock();
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
lock.lock();
//把数据+1
sharedValue.add(0, sharedValue.get(0) + 1);
//输出的结构必然是按顺序的
System.out.println(sharedValue.get(0));
lock.unlock();
});
}
// executorService.awaitTermination(5, TimeUnit.SECONDS);
// //所有线程执行完毕后,查看最终结果
// System.out.println("===========" + sharedValue.get(0));
executorService.shutdown();
//优缺点:
//线程状态不改变,没有线程上下文的切换,响应速度快
//一直占用CPU,所以自旋线程比较多时(竞争比较激烈),性能下降明显
//适合竞争不太激烈,持有锁的时间很短的场景
//解决了Ticket锁的公共属性的问题,但是访问的是上一个线程持有的节点的属性,在NUMA架构下,可能造成频繁访问远程内存问题
}
}
/**
* 同自旋自己节点的属性,解决CLH锁自旋pre节点,在NUMA架构下内存读取比较慢的问题
*/
class MCSLock {
static class MCSNode {
//是否被锁定,默认是true
//volatile
boolean isLocked = true;
MCSNode next;
}
private static final ThreadLocal NODE_HOLDER = new ThreadLocal<>();
volatile MCSNode tail;
private static final AtomicReferenceFieldUpdater UPDATER =
AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class, "tail");
/**
* 自旋当前节点等待锁
*/
public void lock() {
//创建当前节点
MCSNode node = new MCSNode();
NODE_HOLDER.set(node);
//获取之前的节点,即尾节点
MCSNode preTail = UPDATER.getAndSet(this, node);//第1步 设置当前节点并且获取之前的节点,当前节点变为尾节点
if (preTail != null) { //说明之前已经有节点,线程已经被锁定了,那么就自旋等待
preTail.next = node;//第2步 关联节点
while (node.isLocked) {//第3步 自旋等待
//自旋
}
}
}
/**
* 解除next节点的自旋锁
* lock在等待当前节点的状态,unlock需要检查next节点是否存在,如果存在置为false,以结束其他线程的自旋
*/
public void unlock() {
MCSNode node = NODE_HOLDER.get();
//如果当前节点是尾节点,置为null。(当前线程已经结束,后面不能挂下一个节点了(挂了节点后无法取消其自旋))
if (!UPDATER.compareAndSet(this, node, null)) {
while (node.next != null) {//使用while循环,是因为lock里面第1步执行成功,可能第2步还没执行完成
node.next.isLocked = false;
node.next = null;// for GC
}
} else {
if (node.next != null) {
node.next.isLocked = false;
node.next = null;// for GC
}
}
}
}
class MCSTester {
public static void main(String[] args) throws InterruptedException {
//测试锁,循环1000次,要求多线程共享的值能按照顺序+1,最终得到正确的结果 1000
List sharedValue = Lists.newArrayList(new Integer(0));
ExecutorService executorService = Executors.newFixedThreadPool(10);
MCSLock lock = new MCSLock();
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
lock.lock();
//把数据+1
sharedValue.add(0, sharedValue.get(0) + 1);
//输出的结构必然是按顺序的
System.out.println(sharedValue.get(0));
lock.unlock();
});
}
// executorService.awaitTermination(5, TimeUnit.SECONDS);
// //所有线程执行完毕后,查看最终结果
// System.out.println("===========" + sharedValue.get(0));
executorService.shutdown();
//优缺点:
//线程状态不改变,没有线程上下文的切换,响应速度快
//一直占用CPU,所以自旋线程比较多时(竞争比较激烈),性能下降明显
//适合竞争不太激烈,持有锁的时间很短的场景
//解决了CLH锁可能频繁访问远程内存的问题
}
}
附:涉及到的处理器架构知识