0
点赞
收藏
分享

微信扫一扫

【多线程】送你1万朵玫瑰花

伢赞 2022-04-27 阅读 30

一、故事引入

        小明和小红在谈恋爱,小红对小明说:如果你送我一万朵玫瑰花,我们就结婚。小明于是像打了鸡血一样,决定每次送一百朵玫瑰花,只要送够一百次就有1万朵玫瑰花啦!说干就干小明写起了多线程进行计算。

二、思路分析

        创建一个线程类,继承Thread类,重写run方法,并且创建100个线程进行运行。

三、代码实现

        线程类: 

public class MyThread extends Thread{

private static int count = 0;

@Override
public void run() {
for (int i = 0; i < 100; i++) {
count++;
System.out.println("送出了 "+count+" 朵玫瑰花!");
}
}

}

        测试类:

public class DemoTest {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new MyThread().start();
}
}
}

        运行三次的结果分别为:

        可见三次结果是不一样的,我们惊讶的发现,竟然结果有两次9999朵,剩下的那一朵跑哪里去 了。

 四、问题分析

        共享数据是存放在堆里的,当线程访问共享数据时,会复制一份共享数据存放在自己的线程栈中,当修改完成之后,会将自己线程栈中的数据进行修改,然后将线程栈中的数据赋值到堆数据里面去。

        但是,在并发环境中,上面的每一步都可能被其他线程打断。假如说有一个线程,复制了一份共享数据(假设 共享数据 count =100 )在自己的线程栈中,并且进行了修改(count=101),此时,他会把数据赋值给共享数据。此时,线程被打断,另外一个线程也进行了线程的修改(共享数据是count=100,修改后count=101),而且修改的值是同样的,那么这个时候他们分别赋值给了共享数据,此时共享数据被修改了两次,理应从100变成了102,但是由于两个线程一开始获取到的共享数据都是100,所以即使修改了两次,但是共享数据确显示被修改了一次。

五、传统方法:同步代码块【悲观锁】

          看了上面的例子,估计你马上会想到,同步代码块!是的!同步代码块确实能够解决这个问题: 

public class MyThread extends Thread{

private static int count = 0;
// 创建唯一锁
private static Object obj = new Object();

@Override
public void run() {
for (int i = 0; i < 100; i++) {
/* 温馨提示:这里不能使用this关键字,因为我们调用的时候,创建了100个对象,this表示的是当前对象
* 不能确定锁唯一
* */

synchronized (obj){
count++;
System.out.println("送出了 "+count+" 朵玫瑰花!");
}
}
}

}

        运行结果无论运行多少次都是如此:(不信你去试试看)

        

 六、原子操作类AtomicInteger【乐观锁】

        在Java的 java.util.concurrent.atomic包中,给我们提供了:原子更新基本数据类型、原子更新数组、原子更新引用、原子更新属性。我们都知道,使用 synchronized 关键字实际上是给代码块(方法)加锁,但是加锁会影响程序的效率。而java.util.concurrent.atomic包给我们提供了几种常见的原子更新基本数据类型,其中我们将用AtomicInteger来解决我们这个问题,在使用之前,我们大概了解一下这个类的两个方法和两个构造方法

        1、public AtomicInteger():无参构造,默认创建的是 0,使用较多;

        2、public AtomicInteger(int initialValue):给定具体值得构造参数

        具体代码如下图所示:

AtomicInteger i1 = new AtomicInteger();
System.out.println(i1); // 0
AtomicInteger i2 = new AtomicInteger(10);
System.out.println(i2); // 10

        3、 public final int getAndIncrement():先获取,再自增,类似于count++

        4、public final int incrementAndGet():先自增,后获取,类似于++count

        通过下面给得代码对比,你就懂了:

AtomicInteger i1 = new AtomicInteger();   // 0
int andIncrement = i1.getAndIncrement(); // 1++
int incrementAndGet = i1.incrementAndGet(); // ++1
System.out.println("先获取,后自增:"+ andIncrement);
System.out.println("先自增,后获取:"+ incrementAndGet);

int count = 0;
System.out.println("先获取,后自增:"+(count++));
System.out.println("先自增,后获取:"+(++count));

 

        通过结果对比,应该就可以很清晰的知道他们两个的区别哦!

七、AtomicInteger改进程序

        线程类:

import java.util.concurrent.atomic.AtomicInteger;

public class MyThread extends Thread{

private static AtomicInteger atomicInteger = new AtomicInteger(0);

@Override
public void run() {
for (int i = 0; i < 100; i++) {
int count = atomicInteger.incrementAndGet();
System.out.println("送出了 "+count+" 朵玫瑰花!");
}
}

}

        测试类不变。测试结果正常:

         

 八、总结

        当打开AtomicInteger的源码时,你会发现它是没有加锁的。因为它认为每次自己操作数据的时候认为没有人会来修改它,所以不去加锁。很乐观。但是在更新的时候会去判断在此期间数据有没有被修改(CAS算法+自旋)。所以他是乐观锁的实现。

        在以前我们使用同步代码块的时候,我们会发现,它是每一次都会判断锁对象的。它对于数据被外界修改的操作持保守态度,认为数据随时会修改。整个数据处理中需要将数据加锁。这就是悲观锁的实现。。

举报

相关推荐

0 条评论