本文主要是介绍Sychronized Volatile Atomic ReentrantLock工具类 AQS底层实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一、Synchronized
Synchronized保障的有序性是指多个线程依次执行同步代码块或者同步方法,对于代码块中的多条指令无法保障有序性;
Sychronized的对象在new时要加上final(final Object o = new Object; sychronized(o){}),防止该对象被有些线程篡改,因为锁是加在对象头上的,如果o被指向了新的一个对象,那么其他线程就可以重新在该新的对象头上加锁,导致多线程同时访问某个程序块或者方法。
Sychronized重量级的体现:锁是由操作系统管理的,所以一个用户线程想要获得锁就得向操作系统申请,这里就涉及到了内核态与用户态之间的资源交互,比较耗费CPU资源;锁升级就是在成为重量级锁之前可以直接在用户态获得锁(不加锁,只是改变对象头的标志位),不需要通过kernel申请锁。
- 锁升级:除了重量级锁其他锁都没有真正加上锁;
- 重量级Synchronized锁其实和ReentrantLock差不多,底层有有一个waitSet存放想要获取锁的线程,通过OS调度唤醒即将获取锁的线程;
二、Volatile
只能用来修饰简单对象,不能用来修饰引用对象(volatile List<>() list),因为他无法监控引用对象所指向的对象的改变情况,也就是不能保证对象的引用对象的可见性;
- 可见性:(Volatile底层通过总线机制 + 缓存一致性协议实现可见性)
如果不加volatile,CPU之间缓存行改变某一个变量不能控制其实时写入共享内存,也不能控制其他CPU实时去内存中读取该变量的最新值,也就是CPU之间不可见;
- 有序性:(底层是由CPU的原语loadfence和storefence实现的)
DCL(单例懒汉式的double check lock)问题,必须加两次if(Instance != null)判断(防止多线程同时进入第一个if中,第二个线程获得锁后重新new一个Instance对象),而且类的字段必须是加Volatile关键字(防止对象乱序半初始化问题,导致其他线程得到未初始化完的对象);
public class SingletonDCL {/** DCL懒汉模式对象的属性必须使用volatile修饰,防止多线程使用半初始化的对象中的java默认属性值 */private static volatile SingletonDCL Instance;private SingletonDCL(){}/*** Volatile避免多线程获取半初始化对象的java默认属性值* DCL,解决多线程造成的多例现象* @return*/public static SingletonDCL getInstance(){/** 第一重检查,当已经有对象建立了之后,直接复用当前对象 */if(Instance == null){synchronized (SingletonDCL.class){/** 第二重检查,避免两个线程同时进入上述if语句块,当一个线程释放锁后,第二个线程获得锁,如果不加该判断会new一个新的对象 */if(Instance == null){try{Thread.sleep(1);}catch (Exception e){e.printStackTrace();}Instance = new SingletonDCL();}}}/** 当上一个线程创建了一个半初始化对象时,第二个线程判断已有了Instance直接到这里获取这个半初始化对象,后续该线程使用的就是这个半初始化对象的默认属性值了 */return Instance;}
}
三、Atomic原子类
底层都是基于CAS实现的;(CAS是CPU原语级别的指令支持,CAS比较数据的环节虽然是多个步骤,但是被CPU保证是原子性的)
Atomic.getAndIncrement(); // 安全的自增一
四、ReentrantLock以及各工具类
-
note1:Lock.wait()释放锁,但是Lock.notify() 不释放锁,所以如果wait的线程需要的锁是执行notify方法的线程持有的锁,那么wiat的线程无法获取锁,无法执行,所以执行notify的线程必须再执行一次wait让出锁,原先被阻塞的线程才能继续执行;— 是否能唤醒其他线程的问题
-
note2:唤醒者有时需要在唤醒其他线程后,暂停自身线程,等待其他线程唤醒它;线程间可以通信,但是如果要求一个线程唤醒另一个线程时立马得到另一个线程的输出(有输出顺序的要求),此时第一个线程唤醒对方线程后要让自己先停一下,等被唤醒的线程执行完后再来唤醒其本身。— 是否能保证唤醒线程与被唤醒线程的有序输出问题
1) t1 唤醒t2:unpark(t2) — t1自身停止:park() / t2执行 —t2唤醒t1:unpark(t1);
2)CountDownLatch同理,一个线程await被解开后是和另一个线程同时执行的,不能保证被解开await的线程立马可以输出,很大可能是另一个线程继续执行,提前输出,所以唤醒者自身要await等待被唤醒者执行完毕后再把它唤醒;
- note3:wait方法必须在while体里面,不能用if代替while,因为if体中线程被唤醒后不会再执行判断语句,此时可能在线程唤醒期间判断条件由满足又变成了不满足,然而线程却执行了后续的逻辑操作,而while体中,线程被唤醒后会再次判断是否满足执行的条件,保证可以执行后续的逻辑操作。
多种工具类:
1) CountDownLatch
一个线程等多个线程执行完countDown(),该线程再继续执行;
public class CountDownLatchDemo {ReentrantLock lock = new ReentrantLock();CountDownLatch countDownLatch = new CountDownLatch(10);int count = 0;public void add(){lock.lock();try{count ++;countDownLatch.countDown();}finally {lock.unlock();}}public static void main(String[] args) {Thread[] threads = new Thread[10];CountDownLatchDemo countDownLatchDemo = new CountDownLatchDemo();for(int i=0; i<10; i++){threads[i] = new Thread(countDownLatchDemo::add, i +"");}for(Thread t:threads){t.start();}try {/** 主线程阻塞等待countDown计数完毕 */countDownLatchDemo.countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(countDownLatchDemo.count);}
}
2) CyclicBarrier:
多个线程都执行到cyclicBarrier.await()后相互等待,等待的线程数达到阈值后触发CyclicBarrier对象中定义的Runnable方法,然后各自线程再继续执行他们后续的操作;
public class CyclicBarrierDemo {public static void main(String[] args) {CyclicBarrier cyclicBarrier = new CyclicBarrier(30, ()->{System.out.println("满30线程,发车");});Thread[] threads = new Thread[30];for(int i = 0; i<30; i++){threads[i] = new Thread(()->{try {System.out.println(Thread.currentThread().getName() + "等待");/** 每个线程都阻塞在此处,等阻塞了30个线程后,这些线程再一起执行后面的操作 */cyclicBarrier.await();System.out.println(Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}});}for(Thread t:threads){t.start();}}
}
3) ReadWriteLock:
加读锁的方法多线程可以并发执行(读不会修改共享变量值),执行加写锁的方法时,必须单线程串行执行,并且要等当前的读锁全部释放后才能获取写锁,执行写锁方法(如果所有方法都加ReentrantLock那么每个线程都串行执行,效率降低)
public class ReadWriteLockDemo {private Lock lock = new ReentrantLock();private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();private Lock readLock = readWriteLock.readLock();private Lock writeLock = readWriteLock.writeLock();/**定义读方法时设置传入一个读锁 */public void write(Lock lock){lock.lock();try{System.out.println("write val");TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();}}/**定义写方法时设置传入一个写锁 */public void read(Lock lock){lock.lock();try {System.out.println("read val");TimeUnit.SECONDS.sleep(1);}catch (Exception e){e.printStackTrace();}finally {lock.unlock();}}public static void main(String[] args) {ReadWriteLockDemo rw = new ReadWriteLockDemo();Thread[] threads = new Thread[20];Thread[] writeThreads = new Thread[3];for(int i=0; i<20; i++){new Thread(()->{// 读线程加的是读锁rw.read(rw.readLock);},"AA").start();}for(int i=0; i<3; i++){new Thread(()->{// 写线程传入的是写锁rw.write(rw.writeLock);},"BB").start();}}
}
4) Semaphore:
线程new时try中加Semaphore对象和释放Semaphore对象,Semaphore new对象时定义允许几个线程同时并发执行他们各自的操作;
作用:限流
public class SemaphoreDemo {public static void main(String[] args) {/** 允许几个线程并发执行,许可证数量 */Semaphore s = new Semaphore(10);/** 按照线程加入顺序公平获取许可证AQS实现 */Semaphore fairS = new Semaphore(10,true);for(int i=0; i<50; i++){new Thread(()->{try {/** 当前线程想要继续执行,必须从semaphore里获取一个许可 */s.acquire();Date date = new Date();SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");String time = format.format(date);System.out.println(Thread.currentThread().getName() + "执行任务" + time);TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}finally {/** 当前s个线程数都执行完了,刷新一次许可证数量 */s.release();}},"AA").start();}}
}
5) Exchanger.exchange():
两个线程运行到该方法阻塞,等两个线程交换同一个变量的值后继续执行;
6) LockSupport.park() / unpark(t):
用于使指定线程停止,并在任意时候唤醒指定的线程(之前需要通过锁实现Lock.Condition.await,这里不需要锁,只需要调用该类的两个静态方法)
public class LockSupportDemo {public static void main(String[] args) {Thread t =new Thread(()->{for(int i=0; i<20; i++){if(i==5) {/** 停车 */LockSupport.park();}System.out.println(i);try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("经过10s后执行unpark");/** 开车,指定让某个线程继续执行 */LockSupport.unpark(t);}}
7) lock.newCondition:
本质是将等待获取锁的线程所在的队列变成多个,一个condition就是一个队列,可以将线程分类加入到对应的队列中,根据程序逻辑精确唤醒对应condition队列里的线程。
已用Condition实现多线程生产者消费者模式:
public class ProviderConsumer {/*** 实现一个容器,模拟多线程并发加入元素与取出元素*/Lock lock = new ReentrantLock();/** 专门用来唤醒生产者 */Condition provider = lock.newCondition();/** 专门唤醒消费者 */Condition consumer = lock.newCondition();// 存放产品的容器final LinkedList<Object> list = new LinkedList<>();// 最多放十个对象final int max = 10;// 当前容器中的元素个数int count = 0;public void put() {lock.lock();try{Object o = new Object();while(count == max){try {System.out.println("容器满了");/** 所有生产者停止 */provider.await();} catch (InterruptedException e) {e.printStackTrace();}}list.add(o);++ count;System.out.println("生产中" + count);/** 通知消费者 */consumer.signalAll();}finally {lock.unlock();}}public synchronized Object get(){lock.lock();try{while(list.size() == 0){try {System.out.println("容器空了");/** 所有消费者停止 */consumer.await();} catch (InterruptedException e) {e.printStackTrace();}}Object o = list.removeFirst();count --;System.out.println("消费中" + count);/** 通知生产者 */provider.signalAll();return o;}finally {lock.unlock();}}public static void main(String[] args) {ProviderConsumer pc = new ProviderConsumer();// 消费者for(int i=0; i<10; i++){new Thread(()->{for(int j=0; j<10; j++) {System.out.println(pc.get());}},"A" + i).start();}try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}// 生产者for(int i=0; i<10; i++){new Thread(()->{for(int j=0; j<10; j++) {pc.put();}}, "B" + i).start();}}
}
五、AQS底层
ps:阅读源码的方式:
1)在debug模式下阅读,程序没有运行时调用的方法与程序运行时不一定一致,因为多态的存在,运行时可能调用的是子类重写的方法(静态时可能有些接口方法还没有被实现,看不到执行逻辑);遇到一个不知名的方法就在这里打断点运行进去看看;
2)调试时画UML图(方法的调用过程)以及涉及的每个类的子类父类关系图;
除了LockSupport以外,其他锁都是通过AQS底层实现的;
AQS = volatile + CAS
- volatile:对象被锁重入的次数是由变量state表示的,其由volatile修饰;
- CAS:尝试获取锁的线程是通过CAS的方式加入队列的,即自旋比较当前对象锁的state值以及是否是被线程自己占有;
具体AQS源码底层结构看另一篇文章;
补充:在JDK9后,AQS中加入了一个VarHandle,可以用一个handle句柄指向一个对象,得到该对象的新的一个引用,然后VarHandle提供了对该对象的原子操作。比如可以作为一个int值,long值的引用,直接指向这块内存,实现线程安全的变量更改值的操作。(实现原子性的底层原理:VarHandle可以对内存操作,直接操纵二进制码,比基于反射的实现效率高,是由C实现的native方法,被CPU原语所支持)
------------------------------------------------------------------------------------------------------------------
六、底层实现总结
- ReentrantLock:AOS — AQS — Sync — FairSync/NonFairSync — CAS — unsafe(这个类为单例,可以直接操作内存分配释放,因为底层是C实现的) — lock_if_mp
- Atomic类:getAndIncrement — CAS — unsafe — lock_if_mp
- Volatile:1)有序性:load/store fence ;2)可见性:MESI缓存一致性协议 + 总线机制
- Sychronized:1)轻量级锁(CAS); 2)重量级锁:monitorenter / monitorexit / monitorexit
这篇关于Sychronized Volatile Atomic ReentrantLock工具类 AQS底层实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!