这段笔记是参照b站教程BV1Rv411y7MU整理而来的,用于个人备忘以便复习,需要的朋友可以自取。
线程同步(下)
1.原子类
1.1 常用原子类自增自减操作
我们知道++,–不是原子操作,除了使用synchronized进行同步操作外,也可以使用AtomicInteger、AtomicLong进行原子类进行实现。
public class Main02 {
public static void main(String[]args) throws InterruptedException {
for(int i=0;i<100;i++){
new MyThread().start();
}
Thread.sleep(1000);
System.out.println(MyThread.count.get());
}
static class MyThread extends Thread{
private static AtomicInteger count = new AtomicInteger();
public static void addCount(){
for (int i = 0; i < 1000; i++) {
count.getAndIncrement();//count++
}
System.out.println(Thread.currentThread().getName()+" --> "+count.get());
}
@Override
public void run() {
addCount();
}
}
}
上述代码在控制台显示的结果中可能有的线程打印不为1000的倍数,原因是此时代码没有上锁,所以是不同步的,在一个线程打印的时候,其他线程还在执行自增操作,所以导致控制台输出的时候显示不为整数。
但是由于AtomicInteger保证了原子性,所以最后结果肯定是正确的,不会产生脏读。
AtomicInteger常用方法:
.getAndIncrement()
相当于x++.incrementAndGet()
相当于++x.get()
返回int值
1.2 CAS
CAS(Compare and Swap)是由硬件实现的。CAS可以将read-modify-write这类操作转换为原子操作。
i++自增操作包括三个子操作:读取i的值;i的值+1;把新值保存到主内存。而CAS原理为:将数据更新到主内存的时候,再次读取主内存变量的值,如果现在变量的值与期望的值(操作起始读取的值)不一样就更新。
使用CAS实现一个线程安全的计数器
public class CASTest {
public static void main(String[]args){
CASCounter casCounter = new CASCounter();
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(casCounter.incrementAndGet());
}
}).start();
}
}
static class CASCounter{
public volatile long value;//使用volatile修饰 使线程可见
public long getValue(){
return this.value;
}
public boolean compareAndSwap(long expectedValue,long newValue){
/**
* 如果当前value与期望值expectValue一样就把value替换称newValue
*/
synchronized (this){
if(this.value==expectedValue){
this.value = newValue;
return true;
}else
return false;
}
}
//定义自增方法
public long incrementAndGet(){
long oldValue;
long newValue;
do{
oldValue = this.value;
newValue = oldValue+1;
}while ( !compareAndSwap(oldValue,newValue) );
return newValue;
}
}
}
1.3 CAS导致ABA问题
CAS实现原子操作背后有一个假设:共享变量的当前值与当前线程提供的期望值相同的时候,就认为这个变量没有被其他线程修改过。
实际上这种假设并不是总一定成立,加入有共享变量count=0
A线程对count修改为10,
B线程对count修改为20,
C线程对count修改为0,
当前线程看到count变量的值为0,现在是否认为count变量的值是否认为没有被其他线程更新呢?这种结果是否可以被接受,这就是CAS中ABA问题:即共享变量经历A->b->A的更新。
ABA是否被接受和实现算法有关,如果想要规避ABA问题,可以为共享变量引入一个修订号(时间戳),每次修改共享变量的时候,响应的修订号就加1。ABA变量更新过程为:[A,0]->[B,1]->[A,2]。通过修订号就可以判断变量是否被其他线程修改过。
1.4 原子变量类
原子变量类基于CAS实现,当对共享变量进行read-motify-write操作的时候,通过原子变量类可以保障操作的原子性与可见性。对变量的read-motify-write更新操作是指当前操作不是一个简单的赋值,而是指变量的新值依赖变量的旧值,如自增操作i++。
由于volatile只能保障可见性不能保证原子性,原子类内部就是借助了一个volatile变量,并且保障了该变量的read-motify-write操作的原子性,有时候把原子变量类看作是volatile变量的增强。
原子变量类有12个,如:
分组 | 原子变量类 |
---|---|
基础数据型 | AtomicInteger、AtomicLong、AtomicBoolean |
数组型 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray |
字段更新器 | AtomicIntegerFieldUpdater AtomicLongFieldUpdater AtomicReferenceFieldUpdater |
引用型 | AtomicReference AtomicStampedReference(时间戳) AtomicMarkableReference(标志) |
1.4.1 AtomicLong代码演示
模拟服务器请求的计数器:
计数器类:
/**
* 计数器类
* 模拟服务器的请求总数,处理成功数,处理失败数
*/
public class Test {
public static void main(String[]args) throws InterruptedException {
//通过线程模拟操作
//实际中可以使用ServletFilter中调用Indicator计数器的相关方法
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//每个线程就是一个请求,请求总数要+1
Indicator.getInstance().newRequestReceive();
int num = (int) (Math.random()*1000);
if(num % 2 == 0){
Indicator.getInstance().requestProcessSuccess();
}else {
Indicator.getInstance().requestProcessFail();
}
}
}).start();
}
Thread.sleep(1000);
System.out.println("请求总数-->"+Indicator.getInstance().getRequestCount());
System.out.println("成功总数-->"+Indicator.getInstance().getSuccessCount());
System.out.println("失败总数-->"+Indicator.getInstance().getFailCount());
}
}
主类:
/**
* 模拟服务器的请求总数,处理成功数,处理失败数
*/
public class Test {
public static void main(String[]args) throws InterruptedException {
//通过线程模拟操作
//实际中可以使用ServletFilter中调用Indicator计数器的相关方法
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//每个线程就是一个请求,请求总数要+1
Indicator.getInstance().newRequestReceive();
int num = (int) (Math.random()*1000);
if(num % 2 == 0){
Indicator.getInstance().requestProcessSuccess();
}else {
Indicator.getInstance().requestProcessFail();
}
}
}).start();
}
Thread.sleep(1000);
System.out.println("请求总数-->"+Indicator.getInstance().getRequestCount());
System.out.println("成功总数-->"+Indicator.getInstance().getSuccessCount());
System.out.println("失败总数-->"+Indicator.getInstance().getFailCount());
}
}
1.4.2 AtomicIntegerArray
- 基本操作
//创建一个指定长度的原子数组
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
System.out.println(atomicIntegerArray); //[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
//返回指定位置的元素
System.out.println(atomicIntegerArray.get(0));
System.out.println(atomicIntegerArray.get(1));
//设置指定位置元素的方法
atomicIntegerArray.set(0,10);
//设置数组元素新值的时候同时返回旧值
System.out.println(atomicIntegerArray.getAndSet(1,12)); //0
//且数组为[10, 12, 0, 0, 0, 0, 0, 0, 0, 0]
//修改数组某个元素的值,把数组元素加上某个值并返回
System.out.println(atomicIntegerArray.addAndGet(1,10)); //22
//先得到指定位置的值,再修改数组
System.out.println(atomicIntegerArray.getAndAdd(2,33)); //0
//数组为[10, 22, 33, 0, 0, 0, 0, 0, 0, 0]
//CAS操作
//如果索引为0的地方的值为10就将其修改为222 替换成功返回true反之false
System.out.println(atomicIntegerArray.compareAndSet(0,10,222));
//自增自减
System.out.println(atomicIntegerArray.incrementAndGet(0)); //++atomicIntegerArray[0]
System.out.println(atomicIntegerArray.getAndIncrement(0)); //atomicIntegerArray[0]++
System.out.println(atomicIntegerArray.decrementAndGet(1)); //--atomicIntegerArray[1]
System.out.println(atomicIntegerArray.getAndDecrement(1)); //atomicIntegerArray[1]--
- AtomicIntegerArray演示案例:
/**
* 多线程中使用AtomicIntegerArray原子数组
*/
public class Test {
//定义原子数组
static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
public static void main(String[]args) throws InterruptedException {
//定义一个县城数组
Thread[] threads = new Thread[10];
//给线程数组赋值
for (int i = 0; i < 10; i++) {
threads[i] = new AddThread();
}
//开启子线程
for(Thread thread:threads){
thread.start();
}
//在主线程中查看自增完以后原子数组中各个元素的值,在主线程中需要所有的子线程都执行完了再查看
//把所有的子线程合并到主线程之中
for(Thread thread:threads){
thread.join();
}
System.out.println(atomicIntegerArray);
}
//定义线程类
static class AddThread extends Thread{
@Override
public void run() {
//把原子数组的每个元素自增1000次
for(int i=0;i<1000;i++){
for(int j=0;j<atomicIntegerArray.length();j++){
atomicIntegerArray.getAndIncrement(j % atomicIntegerArray.length());
}
}
}
}
}
1.4.3 AtomicIntegerFieldUpdater
AtomicIntegerFieldUpdater可以对原子整数字段进行更新,要求:
- 字符必须使用volatile修饰,使线程之间可见
- 只能是实例变量,不能是静态变量,也不能用final修饰
基本使用通过以下案例介绍:
User类
/**
* 使用AtomicIntegerFieldUpdater更新的字段必须使用volatile修饰
*/
public class User {
int id;
volatile int age;
public User(){}
public User(int id,int age){
this.id=id;
this.age=age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", age=" + age +
'}';
}
}
线程类
public class SubThread extends Thread{
private User user;
//创建一个AtomicIntegerFieldUpdater更新器
private AtomicIntegerFieldUpdater<User> atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
public SubThread(User user){
this.user = user;
}
@Override
public void run() {
//在子线程中对user对象中age字段自增10次
for (int i = 0; i < 10; i++) {
System.out.println(atomicIntegerFieldUpdater.getAndIncrement(user));
}
}
}
主类
public class Test {
public static void main(String[]args) throws InterruptedException {
User user = new User(1,10);
//开启10个线程
for (int i = 0; i < 10; i++) {
new SubThread(user).start();
}
Thread.sleep(1000);
System.out.println(user);
}
}
1.4.4 AtomicReference
可以原子读写一个对象
通过一个代码演示操作:
public class Test {
//创建一个AtomicReference对象
static AtomicReference<String> atomicReference = new AtomicReference<>("abc");
public static void main(String[]args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
if (atomicReference.compareAndSet("abc", "def")) {
System.out.println(Thread.currentThread().getName() + " --> 把字符串abc修改为def");
}
}
}).start();
}
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
if(atomicReference.compareAndSet("def","abc")){
System.out.println(Thread.currentThread().getName()+" --> 把字符串def还原为abc");
}
}
}).start();
}
Thread.sleep(1000);
}
}
- AtomicReference中可能会出现ABA问题
public class Test {
//创建一个AtomicReference对象
static AtomicReference<String> atomicReference = new AtomicReference<>("abc");
public static void main(String[]args) throws InterruptedException {
//创建t1线程,想把adc改为def,再把字符串还原为abc
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
atomicReference.compareAndSet("abc","def");
System.out.println(Thread.currentThread().getName()+"-->"+atomicReference.get());
atomicReference.compareAndSet("def","abc");
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//睡一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet("abc","ghi");
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(atomicReference.get());
}
}
上述代码中引发了ABA问题。在t1线程中我们进行了ABA操作,即将abc改为def再改为abc。t2线程中再进行判断,如果为abc就修改为ghi,此时控制台最后打印ghi。
而CAS的原理表示如果传过来的值和期望值一样就表示数据没有进行修改,这样就设置一个新的值。但实际上我们在t1线程中已经对它的值进行了修改,此时t2中仍然发生了改动。
1.4.5 AtomicStampedReference解决ABA问题
/**
* 在AtomicStampedReference原子类中有一个证数标记值stamped,
* 每次执行CAS操作的时候,需要对比它的stamped。
*/
public class Test {
//public static AtomicReference<String> atomicReference = new AtomicReference<>("abc");
//自定义AtomicStampedReference引用操作abc字符串,指定初始版本号为0
public static AtomicStampedReference<String> stampedReference = new AtomicStampedReference<>("abc",0);
public static void main(String[]args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 固定写法! 后两项为期望版本号和新版本号,期望版本号和当前版本号一样的时候就赋新版本号(即+1)
stampedReference.compareAndSet("abc","def",stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"-->"+stampedReference.getReference());
stampedReference.compareAndSet("def","abc",stampedReference.getStamp(),stampedReference.getStamp()+1);
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int stamp = stampedReference.getStamp();//获得版本号
System.out.println(stampedReference.compareAndSet("abc","ghi",stamp,stamp+1));
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(stampedReference.getReference());
}
}
上述代码中线程t2的int stamp = stampedReference.getStamp();
语句不可以移到Thread.sleep(1000)
之前。
因为在t2 sleep期间t1还在工作,可能再次期间版本号发生了改变,导致CAS操作发生错误。推荐使用stampedReference.getStamp()