并发编程三大特性——原子性

1、通过程序认识什么是原子性,举个例子如下,启动100线程,每个线程执行10000次n++操作,理论上等待100个线程都执行完以后,主线程会输出100 0000,接下来我们来看看执行结果。

package com.yang.Threads;

import java.util.concurrent.CountDownLatch;

/**
 * @Author: Gy
 * @Description: synchronized悲观锁 保证原子性
 * @Date 
 * @Modified By:
 */
public class TestAtomicity {

    public static int num = 0;

    public static Thread[] ts = new Thread[100];

    public static CountDownLatch count = new CountDownLatch(ts.length);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < ts.length; i++) {
            ts[i] = new Thread(()->{

                for (int j = 0; j < 10000; j++) {
//--A --                   synchronized (TestAtomicity.class){
                        			num ++;
//--B---                    }
                }
                count.countDown();
            });
            ts[i].start();

        }
        count.await();
        System.out.println(num);
    }
}

执行结果为:
在这里插入图片描述
看了上面的执行结果,是不是有点出乎意料了,我们预期的结果100 0000呢?(原因是多线程共享变量n时,n++没有保证原子性操作)

2、race condition 竞争条件
指的是多个线程访问共享变量的时候产生竞争,这种环境下容易产生预期之外的结果,也就是数据的不一致。上边的小程序100个线程共同访问共享变量n,每个线程先要从内存中把n读出来,然后执行自增,最后把自增后的值写回内存,如果线程t1在回写到内存之前,线程t2读出n的值,那么t1和t2执行完,相当于只做了一次自增操作,或者说t1和t2做了同样的操作,所以说结果小于预期的100 0000是因为线程之间做了好多相同的操作。如果能够保证每个线程的每一次n++操作都是一个完整的过程,中间不被其他线程捣乱(t1回写完t2再去读),保证线程之间有序执行(线程同步),这个问题就迎刃而解了

3、那么如何保证原子性操作呢呢?答案是上锁,把上面程序中注释的A、B分别释放开,就能够保证预期结果,synchronized代码块中的整体部分,就是同步操作,中间不可被打断。

4、上锁的本质是把并发执行序列化,看小程序

package com.yang.Threads;

import java.util.concurrent.TimeUnit;

/**
 * @Author: Gy
 * @Description:  三个线程并发执行,大约2秒同时执行完
 * @Date 
 * @Modified By:
 */
public class TestSync {
    private static Object obj = new Object();

    public static void main(String[] args) {
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName()+ "start...");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"end");
        };
        for (int i = 0; i < 3; i++) {
            new Thread(runnable).start();
        }
    }
}

在这里插入图片描述
如果加上synchronized,三个原本并发执行的线程变成了序列化执行(同步),每个线程的执行都变成了原子性操作,可以把代码中sleep()方法想象成我们具体的业务代码,经过大约6秒三个线程分别执行完,每个线程执行各自的业务是不会被其他线程干扰的。代码及结果如下:

package com.yang.Threads;

import java.util.concurrent.TimeUnit;

/**
 * @Author: Gy
 * @Description:  三个线程同步(序列化)执行,大约共6秒分别执行完
 * @Date 
 * @Modified By:
 */
public class TestSync {
    private static Object obj = new Object();

    public static void main(String[] args) {
        Runnable runnable = () -> {
            synchronized (obj){

                System.out.println(Thread.currentThread().getName()+ "start...");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"end");
            }
        };
        for (int i = 0; i < 3; i++) {
            new Thread(runnable).start();
        }
    }
}

在这里插入图片描述
通过不加锁和加锁的效果看,加锁后执行时间是延长的,原本的并发操作转为序列化操作后,执行效率势必会有所降低

5、锁的粒度

  • monitor(管程),在操作系统层面,把锁对象叫做monitor,也就是小obj对象
  • critical section (临界区),synchronized大括号包含的代码区域,见下图
  • 在这里插入图片描述
  • 如果临界区代码语句比较多,执行时间长,那么就说明锁的粒度是比较粗的;反之,锁的粒度比较细。所以说我们给业务代码上锁的时候,要看哪些操作需要上锁,哪些没必要上锁,保证锁的粒度尽可能细

6、保证原子性(Atomicity)操作的2种方式

  • 悲观锁(synchronized):悲观的认为当前操作会被别的线程打断或干扰,例子看上面使用synchronized的小程序
  • 乐观锁/自旋锁/无锁(CAS—Compare And Swap):乐观的认为当前操作不会被别的线程打断或干扰。
    下面说说CAS是个啥,假设一个场景,定义一个变量int n = 0;多线程环境下执行n++;操作,n++本身不是原子性操作,那么使用CAS是如何使其达到原子性操作的效果呢?看下图
    在这里插入图片描述
    ABA问题:如果共享变量n是一个基本类型如int,那么不用解决这个问题,可以忽略;如果共享变量是一个引用对象,那么就可能对业务造成影响了。解决ABA问题的办法,可以使用添加版本号或者时间戳等方式,具体的实现方式可以去自学一下。
    Atomic类使用的就是CAS的机制,看小程序
package com.yang.Threads;


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author: Gy
 * @Description: CAS乐观锁  Atomic类使用的就是CAS机制保证原子性
 * @Date 
 * @Modified By:
 */
public class TestAtomicity2 {

    public static AtomicInteger count = new AtomicInteger(0);

    static void m(){
        for (int i = 0; i < 10000; i++) {
            // 这一步保证了原子性  实现compare and swap
            count.incrementAndGet();
        }
    }

    public static void main(String[] args) {
        List<Thread> threadList = new ArrayList<Thread>();

        for (int i = 0; i < 100; i++) {
            threadList.add(new Thread(TestAtomicity2::m,"Thread-" + i));
        }
        threadList.forEach((o)->{
            o.start();
            try {
                //保证每个子线程先执行完
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
      /*  //让主线程睡眠一段时间,保证所有子线程都执行完,主线程再结束
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
        System.out.println(count);
    }
}

7、乐观锁与悲观锁的使用场景
如果临界区代码执行时间比较长,并且并发的线程多,采用重量级锁;如果临界区执行时间短,并发线程数量少,采用自旋锁。原因是使用重量级锁时,等待的线程是不消耗cpu资源的,而采用自旋锁,等待的线程一直(while)问锁什么时候释放,是消耗cpu资源的。理论上的选择规则是这样的,但是经过对synchronized的不断优化,现在的synchrohnized性能已经得到了很大的提升,添加了一个锁升级的过程,所以实战中我们完全可以直接使用synchronized加锁。具体这个锁升级的过程是如何的,自行了解

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值