欢迎访问优讯网!
您当前的位置:首页 > 爱技巧

java高并发(五)线程安全性

时间:2019-07-24 10:26:21  来源:优讯网  作者:小卡司  浏览次数:

定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

线程安全性体现在以下三个方面:

  1.  原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。
  2. 可见性:一个线程对主内存的修改可以及时的被其他线程观察到。
  3. 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

原子性 Atomic包

新建一个测试类,内容如下:

@Slf4j
@ThreadSafe
public class CountExample2 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    // 工作内存
    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        //线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定义信号量
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定义计数器
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for(int i = 0; i < clientTotal; i++) {
            executorService.execute(() ->{
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (InterruptedException e) {

                    log.error("exception", e);
                }
                countDownLatch.countDown();

            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

    public static void add() {
        count.incrementAndGet();
    }

}

使用了AtomicInteger类,这个类的incrementAndGet方法底层使用的unsafe.getAndAddInt(this, valueOffset, 1) + 1;方法,而底层使用了this.compareAndSwapInt方法。这个compareAndSwapInt方法(CAS)是用当前值与主内存的值进行对比,如果值相等则进行相应的操作。

count变量就是工作内存,它与主内存中的数据不一定是一样的,因此需要做同步操作才可以。 

AtomicLong与LongAdder

我们将上面的count用AtomicLong来修饰,同样可以输出正确的效果:

public static AtomicLong count = new AtomicLong(0);

我们为什么要单独说一下AtomicLong?因为JDK8中新增了一个类,与AtomicLong十分像,即LongAdder类。将上面的代码用LongAdder实现一下:

public static LongAdder count = new LongAdder();

public static void add() {
        count.increment();
    }

log.info("count:{}", count);

同样也可以输出正确的结果。

为什么有了AtomicLong后还要新增一个LongAdder?

原因是AtomicLong底层使用CAS来保持同步,是在一个死循环内不断尝试比较值,当工作内存与主内存数据一致的情况下才执行后续操作,竞争不激烈的时候成功几率高,竞争激烈时也就是并发量高时性能就会降低。对于Long和Double变量来说,jvm会将64位的Long或Double变量的读写操作拆分成两个32位的读写操作。因此实际使用过程中可以优先使用LongAdder,而不是继续使用AtomicLong,当竞争比较低的时候可以继续使用AtomicLong。

查看atomic包:

AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。

AtomicIntegerFieldUpdater

原子性更新某个类的实例的某个字段的值,并且这个字段必须用volatile关键字修饰同时不能是static修饰的。

private static AtomicIntegerFieldUpdater<AtomicExample5> updater =
 AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count");
    @Getter
    public volatile int count = 100;

    public static void main(String[] args) {
        private AtomicExample5 example5 = new AtomicExample5();
        if (updater.compareAndSet(example5, 100, 120)){
            log.info("update success 1, {}", example5.getCount());
        }
        if(updater.compareAndSet(example5, 100, 120)){
            log.info("update success 2 ,{}", example5.getCount());
        }else {
            log.info("update failed, {}", example5.getCount());
        }
    }

AtomicStampReference:CAS的ABA问题

ABA问题是:在CAS操作的时候,其他线程将变量的值A改成了B,随后又改成了A,CAS就会被误导。所以ABA问题的解决思路就是将版本号加一,当一个变量被修改,那么这个变量的版本号就增加1,从而解决ABA问题。

AtomicBoolean

@Slf4j
@ThreadSafe
public class AtomicExample6 {

    private static AtomicBoolean isHappened = new AtomicBoolean(false);

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static void main(String[] args) throws InterruptedException {
        //线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定义信号量
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定义计数器
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for(int i = 0; i < clientTotal; i++) {
            executorService.execute(() ->{
                try {
                    semaphore.acquire();
                    test();
                    semaphore.release();
                } catch (InterruptedException e) {

                    log.error("exception", e);
                }
                countDownLatch.countDown();

            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("isHappened:{}", isHappened);
    }

    private static void test() {
        if (isHappened.compareAndSet(false, true)){
            log.info("excute");
        }
    }

}

这段代码test()方法只会被执行5000次而进入log.info("excute")只会被执行一次,因为isHappened变量执行一次之后就变为true了。

这个方法可以保证变量isHappened从false变成true只会执行一次。

这个例子可以解决让一段代码只执行一次绝对不会重复。

原子性 锁

  • synchronized:synchronized关键字主要是依赖JVM实现锁,因此在这个关键字作用对象的作用范围内都是同一时刻只能有一个线程可以进行操作的。
  • lock:依赖特殊的CPU指令,实现类中比较有代表性的是ReentrantLock。

synchronized同步锁

修饰的对象主要有一下四种:

  • 修饰代码块:大括号括起来的代码,作用于调用的对象
  • 修饰方法:整个方法,作用于调用的对象
  • 修饰静态方法:整个静态方法,作用于这个类的所有对象
  • 修饰类:括号括起来的部分,作用于所有对象

举例如下:

@Slf4j
public class SynchronizedExample1 {

    /**
     * 修饰一个代码块,被修饰的代码称为同步语句块,
*作用范围是大括号括起来的代码,作用的对象是调用代码的对象
     */
    public void test1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 - {}", i);
            }
        }
    }
    public static void main(String[] args) {
    SynchronizedExample1 synchronizedExample1 = new SynchronizedExample1();
     ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            synchronizedExample1.test1();
        });
        executorService.execute(() -> {
            synchronizedExample1.test1();
        });
    }

}

为什么我们要使用线程池?如果不使用线程池的话,两次调用了同一个方法,本身就是同步执行的,因此是无法验证具体的影响,而我们加上线程池之后,相当于分别启动了两个线程去执行方法。

输出结果是连续输出两遍test1 0-9。

如果使用synchronized修饰方法:

    /**
     * 修饰一个方法,被修饰的方法称为同步方法,作用范围是整个方法,作用的对象是调用方法的对象
     */
    public synchronized void test2() {
        for (int i = 0; i < 10; i++) {
            log.info("test2 - {}", i);
        }
    }

输出结果跟上面一样,是正确的

来顶一下
返回首页
返回首页

本文未标明来源,如有侵权请联系发布者删除!


推荐资讯
如何下载旧版centos iso镜像 如何下载迷你mini版的centos镜像
如何下载旧版centos i
计算机的正确使用姿势 电脑痴如何正确的使用电脑
计算机的正确使用姿势
好用的后台管理的前端框架模版H-ui H-ui框架模版分享
好用的后台管理的前端
微信电脑多开方法 无需辅助电脑版微信双开方法分享
微信电脑多开方法 无
相关文章
    无相关信息
栏目更新
栏目热门