初识并发编程(二) 并发编程与线程安全

 1:线程安全性

 1.1定义

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

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

 1.2 原子性-Atomic

​         Atomic运用到 比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

 1.2.1 AtomicXXX

        比如:AtomicInteger count = new AtomicInteger();  创建一个Integer类型数据,通过incrementAndGet()方法新增。

Demo:

package com.mmall.concurrency.example.count;

import com.alibaba.fastjson.JSON;
import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 线程安全
 *
 * 比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。
 */
@Slf4j
@ThreadSafe
public class ConcurrencyTestSafe {

    /**
     * 请求总数
     */
    public static int clientTotal = 5000;
    /**
     * 同时并发执行的线程数
     */
    public static int threadTotal = 200;

    /**
     * Atomic
     */
    public static AtomicInteger count = new AtomicInteger();


    /**
     * Semaphore:   允许并发的数量  [Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。]
     *  1):在进行操作的时候,需要先acquire获取到许可,才可以继续执行任务, 如果获取失败,则进入阻塞;处理完成之后需要release释放许可。
     *  2): acquire与release之间的关系:在实现中不包含真正的许可对象,并且Semaphore也不会将许可与线程关联起来,
     * 因此在一个线程中获得的许可可以在另一个线程中释放。可以将acquire操作视为是消费一个许可,而release操作是创建一个许可,Semaphore并不受限于它在创建时的初始许可数量。
     * 也就是说acquire与release并没有强制的一对一关系,release一次就相当于新增一个许可,许可的数量可能会由于没有与acquire操作一对一而导致超出初始化时设置的许可个数。
     */


    /**
     * CountDownLatch:  CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。
     * 1、CountDownLatch countDownLatch = new CountDownLatch(N); //构造对象时候 需要传入参数N
     * 2、countDownLatch.await()  能够阻塞线程 直到调用N次end.countDown() 方法才释放线程
     * 3、countDownLatch.countDown() 可以在多个线程中调用  计算调用次数是所有线程调用次数的总和
     *
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        //1定义一个线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //2定义信号量  运行并发的次数
        final  Semaphore semaphore = new Semaphore(threadTotal);
        //3定义计数器
        final   CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        //4执行 【clientTotal】次循环
        for (int i = 0; i < clientTotal; i++) {

            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("error msg:{};JONSE:{}", e.getMessage(), JSON.toJSONString(e));
                }
            });

            countDownLatch.countDown();
        }
        countDownLatch.await();
        //关闭线程池
        executorService.shutdown();
//此时返回的 count结果确定,此是一个安全的线程
        log.info("count {}", count);

    }
    private static void add() {
        //线程安全主要是因为 compareAndSwap (CAS)
        //compareAndSwapInt 底层方法,不是通过java实现   【不断循环一直到获取想要(正确)的值为止】
        count.incrementAndGet();
//        count.getAndIncrement();
    }

}

 1.2.2 AtomicLong和LongAdder的区别

​        AtomicLong的原理是依靠底层的cas来保障原子性的更新数据,在要添加或者减少的时候,会使用死循环不断地cas到特定的值,从而达到更新数据的目的。【低并发推荐】

​        LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。
  缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。【高并发推荐】

 2:原子性-锁

原子性:提供互斥访问,同一时刻只能有一个线程来对其进行操作

除了Atomic包之外,还有锁

 2.1 jdk提供锁

 synchornized:

​    依赖JVM 。synchornized关键字作用对象的作用范围内,都是同一时刻只有一个线程可以进行操作。【比较耗费资源,推荐Lock】

 Lock:

​    依赖特殊的CPU指定,代码实现,有代表性的是:ReentrantLock。

 2.2 synchornized

        修饰代码块: 大括号括起来的代码,<font style="color:red">作用于调用的对象</font>。 不同对象是互不影响的;

        修饰方法: 整个方法, 作用于调用的对象 。 不同对象是互不影响的;

        修饰静态方法: 整个静态方法, 作用于所有对象 。

        修饰类: 括号括起来的部分, 作用于所有对象 。

代码示例:

        修饰静态方法和修饰类:

package com.mmall.concurrency.example.syncTs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
public class  synchornizedExampleStatusTs {
    public static void main(String[] args) {
        synchornizedExampleStatusTs synchornizedExampleTs=new synchornizedExampleStatusTs();
        synchornizedExampleStatusTs synchornizedExampleTs2=new synchornizedExampleStatusTs();
        //声明线程池
        ExecutorService executorService= Executors.newCachedThreadPool();
//        //两个同时进行,而不是第二个等到第一个执行完毕才执行
        executorService.execute(()->{
            test1(1);
        });

        executorService.execute(()->{
            test2(2);
        });

//        synchornizedExampleTs.test1(1);
//        synchornizedExampleTs2.test1(2);
    }

    /**
     * 修饰一个类
     */
    public static void test1(int num) {
        synchronized (synchornizedExampleStatusTs.class) {
            for (int i = 0; i < 10; i++) {
                log.info("num:{} this is test1.i:{}",num, i);
            }
        }
    }

    /**
     * 修饰一个静态 方法
     */
    public static synchronized void test2(int num){
        for (int i = 0; i < 10; i++) {
            log.info("num :{} this is test2.i:{}",num, i);
        }
    }

}

        修饰代码块和方法:

package com.mmall.concurrency.example.syncTs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
public class synchornizedExampleTs {
    public static void main(String[] args) {
        synchornizedExampleTs synchornizedExampleTs=new synchornizedExampleTs();
        synchornizedExampleTs synchornizedExampleTs2=new synchornizedExampleTs();
        //声明线程池
        ExecutorService executorService= Executors.newCachedThreadPool();
//        //两个同时进行,而不是第二个等到第一个执行完毕才执行
        executorService.execute(()->{
            synchornizedExampleTs.test1(1);
        });

        executorService.execute(()->{
            synchornizedExampleTs2.test2(2);
        });

//        synchornizedExampleTs.test1(1);
//        synchornizedExampleTs2.test1(2);

    }

    /**
     * 修饰一个代码块
     */
    public void test1(int num) {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("num:{} this is test1.i:{}",num, i);
            }
        }
    }

    /**
     * 修饰一个方法
     */
    public synchronized void test2(int num){
        for (int i = 0; i < 10; i++) {
            log.info("num :{} this is test2.i:{}",num, i);
        }
    }
}

 2.3原子性-对比

Lock:可中断锁,多样化同步,竞争激烈时候能维持常态。

synchronized:  不可中断锁,适合不竞争激烈,可读性好。

Atomic:竞争激烈时候能维持常态,比Lock性能好,只能同步一个值。

 2.4可见性

 2.4.1导致共享变量在线程间不可见的原因

        线程交叉执行
​        重排序结合线程交叉执行
​        共享变量更新后的值没有在工作内存与主存间及时更新

 2.4.2可见性- synchronized 

                        JMM 关于 synchronized 的两条规定:
         线程解锁前,必须把共享变量的最新值刷新到主内存
         线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)

 2.4.3可见性- volatile

​        synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:       

         1:对变量的写操作不依赖于当前值(就是不能是自增自减等操作,volatile 不具有原子性,适合作为状态标记量(true/false  ....))
         2:该变量没有包含在具有其他变量的不变式中

  

通过加入内存屏障和禁止重排序优化来实现
​        对 volatile 变量写操作时,会在写操作后加入一条 store 屏障指令,将本地内存中的共享变量值刷新到主内存。
​        对 volatile 变量读操作时,会在读操作前加入一条 load 屏障指令,从主内存中读取共享变量。

 2.4.4有序性

​        Java 内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
         volatile 、 synchronized 、 Lock 

有序性- happens - before 原则
        程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
        锁定规则:一个 unLock 操作先行发生于后面对同一个锁的 lock 操作
        volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
        传递规则:如果操作 A 先行发生于操作 B ,而操作 B 又先行发生于操作 C ,则可以得出操作 A 先行发生于操作 C 
        线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join(方法结束)、Thread.isAlive (的返回值手段检测到线程已经终止执行)
        对象终结规则:一个对象的初始化完成先行发生于他的 finalize ()方法的开始

2.4.5可重入性

        synchronized:同一个线程的外层函数获得锁后,内层函数可以直接获取该锁。

        Synchronized可重入性的两个优点:
                避免死锁
        避免死锁的原因: 如果synchronized不具备可重入性,当一个线程想去访问另一个方法时,它自身已经持有一把锁,而且还没有释放锁,又想获取另一个方法的锁,于是造成了永远等待的僵局,就会造成死锁。有了可重入性后,自己持有一把锁,并且可以直接进入到内层函数中,就避免了死锁。

                提升封装性
        编程人员不需要手动加锁和解锁,统一由JVM管理,提高了可利用性。

关于synchronized 可重入性参考:Java中Synchronized的可重入性和不可中断性的分析和代码验证_五道口-CSDN博客_synchronized 不可重入

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值