Java并发编程之线程安全性

线程安全性:

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

 

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

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

可见性:一个线程对主内存的修改可以及时的被其他线程观察到

有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序

一.原子性:

1.atomic包

我们可以模拟用工具压测例如JMeter,AB等测试程序是否线程安全,也可以用代码来实现,这里我们实现有5000个请求,200个并发的情况来进行程序的压测。

压测代码如下:

@Slf4j
public class CountExample {

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

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

    public static int count = 0;


    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //限制最多只有200个线程同时并发操作
        Semaphore semaphore = new Semaphore(threadTotal);
        //初始值为5000,调用countDownLatch.countDown()之后会自动减1,
        CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();
                        add();
                        semaphore.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                }
            });
        }
        //阻塞这句代码下面的代码的执行,知直到countdown为0
        countDownLatch.await();
        executorService.shutdown();
        log.info("count = {}",count);

    }

    public static void add(){
        count++;
    }
}

Semaphore这个类翻译过来是信号量的意思,表示只能有规定的线程去执行一段代码,这是设置的是200个线程。首先,线程池开启线程,每个线程执行时都会执行semaphore.acquire这句代码,最后是semaphore.release,中间是add()进行count+1的操作,线程在执行add()前都会去看一下当前是否已经有200个线程在操作这个方法,有的话就加上锁,当前线程阻塞,等有线程执行完了就再去执行,然后最后释放锁,这样可以保证每次同时只有最多200个线程操作add()方法。

CountDownLatch这里我们初始化传入了5000,在线程每次执行完add()方法之后就执行countDownLatch.countDown()方法进行自动减1操作,而且在线程池释放前加上了countDownLatch.await(),这句代码作用的当countDownLatch里面的值减为0时就能执行下面的代码,否则就阻塞。

当然上面的代码肯定是线程不安全的。

所以引出了我们的atomic包。

(1)AtomicInteger,AtomicLong

我们对add()方法进行改造:

    public static AtomicInteger count = new AtomicInteger(0);
    

    public static void atomicAdd(){
        count.incrementAndGet();
        //count++;
    }

执行多次,发现结果都是一样的:

 说明这个类是线程安全的。

下面我们来看一下这个类里面的原理:

点进去:

,继续点:

这个就是这个类的核心实现了,其中compareAndSwapInt方法是底层实现的。

 var1是我们的count对象,var2是当前的值,var4是要加多少,var5是根据getIntVolatitle方法从底层取出来的值,然后把值都传进compareAndSwapInt方法中,这个方法首先会判断传进来的var2的值和从底层var5的值进行对比,如果相同的话进行相加的操作,如果不相同的话就重新从底层取出var5再进行判断,以此循环。

(2)AtomicBoolean

这个类与上面的类大同小异,只不过针对的boolean类型的数据类型。

@Slf4j
public class AtomicExample2 {

    private static AtomicBoolean isHappened = new AtomicBoolean(false);

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

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

    private static int count;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //限制最多只有200个线程同时并发操作
        Semaphore semaphore = new Semaphore(threadTotal);
        //初始值为5000,调用countDownLatch.countDown()之后会自动减1,
        CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();
                        test();
                        semaphore.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                }
            });
        }
        //阻塞这句代码下面的代码的执行,知直到countdown为0
        countDownLatch.await();
        executorService.shutdown();
        log.info("count = {}",count);
    }

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

执行结果:

可以看到test方法只被执行了一次,因为我们的初始化的isHappened的值是false,而我们的compareAndSet方法意思是如果当前的值是false的话,就更新为true,而且是保证的是只有一个线程的执行,所以当第一个线程来到这个方法的时候发现值是false然后更新为true,之后的4999个线程拿到的值就都是true了,所以test方法里面的log指挥打印一次了,这个类我们可以保证当并发的时候我们的方法里面的逻辑只被执行一次的时候可以使用该类。

2. synchronized关键字

synchronized关键字也是原子性体现的一种,也是保证每次执行只有一个线程,而且注意一点的是synchronized关键字锁住的不是代码,而是对象。锁住的是对象这怎么理解尼?通过例子来看:

@Slf4j
public class SynchronizedExample {

    public static void main(String[] args) {
        SynchronizedExample synchronizedExample1 = new SynchronizedExample();
        SynchronizedExample synchronizedExample2 = new SynchronizedExample();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                synchronizedExample1.test1(1);
            }
        });

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                synchronizedExample1.test1(3);

            }
        });

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                synchronizedExample2.test1(2);
            }
        });

    }

    //修饰代码块
    public void test1(int j){
        synchronized (this){
            for (int i = 0;i < 3 ;i++){
                log.info("test1 {}-> {}",j,i);
            }
        }
    }

    //修饰方法
    public synchronized void test2(){
        for (int i = 0;i < 3 ;i++){
            log.info("test2 -> {}",i);
        }
    }
}

结果:

 我们可以发现除了最后三条log是顺序的之外,其余的log输出都是乱序的,这是为什么尼?这就是synchronized关键字锁住的是类的实例对象的问题导致的了。我们new了两个对象synchronizedExample1和synchronizedExample2,分别一个在两个线程中都调用了test方法,一个在一个线程中调用了一次test方法,由于synchronized关键字锁住的是类的实例对象,所以synchronizedExample1对象和synchronizedExample2对象调用的test方法都可以互相干扰(即test方法在同一时间并不是只有一个线程去执行,而是两个线程),而synchronizedExample1在两个线程中都调用了test方法,由于是同一个对象的调用,所以此时的synchronized关键字就起作用了,它能使者两个线程中只能有一个执行完test方法,另外一个阻塞知道上面的线程执行完。所以这就很明显了,synchronized关键字锁住的是同一个对象调用的代码段。

synchronized的修饰类型与被锁的对象:

(1)修饰代码块 -->类的实例对象

(2)修饰方法 --> 类的实例对象

(3)修饰类的静态方法 -->类的任意对象(全局锁)

(4)修饰类-->类的任意对象(全局锁)

上面举出了(1)和(2)的修饰类型的例子,(3)和(4)就和(1)(2)的不同了,不同在于对于类的任意的对象,synchronized关键字都能起作用,这就是全局锁的意思,还是上面的例子,如果我们换成的是(3)或者(4)的修饰类型的话,那么得到log日志打印结果应该是顺序的。

 二.可见性------volatile关键字

volatile作为java中的关键词之一,用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。volatile会禁止指令重排。

                                                                 

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。

而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。

private volatile boolean init = flase;

//线程1
   ......
init = true;

//线程2
while(!init){
if(init){
......
}


}

使用volatile关键字我们可以用在两个线程之间作为共享变量条件,当线程1初始化完改变了init值之后,线程2会立马感知到init的值发生了变化,然后进行后续的操作,这就利用了线程安全的可见性。

 

三.有序性

Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的:如果在一个线程中观察另外一个线程,所有的线程操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行的进入。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值