作者:木色雪魂K | 来源:互联网 | 2023-12-13 15:43
本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。
下面的代码演示了一个计数器,两个线程同时对i进行累加操作,各执行10000000次。在很多时候,i的最终值会小于20000000。这就是因为两个线程同时对i进行写入时,其中一个线程的结果会覆盖另一个。
public class AccountingVol implements Runnable {
static AccountingVol instance = new AccountingVol();
static volatile int i =0;
public static void increase() {
i++;
}
@Override
public void run() {
for(int j=0; j<10000000; j++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
要从根本上解决这个问题,我们就必须保证对个线程对i进行操作时完全同步。也就是说,当线程A在写入时,线程B不仅不能写,同时也不能读。
关键字synchronized可以有多种用法。
1)指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
2)直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
3)直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
下述代码中,将synchronized作用于一个给定对象instance,因此,每次当线程进入被synchronized包裹的代码段,就都会要求请求instance实例的锁。如果当前有其他线程正持有这把锁,那么新到的线程就必须等待。这样,就保证了每次只能有一个线程执行i++操作。
public class AccountingSync implements Runnable{
static AccountingSync instance = new AccountingSync();
static int i = 0;
@Override
public void run() {
for(int j=0; j<10000000; j++) {
synchronized (instance) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。从可见性的角度上讲,synchronized 可以完全替代volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁后方能进入代码块读取数据。因此,它们看到的最终结果并不取决于代码的执行过程,从而解决了有序性问题。
注:本篇博客内容摘自《Java高并发程序设计》