目录
一、说明:
二、java内存模型JMM
三、volatile关键字
3.1、可见性验证demo
3.2、不保证原子性demo
3.3、有序性demo
四、锁
4.1、锁的常见种类介绍
4.2、synchronized关键字
4.3、juc.locks
4.3.1、ReentrantLock
4.3.2、ReentrantReadWriteLock
4.4、synchronized/ReentrantLock的比较
4.5、死锁
五、线程
5.1、线程创建方式
5.2、实现Callable接口示例
5.3、线程池的使用说明
5.4、线程池的优势
5.5、手动创建线程池
六、JUC
6.1、atomic包下的原子类
6.1.1、AtomicInteger、AtomicReference、AtomicReferenceArray
6.1.2、CAS思想及ABA问题
6.1.3、ABA问题的解决 AtomicStampedReference
6.2、线程安全的集合
6.3、CountDownLatch、CyclicBarrier、Semaphore
6.4、阻塞队列(BlockingQueue)
七、生产/消费Demo
7.1、铁三角一 synchronized/wait/notify
7.2、铁三角二 lock/await/signal
7.3、阻塞队列
最近学习java并发编程,按照自己理解对并发编程进行了一下整理,仅供参考,如有问题,请留言指正。另外附上自己画的脑图,仅供参考(https://naotu.baidu.com/file/a5aacb2711f5a8177f64aa5954d44051?token=9e38cbba3c6c033b)。
这些年,我们的CPU、内存、硬盘(IO设备),都在不断的更新,多核cpu已是飞入寻常百姓家,但CPU的速度加快并不代表着整个程序的加快,CPU的运算速度远远大于内存的读取速度,而内存的读取速度又远远大于硬盘的IO速度,我们的程序运行不仅仅要计算,更需要更新内存中的数据,还需要进行IO操作进行数据读取和持久化,所以程序运行速度,取决于最慢的IO操作。
为了合理利用CPU的高性能,平衡三者之间的速度差异,各个方面都做出了贡献,CPU增加了缓存,依附于CPU上的,它比内存操作更快(可以采用CPUZ工具查看自己硬件的缓存,在java中工作内存基本上是使用该内存空间)以平衡CPU与内存直接的速度差异。操作系统增加了进程、线程,以分时复用CPU。我们的编译器会进行指令重排序,使得缓存能够更加合理的得到利用。这也恰恰导致了并发程序的问题,总的来说:1、可见性(因为每个线程操作时会将变量拷贝到自己的工作内存中进行操作,而工作内存是私有的,所以对别的线程不可见)2、原子性(cpu在执行某个操作时,会被可能其他线程打断)3、有序性(编译器重排序问题)。为了更好的学习多线程编程,我们需要先了解java的内存模型JMM。
JMM(java memory model) 本身是一种抽象的概念并不真实存在,它描述的是一组规则或者规范,通过这组规范定义了程序中各个变量的访问方式。
JMM关于同步的规定:1、可见性 2、原子性 3、有序性
如下图所示:当线程A和线程B分别操作共享变量x时,A、B两个线程会分别将x的值从主内存(可以理解为我们的内存条)拷贝至自己的工作内存(可理解为CPU高级缓存)中,修改完成后再写回,因为A、B的工作内存是私有的,不可见,两个线程通讯必须依靠主内存完成,所以在写回主内存时,就可能出现覆盖的现象,即线程问题。
volatile关键字是个轻量级同步机制,符合jmm规定的:1、可见性 3、有序性 ,不保证原子性。所以为轻量级。在juc包下被大量使用。使用该关键字不用整体锁定,能够提高并发性,但要注意其不保证原子性。
import java.util.concurrent.TimeUnit;
class VolatileVisibleData {
public volatile int a = 0;
public int b = 0;
public void update() {
a = 10;
b = 10;
}
}
/**
* @author zhengyue
* @date 2019-09-29 21:55
*/
public class VolatileVisibleTest {
public static void main(String[] args) {
VolatileVisibleData data = new VolatileVisibleData();
//t1线程用于改变a、b的值
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "线程启动");
try {
TimeUnit.SECONDS.sleep(3);//休眠3秒确保其他线程已经读取到a、b的值
} catch (InterruptedException e) {
e.printStackTrace();
}
data.update();
System.out.println(Thread.currentThread().getName() + "\t" + "线程已结束"
+ ";a:" + data.a + ";b:" + data.b);
}, "t1").start();
//t2线程用于验证被volatile修饰的变量a 具有可见性,所以当a被修改为10的时候t2线程不在循环直接结束
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "线程启动");
while (data.a == 0) {
}
System.out.println(Thread.currentThread().getName() + "\t" + "线程已结束");
}, "t2").start();
//t3线程用于验证没有被volatile修饰的变量b不具有可见性,所以当b被修改为10的时候t3线程仍在循环
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + "线程启动");
while (data.b == 0) {
}
System.out.println(Thread.currentThread().getName() + "\t" + "线程已结束");
}, "t3").start();
}
}
运行结果:
t1 线程启动
t2 线程启动
t3 线程启动
t1 线程已结束;a:10;b:10
t2 线程已结束
import java.util.concurrent.atomic.AtomicInteger;
class VolatileNoAtomicData {
//a使用volatile修饰 不具有原子性
public volatile int a = 0;
//b是juc下的原子整形类,具有原子性
public AtomicInteger b = new AtomicInteger(0);
/**
* 进行a++的操作,a++是个非原子性操作,根据jmm讲解的可知
* 分为以下3步操作
* 1、从主内存中读取数据到工作内存中
* 2、在工作内存中进行自增长
* 3、将值写回主内存
*/
public void aAdd() {
a++;
}
/**
* b++的原子操作
*/
public void bAdd() {
b.getAndIncrement();
}
}
/**
* @author zhengyue
* @date 2019-09-29 22:13
*/
public class VolatileNoAtomicTest {
public static void main(String[] args) {
VolatileNoAtomicData data = new VolatileNoAtomicData();
for (int i = 0; i < 30; i++) {//循环创建30个线程
new Thread(() -> {
for (int j = 0; j < 1000; j++) {//每个线程a和b都自增1000次
data.aAdd();
data.bAdd();
}
}, ("t" + i)).start();
}
//主线程等待上面30个线程全部运行完成
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("a:" + data.a);//打印a的值,a使用volatile修饰由于不保证原子性,所以应该<=3w
System.out.println("b:" + data.b);//打印b的值,b是原子引用,所以不论怎么加最后值都应该=3w
}
}
运行结果:
a:29734//每次运行结果都不一样<=30000
b:30000//每次运行结果都=30000
因为有序性运行结果不明显,所以仅给出代码及说明,使用经典的单例模式的懒汉式进行说明。
/**
* @author zhengyue
* @date 2019-09-23 14:48
*/
public class SingletonDemo {
/**
* 此处的volatile 并不是保证可见性,而是保证有序性
*/
private static volatile SingletonDemo singletonDemo = null;
/**
* 无惨构造方法,若单例模式起到作用则只会打印一次
*/
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "正在创建对象");
}
/**
* 不对整个方法加锁
* 采用双端检索机制Double Check Lock (DCL)在锁两端进行检查
* singletonDemo = new SingletonDemo();是个非原子操作
* 被分为以下3步:
* 1、划分内存空间 2、初始化对象 3、设置对象引用
* 若不加volatile修饰,则当一个线程A,创建SingletonDemo时,先执行的3再执行2,
* 此时对象已有引用,所以在另一个线程B进行第一层判断不为空,直接返回对象,在对象使用时就会
* 报空指针异常。加上volatile关键字后会禁止指令重排,从而避免该情况发生
* @return
*/
public static SingletonDemo getSingletonDemo() {
if (singletonDemo == null) {
synchronized (SingletonDemo.class) {
if (singletonDemo == null) {
singletonDemo = new SingletonDemo();
}
}
}
return singletonDemo;
}
/**
* 普通get方法
*/
public void test() {
System.out.println("我是test方法!");
}
}
class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() ->{
SingletonDemo singletonDemo = SingletonDemo.getSingletonDemo();
singletonDemo.test();
}, String.valueOf(i)).start();
}
}
}
1、公平锁:采用排队机制,当锁被占用时,线程自动排队,当锁使用完成后,由排在首部的线程使用锁。
2、非公平锁:可以插队,谁抢到谁使用。提高了效率,但容易造成线程饥饿
3、乐观锁:什么都是乐观的,在修改某个值时认为别的线程没有修改过,所以在取值时并不会被加锁,在修改完成后,更新回主内存时,再对比主内存中的值与取到的值。采用版本号和CAS算法实现。java.util.concurrent.atomic包下的原子类大都采用乐观锁的方式。适用于多读类型,可以提高吞吐量
4、悲观锁:认为什么都是悲观的,在修改某个值的时候一定会有别的线程来修改。所以在取值时就进行加锁,一直到写回主内存。sync/ReentrantLock就是典型的悲观锁
5、独占锁:独自占有,只能被占有锁的线程使用。sync/ReentrantLock都是独占锁
6、共享锁:共享锁锁定的资源可以被其它用户读取,但其它用户不能修改它。ReentrantReadWriteLock是共享锁
7、可重入锁(递归锁):当持有锁的线程进入同一锁方法时不需要重新获取锁。sync/Reentrantlock是典型的可重入锁最大作用避免死锁
8、自旋锁:自旋锁在执行单元在获取锁之前,如果发现有其他执行单元正在占用锁,则会不停的循环判断锁状态,直到锁被释放,期间并不会阻塞自己。由于在等待时不断的"自旋",这也是它为什么叫做自旋锁。所以自旋锁使用时,是非常消耗CPU资源的。
手写自旋锁demo:
import java.util.concurrent.atomic.AtomicReference;
public class SpinLockDemo {
AtomicReference atomicReference = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void unlock() {
atomicReference.compareAndSet(Thread.currentThread(), null);
}
private static int num = 0;
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
spinLockDemo.lock();
change();
spinLockDemo.unlock();
}, ("t" + i)).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(num);
}
public static void change() {
num++;
}
}
9、互斥锁:执行单元等待锁释放时,会把自己阻塞并放入到队列中。当锁被释放时,会唤醒队列上执行单元把其放入就绪队列中,并由调度算法进行调度并执行。所以互斥锁使用时会有线程的上下文切换,这可能是非常耗时的一个操作,但是等待锁期间不会浪费CPU资源。
1、底层原理:使用Monitor 通过进入MONITORENTER,和MONITOREXIT退出来保证同步,而且为保证锁会被释放在正常和异常情况下均会被MONITOREXIT退出
2、使用方式:直接写在方法上或者使用代码块的方式,synchronized 在代码块开始和结束会自动加锁释放锁无需人工操作
synchronized (object) {
}
3、适用场景:适用于较少的代码同步,现很少使用,因为其可控性较差,同步后代码运行效率较差
1、使用方式:
Lock lock = new ReentrantLock();
lock.lock();
try {
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
2、 适用场景:适用于大量代码同步,可以通过tryLock()/lockInterruptibly()来定时终止线程等待,tryLock()可以设置等待时间,获取到锁返回true获取不到则返回false,lockInterruptibly()可以通过运行线程的Thread.interrupt方法来中断等待。同时还可以精确唤醒线程。
3、线程的精确唤醒使用方法demo
public class SyncAndReentrantLoackDemo {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
PrintEnum[] values = PrintEnum.values();
for (PrintEnum pr: values) {
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.print(pr);
}
}, pr.getName()).start();
}
}
}
class ShareResource {
private int number = PrintEnum.THREAD_A.getNumber();//A 1,B 2,C 3
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
public void print(PrintEnum printEnum) {
lock.lock();
try {
while (number != printEnum.getNumber()) {
getCondition(printEnum).await();
}
System.out.println(Thread.currentThread().getName() + "\t" + "我爱你" + printEnum.getCount() + "遍");
PrintEnum next = printEnum.next();
number = next.getNumber();
getCondition(next).signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public Condition getCondition(PrintEnum printEnum) {
if (printEnum.equals(PrintEnum.THREAD_A)) {
return conditionA;
} else if (printEnum.equals(PrintEnum.THREAD_B)) {
return conditionB;
} else if (printEnum.equals(PrintEnum.THREAD_C)) {
return conditionC;
}
throw new IllegalArgumentException("没有合适的printEnum");
}
}
@Getter
enum PrintEnum {
THREAD_A(1, "A", 5), THREAD_B(2, "B", 10), THREAD_C(3, "C", 15);
private int number;
private String name;
private int count;
PrintEnum(int number, String name, int count) {
this.number = number;
this.name = name;
this.count = count;
}
public PrintEnum next() {
PrintEnum[] values = PrintEnum.values();
for (int i = 0; i < values.length; i++) {
if (this.equals(values[i])) {
if (i < (values.length -1)) {
return values[i + 1];
} else {
return values[0];
}
}
}
throw new IllegalArgumentException("没有合适的printEnum");
}
}
ReentrantReadWriteLock读写锁,是一种共享锁,允许被锁定资源被别人读取,这样就大大提高了读的并发量。
demo
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache cache = new MyCache();
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(() -> {
cache.put(temp, temp);
}, ("t" + i)).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("==================================");
for (int i = 5; i < 10; i++) {
final int temp = i;
new Thread(() -> {
cache.get(temp - 5);
}, ("t" + i)).start();
}
}
}
class MyCache {
private volatile Map map = new HashMap<>();
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void put(Integer key, Integer val) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 线程正在写入:" + key);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, val);
System.out.println(Thread.currentThread().getName() + "\t 线程写入完成:" + key);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
public void get(Integer key) {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 线程正在读取:" + key);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer val = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 线程读取完成:" + val);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
}
1、层面不一样,synchronized是系统关键字,ReentrantLock是juc下的类
2、使用方式不一样:synchronized会自动在代码块前后加锁及释放锁,ReentrantLock需要手动加锁及释放锁,所以忘记释放锁就容易导致死锁
3、性质不一样:synchronized是非公平的可重入锁,ReentrantLock是可公平(默认是非公平的,但可以通过构造参数fair=true改为公平锁)、可重入锁
4、synchronized是等待不可中断的,ReentrantLock是可中断的,可以通过tryLock()/lockInterruptibly()进行中断等待
5、ReentrantLock可以使用条件锁精确唤醒某些线程,而synchronized没有(代码演示见4.3.1、ReentrantLock)
1、什么是死锁:死锁就是两个及两个以上的线程,当A线程持有A锁的同时尝试获取B锁,B线程持有B锁的同时尝试获取A锁。
2、死锁的代码演示
class HoldThread implements Runnable {
private String lockA;
private String lockB;
public HoldThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "正在持有锁" + lockA + "尝试持有锁" + lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "持有锁" + lockB);
}
}
}
}
public class HoldThreadDemo {
private volatile String aa;
public static void main(String[] args) {
String lockA = "A";
String lockB = "B";
new Thread(new HoldThread(lockA, lockB), "t1").start();
new Thread(new HoldThread(lockB, lockA), "t2").start();
}
}
3、 问题排查
首先使用jps -l命令查找出正在运行的java程序,使用jstack 进程号 打印堆栈信息查找问题,问题截图
1、继承Thread类 (基本上不用,因为java类只能单继承)
2、实现Runable接口(不带返回值及错误信息)
3、实现Callable
4、使用线程池(现在企业级都使用该方式获取线程,其优势见:5.4、线程池的优势)
class Test implements Callable {
@Override
public Integer call() throws Exception {
System.out.println("进来了");
return 10;
}
}
public class MyTest001 {
public static void main(String[] args) {
FutureTask futureTask = new FutureTask(new Test());
Thread test = new Thread(futureTask);
test.start();
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
线程池的创建有以下四种方式,其中1/2/3 是Executor的对应工具类Executors提供,附上源码,大家可以对比手动创建线程池对各个参数的解释,来查看源码,这样你就会明白,为什么公司级不会采用这三种方式了。
1、创建固定线程数的线程池 Executors.newFixedThreadPool(num);附上源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
2、创建单个线程的线程池 Executors.newSingleThreadExecutor();附上源码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
3、创建可扩容的线程池 Executors.newCachedThreadPool();附上源码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
4、手动创建线程池 demo
ExecutorService threadPool = new ThreadPoolExecutor(2, 5,
1l, TimeUnit.SECONDS,new LinkedBlockingDeque<>(5),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());
1、降低资源消耗,重复利用线程,避免了线程被创建和销毁时的消耗
2、提高相应速度,当任务到达时,不需要创建线程就可立即执行
3、提高线程的可管理性,方便调优和监控
附上:阿里巴巴2019 java开发手册多线程开发的部分
1、创建demo
ExecutorService threadPool = new ThreadPoolExecutor(2, 5,
1l, TimeUnit.SECONDS,new LinkedBlockingDeque<>(5),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());
2、ThreadPoolExecutor 构造器7参源码
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
3、七大参数
a、corePoolSize:核心线程数,可以理解为线程池初始化的线程数,这些数量的线程不会被销毁。
b、maximumPoolSize:最大线程数,结合线程池工作的流程,当阻塞队列中等待任务也满时,线程池就会逐渐增加线程数,来分摊任务,增加线程到最大数量maximumPoolSize。
c、keepAliveTime:最大空闲时间,当线程数空闲达到规定的这个时间时,就会被销毁,直至线程数达到核心线程数
d、unit:keepAliveTime的单位,需要传入TimeUnit的枚举类型
e、workQueue:等待队列,阻塞队列,当任务占满核心线程后,后续任务将会在此等待
f、threadFactory:线程创建工厂,一般都使用默认Executors.defaultThreadFactory()
g、handler:拒绝策略,有四大拒绝策略,拒绝策略的意思是,当阻塞队列已满,线程数也增加到最大线程数,任务仍然处理不完,则会被执行拒绝策略,来拒绝后面的任务。
4、拒绝策略
a、new ThreadPoolExecutor.AbortPolicy() 默认的拒绝策略,直接抛异常
b、new ThreadPoolExecutor.CallerRunsPolicy() 将任务返回给调用线程,假如main调用线程池达到拒绝线程数后,会将任务返回给main线程执行
c、new ThreadPoolExecutor.DiscardPolicy() 随机丢弃掉线程
d、new ThreadPoolExecutor.DiscardOldestPolicy() 丢弃掉等待时间最久的线程
5、线程池工作流程(原理)
我们可以把线程池想象为银行,我们来到银行办理业务,假如银行当值窗口有3个(核心线程数),当人们来办理业务时,会挨个去这个三个窗口办理,当人越来越多时,就需要排队,排队人员领取号码牌后去等候区等候(阻塞队列),当等候区人员也满时,工作人员向领导请示,增加工作人员,人可以增加单窗口数量是固定的,假如窗口数为6(最大线程数),随着工作人员的增加,可能会出现两种情况:1、办理的很快,来办理业务的人也越来越少了,这时来加班的工作人员就会被空闲下来,当空闲到一定时间时(最大空闲时间),就会下班走人(线程销毁至核心数)2、增加人员后仍然办理不过来,来办理业务的人仍然是越来越多,这时工作人员就会把后来的人拒绝掉(拒绝策略)
线程池初始化核心线程数,当任务过来时就丢给核心线程去执行,随着任务增加,核心线程被全部占用时,后面任务就进入阻塞队列中进行等待,当阻塞队列中的任务也满时,就会增加线程来分摊任务,当增加到核心线程数后(此时阻塞队列已满),仍有处理不了的任务,就会启动拒绝策略。若随着线程增加任务被处理完成,当非核心线程空闲下来后,当空闲时间达到固定的最大空闲时间时就会被销毁,直至线程数达到核心线程数。
AtomicReference
以AtomicInteger的getAndIncrement()方法为例,说下atomic是如何保证原子性的,先贴上atomicInteger.getAndIncrement() (自身加一的操作)的源代码,结合源代码我们来看下其原理:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
首先AtomicInteger有两个构造方法,无参的和传入int的,最终是控制的成员变量value,无参情况下value是int的默认值0。另外value使用了volatile修饰,使之具有可见性和有序性。getAndIncrement()方法,调用的是unsafe类的方法,(UnSafe类:java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作,其中使用了大量的native方法。)使用了getAndAddInt方法,该方法是做完操作后调用compareAndSwapInt方法(也即cas:比较并交换,如果内存中的值和预期值相同则修改并返回true,若不同则返回false),若返回false则会while判断后会重新获取内存中的值进行计算,直至比较成功(自旋的概念),compareAndSwapInt方法是原子的,是unsafe类cas的实现。这样就保证了原子性。
cas:比较并交换(compare and swap),乐观锁就是根据这个来的,拷贝回自己内存时,不需要进行加锁,在写回内存时会比较期望值和主内存中的真实值,若比较成功就会更新主内存中的值,否则重新获取内存中的值,然后重新操作。
cas的优缺点:优点就是在保证原子性的时候,不需要加锁,能够提高并发量。
缺点:1、循环时间长时,系统资源消耗大 2、只能保证一个共享变量的原子操作 3、ABA问题
何为ABA问题,假设现有两个线程t1和t2,主内存中变量x=A,t1、t2线程都要对x进行操作,分别将x拷贝回自己的工作内存中,这时t1操作比较复杂,运行比较慢,t2运行较快,t2将x修改为B并将其写回主内存,t1仍在运行时,t2又将修改回A,这时t1运行完成要写回主内存时,比较认为期间并没有被修改。
ABA问题的解决思路是,添加版本号,对版本号进行比较。atomic类下的AtomicStampedReference
构造方法
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
构造方法需要传入两个参数:initialRef:初始化值;initialStamp:版本号
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair cmp, Pair val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
compareAndSet(比较并交换)方法:四个参数分别是:expectedReference期望值、newReference更新值、expectedStamp期望版本号、newStamp更新后的版本号,除了会比较期望值还会比较版本号,两者都通过后
1、CopyOnWriteArrayList
线程安全的ArrayList,底层原理是写时复制,读写分离,写时复制加锁,写完后更新回源文件。add方法源码:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
add方法:先加锁获取到原有数组后,进行复制,并扩容1,添加完成后更新原有数组,这样不会影响其他线程读取集合。
2、CopyOnWriteArraySet
底层是使用CopyOnWriteArrayList
注意set底层是用的HashMap 用其键做存储来保证不重复的。
//CopyOnWriteArraySet的构造方法,可以看出就是使用的CopyOnWriteArrayList
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList();
}
//CopyOnWriteArraySet的add方法,调用CopyOnWriteArrayList的addIfAbsent
public boolean add(E e) {
return al.addIfAbsent(e);
}
//CopyOnWriteArrayList的addIfAbsent 判断如果存在就返回false 否则添加
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
3、ConcurrentHashMap
其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。具体请看这篇博客,或者网上有很多关于ConcurrentHashMap底层原理的解释(https://blog.csdn.net/weixin_40413816/article/details/82979744)
CountDownLatch向下计数阻塞,当计数走到0时才能够往下继续,就好比打游戏:一共有10个小怪,你全部打死之后才会出大怪。
使用demo:
public class CountDownDemo {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println("小怪" + Thread.currentThread().getName() + "号,被打死");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("大怪出现。。。");
}
}
CyclicBarrier 向上统计阻塞,向上统计到规定数值时才会被执行,比如打游戏,集宝图碎片,集齐10张宝图碎片后你会获得一张藏宝图。
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(10, () -> {
System.out.println("恭喜获得藏宝图一张");
});
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
System.out.println("收集到" + Thread.currentThread().getName() + "号宝图碎片");
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
Semaphore 信号灯,用于多个线程抢多个资源的情况,比如:小米手机秒杀的时候,多个电脑一同抢99部手机。或者10辆车抢3个车位,同一时间只能有三个车抢到车位,当一辆车开走后另一辆车才能停进去。
使用demo
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "\t" + "抢占到车位");
int random = NumberUtils.random(1, 5);
try {
TimeUnit.SECONDS.sleep(random);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "停车" + random + "秒后离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}, ("t" + i)).start();
}
}
}
阻塞队列的使用方法:
类型 | 插入 | 移除 | 检查 |
---|---|---|---|
抛异常 | add(e) | remove() | element() |
阻塞 | put(e) | take() | 无 |
特殊值 | offer(e) | poll() | peek() |
超时 | offer(e,t,u) | poll(t,u) | 无 |
阻塞队列有七种,介绍一下常用的三种
ArrayBlockingQueue
LinkedBlockingDeque<>() 由链表组成的有界阻塞队列,但默认接线是Integer.MAX_VALUE
SynchronousQueue<>() 不存储元素的阻塞队列,也即单个元素队列
具体使用方法可以看生产者消费者的demo 7.3
-----------------------------------------剩余四种补充说明----------------------------------------------------------
PriorityBlockingQueue<>()支持优先级排序的无界阻塞队列
DelayQueue<>() 使用优先级队列实现的延迟无界阻塞队列
LinkedTransferQueue<>() 由链表组成的无界阻塞队列
LinkedBlockingDeque<>() 由链表组成的双向阻塞队列
class ShareData01 {
private int number = 0;
private static int max = 10;
public synchronized void producer() {
while (number >= max) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number++;
System.out.println(Thread.currentThread().getName() + "\t 生产一个,当前总数:" + number);
this.notifyAll();
}
public synchronized void consumer() {
while (number <= 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number--;
System.out.println(Thread.currentThread().getName() + "\t 消费一个,当前总数:" + number);
this.notifyAll();
}
}
public class TraditionDemo01 {
public static void main(String[] args) {
ShareData01 shareData = new ShareData01();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
shareData.producer();
}
}, "t1").start();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
shareData.consumer();
}
}, "t2").start();
}
}
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class ShareData02 {
private int number = 0;
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void producer() {
lock.lock();
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
while (number != 0) {
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "\t 生产一个,当前总数:" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void consumer() {
lock.lock();
try {
while (number == 0) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "\t 消费一个,当前总数:" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* 1、线程 操作(调用方法) 资源类(类)
* 2、判断(不符合条件则休眠) 干活(业务逻辑) 通知(唤醒其他线程工作)
* 3、防止虚假唤醒机制(用while 代替 if 判断)
*/
public class TraditionDemo02 {
public static void main(String[] args) {
ShareData02 shareData = new ShareData02();
for (int i = 0; i < 30; i++) {
new Thread(() -> {
shareData.producer();
}, ("p" + i)).start();
}
for (int i = 0; i < 30; i++) {
new Thread(() -> {
shareData.consumer();
}, ("c" + i)).start();
}
}
}
import org.apache.commons.lang3.StringUtils;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
class ShareData03 {
private volatile boolean flag = true;
private volatile AtomicInteger atomicInteger = new AtomicInteger();
private BlockingQueue queue = null;
public ShareData03(BlockingQueue queue) {
this.queue = queue;
System.out.println(queue.getClass().getName() + "阻塞队列");
}
public void producer() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "\t" + "生产线程启动");
boolean offer = false;
while (flag) {
offer = queue.offer("蛋糕" + (atomicInteger.get() + 1) + "号", 4l, TimeUnit.SECONDS);
if (offer) {
atomicInteger.incrementAndGet();
System.out.println("生产成功,当前库存:" + atomicInteger.get());
} else {
System.out.println("生产失败!当前库存:" + atomicInteger.get());
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产线程结束!");
}
public void consumer() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "消费线程开启");
String poll = null;
while (flag) {
poll = queue.poll(2l, TimeUnit.SECONDS);
if (StringUtils.isNotBlank(poll)) {
atomicInteger.decrementAndGet();
System.out.println("消费" + poll + "成功!当前库存:" + atomicInteger.get());
} else {
System.out.println("消费失败,当前阻塞队列中没有任何东西,当前库存" + atomicInteger.get());
flag = false;
return;
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者队列已经结束!");
}
public void stop() {
flag = false;
}
}
/**
* 1、线程 操作(调用方法) 资源类(类)
* 2、判断(不符合条件则休眠) 干活(业务逻辑) 通知(唤醒其他线程工作)
* 3、防止虚假唤醒机制(用while 代替 if 判断)
*/
public class TraditionDemo03 {
public static void main(String[] args) {
ShareData03 shareData03 = new ShareData03(new ArrayBlockingQueue(3));
test01(shareData03);
try {
TimeUnit.SECONDS.sleep(8);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println();
System.out.println();
System.out.println();
shareData03.stop();
System.out.println("程序停止!");
}
public static void test02(ShareData03 shareData03) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
shareData03.producer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, ("p" + i)).start();
}
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
shareData03.consumer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, ("c" + i)).start();
}
}
public static void test01(ShareData03 shareData03) {
new Thread(() -> {
try {
shareData03.producer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Prod").start();
new Thread(() -> {
try {
shareData03.consumer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Consumer").start();
}
}