Java并发机制(七)—— 多线程同步

当有多个线程要同时访问一个变量或对象时,如果这些线程中既有读操作又有写操作,就会导致变量值或对象的状态出现混乱,从而导致程序异常。

同步,可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。

异步,执行完函数或方法后,不必等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。

1 实现多线程同步

1.1 同步方法或同步代码块

synchronized关键字修饰的方法或语句块会被自动加上内置锁,需要获取锁才可以调用,保证多个线程在操作时只有一个线程可以持有锁,从而实现同步。

同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

1.2 ReentrantLock

JDK 1.5中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized的方法和语句快具有相同的基本行为和语义,并且扩展了其能力。

ReentrantLock类有一个可以创建公平锁的构造方法,能大幅度降低程序运行效率,但不推荐使用。

1.3 volatile
  • volatile关键字为变量的访问提供了一种免锁机制;
  • 使用volatile修饰变量,相当于告诉虚拟机该变量可能会被其他线程更新;因此每次使用此种变量需要从内存中获取,而不是使用本地内存中的值;
  • volatile不会提供任何原子操作,也不能用来修饰final类型的变量;
1.4 等待/通知机制

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者。

简单的办法是让消费者线程不断地循环检查变量是否符合预期,如下面代码所示,在while循环中设置不满足的条件,如果条件满足则退出while循环,从而完成消费者的工作。

while (value != desire) { 
  Thread.sleep(1000);
}
doSomething();

上面这段伪代码在条件不满足时就睡眠一段时间,这样做的目的是防止过快的“无效”尝试,这种方式看似能够解实现所需的功能,但是却存在如下问题:

  • 难以确保及时性。在睡眠时,基本不消耗处理器资源,但是如果睡得过久,就不能及时发现条件已经变化,也就是及时性难以保证;
  • 难以降低开销。如果降低睡眠的时间,比如休眠1毫秒,这样消费者能更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成了无端的浪费;

以上两个问题,Java通过内置的等待/通知机制能够很好地解决这个问题并实现所需的功能。

等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上:

public class Object {
  ...
  @FastNative
  public final native void notify();
  @FastNative
  public final native void notifyAll();
  public final void wait(long timeout) throws InterruptedException {
    wait(timeout, 0);
  }
  @FastNative
  public final native void wait(long timeout, int nanos) throws InterruptedException;
  public final void wait() throws InterruptedException {
    wait(0);
  }
  protected void finalize() throws Throwable { }
}

方法和描述如下所示:

等待/通知的相关方法

等待/通知机制,是指一个线程A调用了对象Owait()方法进入等待状态,而另一个线程B调用了对象Onotify()或者notifyAll()方法,线程A收到通知后从对象Owait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

notify方法和notifyAll方法:

  • notify/notifyAll方法调用之前需要获取该对象的锁,所以在synchronized方法中或代码块中 使用;
  • notify方法通知那些在对象上等待的线程,如果有多个线程,则线程规划器会任意挑选出其中一个wait状态的线程来发出通知 。notifyAll会通知所有处于wait状态的线程;
  • 对于notify方法唤醒的线程,执行完毕之后会立刻释放掉锁,如果没有再次使用notify,其它wait状态下的线程会因为没有得到通知,继续阻塞在wait的状态,等待其他对象调用notify或者notifyAll来唤醒;

sleep()方法和wait()方法:

共同点:都是使线程暂停一段时间的方法。

不同点:

  • sleepThread类的静态方法;waitObject类的方法;
public class Thread implements Runnable {

  public static void sleep(long millis) throws InterruptedException {
    sleep(millis, 0);
  }

  @FastNative
  private static native void sleep(Object lock, long millis, int nanos) throws InterruptedException;

  public static void sleep(long millis, int nanos) throws InterruptedException {
    if (millis < 0) {
      throw new IllegalArgumentException("millis < 0: " + millis);
    }
    if (nanos < 0) {
      throw new IllegalArgumentException("nanos < 0: " + nanos);
    }
    if (nanos > 999999) {
      throw new IllegalArgumentException("nanos > 999999: " + nanos);
    }
    if (millis == 0 && nanos == 0) {
      if (Thread.interrupted()) {
        throw new InterruptedException();
      }
      return;
    }

    final int nanosPerMilli = 1000000;
    long start = System.nanoTime();
    long duration = (millis * nanosPerMilli) + nanos;

    Object lock = currentThread().lock;

    synchronized (lock) {
      while (true) {
        sleep(lock, millis, nanos);

        long now = System.nanoTime();
        long elapsed = now - start;

        if (elapsed >= duration) {
          break;
        }

        duration -= elapsed;
        start = now;
        millis = duration / nanosPerMilli;
        nanos = (int) (duration % nanosPerMilli);
      }
    }
  }

}

public class Object {

  public final void wait() throws InterruptedException {
    wait(0);
  }

  public final void wait(long timeout) throws InterruptedException {
    wait(timeout, 0);
  }

  @FastNative
  public final native void wait(long timeout, int nanos) throws InterruptedException;

}
  • sleep不出让资源,sleep(milliseconds)方法可以指定时间使它自动唤醒,如果时间不到只能调用Thread.interrupted()强行打断。wait出让资源,进入线程等待池等待,其它线程可以占用CPU。一般wait不会加时间限制,这是因为资源不够,不等待也没有办法,要等待其它线程调用notify/notifyAll唤醒等待池中的线程,才会进入就绪队列等待CPU分配资源。(sleep方法不释放锁,而wait方法释放了锁,使得其它线程可以使用同步控制块或者方法);
  • waitnotifynotifyAll只能在同步控制方法或者同步控制块里面使用(synchronized),而sleep可以在任何地方使用:
synchronized(x){
  x.wait();
  // x.notify()
  // x.notifyAll()
}
  • sleep必须捕获异常,而waitnotifynotifyAll不需要捕获异常

2 ThreadLocal

ThreadLocal,线程本地变量,使用ThreadLocal管理变量,每一个使用该变量的线程都获得变量副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本(get/set/remove),而不会对其他线程的变量产生影响:

  • ThreadLocal.get:获取ThreadLocal中当前线程共享变量的值;
  • ThreadLocal.set:设置ThreadLocal中当前线程共享变量的值;
  • ThreadLocal.remove:移除ThreadLocal中当前线程共享变量的值;
  • ThreadLocal.initialValueThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法返回此方法值;

以下是ThreadLocal的源码:

public class ThreadLocal<T> {

  public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) {
        @SuppressWarnings("unchecked")
        T result = (T)e.value;
        return result;
      }
    }
    return setInitialValue();
  }

  public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
      map.set(this, value);
    else
      createMap(t, value);
  }

  public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
      m.remove(this);
  }

  ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
  }

  static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {

      Object value;

      Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
      }
    }

    private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;
    private int size = 0;
    
    ...
  }

}

ThreadLocal并不能替代同步机制,这是因为:

  • ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题;
  • 同步机制是为了解决多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;
  • ThreadLocal隔离多个线程的数据共享,从根本上就不在多个线程之间共享变量,这样当然就不需要对多个线程进行同步了;
  • ThreadLocal采用以"空间换时间"的方法,线程同步采用以"时间换空间"的方式;

ThreadLocalsynchronized:ThreadLocal是为了在多线程的情况下,每个线程对变量的独立访问,线程间该变量值互不影响。内部是由一个ThreadLocalMapkey为当前ThreadLocal的弱引用,value为变量值。synchronized则是为了多线程时通过锁在某个时刻只能有一个线程对变量/方法访问。

3 线程阻塞的原因

  • 线程执行了sleep(long millis)方法,线程放弃CPU,睡眠millis毫秒后恢复运行;
  • 线程要执行一段同步代码,由于无法获得相关的同步锁,只好进入阻塞状态,等到获得了同步锁,才能恢复运行;
  • 线程执行了一个对象的wait()方法,进入阻塞状态,只有等到其它线程执行了该对象的notify()/notifyAll()方法,才可能将其唤醒;
  • 线程执行I/O操作或进行远程通信时,会因为等待相关的资源而进入阻塞状态,例如,当线程执行System.in.read()方法时,如果用户没有向控制台输入数据,则该线程会一直等读到了用户的输入数据才从read()方法返回。进行远程通信时,在客户程序中,线程在以下情况可能进入阻塞状态;
  • 请求与服务器建立连接时,即当线程执行Socket的带参数的构造方法,或执行Socket.connect()方法时,会进入 阻塞状态,直到连接成功,此线程才从Socket的构造方法或connect()方法返回;
  • 线程从Socket的输入流读取数据时,如果没有足够的数据,就会进入阻塞状态,直到读到了足够的数据,或者到达输入流的末尾,或者出现了异常,才从输入流的read()方法返回或异常中断。输入流中有多少数据才算足够呢?这要看线程执行的read()方法的类型;
    • int read():只要输入流中有一个字节,就算足够;
    • int read(byte[] buff):只要输入流中的字节数目与参数buff数组的长度相同,就算足够;
    • String readLine():只要输入流中有一行字符串,就算足够。值得注意的是,InputStream类并没有readLine方法,在过滤流BufferedReader类中才有此方法;
  • 线程向Socket的输出流写一批数据时,可能会进入阻塞状态,等到输出了所有的数据,或者出现异常,才从输出流的write()方法返回或异常中断;
  • 调用SocketsetSoLinger()方法设置了关闭Socket的延迟时间,那么当线程执行Socket.close方法时,会进入阻塞状态,直到底层Socket发送完所有剩余数据,或者超过了setSoLinger()方法设置的延迟时间,才从close()方法返回;
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值