java多线程与并发原理学习笔记(三)——volatile与JUC之AQS原理

一、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接口的类有很多常用的有:

  1. ReentrantLock。表示重入锁,是唯一一个直接实现Lock接口的类。重入锁指的是线程获得锁之后,再次获得该锁不需要再阻塞,而是直接关联计数器增加一次重入次数即可。
  2. ReentrantReadWriteLock。表示重入读写锁。实现了ReadWriteLock接口。这个类中维护了两个锁,一个ReadLock,一个WriteLock,分别都实现了Lock接口。适合读多写少的的场景下解决线程安全问题。基本原则就是读读不互斥,读写互斥,写写互斥。
  3. 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 队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
在这里插入图片描述

当出现锁竞争时:

  1. 新的线程将自己封装成新的Node节点,设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己
  2. 通过CAS操作将tail 重新指向新的尾部节点

在这里插入图片描述
释放锁时:
head节点表示获取锁成功的节点,当头节点释放锁后,会唤醒后续节点,如果后续节点获得锁成功,会把自己设置为头节点。

  1. 修改 head 节点指向下一个获得锁的节点,删除释放锁的节点
  2. 新获得锁的节点,将 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();
        }

    }
}

注意事项:

  1. 由于指定了计数值,若没有足够的线程执行CyclicBarrier的awai方法。那所有调用CyclicBarrier的await方法的线程都将被阻塞。可以给await设置超时时间await(long timeout, TimeUnit unit)。在设定时间内,如果没有足够线程到达,则解除阻塞状态,继续执行。
  2. 通过 reset 重置计数,会使得进入 await 的线程出现BrokenBarrierException
  3. 如果采用是 CyclicBarrier(int parties, Runnable barrierAction) 构造方法,执行 barrierAction 操作的是最后一个到达的线程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值