总结Java的多线程处理--线程与锁(一)同步synchronized

线程与锁模型

线程与锁模型是比较原始的一种处理并发的方式,主要是对底层硬件的运行过程形式化,这是它的优点也是缺点。
线程与锁模型非常直接,几乎所有的编程语言都提供了支持,但是如果不了解该模型,那么程序会很容易出错,而且难以维护。

为什么需要锁

我们先来看一段多线程的代码:

public class Counting {
  public static void main( String[] args) throws InterruptedException {
    class Counter {
      private int count = 0;
      public void increment() { ++count; }
      public int getCount() { return count;}
    }
    final Counter counter = new Counter();
    class CountingThread extends Thread {
    public void run() {
      for(int x = 0; x < 10000; x++)
        counter.increment();
      }
    }
    CountingThread t1 = new CountingThread();
    CountingThread t2 = new CountingThread();
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.getCount());

这是创建了两个线程t1与t2,每一个线程都调用了counter.increment(),10000次,看上去特别简单,但是每次运行都会有不同的结果,这是因为在操作counter对象的时候发生了竞态条件。

竞态条件是指代码的行为取决于各操作的时序。


如果不理解我们先看一下java编译器是如何处理的++count

//获取值
getfield #2
//将常量i 1 进栈
iconst_1
//加i
iadd
//更新值
putfield#2

问题就出在这里,如果同时调用increment(),两个线程在获取值的时候是同一个值如100,那么放回去的时候虽然操作了两次increment(),但是实际结果是101。

synchronize

java中遇到这种问题可以有一种的解决办法,进行同步(synchronize)访问。
只需要在之前的代码中这么改一下:

······
  public synchronized void increment() { ++count; }
······

那么在线程使用increment函数的时候会获得该函数的锁,其他线程将不能访问,直到该线程返回时释放锁。
现在因为增加了同步的代码,执行都会获得正确的结果--20000。

但是synchronize也会带来很多坑,下面一一介绍。

乱序编译

static boolan isReady = false
static int number = 0;
static Thread t1 = new Thread() {
    public void run() {
      number = 100;
      isReady = true;
    }
static Thread t2 = new Thread() {
    public void run() {
      if (isReady)
     System.out.println(number);
      else 
     System.out.println("not ready");
    }

想想看如果同时运行上面的代码会发生什么事?

结果:

  1. 打印not ready
  2. 打印100。
  3. 打印0。 (为什么?)

为什么 number = 100; isReady = true;语句发生了颠倒?

但是事实上是有可能发生的:

  1. 编译器的静态优化会打乱。
  2. JVM的动态优化会打乱。
  3. 硬件可以通过乱序执行来优化性能。

但实际上还有更糟糕的,有时候一个线程产生的修改可能对于另外一个线程来讲是不可见的。

从常识上来说,无论是编译器,JVM还是硬件都不应该改变代码的原有逻辑,这里我们需要有明确的标准来知道可能会发生什么,那就是Java内存模型。

在Java内存模型中还说明了上面一个问题的答案:

如果读线程与写线程不进行同步,就不能保证可见性。
所以除了increment()之外,也应该对getCount()方法同步,不然可能会得到一个已经失效的值。

死锁

有一个著名的问题--哲学家进餐问题


总结Java的多线程处理--线程与锁(一)同步synchronized_第1张图片
1.png

如图。
哲学家的状态可能是「思考」也可能是「饥饿」,如果是饥饿,他就会将两边的筷子拿起来,并且进餐一段时间。进餐结束后哲学家就会返回筷子。
那么代码可以这么写

synchronized(左边的筷子) {
  synchronized(右边的筷子)
}

这样会出现一个问题,如果所有的哲学家在某个时刻,将左边的筷子都拿起来,就都不能拿到右边的筷子了,而且也不能释放左边的筷子,这样程序就会一直卡组,这就是死锁。

如果解决?
我们可以给筷子设置编号,只能先拿小的,然后拿大的。
或者给哲学家拿筷子的顺序进行设置。
虽然可以解决但是依然暴露出synchronized的问题。

方法内部的陷阱

private synchronized void update {
  for (Person person: persons)
    person.eat();
}

这段逻辑看上去没有问题,方法加上了synchronized,所以多线程使用的时候也会同步访问。
但实际上也是有一个陷阱,在eat方法这个地方。
因为对eat方法不了解,所以可能eat方法中也调用了synchronized函数,这样就是使用了两把锁,就像之前的哲学家进餐问题一样,可能会发生死锁。
解决的办法是将persons拷贝一份:

private void update {
  ArrayList personsCopy;
  synchronized(this) {
  personCopy = (ArrayList)persons.clone();
  for (Person person: persons)
    person.eat();
}

这样调用方法的时候不需要加锁,而且也减少了持有锁的时间。

总结

  1. 对共享变量需要同步化。
  2. 读线程和写线程需要同步化。
  3. 按照约定的全局顺序来获得多把锁。
  4. 持有锁的时候尽量不要调用外部方法。
  5. 持有锁的时间应该尽量短。

你可能感兴趣的:(总结Java的多线程处理--线程与锁(一)同步synchronized)