1 题目描述
从一个经典面试题来探讨一下java中线程同步方面的问题。
编写一个程序,开启 N 个线程,这 N 个线程交替打印 1-10000 的整数,样例 Sample:
Thread1:1 Thread2:2 Thread3:3 Thread1:4 Thread2:5 Thread3:6 … Thread3:99 Thread1:10000
2 解体思路
这道题是java线程同步的题,本篇介绍7个方法:
- 使用synchronized+wait+notify
- 可重入锁
- 可重入锁+condition
- 信号量
- locksupport
- volatile+synchronized
- cas
以下一个个介绍
3 实现
以下是几个通用的属性
//线程数
static int threadCount = 3;
//循环输出最大值
static int maxNum = 10000;
//当前值
static int curNum = 0;
3.1 synchronized+wait+notify
使用同步块和wait、notify的方法控制三个线程的执行次序。具体方法如下:从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,例子中使用了一个全局的锁lock。当执行取模操作满足条件就使用notifyAll唤醒其他所有线程,并把全局的计数器+1,并输出内容,然后调用wait等待。
可以看到,当不满足取模的条件的情况下,始终无法触发,当一个线程调用notifyAll后,所有线程就会尝试取模的判断。所以线程就会根据全局计数器,按照既定顺序输出。
public static void test1() {
Object lock = new Object();
class InnerThread extends Thread {
int modNum;
InnerThread(String threadName, int modNum) {
this.modNum = modNum;
setName(threadName);
}
@Override
public void run() {
while (true) {
if (curNum > maxNum) {
System.out.println(getName() + " end");
break;
}
synchronized (lock) {
while (curNum % threadCount == modNum) {
System.out.println(getName() + curNum);
curNum++;
lock.notifyAll();
}
try {
if (curNum <= maxNum)
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
for (int i = 1; i <= threadCount; i++) {
new InnerThread(String.format("Thread %d:", i), i - 1).start();
}
}
3.2 可重入锁
public static void test2() {
Lock lock = new ReentrantLock();
class InnerThread extends Thread {
int modNum;
InnerThread(String threadName, int modNum) {
this.modNum = modNum;
setName(threadName);
}
@Override
public void run() {
while (true) {
try {
lock.lock();
if (curNum > maxNum) {
System.out.println(getName() + " end");
break;
}
if (curNum % threadCount == modNum) {
System.out.println(getName() + curNum);
curNum++;
}
} finally {
lock.unlock();
}
}
}
}
for (int i = 1; i <= threadCount; i++) {
InnerThread thread = new InnerThread(String.format("Thread %d:", i), i - 1);
thread.start();
}
}
3.3 可重入锁+condition
public static void test3() {
Lock lock = new ReentrantLock();
class InnerThread extends Thread {
Condition curCon, nextCon;
int modNum;
InnerThread(String threadName, int modNum, Condition curCon, Condition nextCon) {
setName(threadName);
this.curCon = curCon;
this.nextCon = nextCon;
this.modNum = modNum;
}
@Override
public void run() {
while (true) {
try {
lock.lock();
if (curNum > maxNum) {
System.out.println(getName() + " end");
nextCon.signal();
break;
}
if (curNum % threadCount == modNum) {
System.out.println(getName() + curNum);
curNum++;
nextCon.signal();
}
curCon.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
Condition a = lock.newCondition();
Condition b = lock.newCondition();
Condition c = lock.newCondition();
new InnerThread(String.format("Thread %d:", 1), 0, a, b).start();
new InnerThread(String.format("Thread %d:", 2), 1, b, c).start();
new InnerThread(String.format("Thread %d:", 3), 2, c, a).start();
}
3.4 信号量
Semaphore又称信号量,是操作系统中的一个概念,在Java并发编程中,信号量控制的是线程并发的数量。
Semaphore实现原理简单理解:
Semaphore是用来保护一个或者多个共享资源的访问,Semaphore信号量内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。
就好比一个厕所管理员,站在门口,只有厕所有空位,就开门允许与空侧数量等量的人进入厕所。多个人进入厕所后,相当于N个人来分配使用N个空位。为避免多个人来同时竞争同一个侧卫,在内部仍然使用锁来控制资源的同步访问。
如果计数器值为0,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
————————————————————————————————————————————————
Semaphore内部主要通过AQS(AbstractQueuedSynchronizer)实现线程的管理。Semaphore有两个构造函数,第一个参数permits表示许可数,它最后传递给了AQS的state值。线程在运行时首先获取许可,如果成功,许可数就减1,线程运行,当线程运行结束就释放许可,许可数就加1。如果许可数为0,则获取失败,线程位于AQS的等待队列中,它会被其它释放许可的线程唤醒。在创建Semaphore对象的时候还可以指定它的公平性。一般常用非公平的信号量,非公平信号量是指在获取许可时先尝试获取许可,而不必关心是否已有需要获取许可的线程位于等待队列中,如果获取失败,才会入列。而公平的信号量在获取许可时首先要查看等待队列中是否已有线程,如果有则入列。
//非公平的构造函数
public Semaphore(int permits);//permits=10,表示允许10个线程获取许可证,最大并发数是10;
通过fair参数决定公平性
public Semaphore(int permits,boolean fair)
Semaphore semaphore = new Semaphore(10,true);
semaphore.acquire(); //线程获取许可证
//do something here
semaphore.release(); //线程归还许可证
public static void test4() {
class InnerThread extends Thread {
Semaphore curSemaphore, nextSemaphore;
int modNum;
InnerThread(String threadName, int modNum, Semaphore curSemaphore, Semaphore nextSemaphore) {
setName(threadName);
this.curSemaphore = curSemaphore;
this.nextSemaphore = nextSemaphore;
this.modNum = modNum;
}
@Override
public void run() {
while (true) {
try {
curSemaphore.acquire();
if (curNum > maxNum) {
System.out.println(getName() + " end");
nextSemaphore.release();
break;
}
if (curNum % threadCount == modNum) {
System.out.println(getName() + curNum);
curNum++;
nextSemaphore.release();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Semaphore a = new Semaphore(1);
Semaphore b = new Semaphore(0);
Semaphore c = new Semaphore(0);
new InnerThread(String.format("Thread %d:", 1), 0, a, b).start();
new InnerThread(String.format("Thread %d:", 2), 1, b, c).start();
new InnerThread(String.format("Thread %d:", 3), 2, c, a).start();
}
3.5 locksupport
static List<Thread> threads = new ArrayList<>(3);
static int curThreadCnt = 0;
public static void test5() {
class InnerThread extends Thread {
InnerThread(String threadName) {
setName(threadName);
}
@Override
public void run() {
while (true) {
//线程堵塞
LockSupport.park();
if (curNum > maxNum) {
System.out.println(getName() + " end");
break;
}
System.out.println(getName() + curNum++);
//唤醒下一个线程
LockSupport.unpark(threads.get(++curThreadCnt % threadCount));
}
threads.forEach(LockSupport::unpark);
}
}
for (int i = 1; i <= threadCount; i++) {
Thread t = new InnerThread(String.format("Thread %d:", i));
threads.add(t);
t.start();
}
//从第一个线程开始唤醒
LockSupport.unpark(threads.get(0));
}
3.6 volatile+synchronized
首先介绍一下volatile关键字很重要的两个特性:
1、保证变量在线程间可见,对volatile变量所有的写操作都能立即反应到其他线程中,换句话说,volatile变量在各个线程中是一致的(得益于java内存模型—“先行发生原则”);
2、禁止指令的重排序优化;
但是volatile也有它的问题,基于volatile变量的运算在并发下不是安全的,原因是自增操作并不是原子性的。
打个比方:
当前主内存的 count = 5,线程A 通过read、load等原子操作把 count = 5加载到本地内存中
在执行引擎执行count++操作时,会有多步操作
1:先初始化count = 5
2:对count执行计算:count = 5+1
3:把count=6 通过assign写入本地内存中
刚好在1,2,3步中间,cpu被线程B抢去了,此时线程B的count也是5,执行count+1后,刷新主内存并通知了线程A.
那么线程A原本的执行引擎中的count已经作废,会被丢弃,重新从主内存中获取最新的值,这样就平白无故丢了一次 +1 的操作,所以最后结果和预计不同
解决方案: 使用synchronized 既可以保证原子性 ,也可以保证可见性。
volatile static int ai = 0;
public static void test6() {
Object writeLock = new Object();
class InnerThread extends Thread {
int modNum;
InnerThread(String threadName, int modNum) {
setName(threadName);
this.modNum = modNum;
}
@Override
public void run() {
while (ai <= maxNum) {
if (ai % threadCount == modNum) {
System.out.println(getName() + ai);
//不是原子操作可能会出问题,加锁保证
synchronized (writeLock) {
ai++;
}
}
}
System.out.println(getName() + " end");
}
}
for (int i = 1; i <= threadCount; i++) {
new InnerThread(String.format("Thread %d:", i), i - 1).start();
}
}
3.7 cas
volatile操作是不具备原子性,所以之前的例子中必须要加上synchronized同步,另外有一种方法是使用juc中的原子操作类比如AtomicInteger。
这些原子操作类都使用了cas无锁算法,cas即compare and swap,是cpu指令。CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS比较与交换的伪代码可以表示为:
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
(上图的解释:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。)
就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的 commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。
AtomicInteger.incrementAndGet的实现用了乐观锁技术,调用了sun.misc.Unsafe类库里面的 CAS算法,用CPU指令来实现无锁自增。所以,AtomicInteger.incrementAndGet的自增比用synchronized的锁效率倍增。
究其原因,其实就是乐观锁和悲观锁的区分。
独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。就是cas算法。
public static void test7() {
AtomicInteger ai = new AtomicInteger(0);
class InnerThread extends Thread {
int modNum;
InnerThread(String threadName, int modNum) {
setName(threadName);
this.modNum = modNum;
}
@Override
public void run() {
int curVal;
while ((curVal=ai.get()) <= maxNum) {
if (curVal % threadCount == modNum) {
System.out.println(getName() + curVal);
ai.getAndIncrement();
}
}
System.out.println(getName() + " end");
}
}
for (int i = 1; i <= threadCount; i++) {
new InnerThread(String.format("Thread %d:", i), i - 1).start();
}
}