前言:
前面我们介绍了要想保证多线程变现过程中的安全问题,我们就要保证在读线程编写过程中保证多线程的三大特性,那么java为了保证多线程的三大特性引入了很多线程控制机制,下面就来介绍场用的几种。
一、ThreadLocal:
ThreadLocal线程本地变量,它可以为每一个线程刚保存一份线程内变量的副本,可以保证线程线程之间的变量时互不影响的,且是原子类可以保证变量的原子操作,当某些数据是以线程为作用域且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。
ThreadLocal常用的方法:
initialValue:创建副本方发:(注:在jdk1.8引入了lambda构造方式,方法变成了withlnitial)
get:获取副本方法:
set:设置副本方法:
在这里我我们来模拟一下两个用户之间的转账,来看一下ThreadLocal的实现:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
public class ThreadLocalDemo { //创建一个银行账户:账户有存款,可以进行存钱取钱操作。 static class Bank{ private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> { return 0; //假设我们每个人的账户初始余额都是零 }); //取款方法 public Integer get(){ return threadLocal.get(); } //存钱方法 public void set(Integer money){ threadLocal.set( get() + money); } } //创建转账兑对象:从银行取钱、转账、保存 static class Transfer implements Runnable{ private Bank bank; public Transfer(Bank bank){ this.bank = bank; } @Override public void run() { //循环转账五次 for(int i = 0; i < 5; i++){ //先转账一次 bank.set(10); System.out.println(Thread.currentThread().getName()+ "账户余额" + bank.get()); } } } public static void main(String[] args) { Bank bank = new Bank(); Transfer transfer = new Transfer(bank); Thread thread1 = new Thread(transfer,"客户1"); Thread thread2 = new Thread(transfer,"客户2"); thread1.start(); thread2.start(); } }
输出结果:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
客户1账户余额10
客户2账户余额10
客户1账户余额20
客户1账户余额30
客户2账户余额20
客户2账户余额30
客户2账户余额40
客户2账户余额50
客户1账户余额40
客户1账户余额50
Process finished with exit code 0
二、原子类:
1、原子类属于java.util.concurrent.atomic包下,该包线面提供了很多可以进行原子操作的类,大致可以分为下面四种
原子更新基本类型:AtomicInteger、AtomicBoolean、AtomicLong
原子更新数组类型:AtomicIntegerArray、AtomicLongArray
原子更新引用类型:AtomicRefernece、AtomicStampedRefernece
原子更新属性类型:AtomicIntegerFieldUpdate、AtomicLongFieldUpdate
2、非原子性操作的问题演示:
还记得我们刚开始演示多线程问题演示的第一个案例吗,其实那就是一个非原子性操作,而在java所谓的++、--操作都是非原子性操作,再来看一下之前的案例:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
public class ThreadDemo { static int number = 0; public static void main(String[] args) throws InterruptedException { int j = 0; while (j < 100) { Thread task1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { number++; } } }); Thread task2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { number++; } } }); task1.start(); task2.start(); task1.join(); task2.join(); j++; System.out.println(number); } } }
测试结果:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
2000 4000 6000 8000 10000 12000 14000 15009 17009 19009
测试结果这里只是粘贴了一部分但是明显可以看到出现了问题,++在非原子性操作下打印数值出现了不确定性。
3、用原子类操作:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
public class ThreadDemo { static AtomicInteger number = null; public static void main(String[] args) throws InterruptedException { int j = 0; while (j < 100) { number = new AtomicInteger(0); Thread task1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { number.getAndIncrement(); } } }); Thread task2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { number.getAndIncrement(); } } }); task1.start(); task2.start(); task1.join(); task2.join(); j++; System.out.println(number.get()); } } }
测试结果:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
2000 2000 2000 2000 2000 2000 2000 2000 2000 2000 2000 2000 2000 2000
4、原子类CAS原理分析:
上面我们用原子类解决了非原子操作的问题,那么我们来分析一下原子类是怎样解决非原子性操作的,这其中就要涉及到CAS原理,每一个原子类解决非原子性操作都离不开CAS原理。
这是源码中getAndIncrement()这个方法,下面getAndAddInt()这是具体的实现,getAndAddInt方法中有三个参数,this指的是当前我们要操作递加的AtomicInteger对象,valueOffset是一个地址偏移量,是用来帮我们找到上面number这个值保存的物理地址的偏移量,第三个参数1就是递增间隔。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
简单介绍了一下上面的方法,下面就来看一下getAndAddInt方法的具体实现:
阅读下面源码我们可以看到do的方括号中有一个getIntVolatile()方法,这个方法主要是根据number的当前值,和地址的偏移量找到number在堆内存中的具体指,之后再while中执行了compareAndSwapInt()这个方法,而这个方法就是我们的CAS操作,具体含义是比较并且替换堆内存中的number指,具体的操作如下:
CAS(compare And SwapInt::比较并替换):首先getIntVolatile方法是根据当前值和地址偏移量去堆内存中找到具体的值,compareAndSwapInt方法是比较当前值var1和堆中的值(v5)是否相同。
相同:说明没有现成更改过该值,当前值(v1)= 预期值(v5)+ 递增间隔(v4)返回true,跳出循环返回v1的值
不相同:说明有现成更改了当前值,我们就需要把当前值设置成物理内存的值,当前值(v1)= 期望值(v5)并且返回false,然后继续执行循环,拿当前值和物理内存比较,直到两者相同,返回true跳出循环
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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; }