背景
在进行多线程编程的时候,经常会遇到线程安全的问题,例如一个线程在修改一个变量的时候,另外一个线程也在修改这个变量,而根据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的作用是让线程在进行原子性操作的时候,如果变量的值发生了变化,会立马从主内存中重新取最新的值,保证在计算的时候,值与主内存中的一致。