• organized according to

Basic Theory

Thread object

Sleep

  • sleep [static func]
    • The thread does not lose ownership of any monitors.
    • The thread is in an interrputible state. [If any thread has interrputed the current thread. The interrupted status of the current thread is cleared when this InterruptedException is thrown.]
      • 测试了一下这个InterruptedException抛出的时间,还是要等待时间片轮转到当前线程才会throw这个异常。(感觉sleep本质上是不是还是会调度这个线程啊?)
    • Thread.sleep() 因为是sleep current thread啦,总不能强行sleep别的线程吧 = =
  • 实现:It calls sleep on the underlying native thread provided by the operating system. [这里参考linux thread&process.]

Interrupt

  • Thread.interrputed()
    • test当前线程是否已经被interrupted。
    • The interrupted status of the thread is cleared by this method. [也就是连续第二次调用该方法时,必然返回false,除非当前线程又被interrupted一次。]
  • Thread#interrupt()
    • 中断该线程
    • 方法反馈:
      • 如果该线程 blocked 在以下方法的调用中
        • Object#wait()
        • Thread#join()
        • Thread.sleep()

         则它的interrupt status会被clear,并且会receive an InterruptedException

      • 如果该线程阻塞在 an I/O operation upon an java.nio.channels.InterruptibleChannel , 那么该channel会被closed,该线程中断状态will be set,并收到ClosedByInterrputException。
      • 如果线程blocked in a java.nio.channels.Selector, 那么interrupt status will be set,并且立即从selection operation返回
      • 如果上述条件都不符合,那么就只是简单地将interrupt status 置位。
    • sleep() + interrput() 的测试
      public static void interrputTest() {
              Thread t = new Thread() {
                  @Override
                  public void run() {
                      System.out.println("The new thread begin...");
                      try {
                          Thread.sleep(100000);
                      } catch (InterruptedException e) {
                          System.out.println("The new thread has been interrputed");
                      }
                  }
              };
              System.out.println("To start a new thread and yield for the new thread.");
              t.start();
              Thread.yield();  // to get the new thread run
              System.out.println("Try to interrupt the new thread.");
              t.interrupt();
          }

Join

  • Thread#join()
    • waits for this thread to die. [join() methods suspends the execution of the calling thread until the object called finishes its execution.]
    • throws InterruptedException
  • join(long millis)的实现
    • /**
           * Waits at most {@code millis} milliseconds for this thread to
           * die. A timeout of {@code 0} means to wait forever.
           *
           * <p> This implementation uses a loop of {@code this.wait} calls
           * conditioned on {@code this.isAlive}. As a thread terminates the
           * {@code this.notifyAll} method is invoked. It is recommended that
           * applications not use {@code wait}, {@code notify}, or
           * {@code notifyAll} on {@code Thread} instances.
           *
           * @param  millis
           *         the time to wait in milliseconds
           *
           * @throws  IllegalArgumentException
           *          if the value of {@code millis} is negative
           *
           * @throws  InterruptedException
           *          if any thread has interrupted the current thread. The
           *          <i>interrupted status</i> of the current thread is
           *          cleared when this exception is thrown.
           */
          public final synchronized void join(long millis)
          throws InterruptedException {
              long base = System.currentTimeMillis();
              long now = 0;
      
              if (millis < 0) {
                  throw new IllegalArgumentException("timeout value is negative");
              }
      
              if (millis == 0) {
                  while (isAlive()) {
                      wait(0);
                  }
              } else {
                  while (isAlive()) {
                      long delay = millis - now;
                      if (delay <= 0) {
                          break;
                      }
                      wait(delay);
                      now = System.currentTimeMillis() - base;
                  }
              }
          }
    • 可以看到,join方法的实现是通过Object#wait() 方法,当t1 调用 t2.join() 时,t1会获得线程对象 t2 的锁,调用该对象的wait()方法,直到被notify。
    • 当线程终止时,会调用notifyAll()。[isAlive() + wait() ]
    • 那么显然join 会throw InterruptedException 是因为wait()。
  • test code
public static void joinTest(){
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("The new thread begins...");
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    System.out.println("The new thread has been interrputed");
                }
                System.out.println("The new thread ends after a long sleep.");
            }
        };
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            System.out.println("The main thread has been interrupted");
        }

        System.out.println("The main thread ends.");
    }

Synchronization

Memory Consistency Errors

  • 内存一致性错误
  • 定义:Memory consistency errors occur when different threads have inconsistent views of what should be the same data.
  • 要避免memory Consistency error,需要理解happens-before关系。

Happens-before

  • 一种偏序关系
  • 在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须存在happens-before关系。

Synchronized

  • java关键字,当用它来修饰一个方法or一个代码块时,能够保证在同一时刻最多只有一个线程执行该段代码。

Implementation

  • synchronized语句:
    • 在同步块的入口 and 出口位置 分别插入 monitorenter 和 monitorexit字节码指令
      • monitorenter会弹出objectref,获得和objectref相关联的锁:
        • 获得栈中objectref所引用对象的锁。
        • 如果线程已经拥有了该对象的锁,锁的计数器就+1。
        • 一大段关于monitorenter的介绍:

          Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

          If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

          If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

          If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

      • monitorexit会弹出objectref,释放和objectref相关联的锁
    • monitorenter 和 monitorexit 字节码依赖于底层的os的Mutex Lock来实现。[但由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种转换的代价是昂贵的。所以可以采用一些锁优化的方法(具体见下面)来减少开销。]
  • synchronized方法:
    • synchronized方法会被翻译成普通的方法调用和返回指令,如:invokevirtual,areturn指令。
    • 在JVM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象or该方法所属的Class在JVM的内部对象表示Klass作为锁对象。
    • 一大段关于方法级别synchronized的实现原理:
      Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool’s method_info structure by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.
  • 对象头:JVM创建对象时会在对象前面加上两个字大小的对象头。[对象 = 对象头 + 实例数据 + 对其填充]

    • 根据不同的状态位Mark World中存放不同的内容。
    • Kclass ptr指向Class字节码在虚拟机内部的对象表示的地址
  • Monitor Record:是线程的私有数据,每一个线程都有一个可用的monitor record列表,同时还有一个全局的可以用列表。

    • 用处:每一个被锁住的对象都会和一个monitor record关联(对象头中的LockWord指向monitor record的起始地址)
    • 各个字段:
      • Owner:初始化为Null时表示没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放后又设置为Null。
      • EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图获取monitor record失败的线程。
      • RcThis:表示blocked或waiting在该monitor record上所有线程的个数
      • Nest:用来实现重入锁的计数。
      • HashCode:保存从对象头拷贝过来的HashCode值。(可能还包含GC age)
      • Candidate:用来避免不必要的阻塞或等待线程唤醒。因为每一次只有一个线程能成功拥有锁,如果前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换,从而性能验证下降。Candidate只有两种可能的值:0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

 Atomic Operation

  •  原子操作是指不会被线程调度机制打断的操作,不需要synchronized。
  • 这种操作一旦开始,就一直运行到结束,中间不会有任何context switch

Backgroud

  • 现代操作系统支持多任务的并发,并发在提高计算资源利用率的同时也带来了资源竞争的问题。
  • 对一个最basic的increment: count++,都不是线程安全的 --> 这里面有三个独立的操作:
    • 获取变量当前值
    • 为该值加1
    • 然后写回新的值

   (但是Java5 新增了AtomicInteger类,该类包含方法getAndIncrement()实现原子加操作。其本质并不是通过加锁来实现的哦,属于无锁操作。

            关于java.util.concurrent.atomic包,硬件实现,CAS我们会在后面讲到。

 在OS中考虑

  • 可能的solution
    • 单处理器原子操作:最简单的做法就是,将count++翻译为单指令操作。(在单处理器中,一条处理器指令就是一个原子操作。
    • 多处理器原子操作:在多处理器,例如SMP架构下,上述结论就不再成立。
      • Intel x86指令集提供了指令前缀lock用于锁定前端串行总线(FSB),保证了指令执行时不会受到其他处理器的干扰。

 Java怎么实现

  • 使用循环CAS实现原子操作:
    • 自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
    • Java并发包中有一些框架也使用了自旋CAS来实现原子操作。(当然,CAS也存在一些问题,下面有详细介绍~
  • 使用锁机制实现原子操作:
    • 锁机制保证了只有获得锁的线程能够操作锁定的内存区域。
    • JVM内部实现了很多种锁机制,有偏向锁,轻量级锁和互斥锁,有意思的是除了偏向锁,JVM实现锁的方式都用到的循环CAS,当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。详细说明可以参见文章Java SE1.6中的Synchronized。 

The Java Monitor Pattern 

Thread Confinement

  • 线程封闭(Thread confinement): Thread confinement is basically used to avoid external synchronization.
  • 思想:当访问共享的可变数据时,通常需要同步。一种避免使用同步的方式就是不共享数据。
    • 线程封闭:如果仅在单线程内访问数据,就不需要同步。
      • if you ensure that no other thread has access to an object, then that object is said to be confined to a single thread.
      • 在语言 or JVM层面都没有对象单线程封闭的机制,简单来说,你需要保证 no reference to the object escapes to a place that could be accessed by another thread.
      • 有一个工具可以帮助避免leaking reference,比如ThreadLocal类。
    • 栈封闭局部变量的固有属性之一就是封闭在线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。

ThreadLocal

  • ThreadLocal为每一个使用该变量的线程提供一个变量值的副本,是Java中一种比较特殊的绑定机制。
  • 实现:ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。
  • 时间vs空间:同步机制是“时间换空间”的方式,而ThreadLocal采用“以空间换时间”的方式。前者是一份变量,不同的线程排队访问,而后者为每一个线程都提供一份变量。
  • use case
    • 一个很常用的use case就是在多个线程连接数据库操作时:
      ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
              @Override
              protected Connection initialValue() {
                  Connection conn = null;
                  try {
                      conn = DriverManager.getConnection("");
                  } catch (SQLException e) {
                      e.printStackTrace();
                  }
                  return conn;
              }
          };

The client-side locking

 

 

 

锁优化

基本优化方式

  • 锁粗化(Lock Coarsening):也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。
  • 锁消除(Lock Elimination):通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。
  • 轻量级锁(Lightweight Locking):这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒(具体处理步骤下面详细讨论)。
  • 偏向锁(Biased Locking):是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟(可参考这篇文章)。
  • 适应性自旋(Adaptive Spinning):当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态。

Java 1.6

  • JAVA SE 1.6 为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
  • 所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。

  

 Producer-Consumer

  • 参考link,实现得很好。
package basicTheroy;

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * Author: wttttt
 * Github: https://github.com/wttttt-wang/hadoop_inaction
 * Date: 2018-04-09
 * Time: 21:10
 */

/**
 * ProducerConsumer: Implement using object.wait() & object.notifyAll()
 * @param <T>
 */
public class ProConsu<T> {
    private int queueSize;
    private final LinkedList<T> list = new LinkedList<T>();
    private final Object lock = new Object();

    public ProConsu(int queueSize) {
        if (queueSize < 1) throw new IllegalArgumentException("queueSize must be positive number");
        this.queueSize = queueSize;
    }
    public void put(T data) throws InterruptedException {
        synchronized (lock) {
            while (list.size() >= queueSize) lock.wait();  // while instead of if!!!

            list.addLast(data);
            lock.notifyAll();  // notifyAll instead of notify
        }
    }

    public T take() throws InterruptedException {
        synchronized (lock) {
            while (list.size() == 0) lock.wait();
            T data = list.removeFirst();
            lock.notifyAll();
            return data;
        }
    }
}


/**
 * ProducerConsumer: Implement using ReentrantLock condition
 * @param <T>
 */
class ProConsu2<T> {
    private Object[] items;
    int putptr, takeptr, count;
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public ProConsu2() {
        this(10);
    }

    public ProConsu2(int queueSize) {
        if (queueSize < 1) throw new IllegalArgumentException("queueSize must be positive number");
        items = new Object[queueSize];
    }

    public void put(T data) throws InterruptedException{
        lock.lock();
        try {
            while (count == items.length) {
                notFull.await();
            }
            items[putptr] = data;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();  // Must put this in the finally block
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) notEmpty.await();
            T data = (T) items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();
            return data;
        } finally {
            lock.unlock();
        }
    }
}


/**
 * ProducerConsumer: Implement using BlockingQueue
 * @param <T>
 */
class ProConsu3<T> {
    // please refer to the interface java.util.concurrent. BlockingQueue
    // class: ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue...
}
  1. 用object自带的 wait & nofityAll 的问题在于:只有一个condition,那么有可能notifyAll后,抢到CPU的线程并不是valid的,从而又引起很多不必要的线程切换。(比如一个producer notifyAll之后,被另一个producer抢到了CPU,但是此时可能已经满了)  【所以相比之下ReentrantLock更适用于一个lock需要绑定多个condition的情况呀】
  2. 用ReentrantLock的话,可以有两个condition(但是它们必须绑定在同一个lock上啊)。不过实现上要注意的是 lock.unlock() 必须放在finally中。  

 Read-Write

* 如下都是第一读者-写者的实现方式(即只要有读者在读,其他读者就可以直接读)。 [有一个说法是说公平锁可能更适合该场景,否则写者容易饿死。]

// TODO: 下面代码是直接粘过来的,需要重新写再整理。

  1. 用ReentrantLock的ReentrantReadWriteLock
    class Queue3 {
    	private Object data = null;// 共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
    	private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    
    	public void get() {
    		rwl.readLock().lock();// 上读锁,其他线程只能读不能写
    		System.out.println(Thread.currentThread().getName()
    				+ " be ready to read data!");
    		try {
    			Thread.sleep((long) (Math.random() * 1000));
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		System.out.println(Thread.currentThread().getName()
    				+ "have read data :" + data);
    		rwl.readLock().unlock(); // 释放读锁,最好放在finnaly里面
    	}
    
    	public void put(Object data) {
    		rwl.writeLock().lock();// 上写锁,不允许其他线程读也不允许写
    		System.out.println(Thread.currentThread().getName()
    				+ " be ready to write data!");
    		try {
    			Thread.sleep((long) (Math.random() * 1000));
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		this.data = data;
    		System.out.println(Thread.currentThread().getName()
    				+ " have write data: " + data);
    		rwl.writeLock().unlock();// 释放写锁
    	}
    }
  2. 用java自带的Semaphore信号量
    class Queue3
    {
    	private Object data = null;// 共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
    	private Semaphore wmutex = new Semaphore(1);
    	private Semaphore rmutex = new Semaphore(2);
    	private int count = 0;
    
    	public void get()
    	{
    		try
    		{
    			rmutex.acquire();
    			if (count == 0)
    				wmutex.acquire();// 当第一读进程欲读数据库时,阻止写进程写
    			count++;
    			System.out.println(Thread.currentThread().getName()
    					+ " be ready to read data!");
    			try
    			{
    				Thread.sleep((long) (Math.random() * 1000));
    			}
    			catch (InterruptedException e)
    			{
    				e.printStackTrace();
    			}
    			System.out.println(Thread.currentThread().getName()
    					+ "have read data :" + data);
    			count--;
    			if (count == 0)
    				wmutex.release();
    			rmutex.release();
    		}
    		catch (Exception e)
    		{
    			e.printStackTrace();
    		}
    	}
    
    	public void put(Object data)
    	{
    		try
    		{
    			wmutex.acquire();
    			System.out.println(Thread.currentThread().getName()
    					+ " be ready to write data!");
    			try
    			{
    				Thread.sleep((long) (Math.random() * 1000));
    			}
    			catch (InterruptedException e)
    			{
    				e.printStackTrace();
    			}
    			this.data = data;
    			System.out.println(Thread.currentThread().getName()
    					+ " have write data: " + data);
    		}
    		catch (Exception e)
    		{
    			e.printStackTrace();
    		}
    		finally
    		{
    			wmutex.release();
    		}
    	}
    }
    

     

 

 

FYI