并发编程之原子性

原子性

多线程访问共享数据的时候会产生竞争

多线程并发访问之下产生的不期望出现的结果

请看如下小程序:

public class T00_00_IPlusPlus {
    private static long n = 0L;

    public static void main(String[] args) throws Exception {

        //Lock lock = new ReentrantLock();

        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
//                    synchronized (T00_00_IPlusPlus.class) {
                        //lock.lock();
                        n++;
                        //lock.unlock();
                    //}
                }
                latch.countDown();
            });
        }

        for (Thread t : threads) {
            t.start();
        }

        latch.await();

        System.out.println(n);
    }
}

结果如下所示:

image-20221203151746018

正常来说:应该显示的结果是100个线程,每个线程分别对n加了10000次,最终的结果应该是1000000次。但是由于多个线程访问共享数据n时产生了竞争,最终导致了数据的不一致,最终结果也不是我们所需要的

具体点来说: ++操作是可以被其它线程打断的,线程1从内存中读取n的值0送给寄存器计算后,然后由寄存器把计算后的新值写入到内存中。在写入之前,线程2直接从内存中读取了还未更新的n的值为0,然后由寄存器计算,再次写入内存。那么就造成了线程1与线程2对n++的操作最终的值都变为1,而不是我们想要的最终值2。

上锁的本质

上锁的本质就是把并发编程序列化

请看如下小程序:

public class T00_01_WhatIsLock {
    private static Object o = new Object();

    public static void main(String[] args) {
        Runnable r = () -> {
            //synchronized (o) { //打开注释试试看,对比结果
                System.out.println(Thread.currentThread().getName() + " start!");
                SleepHelper.sleepSeconds(2);
                System.out.println(Thread.currentThread().getName() + " end!");
            //}
        };

        for (int i = 0; i < 3; i++) {
            new Thread(r).start();
        }
    }
}

结果如下所示:

两秒钟,三个线程同时开始同时结束,也就是并发操作

image-20221203154228919

当加上Synchronized后,一个线程执行完毕后,下一个线程才会继续执行,也就是序列化操作,效率会比较低。结果变为:

image-20221203154410965

总结:Synchronized保证可见性与原子性,但是不保证有序性

思考 为什么Synchronized不保证有序性?

如果说有序性是获取锁的公平锁概念的话,因为syn是非公平锁,无法保证获取锁的顺序

如果说的是指令的有序执行,因为这个是CPU的事,CPU底层会为了优化执行效率或者安全性考虑,从而进行指令的重排序,所以无论是否使用syn,都没有办法保证指令的有序性

如何保障操作的原子性?
  1. 悲观的认为这个操作会被别的线程打断(悲观锁)synchronized
  2. 乐观的认为这个操作不会被别的线程打断(乐观锁也称自旋锁、无锁)cas操作
CAS基础概念

image-20221203170148919

将内存中指定位置的值与所期望的值比较,只有当内存当中的值和期望的值相同的时候,才会把内存中的值更新为一个新的值,这是一个原子的操作。如果内存中的值同时被其他线程操作那么此次更改将会失败。

CAS的ABA问题?

线程1从内存中拿到了n的值0,并对值0进行加1操作后,然后由线程1去查看最初的值是否还为0时,在这期间,来了线程2,把0的值改为了8,紧接着线程3把值8又改回了0。这个时候用CAS检查时值是没有发生变化的,虽然最终的值是0。但是它的引用指向发生了变化,也就是CAS经典的ABA问题。

如何解决该问题?

为CAS中的每个操作添加一个版本号,每次执行成功之后,都会对版本号进行+1操作,每次更新数据之前都会对比版本号,观察是否有其他线程进行干扰.如果版本号一制则继续,不一致进入自旋

请看如下小程序,认知CAS为何一定要保证原子性?

if(v == 0) v = 1

线程1在读取0时,然后对0进行加1操作,往回写入判断v == 0时并还没有执行v = 1操作时,突然来了一个线程2打断了该操作,把v的值该为了8,然后线程1继续操作,把v的值又改回了1,这时就出现问题了。也就是说CAS为何一定要保证原子性的原因

CAS如何保证原子性?

请看如下小程序:

使用AtomicInteger保证原子性

public class T01_AtomicInteger {
	/*volatile*/ //int count1 = 0;

	AtomicInteger count = new AtomicInteger(0);

	/* synchronized */void m() {
		for (int i = 0; i < 10000; i++)
			//if count1.get() < 1000
			count.incrementAndGet(); //count1++
	}

	public static void main(String[] args) {
		T01_AtomicInteger t = new T01_AtomicInteger();

		List<Thread> threads = new ArrayList<Thread>();

		for (int i = 0; i < 100; i++) {
			threads.add(new Thread(t::m, "thread-" + i));
		}

		threads.forEach((o) -> o.start());

		threads.forEach((o) -> {
			try {
				o.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		});

		System.out.println(t.count);
	}
}

结果如下所示:

image-20221203173903964

底层实现:调用unsafe类的CompareAndSwapInt方法,最终实现是lock cmpxchg 指令

CAS 宏观上我们认为是自旋锁、乐观锁

CAS微观上我们认为是悲观锁,lock锁,锁的是总线或者是锁的所在的缓存行

单核底层没有必要在cmpxchg指令前加lock,只有多核才有必要加lock指令

悲观锁与乐观锁的效率?

针对不同场景,有不同的选择

悲观锁:临界区执行时间比较长,等的人很多,线程都在队列里进行等待,不消耗CPU资源

乐观锁(自旋锁):时间短,等的人少,没抢占到锁,会一直重试,会进行线程的切换,自旋次数过多会大量消耗CPU资源

思考:Synchronized如何保障可见性的?

image-20221203182043496

ThreadA上锁,上完锁之后执行数据,然后解锁,解锁完成之后,会将缓存中修改的内容刷新到主存中,确保了共享变量的值是最新的,这个地方底层也是使用到了lock语句,也是做了内存同步,然后ThreadB才会执行,获取到最新的x的值

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

连梓豪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值