IOTXING

记录技术学习之路

0%

Java多线程(2)线程安全

背景

在进行多线程编程的时候,经常会遇到线程安全的问题,例如一个线程在修改一个变量的时候,另外一个线程也在修改这个变量,而根据java的内存模型,每个工作线程都会先把变量从主内存中拉回到自己的内存,在修改了之后再刷新回主内存,所以就会出现数据不一致的问题,例如下面的对count进行累加

private static ExecutorService executorService;
private int count = 0;

void addCount() {
    while (count < 1000000) {
        try {

            count += 1;
            System.out.println(count + "-------" + Thread.currentThread().getName());
            Thread.sleep(10);
        } catch (InterruptedException e) {
            System.out.println("捕获到异常");
        }
    }

}
public Testsynchronized() {
    executorService = Executors.newFixedThreadPool(50);
    for (int i = 0; i < 50; i++) {
        executorService.execute(() -> addCount());
    }
}

这里起了一个线程池,然后用50个线程同时去对count进行累加,结果如下

可以看见结果完全是乱序的,而且会出现重复的情况,不难推算最终的结果会是错误的。

sychronized

sychronized是一个关键字,它能够保证被装饰的对象或者方法,同时只能有一个线程进行操作,进而来保证线程的安全。

private static ExecutorService executorService;
private int count = 0;
static int totalCount = 100000;
final CountDownLatch countDownLatch = new CountDownLatch(50);

synchronized void addCount() {
while (count < totalCount) {

        count += 1;
        System.out.println(count + "-------" + Thread.currentThread().getName());
    }
    countDownLatch.countDown();
}

public Testsynchronized() {
    Long startTime = System.currentTimeMillis();
    executorService = Executors.newFixedThreadPool(50);
    for (int i = 0; i < 50; i++) {
        executorService.execute(() -> addCount());
    }

    try {
        countDownLatch.await();
        Long stopTime = System.currentTimeMillis();
        System.out.println("耗时为" + (stopTime - startTime));
    } catch (InterruptedException e) {
        System.out.println("捕获到异常");
    }

}

上面我们对addCount方法使用synchronized进行修饰,在多线程调用addCount的时候,同时只有一个线程对count进行操作,所以最终的结果能够满足我们的期望 从结果我们能够看到,都是thread—1执行的addCount,这是因为synchronized方式是可重入锁,也就是说在获得锁的时候,更倾向于之前获得过锁的线程,去获得新锁,这个会有专门文章对此进行介绍。

Lock

java的Lock有多种,这里我们使用ReentrantLock可重入锁来进行验收。通过在方法体内,我们获取锁,然后执行方法,在执行完之后再释放锁,这样能够保证同时只有一个线程获取锁,然后进行相关操作。

private static ExecutorService executorService;
private int count = 0;
static int totalCount = 100000;
private Lock lock = new ReentrantLock();
final CountDownLatch countDownLatch = new CountDownLatch(50);

void addCount() {
    lock.lock();
    while (count < totalCount) {
        count += 1;
        System.out.println(count + "-------" + Thread.currentThread().getName());
    }
    countDownLatch.countDown();
    lock.unlock();
}


public Testsynchronized() {
    Long startTime = System.currentTimeMillis();
    executorService = Executors.newFixedThreadPool(50);
    for (int i = 0; i < 50; i++) {
        executorService.execute(() -> addCount());
    }

    try {
        countDownLatch.await();
        Long stopTime = System.currentTimeMillis();
        System.out.println("耗时为" + (stopTime - startTime));
    } catch (InterruptedException e) {
        System.out.println("捕获到异常");
    }

}

volatile

volatile关键字能够让线程在使用被装饰的对象时,从主内存刷新最新的值,然后再进行使用。因为jmm定义的内存模型中,是每个线程拥有自己的工作内存,所需的变量会从主内存中复制一份到本地来,计算结束之后再刷新回主内存中。如果没有任何限制的话,就会出现a,b线程同时在本地修改了变量count,然后a先刷新回主内存,之后b再刷新回主内存的时候,就会把a的值给覆盖掉了。还有种情况是a已经取了所需的变量到工作内存里,这时候b修改了count变量,等到a开始计算count变量的时候,值已经不是最新的值了,就会产生数据不一致。 votile的作用是让线程在进行原子性操作的时候,如果变量的值发生了变化,会立马从主内存中重新取最新的值,保证在计算的时候,值与主内存中的一致。