线程池相关后台开发大数据

Java进阶(三)多线程开发关键技术

2016-06-21  本文已影响634人  郭俊Jason

创文章,转载请务必将下面这段话置于文章开头处。
  本文转发自Jason's Blog原文链接 http://www.jasongj.com/java/multi_thread/

sleep和wait到底什么区别

其实这个问题应该这么问——sleep和wait有什么相同点。因为这两个方法除了都能让当前线程暂停执行完,几乎没有其它相同点。

wait方法是Object类的方法,这意味着所有的Java类都可以调用该方法。sleep方法是Thread类的静态方法。

wait是在当前线程持有wait对象锁的情况下,暂时放弃锁,并让出CPU资源,并积极等待其它线程调用同一对象的notify或者notifyAll方法。注意,即使只有一个线程在等待,并且有其它线程调用了notify或者notifyAll方法,等待的线程只是被激活,但是它必须得再次获得锁才能继续往下执行。换言之,即使notify被调用,但只要锁没有被释放,原等待线程因为未获得锁仍然无法继续执行。测试代码如下所示

import java.util.Date;

public class Wait {

  public static void main(String[] args) {
    Thread thread1 = new Thread(() -> {
      synchronized (Wait.class) {
        try {
          System.out.println(new Date() + " Thread1 is running");
          Wait.class.wait();
          System.out.println(new Date() + " Thread1 ended");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
    });
    thread1.start();
    
    Thread thread2 = new Thread(() -> {
      synchronized (Wait.class) {
        try {
          System.out.println(new Date() + " Thread2 is running");
          Wait.class.notify();
          // Don't use sleep method to avoid confusing
          for(long i = 0; i < 200000; i++) {
            for(long j = 0; j < 100000; j++) {}
          }
          System.out.println(new Date() + " Thread2 release lock");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
      
      for(long i = 0; i < 200000; i++) {
        for(long j = 0; j < 100000; j++) {}
      }
      System.out.println(new Date() + " Thread2 ended");
    });
    
    // Don't use sleep method to avoid confusing
    for(long i = 0; i < 200000; i++) {
      for(long j = 0; j < 100000; j++) {}
    }
    thread2.start();
  }
}

执行结果如下

Tue Jun 14 22:51:11 CST 2016 Thread1 is running
Tue Jun 14 22:51:23 CST 2016 Thread2 is running
Tue Jun 14 22:51:36 CST 2016 Thread2 release lock
Tue Jun 14 22:51:36 CST 2016 Thread1 ended
Tue Jun 14 22:51:49 CST 2016 Thread2 ended

从运行结果可以看出

注意:wait方法需要释放锁,前提条件是它已经持有锁。所以wait和notify(或者notifyAll)方法都必须被包裹在synchronized语句块中,并且synchronized后锁的对象应该与调用wait方法的对象一样。否则抛出IllegalMonitorStateException

sleep方法告诉操作系统至少指定时间内不需为线程调度器为该线程分配执行时间片,并不释放锁(如果当前已经持有锁)。实际上,调用sleep方法时并不要求持有任何锁。

package com.test.thread;

import java.util.Date;

public class Sleep {

  public static void main(String[] args) {
    Thread thread1 = new Thread(() -> {
      synchronized (Sleep.class) {
        try {
          System.out.println(new Date() + " Thread1 is running");
          Thread.sleep(2000);
          System.out.println(new Date() + " Thread1 ended");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
    });
    thread1.start();
    
    Thread thread2 = new Thread(() -> {
      synchronized (Sleep.class) {
        try {
          System.out.println(new Date() + " Thread2 is running");
          Thread.sleep(2000);
          System.out.println(new Date() + " Thread2 ended");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
      
      for(long i = 0; i < 200000; i++) {
        for(long j = 0; j < 100000; j++) {}
      }
    });
    
    // Don't use sleep method to avoid confusing
    for(long i = 0; i < 200000; i++) {
      for(long j = 0; j < 100000; j++) {}
    }
    thread2.start();
  }
}

执行结果如下

Thu Jun 16 19:46:06 CST 2016 Thread1 is running
Thu Jun 16 19:46:08 CST 2016 Thread1 ended
Thu Jun 16 19:46:13 CST 2016 Thread2 is running
Thu Jun 16 19:46:15 CST 2016 Thread2 ended

由于thread 1和thread 2的run方法实现都在同步块中,无论哪个线程先拿到锁,执行sleep时并不释放锁,因此其它线程无法执行。直到前面的线程sleep结束并退出同步块(释放锁),另一个线程才得到锁并执行。

注意:sleep方法并不需要持有任何形式的锁,也就不需要包裹在synchronized中。

synchronized几种用法

每个Java对象都可以用做一个实现同步的互斥锁,这些锁被称为内置锁。线程进入同步代码块或方法时自动获得内置锁,退出同步代码块或方法时自动释放该内置锁。进入同步代码块或者同步方法是获得内置锁的唯一途径。

实例同步方法

synchronized用于修饰实例方法(非静态方法)时,执行该方法需要获得的是该类实例对象的内置锁(同一个类的不同实例拥有不同的内置锁)。如果多个实例方法都被synchronized修饰,则当多个线程调用同一实例的不同同步方法(或者同一方法)时,需要竞争锁。但当调用的是不同实例的方法时,并不需要竞争锁。

静态同步方法

synchronized用于修饰静态方法时,执行该方法需要获得的是该类的class对象的内置锁(一个类只有唯一一个class对象)。调用同一个类的不同静态同步方法时会产生锁竞争。

同步代码块

synchronized用于修饰代码块时,进入同步代码块需要获得synchronized关键字后面括号内的对象(可以是实例对象也可以是class对象)的内置锁。

synchronized使用总结

锁的使用是为了操作临界资源的正确性,而往往一个方法中并非所有的代码都操作临界资源。换句话说,方法中的代码往往并不都需要同步。此时建议不使用同步方法,而使用同步代码块,只对操作临界资源的代码,也即需要同步的代码加锁。这样做的好处是,当一个线程在执行同步代码块时,其它线程仍然可以执行该方法内同步代码块以外的部分,充分发挥多线程并发的优势,从而相较于同步整个方法而言提升性能。

释放Java内置锁的唯一方式是synchronized方法或者代码块执行结束。若某一线程在synchronized方法或代码块内发生死锁,则对应的内置锁无法释放,其它线程也无法获取该内置锁(即进入跟该内置锁相关的synchronized方法或者代码块)。

Java中的锁

重入锁

Java中的重入锁(即ReentrantLock)与Java内置锁一样,是一种排它锁。使用synchronized的地方一定可以用ReentrantLock代替。

重入锁需要显示请求获取锁,并显示释放锁。为了避免获得锁后,没有释放锁,而造成其它线程无法获得锁而造成死锁,一般建议将释放锁操作放在finally块里,如下所示。

try{
  renentrantLock.lock();
  // 用户操作
} finally {
  renentrantLock.unlock();
}

如果重入锁已经被其它线程持有,则当前线程的lock操作会被阻塞。除了lock()方法之外,重入锁(或者说锁接口)还提供了其它获取锁的方法以实现不同的效果。

重入锁可定义为公平锁或非公平锁,默认实现为非公平锁。

读写锁

如上文《Java进阶(二)当我们说线程安全时,到底在说什么》所述,锁可以保证原子性和可见性。而原子性更多是针对写操作而言。对于读多写少的场景,一个读操作无须阻塞其它读操作,只需要保证读和写或者写与写不同时发生即可。此时,如果使用重入锁(即排它锁),对性能影响较大。Java中的读写锁(ReadWriteLock)就是为这种读多写少的场景而创造的。

实际上,ReadWriteLock接口并非继承自Lock接口,ReentrantReadWriteLock也只实现了ReadWriteLock接口而未实现Lock接口。ReentrantReadWriteLock的子类(ReadLock和WriteLock,是ReentrantReadWriteLock类的静态内部类)实现了Lock接口。

一个ReentrantReadWriteLock实例包含一个ReentrantReadWriteLock.ReadLock实例和一个ReentrantReadWriteLock.WriteLock实例。通过readLock()writeLock()方法可分别获得读锁实例和写锁实例,并通过Lock接口提供的获取锁方法获得对应的锁。

读写锁的锁定规则如下:

package com.test.thread;

import java.util.Date;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {

  public static void main(String[] args) {
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    new Thread(() -> {
      readWriteLock.readLock().lock();
      try {
        System.out.println(new Date() + "\tThread 1 started with read lock");
        try {
          Thread.sleep(2000);
        } catch (Exception ex) {
        }
        System.out.println(new Date() + "\tThread 1 ended");
      } finally {
        readWriteLock.readLock().unlock();
      }
    }).start();

    new Thread(() -> {
      readWriteLock.readLock().lock();
      try {
        System.out.println(new Date() + "\tThread 2 started with read lock");
        try {
          Thread.sleep(2000);
        } catch (Exception ex) {
        }
        System.out.println(new Date() + "\tThread 2 ended");
      } finally {
        readWriteLock.readLock().unlock();
      }
    }).start();

    new Thread(() -> {
      Lock lock = readWriteLock.writeLock();
      lock.lock();
      try {
        System.out.println(new Date() + "\tThread 3 started with write lock");
        try {
          Thread.sleep(2000);
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        System.out.println(new Date() + "\tThread 3 ended");
      } finally {
        lock.unlock();
      }
    }).start();
  }
}

执行结果如下

Sat Jun 18 21:33:46 CST 2016  Thread 1 started with read lock
Sat Jun 18 21:33:46 CST 2016  Thread 2 started with read lock
Sat Jun 18 21:33:48 CST 2016  Thread 2 ended
Sat Jun 18 21:33:48 CST 2016  Thread 1 ended
Sat Jun 18 21:33:48 CST 2016  Thread 3 started with write lock
Sat Jun 18 21:33:50 CST 2016  Thread 3 ended

从上面的执行结果可见,thread 1和thread 2都只需获得读锁,因此它们可以并行执行。而thread 3因为需要获取写锁,必须等到thread 1和thread 2释放锁后才能获得锁。

条件锁

条件锁只是一个帮助用户理念的概念,实际上并没有条件锁这种锁。对于每个重入锁,都可以通过newCondition()方法绑定若干个条件对象。

条件对象提供以下方法以实现不同的等待语义

调用条件等待的注意事项

signal()signalAll()

package com.test.thread;

import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionTest {

  public static void main(String[] args) throws InterruptedException {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    new Thread(() -> {
      lock.lock();
      try {
        System.out.println(new Date() + "\tThread 1 is waiting");
        try {
          long waitTime = condition.awaitNanos(TimeUnit.SECONDS.toNanos(2));
          System.out.println(new Date() + "\tThread 1 remaining time " + waitTime);
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        System.out.println(new Date() + "\tThread 1 is waken up");
      } finally {
        lock.unlock();
      }
    }).start();
    
    new Thread(() -> {
      lock.lock();
      try{
        System.out.println(new Date() + "\tThread 2 is running");
        try {
          Thread.sleep(4000);
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        condition.signal();
        System.out.println(new Date() + "\tThread 2 ended");
      } finally {
        lock.unlock();
      }
    }).start();
  }
}

执行结果如下

Sun Jun 19 15:59:09 CST 2016  Thread 1 is waiting
Sun Jun 19 15:59:09 CST 2016  Thread 2 is running
Sun Jun 19 15:59:13 CST 2016  Thread 2 ended
Sun Jun 19 15:59:13 CST 2016  Thread 1 remaining time -2003467560
Sun Jun 19 15:59:13 CST 2016  Thread 1 is waken up

从执行结果可以看出,虽然thread 2一开始就调用了signal()方法去唤醒thread 1,但是因为thread 2在4秒钟后才释放锁,也即thread 1在4秒后才获得锁,所以thread 1的await方法在4秒钟后才返回,并且返回负值。

信号量Semaphore

信号量维护一个许可集,可通过acquire()获取许可(若无可用许可则阻塞),通过release()释放许可,从而可能唤醒一个阻塞等待许可的线程。

与互斥锁类似,信号量限制了同一时间访问临界资源的线程的个数,并且信号量也分公平信号量与非公平信号量。而不同的是,互斥锁保证同一时间只会有一个线程访问临界资源,而信号量可以允许同一时间多个线程访问特定资源。所以信号量并不能保证原子性。

信号量的一个典型使用场景是限制系统访问量。每个请求进来后,处理之前都通过acquire获取许可,若获取许可成功则处理该请求,若获取失败则等待处理或者直接不处理该请求。

信号量的使用方法

注意:与wait/notify和await/signal不同,acquire/release完全与锁无关,因此acquire等待过程中,可用许可满足要求时acquire可立即返回,而不用像锁的wait和条件变量的await那样重新获取锁才能返回。或者可以理解成,只要可用许可满足需求,就已经获得了锁。

Java进阶系列

上一篇 下一篇

猜你喜欢

热点阅读