并发编程(六):悲观锁与乐观锁、atomic常见类、Unsafe对象

并发编程(六):悲观锁与乐观锁、atomic常见类、Unsafe对象

一、悲观锁与乐观锁

1.synchronized保证线程安全

//使用synchronized
public class Test1 {

    public static void main(String[] args) {
        Account.demo(new AccountSafe(10000));
        //Account.demo(new AccountCas(10000));
    }
}

class AccountSafe implements Account {
    private Integer balance;

    public AccountSafe(Integer balance) {
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
        synchronized (this) {
            return balance;
        }
    }

    @Override
    public void withdraw(Integer amount) {
        // 通过这里加锁就可以实现线程安全,不加就会导致线程安全问题
        synchronized (this) {
            balance -= amount;
        }
    }
}


interface Account {
    // 获取余额
    Integer getBalance();

    // 取款
    void withdraw(Integer amount);

    /**
     * 如果初始余额为 10000 那么正确的结果应当是 0
     */
    static void demo(Account account) {

		//启动1000个线程,每个线程取款10元
        List<Thread> ts = new ArrayList<>();
        long start = System.nanoTime();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        ts.forEach(thread -> thread.start());

		//等待所有线程运行结束
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getBalance()
                + " cost: " + (end - start) / 1000_000 + " ms");
    }
}

上面的代码中使用synchronized加锁(悲观锁)操作来保证线程安全,但是synchronized加锁操作太耗费资源,这里我们使用无锁(乐观锁)来解决此问题

class AccountCas implements Account {
	//使用原子整数: 底层使用CAS+重试的机制
	private AtomicInteger balance;

	public AccountCas(int balance) {
		this.balance = new AtomicInteger(balance);
	}

	@Override
	public Integer getBalance() {
		//得到原子整数的值
		return balance.get();
	}

	@Override
	public void withdraw(Integer amount) {
		while(true) {
			//获得修改前的值
			int prev = balance.get();
			//获得修改后的值
			int next = prev - amount;
			//比较并设置值
			/*
				此时的prev为共享变量的值, 如果prev被别的线程改了.也就是说: 自己读到的共享变量的值 和 共享变量最新值 不匹配,
				就继续where(true),如果匹配上了, 将next值设置给共享变量.
				AtomicInteger中value属性, 被volatile修饰, 就是为了确保线程之间共享变量的可见性.
			*/
			if(balance.compareAndSet(prev, next)) {
				break;
			}
		}
	}
}

使用原子操作来保证线程访问共享资源的安全性, cas+重试的机制来确保(乐观锁思想), 相对于悲观锁思想的synchronized,reentrantLock来说, cas的方式效率会更好。

2.CAS保证线程安全的原理

  • 前面看到的AtomicInteger的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?
@Override
public void withdraw(Integer amount) {
    // 需要不断尝试,直到成功为止
    while (true){
        // 比如拿到了旧值 100
        int prev = balance.get();
        // 在这个基础上 100-10 = 90
        int next = prev - amount;
        //比较并设置
        if (atomicInteger.compareAndSet(prev,next)){
            break;
        }
    }
}

在这里插入图片描述
当一个线程要去修改Account对象中的值时,先获取值prev(调用get方法),然后再将其设置为新的值next(调用cas方法)。在调用cas方法时,会将prev与Account中的余额进行比较。

  • 如果两者相等,就说明该值还未被其他线程修改,此时便可以进行修改操作。
  • 如果两者不相等,就不设置值,重新获取值prev(调用get方法),然后再将其设置为新的值next(调用cas方法),直到修改成功为止。
  • 其中关键的三个操作为:
    A.compareAndSet:原子操作
    B.while(true):整体语句非原子,用此判断重试
    C.volatile(修饰原子类中的value):CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

3.CAS底层原理

  • 其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的 原子性。
  • 在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线,这个过程中不会被线程的调度机制所打断, 保证了多个线程对内存操作的准确性,是原子的。

在这里插入图片描述

4. CAS的特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,重试就行。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。

CAS 体现的是无锁并发、无阻塞并发:

  • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
  • 但如果竞争激烈(写操作多),可以想到重试必然频繁发生,反而效率会受影响

二、atomic常见类

java.util.concurrent.atomic并发包提供了一些并发工具类,其均借助CAS和volatile实现乐观锁并发:

1.基本数据类型的原子类

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean :布尔型原子类

上面三个类提供的方法几乎相同,所以我们将以 AtomicInteger为例子来介绍。

public static void main(String[] args) {
    AtomicInteger i = new AtomicInteger(0);
    
    // 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
    System.out.println(i.getAndIncrement());
    
    // 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
    System.out.println(i.incrementAndGet());
    
    // 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
    System.out.println(i.decrementAndGet());
    
    // 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
    System.out.println(i.getAndDecrement());
    
    // 获取并加值(i = 0, 结果 i = 5, 返回 0)
    System.out.println(i.getAndAdd(5));
    
    // 加值并获取(i = 5, 结果 i = 0, 返回 0)
    System.out.println(i.addAndGet(-5));
    
    // 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
    System.out.println(i.getAndUpdate(p -> p - 2));
    
    // 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
    System.out.println(i.updateAndGet(p -> p + 2));
    
    // 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
    System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
    
    // 计算并获取(i = 10, p 为 i 的当前值, x 为参数1值, 结果 i = 0, 返回 0)
    System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));
}

以updateAndGet为例:

public static void main(String[] args) {

	AtomicInteger i = new AtomicInteger(6);
	
    updateAndGet(i, new IntUnaryOperator() {
        @Override
        public int applyAsInt(int operand) {
            return operand / 2; //6/2
        }
    });
    System.out.println(i.get()); // i==3
}

public static void updateAndGet(AtomicInteger i, IntUnaryOperator operator) {
    while (true) {
        int prev = i.get(); // 6
        int next = operator.applyAsInt(prev); //传入一个接口(内部带一个方法),自定义实现update内容
        if (i.compareAndSet(prev, next)) {
            break;
        }
    }
}
  • 调用updateAndGet方法, 将共享变量i, IntUnaryOperator对象传递过去
  • 传过来的operator对象, 调用IntUnaryOperator中的applyAsInt方法, 实际调用的就是传递过来的方法, 进行除法操作。

2.引用类型的原子类

原子引用的作用: 保证引用类型的共享变量是线程安全的(确保这个原子引用没有引用过别人。除了基本类型之外的,也能保证其线程安全。

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedReference

例子:使用原子引用实现BigDecimal存取款的线程安全:

//线程不安全的实现
class DecimalAccountUnsafe implements DecimalAccount {
    BigDecimal balance;
    public DecimalAccountUnsafe(BigDecimal balance) {
        this.balance = balance;
    }
    @Override
    public BigDecimal getBalance() {
        return balance;
    }
    @Override
    public void withdraw(BigDecimal amount) {
        BigDecimal balance = this.getBalance();
        this.balance = balance.subtract(amount);
    }
}


//线程安全的实现
class DecimalAccountCas implements DecimalAccount {

    //原子引用,泛型类型为BigDecimal类型
    private final AtomicReference<BigDecimal> balance;

    public DecimalAccountCas(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

    @Override
    public BigDecimal getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(BigDecimal amount) {
        while (true) {
            BigDecimal prev = balance.get();
            BigDecimal next = prev.subtract(amount);
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

(1)ABA问题

如果存在共享变量修改为从A修改为B,又从B修改为A,这时候仅通过判断共享变量值是否和预期值相同 无法确定是否被修改过。

(2)例子

public class Test1 {

    static AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) {
        new Thread(() -> {
            String pre = ref.get();
            System.out.println("change");
            try {
                other();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Sleeper.sleep(1);
            //把ref中的A改为C
            System.out.println("change A->C " + ref.compareAndSet(pre, "C"));
        }).start();
    }

    static void other() throws InterruptedException {
        new Thread(() -> {
        	// 此时ref.get()为A,此时共享变量ref也是A,没有被改过, 此时CAS
        	// 可以修改成功, B
            System.out.println("change A->B " + ref.compareAndSet(ref.get(), "B"));
        }).start();
        Thread.sleep(500);
        new Thread(() -> {
        	// 同上, 修改为A
            System.out.println("change B->A " + ref.compareAndSet(ref.get(), "A"));
        }).start();
    }
}

在这里插入图片描述

(3)使用AtomicStampedReference

public class Test1 {
    //指定版本号
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) {
        new Thread(() -> {
            String pre = ref.getReference();
            //获得版本号
            int stamp = ref.getStamp(); // 此时的版本号还是第一次获取的
            System.out.println("change");
            try {
                other();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //把ref中的A改为C,并比对版本号,如果版本号相同,就执行替换,并让版本号+1
            System.out.println("change A->C stamp " + stamp + ref.compareAndSet(pre, "C", stamp, stamp + 1));
        }).start();
    }

    static void other() throws InterruptedException {
        new Thread(() -> {
            int stamp = ref.getStamp();
            System.out.println("change A->B stamp " + stamp + ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1));
        }).start();
        Thread.sleep(500);
        new Thread(() -> {
            int stamp = ref.getStamp();
            System.out.println("change B->A stamp " + stamp + ref.compareAndSet(ref.getReference(), "A", stamp, stamp + 1));
        }).start();
    }
}

在这里插入图片描述

(4)使用AtomicMarkableReference

  • 标记cas的共享变量是否被修改过,AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A -> B -> A ->C,我们可以知道,引用变量中途被更改了几次。
  • 但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference
  • AtomicStampedReference 需要我们传入 整型变量 作为版本号,来判定是否被更改过
  • AtomicMarkableReference需要我们传入布尔变量 作为标记,来判断是否被更改过

3.字段更新器

保证多线程访问同一个对象的成员变量时, 成员变量的线程安全性。

  • AtomicIntegerFieldUpdater —整形的属性
  • AtomicLongFieldUpdater —长整形的属性
  • AtomicReferenceFieldUpdater —引用类型的属性

注意:利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常。

@Slf4j(topic = "guizy.AtomicFieldTest")
public class AtomicFieldTest {
    public static void main(String[] args) {
        Student stu = new Student();
        // 获得原子更新器
      	// 泛型
      	// 参数1 持有属性的类 参数2 被更新的属性的类
      	// newUpdater中的参数:第三个为属性的名称
        AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
        // 期望的为null, 如果name属性没有被别的线程更改过, 默认就为null, 此时匹配, 就可以设置name为张三
        System.out.println(updater.compareAndSet(stu, null, "张三"));
        System.out.println(updater.compareAndSet(stu, stu.name, "王五"));
        System.out.println(stu);
    }
}

class Student {
    volatile String name;
    //String name; 会抛异常

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }
}

//不加volatile的结果:
Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
//加volatile后打印的结果
true
true
Student{name='王五'}

三、Unsafe对象

AtomicInteger以及其他的原子类, 底层都使用的是Unsafe类。
在这里插入图片描述

1.获取Unsafe对象

在这里插入图片描述
Unsafe 对象提供了非常底层的操作内存、线程的方法,Unsafe 对象不能直接调用,可以通过反射获得。

public class UnsafeAccessor {
    private static final Unsafe unsafe;

    static {
        try {
        	//获取字段
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            //设置可访问
            theUnsafe.setAccessible(true);
            //获取Unsafe对象
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
    }

	//封装的获取Unsafe对象的方法
    public static Unsafe getUnsafe() {
        return unsafe;
    }
}

2.Unsafe对象的CAS

public class TestUnsafe {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    	//获取Unsafe对象
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        System.out.println(unsafe);

        // 1. 获取域的偏移地址
        long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
        long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));

        Teacher t = new Teacher();
        // 2. 执行 cas 操作
        unsafe.compareAndSwapInt(t, idOffset, 0, 20);
        unsafe.compareAndSwapObject(t, nameOffset, null, "张三");

        // 3. 验证
        System.out.println(t);
    }
}
@Data
class Teacher {
    volatile int id;
    volatile String name;
}
//输出结果
Student(id=20, name=张三)

3.自定义简易AtomicInteger类

@Slf4j(topic = "c.Test42")
public class Test42 {
    public static void main(String[] args) {
        Account.demo(new MyAtomicInteger(10000));
    }
}

class MyAtomicInteger implements Account {
    private volatile int value;
    private static final long valueOffset;
    private static final Unsafe UNSAFE;
    static {
        UNSAFE = UnsafeAccessor.getUnsafe();
        try {
            valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    public int getValue() {
        return value;
    }

    public void decrement(int amount) {
        while(true) {
            int prev = this.value;
            int next = prev - amount;
            if (UNSAFE.compareAndSwapInt(this, valueOffset, prev, next)) {
                break;
            }
        }
    }

    public MyAtomicInteger(int value) {
        this.value = value;
    }

    @Override
    public Integer getBalance() {
        return getValue();
    }

    @Override
    public void withdraw(Integer amount) {
        decrement(amount);
    }
}

四、总结

暂无

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值