一、volatile关键字
运行下面的代码,观察加了volatile和没加的区别。可以发现没加volatile,程序不会正常停止,而加了volatile程序会正常停止。这就说明thread线程没有读取到flag修改后的值,跨线程之间共享数据不可见。使用volatile关键字保证了跨线程之间数据的可见性。这是因为为了提高程序的执行性能,编译器和处理都会对指令做重排序,而加了volatile的变量会被禁止重排序。
public class VolatileDemo {
public volatile static boolean flag=false; //观察加了volatile和没加的区别
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
int i=0;
while(!flag){
i++; }
System.out.println("i="+i);
System.out.println("Thread end");
});
thread.start();
System.out.println("Thread start");
Thread.sleep(1000);
flag=true;
}
}
说明:要注意volatile和synchronized的区别。volatile只能保证变量修改订的可见性,不能保证原子性。而synchronized则可以保证变量的修改可见性和原子性。例如多个线程执行count++操作。因为count++实际上执行了三个操作:1.读取变量count的值 2.count的值+1 3.将值赋予变量count。这三个操作过程中,count的值被篡改,都不会出现预期的效果,所以必须要保证这三个操作的原子性,也就是要必须连续完成。而volatile只能保证多个线程都在主内存中操作count,而不能保证原子性。
二、AQS原理及Lock锁
java.util.concurrent是在并发编程中比较常用的工具类,里面包含很多用来在并发场景中使用的组件。比如线程池、阻塞队列、计时器、同步器并发集合等等。简称JUC。Lock是JUC中最核心的组件,本质上是一个接口,定义了锁的获得和释放的抽象方法。实现Lock接口的类有很多常用的有:
- ReentrantLock。表示重入锁,是唯一一个直接实现Lock接口的类。重入锁指的是线程获得锁之后,再次获得该锁不需要再阻塞,而是直接关联计数器增加一次重入次数即可。
- ReentrantReadWriteLock。表示重入读写锁。实现了ReadWriteLock接口。这个类中维护了两个锁,一个ReadLock,一个WriteLock,分别都实现了Lock接口。适合读多写少的的场景下解决线程安全问题。基本原则就是读读不互斥,读写互斥,写写互斥。
- StampedLock。可以认为是读写锁的改进版本。读写锁的读和写是互斥的,如果存在大量读线程,可能会引起写线程的饥饿,StampedLock是一种乐观读策略,使得乐观锁不会阻塞写线程。
ReentrantLock重入锁
重入锁实例:
public class ReentrantLockDemo {
private static int count=0;
static Lock lock=new ReentrantLock();
public static void inc(){
lock.lock();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
lock.unlock();
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<1000;i++){
new Thread(()->{ReentrantLockDemo.inc();}).start();;
}
Thread.sleep(3000);
System.out.println("result:"+count);
}
}
ReentrantReadWriteLock锁
ReentrantReadWriteLock锁是读写锁,维护了两个锁,一个ReadLock,一个WriteLock。允许多个线程在同一时刻访问,基本原则就是读读不互斥,读写互斥,写写互斥。
public class ReentrantReadWriteLockDemo{
public static Map<String,Object> map=new HashMap<>();
public static ReentrantReadWriteLock rwl=new ReentrantReadWriteLock();
public static Lock readLock=rwl.readLock();
public static Lock writeLock=rwl.writeLock();
public static final Object get(String key) {
System.out.println("读数据");
readLock.lock(); //读锁
try {
return map.get(key);
}finally {
readLock.unlock();
}
}
public static final Object put(String key,Object value){
writeLock.lock();
System.out.println("写数据");
try{
return map.put(key,value);
}finally {
writeLock.unlock();
}
}
}
AQS原理
在Lock中,用到了一个同步队列AQS,全称AbstractQueuedSynchronizer。是Lock用来实现线程同步的核心组件。AQS功能分为两种,独占锁和共享锁。
独占锁:每次只能有一个线程获得锁。如 ReentrantLock。
共享锁:允许多个线程同时获取锁,如ReentrantReadWriteLock
AQS内部实际上是维护一个FIFO双向链表。这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
当出现锁竞争时:
- 新的线程将自己封装成新的Node节点,设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己
- 通过CAS操作将tail 重新指向新的尾部节点
释放锁时:
head节点表示获取锁成功的节点,当头节点释放锁后,会唤醒后续节点,如果后续节点获得锁成功,会把自己设置为头节点。
- 修改 head 节点指向下一个获得锁的节点,删除释放锁的节点
- 新获得锁的节点,将 prev 的指针指向 null
AQS的原理基本就是这样,当然,程序中更复杂。还要考虑公平锁和非公平锁。
非公平锁:非公平锁在获取锁的时候,各个节点会先通过 CAS 进行抢占,谁抢到就是谁的。
公平锁:锁的获取顺序就应该符合请求的绝对时间顺序。
其他并发工具
Condition
Condition的用法和wait/notify方法类似。都是用来进行线程间的通信。代码示例:
import java.lang.management.ThreadMXBean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Thread threadA = new Thread(()->{
System.out.println("begin condition await");
try {
lock.lock();
condition.await(); //相当于wait
System.out.println("end condition await");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread threadB = new Thread(()->{
System.out.println("begin condition signal");
try {
lock.lock();
condition.signal(); //相当于notify,signalAll相当于notifyAll
System.out.println("end condition signal");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
threadA.start();
threadB.start();
}
}
结果是
begin condition await
begin condition signal
end condition signal
end condition await
Condition的原理
condition的await/signal方法的用法和wait/notify类似。原理也类似。执行await时会将当前线程加入到一个等待队列中,随即释放锁,然后挂起,等待同一个condition对象执行signal方法唤醒。执行signal方法时,会将当前线程加入到同步队列中(即上文提到的AQS队列),然后去争抢锁。
CountDownLatch
Countdownlatch 是一个同步工具类。它允许一个或多个线程一直等待,直到其他线程的操作执行完毕再执行。Countdownlatch提供了两个方法。await()和countDown()。Countdownlatch初始化时需要传一个整数参数,这个参数相当于一个倒计时,countDown()每次被调用,都会减一,直到这个数变为0。这时,所有因为执行了Countdownlatch的await()方法被阻塞的线程都会被唤醒,继续执行。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(1);
System.out.println("begin create Thread");
for(int i=0;i<10;i++){
new Thread(()->{
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadName:"+Thread.currentThread().getName());
}).start();
}
System.out.println("end create Thread");
System.out.println("begin run Thread");
countDownLatch.countDown();
}
}
Semaphore
Semaphore用来控制同时访问的线程个数。创建实例时需要一个permits参数,代码许可数,提供了acquire和release方法。acquire方法获取一个许可,当达到许可的最大值时,阻塞当前线程。release方法释放一个许可,并唤醒被acquire方法阻塞的线程。
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i=0;i<5;i++){
new Thread(()->{
try {
semaphore.acquire();
System.out.println("ThreadName:"+Thread.currentThread().getName()+"获取一个许可");
Thread.currentThread().sleep(2000);
System.out.println("ThreadName:"+Thread.currentThread().getName()+"释放一个许可");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
CyclicBarrier
CyclicBarrier表示循环屏障。功能是设置一个屏障,也叫同步点,让一组线程到达这个屏障处时阻塞,直到最后一个线程到达后,屏障才会打开,所有被拦截的线程才会继续工作。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier =new CyclicBarrier(3,
new Thread(()->{
System.out.println("所有线程都已到达屏障,继续执行!");
})
);
for (int i=0;i<3;i++){
new Thread(()->{
System.out.println("ThreadName:"+Thread.currentThread().getName()+"到达屏障");
try {
cyclicBarrier.await();
System.out.println("ThreadName:"+Thread.currentThread().getName()+"继续执行!");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
注意事项:
- 由于指定了计数值,若没有足够的线程执行CyclicBarrier的awai方法。那所有调用CyclicBarrier的await方法的线程都将被阻塞。可以给await设置超时时间await(long timeout, TimeUnit unit)。在设定时间内,如果没有足够线程到达,则解除阻塞状态,继续执行。
- 通过 reset 重置计数,会使得进入 await 的线程出现BrokenBarrierException
- 如果采用是 CyclicBarrier(int parties, Runnable barrierAction) 构造方法,执行 barrierAction 操作的是最后一个到达的线程。