多线程 (四) 同步--非常重要

本文探讨了Java中多线程环境下数据共享可能导致的问题及解决方案,重点介绍了synchronized关键字的作用和使用方法,包括同步方法与同步语句的区别。

同步非常非常重要,我们在处理多线程问题时,基本上都要使用Synchronized关键字。

因此必须重点理解。

 

首先,多线程会出现问题的根本原因是数据共享。

Threads communicate primarily by sharing access to fields and the objects reference fields refer to. This form of communication is extremely efficient, but makes two kinds of errors possible:thread interference and memory consistency errors . The tool needed to prevent these errors is synchronization .

 

这篇文章将要谈到的问题是,为什么多线程会有问题,以及怎么解决。

 

Thread interference 线程间干扰

一个简单的例子:一个class有一个成员变量,两个方法,一个加一个减,用成员变量保存运算结果,用get方法提供给外部代码。

Threads communicate primarily by sharing access to fields and the objects reference fields refer to。也就是说,多个线程操作的是同一个对象。

即使是最简单的c++, c--操作,实际上也不是原子操作(Atomic access)。在执行时,实际可能是以下步骤:

1, 取到c的值。

2, 加1。

3, 写回c的值。

而多任务操作系统的特点就是:线程会随时被挂起,cpu将时间slice交给另外一个线程执行。假如上述步骤中应执行3的时候被挂起,另一个线程修改了c的值。之后第一个线程又被唤醒,写回了c值。那样第二个线程的修改就丢失了。

Java的Object基类实现了一个monitor,本质上是一种内置锁,因此所有Object的子类都具有该功能,实例化出来的每一个对象都有这个锁。当一个线程需要操作对象的时候,需要先检查对象的锁是否被占用,如果被占用则需要等待,如果未被占用则取得这个锁。

我们可以看到之后将要谈到的同步synchronized,在本质上就是:在一个线程取得内置锁,执行代码段完毕并释放内置锁之前,该线程的操作绝对不会被干扰(线程一样可能在执行代码段时被挂起,但是由于该线程持有内置锁,因此其他线程绝对无法改变共享对象)。如此就保证了一个线程一定能完整的,不被干扰的执行完一段代码,就形成了类似原子操作。

 

Memory consistency error也会产生问题,但是原因复杂没有详述。

避免这种error的方法就是使用happens-before relationships.  希望一个线程一定在另一个线程前执行。

 

synchronized

解决以上两种问题的方法就是使用synchronized 关键字。

Java有两种synchronization idioms:synchronized method and synchronized statement。

  • 同步方法 synchronized method
public class SynchronizedCounter {

  private int c = 0;

  public synchronized void increment() {
      c++;
      //仅仅需要简单的在方法前添加synchronized关键字
  }

  public synchronized void decrement() {
     c--;
}

  public synchronized int value() {
     return c;
  }

}

//这样在线程调用SynchronizedCounter 类的实例的方法时,只有先获得这个实例的锁,才能运行这几个方法。也就是说,在一个线程执行完同步方法,并释放该实例的内部锁前,其他线程都会被阻塞。

//注意只有多个线程试图操作同一个类的实例对象时,synchronized 才会发挥作用。

//在构造函数上使用synchronized关键字是没有意义的,因为synchronized对于每一个实例才有意义。

//final 成员变量也不需要同步方法. which cannot be modified after the object is constructed。

 

下面是关于使用同步方法要注意的两点:

  • First, it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object.
  • Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads.

进一步解释synchronized工作的原理是:

只有当一个线程取得了实例对象的锁(intrinsic lock)后,这个线程才能执行类中的synchronized方法

当一个线程占有一个对象的锁时,其它线程无法取得该锁,直到第一个线程释放锁。

一个线程企图调用一个synchronized方法时,它会自动请求对象的锁,同时在使用完毕后释放。

不管该线程执行synchronized方法是否正常结束(例如遇到了异常),仍然会释放锁。

静态同步方法让人有些奇怪,因为静态方法是属于类,而不是实例的。使用静态同步方法会控制线程对该类的静态成员变量的访问。

 

  • 同步语句 synchronized statement

上面取得对象锁的同步方案隐含了一个问题:一个对象上的所有同步方法都只能串行执行,即使其中一些同步方法并不是互斥的即使同时运行也没问题。另外如果一个同步方法操作了多个对象,第一个对象锁被占用会导致整个线程被挂起,block了对其他对象的操作。因此使用同步方法的本质是:方法中所有操作成为了一个原子操作,成为了一个整体,是以牺牲性能为代价的。

另外一种实现同步的方法就是同步语句。同步语句必须指明提供内部锁的对象:

public void addName(String name) {
    synchronized(this) {  //即就是当前实例
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}

在这个例子中,实现的是同步对当前实例的lastName 和nameCount的修改,但是没有限制对另外一个对象nameList.add调用。实现了更细粒度的控制。而如果是synchronized method,则无法实现,必须将原方法劈成两个。

所以同步语句很实用,因为一个方法操作的可能是多个对象实例。

使用同步语句的另一个好处就是可以提高并发度。比如下面的例子:c1和c2之间没有任何关系,我们需要的是不同线程不能同时修改c1或c2,但是如果使用同步方法就会导致一个线程在修改c1时,其他线程也不能修改c2了。因为同步方法获得的是整个对象实例的锁。

public class MsLunch {

    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
      synchronized(lock1) {
      c1++;
  }
}
  public void inc2() {

    synchronized(lock2) {
    c2++;
   //通过提供两个不用的对象lock1和lock2,线程修改两个变量就可以互相不影响了。
   //这种解决办法的本质是:在同步方法中只能使用object的内置锁,因此无法实现对不同成员变量的分别控制
   //而同步代码块则是自己指定要使用的锁,并实现控制分离。
  }
  }
}

 

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值