目录

Java 线程

线程是操作系统中一种概念,Java 对其进行了封装,Java 线程本质上就是操作系统的中线程,其状态与操作系统的状态大致相同,但还是存在一些区别。

Java 线程状态

Java 线程状态定义在 Thread.State 枚举中,使用 thread#getState 方法可以获取当前线程的状态。

/images/java/multithreading/java-thread-state.png

Java 线程总共存在 6 中状态,分别为:

  • NEW(初始状态)新创建了一个线程对象
  • RUNNABLE(运行状态) Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。调用 start() 方法后,线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  • BLOCKED(阻塞状态)表示线程阻塞于锁。线程阻塞在进入synchronized 修饰的方法或代码块时的状态
  • WATTING(等待状态)进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIMED_WAITING(限时等待状态)该状态不同于WAITING,它可以在指定的时间后自行返回。
  • TERMINATED(终止状态)表示该线程已经执行完毕。线程一旦执行结束或者线程执行过程发生异常且未正常捕获处理,状态都将会自动变成 TERMINATED。

RUNNABLE 与 WATTING

Object#wait

线程在获取到 synchronized 隐式锁后,显示的调用 Object#wait()方法。这种情况下该线程将会让出隐式锁,一旦其他线程获取到该锁,且调用了 Object.notify() 或object.notifyAll(),线程将会唤醒,然后变成 RUNNABLE。

Thread#join

join 方法是一种线程同步方法。假设我们在 main 方法中执行 Thread A.join() 方法,main 线程状态就会变成 WATTING。直到 A 线程执行完毕,main 线程才会再变成 RUNNABLE。

LockSupport#park()

LockSupport 是 JDK 并发包里重要对象,很多锁的实现都依靠该对象。一旦调用 LockSupport#park(),线程就将会变为 WATTING 状态。如果需要唤醒线程就需要调用 LockSupport#unpark,然后线程状态重新变为 RUNNABLE。

RUNNABLE 与 TIMED_WAITING

  • Thread#sleep(long millis)
  • 占有 synchronized 隐式锁的线程调用 Object.wait (long timeout) 方法
  • Thread#join (long millis)
  • LockSupport#parkNanos (Object blocker, long deadline)
  • LockSupport#parkUntil (long deadline)

几个方法的比较

Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。

1
2
3
4
5
Thread.sleep(8575899L);
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(22);
TimeUnit.SECONDS.sleep(55);
TimeUnit.MILLISECONDS.sleep(899);

Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。 作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。

thread.join()/thread.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)。

Object.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。

Object.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒。

调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内。 线程等待时间到了或被notify/notifyAll唤醒后,会进入同步队列竞争锁,如果获得锁,进入RUNNABLE状态,否则进入BLOCKED状态等待获取锁。

创建线程

  • 线程的ID是唯一标识getId()
  • 线程的名称:getName(),如果不设置线程名称默认为“Thread-xx”
  • 线程的优先级:getPriority,线程优先级从1-10,其中数字越大表示优先级别越高,同时获得JVM调度执行的可能性越大
  • 线程池执行
1
2
3
4
public final static int MIN_PRIORITY = 1;
//一般优先级
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

一般不推荐设置线程的优先级,如果进行设置了非法的优先级程序就会出现IllegalArgumentException异常。

  • 继承Thread类。覆盖Thread类中的run方法。调用start方法开启线程
  • 实现Runnable接口,覆盖run方法,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
  • 实现Callable接口

Q&A

sleep 和 yield的不同之处:

  • sleep(long)方法会使线程转入超时等待状态,时间到了之后才会转入就绪状态。而yield()方法不会将线程转入等待,而是强制线程进入就绪状态。
  • 使用sleep(long)方法需要处理异常,而yield()不用。

停止线程

  • run方法执行完成,自然终止。
  • stop()方法,suspend()以及resume()都是过期作废方法,使用它们结果不可预期。
  • 大多数停止一个线程的操作使用Thread.interrupt()等于说给线程打一个停止的标记, 此方法不回去终止一个正在运行的线程,需要加入一个判断才能可以完成线程的停止。

interrupted 和 isInterrupted

  • interrupted : 判断当前线程是否已经中断,会清除状态。
  • isInterrupted :判断线程是否已经中断,不会清除状态。

Java线程有两种

  • 一种是用户线程
  • 一种是守护线程。

守护线程是一个比较特殊的线程,主要被用做程序中后台调度以及支持性工作。当Java虚拟机中不存在非守护线程时,守护线程才会随着JVM一同结束工作。典型的守护线程-GC(垃圾回收器)。启动线程之前设置 Thread.setDaemon(true);

锁的升降级规则 Java SE 1.6 为了提高锁的性能。引入了“偏向锁”和轻量级锁“。

Java SE 1.6 中锁有4种状态。级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

锁只能升级不能降级。

偏向锁

大多数情况,锁不仅不存在多线程竞争,而且总由同一线程多次获得。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行 cas操作来加锁和解锁,只需测试一下对象头 Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁,如果失败,则需要测试下Mark Word中偏向锁的标示是否已经设置成1(表示当前时偏向锁),如果没有设置,则使用cas竞争锁,如果设置了,则尝试使用cas将对象头的偏向锁只想当前线程。

轻量级锁

线程在执行同步块,jvm会现在当前线程的栈帧中创建用于储存锁记录的空间。并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用cas将对象头中的Mark Word替换为之乡锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

Java如何实现原子操作

Java中通过锁和循环cas的方式来实现原子操作,JVM的CAS操作利用了处理器提供的CMPXCHG指令来实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

Condition接口

提供了类似Object监视器方法,与 Lock配合使用实现等待/通知模式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public calss ConditionDemo {

  Lock lock = new ReentrantLock();
  Condition cond = lock.newCondition();
  public void await() throws InterruptedException {
    lock.lock();
    try{
      cond.await();
    } finally {
      lock.unlock();
    }
  }

  public void signal() throws InterruptedException {
    lock.lock();
    try{
      cond.signal();
    } finally {
      lock.unlock();
    }
  }
}

实际上,Java 的BlockingQueue接口的实现类中都采用了这种方式,在集合内部针对 put 和 take 有不同的条件 Condition,保证了put完成后,只有调用take的线程被唤醒,take完成后,只有调用put的线程被唤醒。

1
2
3
4
5
    final ReentrantLock lock;
    /** Condition for waiting takes */
    private final Condition notEmpty;
    /** Condition for waiting puts */
    private final Condition notFull;