关于JUC的笔记
J.U.C 即 java.util.concurrent,是 JSR 166 标准规范的一个实现; JSR 166 以及 J.U.C 包的作者是 Doug Lea 。
CAS
核心
1、先比较后修改
2、没有获取锁,释放锁的操作
3、硬件层面的原子操作
原理
围绕3点:
1、当前内存值V
2、旧的预期值A
3、即将更新的值B
如图:
主内存中有num=10为当前内存值V
线程A将num拷贝到副本中时为旧的预期值A
线程A将num改成9为即将更新的值B,然后A值会和V值对比,相同就修改主内存的数据,若不相同就自旋等待并再次尝试,直到成功
多CPU的CAS处理
1、总线加锁
- 处理器提供的一个LOCK#信号
- 在锁定期间,其他处理器都不能其他内存地址的数据,其开销有点儿大。
2、缓存加锁
- 同一个内存区域的数据仅能被一个处理器修改
缺陷
1、ABA问题:(如图)
若线程A、B同时拷贝主内存的num值到副本中,线程A修改多次后结果仍然为10并同步到主内存中时,此时线程B修改为11,进行A值和V值对比后发现相同,并修改主内存的数据。该现象就是ABA问题(正常来说线程A已修改,根据CAS,线程B应该会修改失败,但因为线程A改后的值与改前一致,导致线程B可以修改成功)
解决办法:
使用AtomicStampedReference(基于乐观锁)
public class Demo4ABA {
private static AtomicInteger ai = new AtomicInteger(100);
private static AtomicStampedReference air = new AtomicStampedReference(100, 1);
//ABA问题演示:
//1. 线程1先对数据进行修改 A-B-A过程
//2. 线程2也对数据进行修改 A-C的过程
public static void main(String[] args) throws InterruptedException {
// AtomicInteger可以看到不会有任何限制随便改
// 线程2修改的时候也不可能知道要A-C 的时候,A是原来的A还是修改之后的A
Thread at1 = new Thread(new Runnable() {
public void run() {
ai.compareAndSet(100, 110);
ai.compareAndSet(110, 100);
}
});
Thread at2 = new Thread(new Runnable() {
public void run() {
try {
//为了让线程1先执行完,等一会
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicInteger:" + ai.compareAndSet(100, 120));
System.out.println("执行结果:" + ai.get());
}
});
at1.start();
at2.start();
//顺序执行,AtomicInteger案例先执行
at1.join();
at2.join();
//AtomicStampedReference可以看到每次修改都需要设置标识Stamp,相当于进行了1A-2B-3A的操作
//线程2进行操作的时候,虽然数值都一样,但是可以根据标识很容易的知道A是以前的1A,还是现在的3A
Thread tsf1 = new Thread(new Runnable() {
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 预期引用:100,更新后的引用:110,预期标识getStamp() 更新后的标识getStamp() + 1
air.compareAndSet(100, 110, air.getStamp(), air.getStamp() + 1);
air.compareAndSet(110, 100, air.getStamp(), air.getStamp() + 1);
}
});
Thread tsf2 = new Thread(new Runnable() {
public void run() {
//tsf2先获取stamp,导致预期时间戳不一致
int stamp = air.getStamp();
try {
TimeUnit.MILLISECONDS.sleep(100); //线程tsf1执行完
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicStampedReference:" + air.compareAndSet(100, 120, stamp, stamp + 1));
int[] stampArr = {stamp + 1};
System.out.public class Demo4ABA {
private static AtomicInteger ai = new AtomicInteger(100);
private static AtomicStampedReference air = new AtomicStampedReference(100, 1);
//ABA问题演示:
//1. 线程1先对数据进行修改 A-B-A过程
//2. 线程2也对数据进行修改 A-C的过程
public static void main(String[] args) throws InterruptedException {
// AtomicInteger可以看到不会有任何限制随便改
// 线程2修改的时候也不可能知道要A-C 的时候,A是原来的A还是修改之后的A
Thread at1 = new Thread(new Runnable() {
public void run() {
ai.compareAndSet(100, 110);
ai.compareAndSet(110, 100);
}
});
Thread at2 = new Thread(new Runnable() {
public void run() {
try {
//为了让线程1先执行完,等一会
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicInteger:" + ai.compareAndSet(100, 120));
System.out.println("执行结果:" + ai.get());
}
});
at1.start();
at2.start();
//顺序执行,AtomicInteger案例先执行
at1.join();
at2.join();
//AtomicStampedReference可以看到每次修改都需要设置标识Stamp,相当于进行了1A-2B-3A的操作
//线程2进行操作的时候,虽然数值都一样,但是可以根据标识很容易的知道A是以前的1A,还是现在的3A
Thread tsf1 = new Thread(new Runnable() {
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 预期引用:100,更新后的引用:110,预期标识getStamp() 更新后的标识getStamp() + 1
air.compareAndSet(100, 110, air.getStamp(), air.getStamp() + 1);
air.compareAndSet(110, 100, air.getStamp(), air.getStamp() + 1);
}
});
Thread tsf2 = new Thread(new Runnable() {
public void run() {
//tsf2先获取stamp,导致预期时间戳不一致
int stamp = air.getStamp();
try {
TimeUnit.MILLISECONDS.sleep(100); //线程tsf1执行完
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicStampedReference:" + air.compareAndSet(100, 120, stamp, stamp + 1));
int[] stampArr = {stamp + 1};
System.out.println("执行结果:" + air.get(stampArr));
}
});
tsf1.start();
tsf2.start();
}
}println("执行结果:" + air.get(stampArr));
}
});
tsf1.start();
tsf2.start();
}
}
运行结果充分展示了AtomicInteger的ABA问题和AtomicStampedReference解决ABA问题
2、循环时间太长
3、只能保证一个共享变量原子操作
AQS
AQS(AbstractQueuedSynchronizer),即队列同步器
AQS维护了一个volatile int类型的变量state表示当前同步状态。当state>0时表示已经获取了锁,当state = 0时表示释放了锁
入列
出列
Condition
作用:可以精确的唤醒某个线程
Condition提供了一系列的方法来对阻塞和唤醒线程:
-
await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
-
**await(long time, TimeUnit unit) **:造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
-
**awaitNanos(long nanosTimeout) **:造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout – 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
-
**awaitUninterruptibly() **:造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
-
**awaitUntil(Date deadline) **:造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
-
signal():唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
-
signalAll():唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。
Condition是一种广义上的条件队列(等待队列)。他为线程提供了一种更为灵活的等待/通知模式,线程在调用await方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。
public class DemoCondition {
private Lock reentrantLock = new ReentrantLock();
private Condition condition1 = reentrantLock.newCondition();
private Condition condition2 = reentrantLock.newCondition();
public void m1() {
reentrantLock.lock();
try {
System.out.println("线程 " + Thread.currentThread().getName() + " 已经进入执行等待。。。");
condition1.await();
System.out.println("线程 " + Thread.currentThread().getName() + " 已被唤醒,继续执行。。。");
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public void m2() {
reentrantLock.lock();
try {
System.out.println("线程 " + Thread.currentThread().getName() + " 已经进入执行等待。。。");
condition1.await();
System.out.println("线程 " + Thread.currentThread().getName() + " 已被唤醒,继续执行。。。");
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public void m3() {
reentrantLock.lock();
try {
System.out.println("线程 " + Thread.currentThread().getName() + " 已经进入执行等待。。。");
condition2.await();
System.out.println("线程 " + Thread.currentThread().getName() + " 已被唤醒,继续执行。。。");
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public void m4() {
reentrantLock.lock();
try {
System.out.println("线程 " + Thread.currentThread().getName() + " 已经进入发出condition1唤醒信号。。。");
condition1.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public void m5() {
reentrantLock.lock();
try {
System.out.println("线程 " + Thread.currentThread().getName() + " 已经进入发出condition2唤醒信号。。。");
condition2.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public static void main(String[] args) throws Exception {
final Demo11Condition useCondition = new Demo11Condition();
Thread t1 = new Thread(new Runnable() {
public void run() {
useCondition.m1();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
public void run() {
useCondition.m2();
}
}, "t2");
Thread t3 = new Thread(new Runnable() {
public void run() {
useCondition.m3();
}
}, "t3");
Thread t4 = new Thread(new Runnable() {
public void run() {
useCondition.m4();
}
}, "t4");
Thread t5 = new Thread(new Runnable() {
public void run() {
useCondition.m5();
}
}, "t5");
t1.start();
t2.start();
t3.start();
Thread.sleep(2000);
t4.start();
Thread.sleep(2000);
t5.start();
}
}
工具包
CyclicBarrier 同步屏障
作用
等待所有线程准备好再执行
案例
田径比赛,所有运动员准备好了之后,大家一起跑,代码如下
public class Demo1CyclicBarrier {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(new Athlete(cyclicBarrier, "运动员" + i));
threadList.add(t);
}
for (Thread t : threadList) {
t.start();
}
}
static class Athlete implements Runnable {
private CyclicBarrier cyclicBarrier;
private String name;
public Athlete(CyclicBarrier cyclicBarrier, String name) {
this.cyclicBarrier = cyclicBarrier;
this.name = name;
}
@Override
public void run() {
System.out.println(name + "就位");
try {
cyclicBarrier.await();
System.out.println(name + "跑到终点。");
} catch (Exception e) {
}
}
}
}
CountDownLatch 计数器
作用
一个线程执行前必须由前面n个线程执行完了才可以执行(比如接力)
案例
public class Demo2CountDownLatch {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
CountDownLatch countDownLatch = new CountDownLatch(1);
//起点运动员
Thread t1 = new Thread(new Athlete(cyclicBarrier, countDownLatch, "起点运动员" + i));
//接力运动员
Thread t2 = new Thread(new Athlete(countDownLatch, "接力运动员" + i));
threadList.add(t1);
threadList.add(t2);
}
for (Thread t : threadList) {
t.start();
}
}
static class Athlete implements Runnable {
private CyclicBarrier cyclicBarrier;
private String name;
CountDownLatch countDownLatch;
//起点运动员
public Athlete(CyclicBarrier cyclicBarrier, CountDownLatch countDownLatch, String name) {
this.cyclicBarrier = cyclicBarrier;
this.countDownLatch = countDownLatch;
this.name = name;
}
//接力运动员
public Athlete(CountDownLatch countDownLatch, String name) {
this.countDownLatch = countDownLatch;
this.name = name;
}
@Override
public void run() {
//判断是否是起点运动员
if (cyclicBarrier != null) {
System.out.println(name + "就位");
try {
cyclicBarrier.await();
System.out.println(name + "到达交接点。");
//已经到达交接点
countDownLatch.countDown();
} catch (Exception e) {
}
}
//判断是否是接力运动员
if (cyclicBarrier == null) {
System.out.println(name + "就位");
try {
countDownLatch.await();
System.out.println(name + "到达终点。");
} catch (Exception e) {
}
}
}
}
}
Semaphore 信号量
作用
开辟一个空间,里面可以存放多个线程并允许同时执行(比如停车场)
线程池是生成多个线程,然后取出线程进行执行
ConcurrentHashMap
hashMap是线程不安全的,多线程下有可能会死循环
在1.8版本以前,ConcurrentHashMap采用分段锁的概念,使锁更加细化,但是1.8已经改变了这种思路,而是利用CAS+Synchronized来保证并发更新的安全,当然底层采用数组+链表+红黑树的存储结构。
Synchronized只给某个hash槽中的某个元素加锁