JAVA多线程基础

269 阅读6分钟

线程状态以及转换图(共6种状态)

每次调用线程相关方法,需要知道线程状态的转换

同步Synchronization(摘自JLS)

同步synchronization是线程通信的一种机制,JAVA用monitor监视器(锁)来实现同步。每一个JAVA中的对象都关联一个监视器(锁)。任何一个时刻只有一个线程可以锁住监视器,其他线程会阻塞直到他们可以锁住监视器。一个线程一次可能锁住多个监视器(AKA获得多个监视器锁)。通过监视器锁,JAVA让程序员控制线程对共享资源的独占访问。而在底层,监视器锁有JVM XX实现。

JAVA中提供给程序员的同步有synchronized语句块与synchronized方法。进入同步语句需要获得该对象监视器锁。退出同步语句块会释放监视器锁。监视器锁一次只能被一个线程获取,无法获取的线程会被阻塞。

synchronized语句块用一个对象作参数,实例synchronized方法以该方法的实例作为监视器。静态synchronized方法以类的class对象作为监视器。

JAVA不会阻止死锁的产生,需要开发者自己规避。

volatile变量的读写,JUC包下的类 也可以作为同步手段。

The Java programming language provides multiple mechanisms for communicating between threads. The most basic of these methods is synchronization, which is implemented using monitors. Each object in Java is associated with a monitor, which a thread can lock or unlock. Only one thread at a time may hold a lock on a monitor. Any other threads attempting to lock that monitor are blocked until they can obtain a lock on that monitor. A thread t may lock a particular monitor multiple times; each unlock reverses the effect of one lock operation.

The synchronized statement (§14.19) computes a reference to an object; it then attempts to perform a lock action on that object's monitor and does not proceed further until the lock action has successfully completed. After the lock action has been performed, the body of the synchronized statement is executed. If execution of the body is ever completed, either normally or abruptly, an unlock action is automatically performed on that same monitor.

A synchronized method (§8.4.3.6) automatically performs a lock action when it is invoked; its body is not executed until the lock action has successfully completed. If the method is an instance method, it locks the monitor associated with the instance for which it was invoked (that is, the object that will be known as this during execution of the body of the method). If the method is static, it locks the monitor associated with the Class object that represents the class in which the method is defined. If execution of the method's body is ever completed, either normally or abruptly, an unlock action is automatically performed on that same monitor.

The Java programming language neither prevents nor requires detection of deadlock conditions. Programs where threads hold (directly or indirectly) locks on multiple objects should use conventional techniques for deadlock avoidance, creating higher-level locking primitives that do not deadlock, if necessary.

Other mechanisms, such as reads and writes of volatile variables and the use of classes in the java.util.concurrent package, provide alternative ways of synchronization.

监视器monitor是什么?如何实现?

监视器是一个特殊的对象,用来控制对共享资源的访问使得一次只有一个线程可以访问共享资源,只有拥有监视器的线程才可以访问共享资源/执行临界区代码。

在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,

当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。

若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示

等待更新(偷看OS线程内容中)

JAVA中获取对象监视器锁(become the owner of a monitor / lock the monitor)

  1. 是通过monitor enter 与 monitor exit 。JAVA在语言层面并没有显式的让程序员控制监视器锁的获取与释放。而是sychronized 代码块的进入与退出时编译器自动生成监视器锁的获取与释放指令。
  2. 另一种获取监视器锁的方式是用ACC_SYNCHRONIZED标识的方法(synchronized方法会用这个标识),JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法。

监视器锁优化

@等待

等待集wait sets 与通知notication(也翻译为唤醒)

每一个对象,除了有一个关联的监视器,还有一个关联的等待集,等待集是线程的集合。

从等待集添加与移除线程是原子操作。等待集可以通过方法Object.wait/notify/notifyAll 来操作。

等待集的操作也受到线程中断状态和线程类的处理中断的方法的影响,线程的sleep与join会影响wait与notification动作。

Wait等待

执行object.wait方法发生wait动作

Notication通知

执行notify/notifyAll方法 发生通知(唤醒)动作

Interruptions中断

Thread.interrupt 会将对应线程中断状态设置为true。

此外,如果对应线程处在某个对象m的等待集中,那么该线程会被被移除等待集,重新获得m的监视器锁,抛出中断异常。

Thread.isInterrupted会返回线程的中断状态

静态方法Thread.interrupted会返回并清除线程的中断状态

等待,中断,通知的交互

如果一个线程同时被中断与通知,那么可能有2种情况:

  1. 正常从wait方法返回,然后被中断(中断状态被设置为true)。
  2. 从wait方法返回并抛出中断异常

这意味着一个线程从wait方法返回但是没有抛出中断异常并不代表线程没有被中断过,因此需要手动编写代码检测是否被中断。

这还意味着线程收到的通知并不会因为对该线程的中断而丢失。

如果一个线程同时被中断与通知,并且该线程从wait方法返回并抛出中断异常,那么等待集的其他线程会被通知。

Note that if a thread is both interrupted and woken via notify, and that thread returns from wait by throwing an InterruptedException, then some other thread in the wait set must be notified.(from JLS 17.2)

Thread 重点方法

static Thread	currentThread()
//Returns a reference to the currently executing thread object.

boolean	isInterrupted()
void	interrupt()
//Interrupts this thread.
static boolean	interrupted()
//Tests whether the current thread has been interrupted.

void	join()
//Waits for this thread to die.
void	join(long millis)
//Waits at most millis milliseconds for this thread to die.
void	join(long millis, int nanos)
//Waits at most millis milliseconds plus nanos nanoseconds for this thread to die.

static void	sleep(long millis)dulers.
static void	sleep(long millis, int nanos)

static void	yield()
//A hint to the scheduler that the current thread is willing to yield its current use of a processor.

休眠sleep与让出yield

Thread.sleep 使得线程休眠指定时间,然后再参与CPU调度

Thread.yield 使得线程停止执行,重新参与CPU调度

这2个方法不具有任何同步语义,尤其,编译器并不会在调用sleep/yield之前冲刷缓存(将缓存值写入到共享主存上),调用之后也不会刷新缓存(从共享主存重新加载缓存)。

例:

while (!this.done) 
    Thread.sleep(1000)

可能导致线程无限休眠,即使this.done已经被其他线程改变。

Ref

  1. JAVA并发编程的艺术
  2. JLS 17 docs.oracle.com/javase/spec…
  3. juejin.cn/post/684490…