JAVA学习总结之线程概述

参考文章如下:
Java核心技术点之多线程
聊聊并发(一)——深入分析Volatile的实现原理

储备知识

进程与线程

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。
线程与进程间的一个显著区别在于每个进程都有一整套变量,而同一个进程间的多个线程共享该进程的数据。也就是说在通常情况下,多线程在数据共享上要比多进程更加便捷。

并发与并行

我们知道,在单核机器上,“多进程”并不是真正的多个进程在同时执行,而是通过CPU时间分片,操作系统快速在进程间切换而模拟出来的多进程。我们通常把这种情况成为并发,也就是多个进程的运行行为是“一并发生”的,但不是同时执行的,因为CPU核数的限制(PC和通用寄存器只有一套,严格来说在同一时刻只能存在一个进程的上下文)。
现在,我们使用的计算机基本上都搭载了多核CPU,这时,我们能真正的实现多个进程并行执行,这种情况叫做并行,因为多个进程是真正“一并执行”的(具体多少个进程可以并行执行取决于CPU核数)。综合以上,我们知道,并发是一个比并行更加宽泛的概念。也就是说,在单核情况下,并发只是并发;而在多核的情况下,并发就变为了并行。下文中我们将统一用并发来指代这一概念。

阻塞与非阻塞

阻塞最常见的就是IO阻塞了,大家可以这样去理解,有一个前台人员(线程),有客人上门来访,她就招待一下(任务),但是客人不会一直连续的来,而是间断性的来(内存速度大于磁盘速度),那么如果她在客人没有来的时候,还是在门口等待着,那么我们可以理解这个前台阻塞,如果当客人没有来的时候,这个前台去做别的事情了,例如打印文件,写代码等,那么可以理解这个前台非阻塞
在真正的IO操作,磁盘通常是一种慢速I/O设备,这意味着我们用read函数读取磁盘文件内容时(或者是wirte函数),往往需要比较长的时间(相对于访问内存来说)。那么阻塞的时候我们当然不想让系统傻等着,我们想在这期间做点儿别的事情,等着磁盘准备好了通知我们一下,我们再来读取文件内容。实际上,操作系统正是这样做的。当阻塞在read这类系统调用中的时候,操作系统通常都会让该进程暂时休眠,调度一个别的进程来执行,以免干等着浪费时间,等到磁盘准备好了可以让我们来进行I/O了,它会发送一个中断信号通知操作系统,这时候操作系统重新调度原来的进程来继续执行read函数。这就是通过多进程实现的并发。

线程

每个进程刚被创建时都只含有一个线程,这个线程通常被称作主线程(main thread)。而后随着进程的执行,若遇到创建新线程的代码,就会创建出新线程,而后随着新线程被启动,多个线程就会并发地运行。

创建一个线程

使用java.lang.Thread类或者java.lang.Runnable接口编写代码来定义、实例化和启动新线程。一个Thread类实例只是一个对象,像Java中的任何其他对象一样,具有变量和方法,生死于堆上。Java中,每个线程都有一个调用栈,即使不在程序中创建任何新的线程,线程也在后台运行着。一旦创建一个新的线程,就产生一个新的调用栈。

继承java.lang.Thread类
  1. 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
  2. 创建Thread子类的实例,即创建了线程对象。
  3. 调用线程对象的start()方法来启动该线程。
public class SonThread extends Thread{
    @Override
    public void run() {
        //这里面是线程要执行的逻辑代码
        super.run();
    }
}
public static void main(String[] args) {
    Thread thread = new SonThread();
    thread.start();
}
实现java.lang.Runnable接口

使用实现接口Runnable的对象创建一个线程时,Thread会执行Runnable里run的代码,这是因为Thread这个类中的run方法,会判断一次target(Runnable接口对象)是否为null,如果不为空会执行Runnable接口对象的run方法。

public class RunnableThread implements Runnable{
    @Override
    public void run() {
        //在这里面实现该线程的逻辑代码
    }
}
public static void main(String[] args) {
    Thread thread = new Thread(new RunnableThread());
    thread.start();
}

此处还有一中匿名内部类的方法创建线程,这经常在Android中使用到

//1.8之前的版本
public static void main(String[] args) {
     new Thread(new Runnable() {
         @Override
         public void run() {
             //这里是线程的逻辑代码
         }
     }).start();
}
//1.8之后的写法    
public static void main(String[] args) {
    new Thread(() -> {
        //这里是线程的逻辑代码
    }).start();
}
两种方式的比较

既然有两种方式可以创建线程,那么我们该使用哪一种呢?首先,直接继承Thread类的方法看起来更加方便,但它存在一个局限性:由于Java中不允许多继承,我们自定义的类继承了Thread后便不能再继承其他类,这在有些场景下会很不方便;实现Runnable接口的那个方法虽然稍微繁琐些,但是它的优点在于自定义的类可以继承其他的类。

线程的属性

线程的状态

线程在它的生命周期中可能处于以下几种状态之一:
- New(新生)
- 当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
- 例如:Thread thread=new Thread();
- Runnable(就绪/可运行)
- 线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。
- 例如:thread.start();
- Running(运行)
- 线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。
- Dead(死亡)
- 当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
- 自然终止:正常运行run()方法后终止
- 异常终止:调用stop()方法让一个线程终止运行
- Blocked(被阻塞)
- 若一段代码被线程A”上锁“,此时线程B尝试执行这段代码,线程B就会进入Blocked状态;
- Waiting(等待)
- 当线程等待另一个线程通知线程调度器一个条件时,它本身就会进入Waiting状态;
- Time Waiting(计时等待)
- 计时等待与等待的区别是,线程只等待一定的时间,若超时则不再等待;

线程状态切换

线程的优先级

在Java中,每个线程都有一个优先级,默认情况下,线程会继承它的父线程的优先级。可以用setPriority方法来改变线程的优先级。Java中定义了三个描述线程优先级的常量:MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY。
每当线程调度器要调度一个新的线程时,它会首先选择优先级较高的线程。然而线程优先级是高度依赖于操作系统的,在有些系统的Java虚拟机中,甚至会忽略线程的优先级。因此我们不应该将程序逻辑的正确性依赖于优先级。线程优先级相关的API如下:

void setPriority(int newPriority) //设置线程的优先级,可以使用系统提供的三个优先级常量
static void yield() //使当前线程处于让步状态,这样当存在其他优先级大于等于本线程的线程时,线程调度程序会调用那个线程
线程的类别

线程总体分两类:用户线程和守候线程。
当所有用户线程执行完毕的时候,JVM自动关闭。但是守候线程却独立于JVM,守候线程一般是由操作系统或者用户自己创建的

普通线程

普通线程又可以称为用户线程,只完成用户自己想要完成的任务,不提供公共服务。正常情况下,我们建立的线程都是用户线程。用户线程除非都结束了,JVM才结束。

后台线程(守护线程)

后台线程指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。后台线程通过如下步骤创建后台线程,当所有的非后台线程都结束了,JVM也会停止,即使后台线程在finally中执行一些步骤,也不会执行。当所有的非后台线程执行完了,后台线程会突然停止。

public static void main(String[] args) {
    Thread thread = new Thread();
    thread.setDaemon(true);//此步骤一定在start方法之前执行
    thread.start();
}

Thread类

Thread实现了Runnable接口,关于这个类的以下实例域需要我们了解它的几个属性:

private volatile char name[]; //当前线程的名字,可在构造器中指定
private int priority; //当前线程优先级
private Runnable target; //当前要执行的任务
private long tid; //当前线程的ID

Thread类的常用方法除了我们之前提到的用于启动线程的start外还有:
- sleep方法:这是一个静态方法,作用是让当前线程进入休眠状态(但线程不会释放已获取的锁),这个休眠状态其实就是我们上面提到过的阻塞状态,从休眠状态“苏醒”后,线程会进入到Runnable状态。sleep方法有两个重载版本,声明分别如下:

//让当前线程休眠millis指定的毫秒数
public static native void sleep(long millis) throws InterruptedException;
//在毫秒数的基础上还指定了纳秒数,控制粒度更加精细
public static native void sleep(long millis, int nanos) throws InterruptedException;
  • join方法:这是一个实例方法,在当前线程中对一个线程对象调用join方法会导致当前线程停止运行,等那个线程运行完毕后再接着运行当前线程。也就是说,把当前线程还没执行的部分“接到”另一个线程后面去,另一个线程运行完毕后,当前线程再接着运行。join方法有以下重载版本:
public final synchronized void join() throws InterruptedException;
public final synchronized void join(long millis) throws InterruptedException;
public final synchronized void join(long millis, int nanos) throws InterruptedException;

无参数的join表示当前线程一直等到另一个线程运行完毕,这种情况下当前线程会处于阻塞状态;带参数的表示当前线程只等待指定的时间,这种情况下当前线程也会处于阻塞状态。当前线程通过调用join方法进入阻塞状态后,会释放已经获取的锁。实际上,join方法内部调用了Object类的实例方法wait,关于这个方法我们下面会具体介绍。
- yield方法,这是一个静态方法,作用是让当前线程“让步”,目的是为了让优先级不低于当前线程的线程有机会运行,这个方法不会释放锁。yield方法声明如下:

public static native void yield();
  • interrupt方法,这是一个实例方法。每个线程都有一个中断状态标识,这个方法的作用就是将相应线程的中断状态标记为true,这样相应的线程调用isInterrupted方法就会返回true。通过使用这个方法,能够终止那些通过调用可中断方法进入阻塞状态的线程。常见的可中断方法有sleep、wait、join,这些方法的内部实现会时不时的检查当前线程的中断状态,若为true会立刻抛出一个InterruptedException异常,从而终止当前线程。interrupt声明如下:
public void interrupt()

Object类中与线程使用的相关方法

  • wait方法
    • wait方法是Object类中定义的实例方法。在指定对象上调用wait方法能够让当前线程进入阻塞状态(前提时当前线程持有该对象的内部锁(monitor)),此时当前线程会释放已经获取的那个对象的内部锁,这样一来其他线程就可以获取这个对象的内部锁了。当其他线程获取了这个对象的内部锁,进行了一些操作后可以调用notify方法来唤醒正在等待该对象的线程。wait几个重载方法声明如下:
public final void wait() throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException;
  • notify/notifyAll方法
    • notify/notifyAll方法也是Object类中定义的实例方法。它俩的作用是唤醒正在等待相应对象的线程,区别在于前者唤醒一个等待该对象的线程,而后者唤醒所有等待该对象的线程。notify/notifyAll方法声明如下:
public final native void notify();
public final native void notifyAll();

如何保证线程安全

所谓线程安全,指的是当多个线程并发访问数据对象时,不会造成对数据对象的“破坏”。保证线程安全的一个基本思路就是让访问同一个数据对象的多个线程进行“排队”,一个接一个的来,这样就不会对数据造成破坏,但带来的代价是降低了并发性。

race condition(竟争条件)

当两个或两个以上的线程同时修改同一数据对象时,可能会产生不正确的结果,我们称这个时候存在一个竞争条件(race condition)。在多线程程序中,我们必须要充分考虑到多个线程同时访问一个数据时可能出现的各种情况,确保对数据进行同步存取,以防止错误结果的产生。请考虑以下代码:

public class Counter { 
  private long count = 0; 
  public void add(long value) { 
    this.count = this.count + value; 
  }
}

我们注意一下改变count值的那一行,通常这个操作不是一步完成的,它大概分为以下三步:
- 第一步,把count的值加载到寄存器中;
- 第二步,把相应寄存器的值加上value的值;
- 第三步,把寄存器的值写回count变量。

我们可以编译以上代码然后用javap查看下编译器为我们生成的字节码:

字节码

我们可以看到,大致过程和我们以上描述的基本一样。那么我们考虑下面这样一个场景:假设count的初值为0,首先线程A加载了count到寄存器中,并且加上了1,而就当它要写回之前,线程B进入了add方法,它加载了count到寄存器中(由于此时线程A还没有把count写回,因此count还是0),并加上了2,然后线程B写回了count。在线程B完成了写回后,线程调度程序调度了线程A,线程A也写回了count。注意,此时count的值为1而不是我们希望的三。我们不希望一个线程在执行add方法时被其他线程打断,因为这会造成数据的破坏。我们希望的情况是这样的:线程A完整执行完毕add方法后,待count变量的值更新为1时,线程B开始执行add方法,在线程B完整执行完毕之前, 没有别的线程能够打断它,若有别的线程想调用add,也得等线程B执行完毕写回count值后。
像add这种方法代码所在的内存区,我们称之为临界区(critical area)。对于临界区,在同一时刻我们只希望有一个线程能够访问它,我们希望在一个线程进入临界区后把通往这个区的门“上锁”,离开后把门”解锁“,这样当一个线程执行临界区的代码时其他想要进来的线程只能在门外等着,这样可以保证了多个线程共享的数据不会被破坏。下面我们来介绍下为临界区“上锁”的方法。

锁对象

Java类库中为我们提供了能够给临界区“上锁”的ReentrantLock(重入锁)类,它实现了Lock接口,在进一步介绍ReentrantLock类之前,我们先来看一下Lock接口的定义:

public interface Lock { 
  void lock(); 
  void lockInterruptibly() throws InterruptedException; 
  boolean tryLock(); 
  boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 
  void unlock(); 
  Condition newCondition();
}
  • lock方法用来获取锁,在锁被占用时它会一直阻塞,并且这个方法不能被中断;
  • lockInterruptibly方法在获取不到锁时也会阻塞,它与lock方法的区别在于阻塞在该方法时可以被中断;
  • tryLock方法也是用来获取锁的,它的无参版本在获取不到锁时会立刻返回false,它的计时等待版本会在等待指定时间还获取不到锁时返回false,计时等待的tryLock在阻塞期间也能够被中断。使用tryLock方法的典型代码如下:
if (myLock.tryLock()) { 
  try { 
    … 
  } finally { 
    myLock.unlock(); 
  }
} else { 
//做其他的工作
}
  • unlock方法用来释放锁;
  • newCondition方法用来获取当前锁对象相关的条件对象。

现在有了ReentrantLock(重入锁)我们就可以给add()方法加锁了,代码如下:

Lock myLock = new ReentrantLock();
public void add(long value) { 
  myLock.lock(); 
  try { 
    this.count = this.count + value; 
  } finally { 
    myLock.unlock(); 
  }
}

从以上代码可以看到,使用ReentrantLock对象来上锁时只需要先获取一个它的实例。然后通过lock方法进行上锁,通过unlock方法进行解锁。注意,我们使用了一个try-finally块,以确保即使发生异常也总是会解锁,不然其他线程会一直无法执行add方法。当一个线程执行完“myLock.lock()”时,它就获得了一个锁对象,这就相当于它给临界区上了锁,其他线程都无法进来,只有这个线程执行完“myLock.unlock()”时,释放了锁对象,其他线程才能再通过“myLock.lock()”获得锁对象,从而进入临界区。也就是说,当一个线程获取了锁对象后,其他尝试获取锁对象的线程都会被阻塞,进入Blocked状态,直至获取锁对象的线程释放了锁对象。
有了锁对象,尽管线程A在执行add方法的过程中被线程调度程序剥夺了运行权,其他的线程也进入不了临界区,因为线程A还在持有锁对象。这样一来,我们就很好的保护了临界区。
ReentrantLock锁是可重入的,这意味着线程可以重复获得已经持有的锁,每个锁对象内部都持有一个计数,每当线程获取依次锁对象,这个计数就加1,释放一次就减1。只有当计数值变为0时,才意味着这个线程释放了锁对象,这时其他线程才可以来获取。

条件对象

有些时候,线程进入临界区后不能立即执行,它需要等某一条件满足后才开始执行。比如,我们希望count值大于5的时候才增加它的值,我们最先想到的是加个条件判断:

public void add(int value) { 
  if (this.count > 5) { 
    this.count = this.count + value; 
  }
}

然而上面的代码存在一个问题。假设线程A执行完了条件判断并的值count值大于5,而在此时该线程被线程调度程序中断执行,转而调度线程B,线程B对同一counter对象的count值进行了修改,使得它不再大于5,这时线程调度程序又来调度线程A,线程A刚才判定了条件为真,所以会执行add方法,尽管此时count值已不再大于5。显然,这与我们所希望的情况的不符的。对于这种问题,我们想到了可以在条件判断前后加锁与解锁:

public void add(int value) {
  myLock.lock(); 
  try { 
    while (counter.getCount() <= 5) { 
      //等待直到大于5 
    } 
    this.count = this.count + value; 
  } finally { 
    myLock.unlock(); 
  }
}

在以上代码中,若线程A发现count值小于等于5,它会一直等到别的线程增加它的值直到它大于5。然而线程A此时持有锁对象,其他线程无法进入临界区(add方法内部)来改变count的值,所以当线程A进入临界区时若count小于等于5,线程A会一直在循环中等待,其他的线程也无法进入临界区。这种情况下,我们可以使用条件对象来管理那些已经获得了一个锁却不能开始干活的线程。一个锁对象可以有一个或多个相关的条件对象,在锁对象上调用newCondition方法就可以获得一个条件对象。比如我们可以为“count值大于5”获得一个条件对象:

Condition enoughCount = myLock.newCondition();

然后,线程A发现count值不够时,调用“enoughCount.await()”即可,这时它便会进入Waiting状态,放弃它持有的锁对象,以便其他线程能够进入临界区。当线程B进入临界区修改了count值后,发现了count值大于5,线程B可通过”enoughCount.signalAll()”来“唤醒所有等待这一条件满足的线程(这里只有线程A)。此时线程A会从Waiting状态进入Runnable状态。当线程A再次被调度时,它便会从await方法返回,重新获得锁并接着刚才继续执行。注意,此时线程A会再次测试条件是否满足,若满足则执行相应操作。也就是说signalAll方法仅仅是通知线程A一声count的值可能大于5了,应该再测试一下。还有一个signal方法,会随机唤醒一个正在等待某条件的线程,这个方法的风险在于若随机唤醒的线程测试条件后发现仍然不满足,它还是会再次进入Waiting状态,若以后不再有线程唤醒它,它便不能再运行了。

synchronized关键字

Java中的每个对象都有一个内部锁,这个内部锁也被称为监视器(monitor);每个类内部也有一个锁,用于控制多个线程对其静态成员的并发访问。若一个实例方法用synchronized关键字修饰,那么这个对象的内部锁会“保护”此方法,我们称此方法为同步方法。这意味着只有获取了该对象内部锁的线程才能够执行此方法。也就是说,以下的代码:

public synchronized void add(int value) { 
  ...
}

等价于

public void add(int value) { 
  this.innerLock.lock(); 
  try { 
    ... 
  } finally { 
    this.innerLock.unlock(); 
  }
}

这意味着,我们通过给add方法加上synchronized关键字即可保护它,加锁解锁的工作不需要我们再手动完成。对象的内部锁在同一时刻只能由一个线程持有,其他尝试获取的线程都会被阻塞直至该线程释放锁,这种情况下被阻塞的线程无法被中断。
内部锁对象只有一个相关条件。wait方法添加一个线程到这个条件的等待集中;notifyAll / notify方法会唤醒等待集中的线程。也就是说wait() / notify()等价于enoughCount.await() / enoughCount.signAll()。以上add方法我们可以这么实现:

public synchronized void add(int value) { 
  while (this.count <= 5) { 
    wait(); 
  } 
  this.count += value; 
  notifyAll();
}

这份代码显然比我们上面的实现要简洁得多,实际开发中也更加常用。
我们也可以用synchronized关键字修饰静态方法,这样的话,进入该方法的线程获取相关类的Class对象的内部锁。例如,若Counter中含有一个synchronized关键字修饰的静态方法,那么进入该方法的线程会获得Bank.class的内部锁。这意味着其他任何线程不能执行Counter类的任何同步静态方法。

这里涉及一个知识点,类锁和对象锁
静态方法上面synchronized关键字加上的是类的锁,而实例方法上面synchronized关键字加上的是对象的锁,不同的对象,调用相同的同步方法(加synchronized关键字的方法)所使用的锁对象不同,所以不可以同步
- 在静态方法上的锁,和实例方法上的锁,默认不是同样的,如果同步需要制定两把锁一样。
- 关于同一个类的方法上的锁,来自于调用该方法的对象,如果调用该方法的对象是相同的,那么锁必然相同,否则就不相同。比如 new A().x() 和 new A().x(),对象不同,锁不同,如果A的单利的,就能互斥。
- 静态方法加锁,能和所有该类其他静态方法加锁的进行互斥
- 静态方法加锁,和xx.class 锁效果一样,直接属于类的

对象内部锁存在一些局限性:
- 不能中断一个正在试图获取锁的线程;
- 试图获取锁时不能设定超时;
- 每个锁仅有一个相关条件。

那么我们究竟应该使用Lock/Condition还是synchronized关键字呢?答案是能不用尽量都不用,我们应尽可能使用java.util.concurrent包中提供给我们的相应机制(后面会介绍)。
当我们要在synchronized关键字与Lock间做出选择时我们需要考虑以下几点:
若我们需要多个线程进行读操作,应该使用实现了Lock接口的ReentrantReadWriteLock类,这个类允许多个线程同时读一个数据对象(这个类的使用后面会介绍);
当我们需要Lock/Condition的特性时,应该考虑使用它(比如多个条件还有计时等待版本的await函数);
一般场景我们可以考虑使用synchronized关键字,因为它的简洁性一定程度上能够减少出错的可能。关于synchronized关键字需要注意的一点是:synchronized方法或者synchronized代码块出现异常时,Java虚拟机会自动释放当前线程已获取的锁。

同步阻塞

上面我们提到了一个线程调用synchronized方法可以获得对象的内部锁(前提是还未被其他线程获取),获得对象内部锁的另一种方法就是通过同步阻塞:

synchronized (obj) { 
  //临界区
}

一个线程执行上面的代码块便可以获取obj对象的内部锁,直至它离开这个代码块才会释放锁。
我们经常会看到一种特殊的锁,如下所示:

public class Counter { 
  private Object lock = new Object(); 
  synchronized (lock) { 
    //临界区 
  } 
  ...
}

那么这种使用这种锁有什么好处呢?我们知道Counter对象只有一个内部锁,这个内部锁在同一时刻只能被一个对象持有,那么设想Counter对象中定义了两个synchronized方法。在某一时刻,线程A进入了其中一个synchronized方法并获取了内部锁,此时线程B尝试进去另一个synchronized方法时由于对象内部锁还没有被线程A释放,因此线程B只能被阻塞。然而我们的两个synchronized方法是两个不同的临界区,它们不会相互影响,所以它们可以在同一时刻被不同的线程所执行。这时我们就可以使用如上面所示的显式的锁对象,它允许不同的方法同步在不同的锁上。

volatile关键字

Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。它在某些情况下比synchronized的开销更小。

Volatile的官方定义

Java语言规范第三版中对volatile的定义如下: java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

为什么要使用Volatile

Volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。使用volatile关键字修饰一个实例域会告诉编译器和虚拟机这个域可能会被多线程并发访问,这样编译器和虚拟机就能确保它的值总是我们所期望的。

Volatile的实现原理

volatile关键字的实现原理大致是这样的:我们在访问内存中的变量时,通常都会把它缓存在寄存器中,以后再需要读它的值时,只需从相应寄存器中读取,若要对该变量进行写操作,则直接写相应寄存器,最后写回该变量所在的内存单元。若线程A把count变量的值缓存在寄存器中,并将count加2(将相应寄存器的值加2),这时线程B被调度,它读取count变量加2后并写回。然后线程A又被调度,它会接着刚才的操作,也就是会把count值写回,此时线程A是直接把寄存器中的值写回count所在单元,而这个值是过期的。若count被volatile关键字修饰,这个问题便可被圆满解决。volatile变量有一个性质,就是任何时候读取它的值时,都会直接去相应内存单元读取,而不是读取缓存在寄存器中的值。这样一来,在上面那个场景中,线程A把count写回时,会从内存中读取count最新的值,从而确保了count的值总是我们所期望的。

死锁

假设现在进程中只有线程A和线程B这两个线程,考虑下面这样一种情形:
线程A获取了counterA对象的内部锁,线程B获取了counterB对象的内部锁。而线程A只有在获取counterB的内部锁后才能继续执行,线程B只有在获取线程A的内部锁后才能继续执行。这样一来,两个线程在互相等待对方释放锁从而谁也没法继续执行,这种现象就叫做死锁(deadlock)。
除了以上情况,还有一种类似的死锁情况是两个线程获取锁后都不满足条件从而进入条件的等待集中,相互等待对方唤醒自己。
Java没有为解决死锁提供内在机制,因此我们只有在开发时格外小心,以避免死锁的发生。

读/写锁

若很多线程从一个内存区域读取数据,但其中只有极少的一部分线程会对其中的数据进行修改,此时我们希望所有Reader线程共享数据,而所有Writer线程对数据的访问要互斥。我们可以使用读/写锁来达到这一目的。
Java中的读/写锁对应着ReentrantReadWriteLock类,它实现了ReadWriteLock接口,这个接口的定义如下:

public interface ReadWriteLock { 
  Lock readLock(); 
  Lock writeLock();
}

我们可以看到这个接口就定义了两个方法,其中readLock方法用来获取一个“读锁”,writeLock方法用来获取一个“写锁”。ReentrantReadWriteLock类的使用步骤通常如下所示:

//构造一个ReentrantReadWriteLock对象
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
//分别从中“提取”读锁和写锁
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
//对所有的Reader线程加读锁
readLock.lock();
try { 
  //读操作可并发,但写操作会互斥
} finally { 
  readLock.unlock();
}
//对所有的Writer线程加写锁
writeLock.lock();
try { 
  //排斥所有其他线程的读和写操作
} finally { 
  writeLock.unlock();
}

在使用ReentrantReadWriteLock类时,我们需要注意以下两点:
- 若当前已经有线程占用了读锁,其他要申请写锁的线程需要占用读锁的线程释放了读锁才能申请成功;
- 若当前已经有线程占用了写锁,其他要申请读锁或写锁的线程都需要等待占用写锁的线程释放了写锁才能申请成功。

阻塞队列

以上我们所介绍的都属于Java并发机制的底层基础设施。在实际编程我们应该尽量避免使用以上介绍的较为底层的机制,而使用Java类库中提供给我们封装好的较高层次的抽象。对于许多同步问题,我们可以通过使用一个或多个队列来解决。
当试图向满队列中添加元素或者向空队列中移除元素时,阻塞队列(blocking queue)会导致线程阻塞。通过阻塞队列,我们可以按以下模式来工作:工作者线程可以周期性的将中间结果放入阻塞队列中,其他线程可取出中间结果并进行进一步操作。若前者工作的比较慢(还没来得及向队列中插入元素),后者会等待它(试图从空队列中取元素从而阻塞);若前者运行的快(试图向满队列中插元素),它会等待其他线程。阻塞队列提供了以下方法:
- add方法:添加一个元素。若队列已满,会抛出IllegalStateException异常。
- element方法:返回队列的头元素。若队列为空,会抛出NoSuchElementException异常。
- offer方法:添加一个元素,若成功则返回true。若队列已满,则返回false。
- peek方法:返回队列的头元素。若队列为空,则返回null。
- poll方法:删除并返回队列的头元素。若队列为空,则返回null。
- put方法:添加一个元素。若队列已满,则阻塞。
- remove方法:移除并返回头元素。若队列为空,会抛出NoSuchElementException。
- take方法:移除并返回头元素。若队列为空,则阻塞。

java.util.concurrent包提供了以下几种阻塞队列:
- LinkedBlockingQueue是一个基于链表实现的阻塞队列。默认容量没有上限,但也有可以指定最大容量的构造方法。它有的“双端队列版本”为LinkedBlockingDeque。
- ArrayBlockingQueue是一个基于数组实现的阻塞队列,它在构造时需要指定容量。它还有一个构造方法可以指定一个公平性参数,若这个参数为true,那么等待了最长时间的线程会得到优先处理(指定公平性参数会降低性能)。
- PriorityBlockingQueue是一个基于堆实现的带优先级的阻塞队列。元素会按照它们的优先级被移除队列。

java.util.concurrent.Callable接口

Callable接口与Runnable接口很相似,都是去描述一个任务,Runnable用run( )方法承载任务的信息,而在Callable用call( )方法去承载任务信息。
Callable和Runnable有几点不同:
1. Callable规定的方法是call(),而Runnable规定的方法是run().
2. Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
3. call()方法可抛出异常,而run()方法是不能抛出异常的。
4. 运行Callable任务可拿到一个Future对象

Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。声明如下

public interface Future<V> { 
  boolean cancel(boolean mayInterruptIfRunning); 
  boolean isCancelled(); 
  boolean isDone(); 
  V get() throws InterruptedException, ExecutionException; 
  V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

在Future接口中声明了5个方法,每个方法的作用如下:
- cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false(即如果取消已经完成的任务会返回false);如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
- isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
- isDone方法表示任务是否已经完成,若任务完成,则返回true;
- get()方法用来获取执行结果,这个方法会阻塞,一直等到任务执行完才返回;
- get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。

Future提供了三种功能:
- 判断任务是否完成;
- 能够中断任务;
- 能够获取任务执行结果。

下面这段代码将演示Callable与Future如何演示

public static void main(String[] args) {
        FutureTask<String> future = new FutureTask<String>(new CallableImpl());//FutureTask实现了Runnable, Future
        Thread thread = new Thread(future);//其实这里面接受的是Runnable
        thread.start();
        System.out.println("begin");
        try {
            System.out.println(future.get());//get()方法是阻塞式的
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
            System.out.println("Oh my god! It error");
        }
        System.out.println("finish");
    }

    private static class CallableImpl implements Callable<String> {

        @Override
        public String call() throws Exception {
            Thread.sleep(10 * 1000);//模拟任务耗时
            return "the call method is finish";
        }
    }

下面这段代码将采用内部类的方式去演示,如何用Runnable与Future创建一个线程

public static void main(String[] args) {
    new Thread(new FutureTask<String>(new Callable<String>() {
        @Override
        public String call() throws Exception {
            System.out.println("task is over");
            return "task is over";
        }
    })).start();
}

Executor(执行器)

创建一个新线程涉及和操作系统的交互,因此会产生一定的开销。在有些应用场景下,我们会在程序中创建大量生命周期很短的线程,这时我们应该使用线程池(thread pool)。通常,一个线程池中包含一些准备运行的空闲线程,每次将Runnable对象交给线程池,就会有一个线程执行run方法。当run方法执行完毕时,线程不会进入Dead状态,而是在线程池中准备等下一个Runnable到来时提供服务。使用线程池统一管理线程可以减少并发线程的数目,线程数过多往往会在线程上下文切换上以及同步操作上浪费过多时间。执行器类(java.util.concurrent.Executors)提供了许多静态工厂方法来构建线程池。

简介

1.5后引入的Executor的最大优点是把任务的提交和执行解耦。要执行任务的人只需把Task描述清楚,然后提交即可。这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。

    public void executorServiceAndCallableDemo() throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(7);
        Future<String> future = executorService.submit(
                new Callable<String>() {
                    public String call() throws Exception {
                        Thread.sleep(10 * 1000);//模拟线程的耗时时间
                        return "I am a task;
                    }
                });
        System.out.println("begin");
        System.out.println(future.get());//阻塞式
        System.out.println("finish");
    }
线程池

上述代码中设计到线程池的概念,线程池是预先创建线程的一种技术。线程池在还没有任务到来之前,创建一定数量的线程,放入空闲队列中。这些线程都是处于睡眠状态,即均为启动,不消耗CPU,而只是占用较小的内存空间。当请求到来之后,缓冲池给这次请求分配一个空闲线程,把请求传入此线程中运行,进行处理。当预先创建的线程都处于运行状态,即预制线程不够,线程池可以自由创建一定数量的新线程,用于处理更多的请求。当系统比较闲的时候,也可以通过移除一部分一直处于停用状态的线程。

线程池作用

线程池作用就是限制系统中执行线程的数量。根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

为什么要用线程池:
  • 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
相比new Thread,Java提供的线程池的好处在于:
  • 重用存在的线程,减少对象创建、消亡的开销,性能佳。
  • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
  • 提供定时执行、定期执行、单线程、并发数控制等功能。
简单线程池的设计

一个典型的线程池,应该包括如下几个部分:
1. 线程池管理器(ThreadPool),用于启动、停用,管理线程池
2. 工作线程(WorkThread),线程池中的线程
3. 请求接口(WorkRequest),创建请求对象,以供工作线程调度任务的执行
4. 请求队列(RequestQueue),用于存放和提取请求
5. 结果队列(ResultQueue),用于存储请求执行后返回的结果

线程池的创建

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。
1. newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2. newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3. newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
4. newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

ThreadPoolExecutor(线程池)类

java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。下面我们来看一下ThreadPoolExecutor类的具体实现源码。
在ThreadPoolExecutor类中提供了四个构造方法:

public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}

从上面的代码可以得知,ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。
下面解释下一下构造器中各个参数的含义:
- corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
- maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
- TimeUnit.DAYS; //天
- TimeUnit.HOURS; //小时
- TimeUnit.MINUTES; //分钟
- TimeUnit.SECONDS; //秒
- TimeUnit.MILLISECONDS; //毫秒
- TimeUnit.MICROSECONDS; //微妙
- TimeUnit.NANOSECONDS; //纳秒
- workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
- ArrayBlockingQueue
是一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素
- LinkedBlockingQueue
基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成)
- SynchronousQueue
SynchronousQueue是无界的,是一种无缓冲的等待队列,但是由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加;可以认为SynchronousQueue是一个缓存值为1的阻塞队列,但是 isEmpty()方法永远返回是true,remainingCapacity() 方法永远返回是0,remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null。

ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
- threadFactory:线程工厂,主要用来创建线程;
- handler:表示当拒绝处理任务时的策略,有以下四种取值:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

同步容器与并发容器

同步容器

Java中的同步容器指的是线程安全的集合类,同步容器主要包含以下两类:
- 通过Collections类中的相应方法把普通容器类包装成线程安全的版本;
- Vector、HashTable等系统为我们封装好的线程安全的集合类。

相比与并发容器(下面会介绍),同步容器存在以下缺点:
- 对于并发读访问的支持不够好;
- 由于内部多采用synchronized关键字实现,所以性能上不如并发容器;
- 对同步容器进行迭代的同时修改它的内容,会报ConcurrentModificationException异常。

并发容器

并发容器相比于同步容器,具有更强的并发访问支持,主要体现在以下方面:
- 在迭代并发容器时修改其内容并不会抛出ConcurrentModificationException异常;
- 在并发容器的内部实现中尽量避免了使用synchronized关键字,从而增强了并发性。

Java在java.util.concurrent包中提供了主要以下并发容器类:
- ConcurrentHashMap,这个并发容器是为了取代同步的HashMap;
- CopyOnWriteArrayList,使用这个类在迭代时进行修改不抛异常;
- ConcurrentLinkedQuerue是一个非阻塞队列;
- ConcurrentSkipListMap用于在并发环境下替代SortedMap;
- ConcurrentSkipSetMap用于在并发环境下替代SortedSet。

关于这些类的具体使用,大家可以参考官方文档及相关博文。通常来说,并发容器的内部实现做到了并发读取不用加锁,并发写时加锁的粒度尽可能小。

同步器(Synchronizer)

CountDownLatch

CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。

CountDownLatch类有3个基本元素:
1. 初始值决定CountDownLatch类需要等待的事件的数量。
2. await() 方法, 被等待全部事件终结的线程调用。
3. countDown() 方法,事件在结束执行后调用。

当创建 CountDownLatch 对象时,对象使用构造函数的参数来初始化内部计数器。每次调用 countDown() 方法, CountDownLatch 对象内部计数器减一。当内部计数器达到0时, CountDownLatch 对象唤醒全部使用 await() 方法睡眠的线程们。
不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。一旦计数器的值初始后,唯一可以修改它的方法就是之前用的 countDown() 方法。当计数器到达0时, 全部调用 await() 方法会立刻返回,接下来任何countDown() 方法的调用都将不会造成任何影响。
此方法与其他同步方法有这些不同:
CountDownLatch 机制不是用来保护共享资源或者临界区。它是用来同步一个或者多个执行多个任务的线程。它只能使用一次。像之前解说的,一旦CountDownLatch的计数器到达0,任何对它的方法的调用都是无效的。如果你想再次同步,你必须创建新的对象。
CountDownLatch 类有另一种版本的 await() 方法,它是:await(long time, TimeUnit unit): 此方法会休眠直到被中断; CountDownLatch 内部计数器到达0或者特定的时间过去了。TimeUnit 类包含了:DAYS, HOURS, MICROSECONDS, MILLISECONDS, MINUTES, NANOSECONDS, 和 SECONDS.

/**
 * Created by SunShine on 16/10/11.
 * CountDownLatch类
 */
public class CountDownLatchDemo {

    private static CountDownLatch countDownLatch = new CountDownLatch(2);//2为初始值,也即CountDownLatch一开始就默认有2件要等待的事件。

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(20);//睡一定的时间,让别的线程能够有时间创建
                    countDownLatch.await();
                    System.out.println("await exe");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);//此地10<20是为了,测试先countDown会不会影响后来的await方法,事实证明不会,只要CountDownLatch降成0,所有的await线程都会可执行
                    System.out.println("countDown1 is begin exe");
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);//模拟等待
                    System.out.println("countDown2 is begin exe");
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

//countDown1 is begin exe
//countDown2 is begin exe
//await exe
CyclicBarrier

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。
CyclicBarrier有两个构造函数:
- public CyclicBarrier(int parties)
接受阻塞的线程个数,到达这么多个在该CyclicBarrier阻塞的线程时,就开始唤醒所有线程
- public CyclicBarrier(int parties, Runnable barrierAction)
可以自定义一个任务,在达到parties个线程阻塞时前调用该任务,调用结束之后,才开始唤醒其他线程

/**
 * Created by SunShine on 16/10/11.
 * CyclicBarrier类
 */
public class CyclicBarrierDemo {

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
        @Override
        public void run() {
            System.out.println("主线程开始执行");
        }
    });

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        Thread.sleep(1000);
                        System.out.println("cyclicBarrier1 begin await");
                        cyclicBarrier.await();
                        System.out.println("cyclicBarrier1 await");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        Thread.sleep(500);
                        System.out.println("cyclicBarrier2 begin await");
                        cyclicBarrier.await();
                        System.out.println("cyclicBarrier2 await");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true){
                        Thread.sleep(100);
                        System.out.println("cyclicBarrier3 begin await");
                        cyclicBarrier.await();
                        System.out.println("cyclicBarrier3 await");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

cyclicBarrier3 begin await
cyclicBarrier2 begin await
cyclicBarrier1 begin await
主线程开始执行
cyclicBarrier1 await
cyclicBarrier3 await
cyclicBarrier2 await
cyclicBarrier3 begin await
cyclicBarrier2 begin await
cyclicBarrier1 begin await
主线程开始执行
cyclicBarrier1 await
cyclicBarrier3 await
cyclicBarrier2 await
DelayQueue

这是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走,这种队列是有序的,即对头独享的延迟到期的时间最长,如果没有任何延迟到期,那么就不会有任何头对象,并且poll()将返回null(正因为这样,你不能把null放置到这种队列中)
在实际开发中,可能有如下场景
1. 关闭空闲连接。服务器中,有很多客户端的连接,空闲一段时间之后需要关闭之。
2. 缓存。缓存中的对象,超过了空闲时间,需要从缓存中移出。
3. 任务超时处理。在网络协议滑动窗口请求应答式交互时,处理超时未响应的请求。

一种笨笨的办法就是,使用一个后台线程,遍历所有对象,挨个检查。这种笨笨的办法简单好用,但是对象数量过多时,可能存在性能问题,检查间隔时间不好设置,间隔时间过大,影响精确度,多小则存在效率问题。而且做不到按超时的时间顺序处理。
这场景,使用DelayQueue最适合了。
DelayQueue是一个BlockingQueue,其特化的参数是Delayed。Delayed扩展了Comparable接口,比较的基准为延时的时间值,Delayed接口的实现类getDelay的返回值应为固定值(final)。DelayQueue内部是使用PriorityQueue实现的。
DelayQueue = BlockingQueue + PriorityQueue + Delayed
DelayQueue的关键元素BlockingQueue、PriorityQueue、Delayed。可以这么说,DelayQueue是一个使用优先队列(PriorityQueue)实现的BlockingQueue,优先队列的比较基准值是时间。

PriorityBlockingQueue

这是一个很基础的优先级队列,它具有可阻塞的读取操作。优先级队列中的对象是按照优先级顺序从对列中出现的任何。

Semaphore

正常的锁(来自concurrent.lock或内建的synchronized锁)在任何时刻都只允许一个任务访问一项资源,而计数信号量允许n个任务同时访问这个资源。你还可以将信号量看做是在向外分发使用的资源的“许可证”

Exchanger

如果两个线程在运行过程中需要交换彼此的信息,比如一个数据或者使用的空间,就需要用到Exchanger这个类,Exchanger为线程交换信息提供了非常方便的途径,它可以作为两个线程交换对象的同步点,只有当每个线程都在进入 exchange ()方法并给出对象时,才能接受其他线程返回时给出的对象。
Exchanger大多用于兄弟线程的信息交换,彼此需要彼此的数据

private static Exchanger<String> exchanger = new Exchanger<>();

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                    System.out.println("thread1 is 张三");
                    String exchangeMsg = exchanger.exchange("张三");
                    System.out.println("finish: thread1 is " + exchangeMsg);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(() -> {
            try {
                Thread.sleep(10);
                System.out.println("thread2 is 李四");
                String exchangeMsg = exchanger.exchange("李四");
                System.out.println("finish: thread2 is " + exchangeMsg);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值