返回介绍

2.7 线程安全的概念与 synchronized

发布于 2024-08-21 22:20:21 字数 4350 浏览 0 评论 0 收藏 0

并行程序开发的一大关注重点就是线程安全。一般来说,程序并行化是为了获得更高的执行效率,但前提是,高效率不能以牺牲正确性为代价。如果程序并行化后,连基本的执行结果的正确性都无法保证,那么并行程序本身也就没有任何意义了。因此,线程安全就是并行程序的根本和根基。大家还记得那个多线程读写long型数据的案例吧!这就是一个典型的反例。但在使用volatile关键字后,这种错误的情况有所改善。但是,volatile并不能真正的保证线程安全。它只能确保一个线程修改了数据后,其他线程能够看到这个改动。但当两个线程同时修改某一个数据时,却依然会产生冲突。

下面的代码演示了一个计数器,两个线程同时对i进行累加操作,各执行10000000次。我们希望的执行结果当然是最终i的值可以达到20000000,但事实并非总是如此。如果你多执行几次下述代码,你会发现,在很多时候,i的最终值会小于20000000。这就是因为两个线程同时对i进行写入时,其中一个线程的结果会覆盖另外一个(虽然这个时候i被声明为volatile变量)。

01 public class AccountingVol implements Runnable{
02   static AccountingVol instance=new AccountingVol();
03   static volatile int i=0;
04   public static void increase(){
05     i++;
06   }
07   @Override
08   public void run() {
09     for(int j=0;j<10000000;j++){
10       increase();
11     }
12   }
13   public static void main(String[] args) throws InterruptedException {
14     Thread t1=new Thread(instance);
15     Thread t2=new Thread(instance);
16     t1.start();t2.start();
17     t1.join();t2.join();
18     System.out.println(i);
19   }
20 }

图2.8展示了这种可能的冲突,如果在代码中发生了类似的情况,这就是多线程不安全的恶果。线程1和线程2同时读取i为0,并各自计算得到i=1,并先后写入这个结果,因此,虽然i++被执行了2次,但是实际i的值只增加了1。

图2.8 多线程的写入冲突

要从根本上解决这个问题,我们就必须保证多个线程在对i进行操作时完全同步。也就是说,当线程A在写入时,线程B不仅不能写,同时也不能读。因为在线程A写完之前,线程B读取的一定是一个过期数据。Java中,提供了一个重要的关键字synchronized来实现这个功能。

关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性(也就是说在上述代码的第5行,每次应该只有一个线程可以执行)。

关键字synchronized可以有多种用法。这里做一个简单的整理。

· 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。

· 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。

· 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

下述代码,将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++;
      }
    }
  }
  //main函数参见本节第一段代码

当然,上述代码也可以写成如下形式,两者是等价的:

01 public class AccountingSync2 implements Runnable{
02   static AccountingSync2 instance=new AccountingSync2();
03   static int i=0;
04   public synchronized void increase(){
05     i++;
06   }
07   @Override
08   public void run() {
09     for(int j=0;j<10000000;j++){
10       increase();
11     }
12   }
13   public static void main(String[] args) throws InterruptedException {
14     Thread t1=new Thread(instance);
15     Thread t2=new Thread(instance);
16     t1.start();t2.start();
17     t1.join();t2.join();
18     System.out.println(i);
19   }
20 }

上述代码中,synchronized关键字作用于一个实例方法。这就是说在进入increase()方法前,线程必须获得当前对象实例的锁。在本例中就是instance对象。在这里,我不厌其烦地再次给出main函数的实现,是希望强调第14、15行代码,也就是Thread的创建方式。这里使用Runnable接口创建两个线程,并且这两个线程都指向同一个Runnable接口实例(instance对象),这样才能保证两个线程在工作时,能够关注到同一个对象锁上去,从而保证线程安全。

一种错误的同步方式如下:

01 public class AccountingSyncBad implements Runnable{
02   static int i=0;
03   public synchronized void increase(){
04     i++;
05   }
06   @Override
07   public void run() {
08     for(int j=0;j<10000000;j++){
09       increase();
10     }
11   }
12   public static void main(String[] args) throws InterruptedException {
13     Thread t1=new Thread(new AccountingSyncBad());
14     Thread t2=new Thread(new AccountingSyncBad());
15     t1.start();t2.start();
16     t1.join();t2.join();
17     System.out.println(i);
18   }
19 }

上述代码就犯了一个严重的错误。虽然在第3行的increase()方法中,申明这是一个同步方法。但很不幸的是,执行这段代码的两个线程都指向了不同的Runnable实例。由第13、14行可以看到,这两个线程的Runnable实例并不是同一个对象。因此,线程t1会在进入同步方法前加锁自己的Runnable实例,而线程t2也关注于自己的对象锁。换言之,这两个线程使用的是两把不同的锁。因此,线程安全是无法保证的。

但我们只要简单地修改上述代码,就能使其正确执行。那就是使用synchronized的第三种用法,将其作用于静态方法。将increase()方法修改如下:

public static synchronized void increase(){ i++; }

这样,即使两个线程指向不同的Runnable对象,但由于方法块需要请求的是当前类的锁,而非当前实例,因此,线程间还是可以正确同步。

除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。从可见性的角度上讲,synchronized可以完全替代volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步块,因此,无论同步块内的代码如何被乱序执行,只要保证串行语义一致,那么执行结果总是一样的。而其他访问线程,又必须在获得锁后方能进入代码块读取数据,因此,它们看到的最终结果并不取决于代码的执行过程,从而有序性问题自然得到了解决(换言之,被synchronized限制的多个线程是串行执行的)。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文