Java并发和多线程(一)基础知识
1.java线程状态
Java中的线程可以处于下列状态之一:
- NEW: 至今尚未启动的线程处于这种状态。
- RUNNABLE: 正在 Java 虚拟机中执行的线程处于这种状态。
- BLOCKED: 受阻塞并等待某个监视器锁的线程处于这种状态。
- WAITING: 无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
- TIMED_WAITING: 等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
- TERMINATED: 已退出的线程处于这种状态。
在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态。
1.1 New
Thread t = new MyThread(r);后线程处于new状态。
1.2 Runnable
调用start方法后,线程处于Runnable状态。一个Runnable(可运行)的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。
1.3 Blocked
一个线程试图获取一个内部的对象锁(不是java.util.concurrent中的锁),而该锁被其他线程持有的时候,该对象进入阻塞状态(Blocked)。
1.4 waiting
某一线程因为调用下列方法之一而处于等待状态:
- 不带超时值的 Object.wait
- 不带超时值的 Thread.join
- java.util.concurrent中的Lock或Condition
1.5 timed_waiting
某一线程因为调用以下带有指定正等待时间的方法之一而处于定时等待状态:
- Thread.sleep
- 带有超时值的 Object.wait
- 带有超时值的 Thread.join
- java.util.concurrent中的Lock.tryLock以及Condition.await的计时版
1.6 terminated
线程被终止的两种原因:
- run方法正常退出而自然终止
- 因为一个没有捕获的异常终止了run方法而意外终止
2.同步(一些比较底层的解决方案)
同步有两方面的含义:互斥与内存可见性
- 2.1 锁对象Lock和条件对象Condition
- 2.2synchronized关键字
- 2.3volatile域
- 2.4原子类
- 2.5ThreadLocal
2.1 锁对象Lock和条件对象Condition
Java中为了方便程序员编写并发代码引入了synchronized关键字。为了深刻理解synchronized关键字代表的含义就必须得先知道两个概念:锁对象 和 条件对象。
2.1.1 锁对象
引入锁对象是为了确保任何时候只有一个线程能访问临界区。使用Lock保护代码的基本结构如下:
myLck.lock(); try{ critical section } finally{ myLock.unlock(); }
这种结果确保了任何时刻只有一个线程进入临界区。一旦一个线程持有了该锁对象,任何其他线程都无法通过lock语句。当其他其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
使用一个锁来保护Bank类的transfer方法的例子:
public class Bank { private Lock bankLock = new ReentrantLock(); //... public void transfer(int from, int to,int amount){ bankLock.lock(); try{ System.out.println(Thread.currentThread()); accounts[from] -= amount; accounts[to] += amount; System.out.printf(" Total Balance: %10.2f%n",getTotalBalance()); } finally{ bankLock.unlock(); } } }
可重入锁?
上述Bank例子中的锁是可重入的。可重入锁可参考这儿。
2.1.2 条件对象
为什么需要条件对象?
上述Bank例子演示了从一个银行账户向另一个银行账户转账的过程。但上述内部转账流程仍需完善:如果转出账户的余额不够的话,应该等待其他账户转给自己后再执行上述转账操作。
public void transfer(int from, int to,int amount){ bankLock.lock(); try{ while (accounts[from] < amount) { // wait ... } // transfer funds ... } finally{ bankLock.unlock(); } }
如果转出账户的余额不够的话,应该等待其他账户转给自己后再执行上述转账操作。但是,当前线程刚刚获得了对bankLock的排他性访问,因此别的线程不可能有执行该transfer方法的机会。怎么办呢?解决方案就是让当前线程释放bankLock锁对象并且等待账户余额满足条件后继续执行。条件对象可以帮我们解决这个问题,这就是为什么我们需要条件对象的原因。
一个锁可以有一个或者多个相关的条件对象。可以使用Lock对象的newCondition()实例方法获取一个条件对象。
调用Condition对象的await()方法,会使得当前线程进入该条件的等待集。
调用Condition对象的signalAll()方法,会使得所有等待在该条件对象上的进程被唤醒。
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Bank { private Lock bankLock = new ReentrantLock(); private Condition sufficientFunds = bankLock.newCondition(); private final double[] accounts; public Bank(int n,double initialBalance){ accounts = new double[n]; for (int i = 0; i < accounts.length; i++) { accounts[i] = initialBalance; } } public void transfer(int from, int to,int amount) throws InterruptedException{ bankLock.lock(); try{ while (accounts[from] < amount) sufficientFunds.await(); System.out.println(Thread.currentThread()); accounts[from] -= amount; System.out.printf(" %10.2f from %d to %d",amount,from,to); accounts[to] += amount; System.out.printf(" Total Balance: %10.2f%n",getTotalBalance()); sufficientFunds.signalAll(); } finally{ bankLock.unlock(); } } public double getTotalBalance() { bankLock.lock(); try{ double sum = 0; for (double d : accounts) { sum += d; } return sum; } finally{ bankLock.unlock(); } } }
注意:对await的调用应该在如下循环体中:
while(!(ok to proceed)) condition.await();
总结:
- 锁用来保护代码片段,任何时刻只有一个线程执行被保护的代码。
- 锁可以管理试图进入被保护代码段的线程。
- 锁一个拥有一个或者多个相关的条件对象。
- 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
2.2 synchronized关键字
从1.0版开始,java中的每个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么该方法所属对象的锁将保护整个方法。换句话说,要调用该方法,线程必须获得内部的对象锁。
换句话说,
public synchronized void method(){ method body }
等价于
public void method(){ this.intrinsicLock.lock(); try{ method body } finally{ this.intrinsicLock.unlock(); } }
内部对象锁只有一个相关的条件对象。Object类中的wait方法添加一个线程到等待集中,notify/notifyAll方法解除等待线程的阻塞状态。调用wait或notifyAll等价于
intrinsicLock.await();
intrinsicLock.signalAll();
理解了wait,notify/notifyAll等方法的内部等价形式就很容易明白为什么对wait,notify/notifyAll等方法的调用只能在同步控制方法或者同步控制块里面使用。
面试题:Java中sleep和wait的区别?
- 来源:sleep来自Thread类,和wait来自Object类。
- 锁: 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
- 使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
synchronized版的Bank类如下:
public class Bank { private final double[] accounts; public synchronized void transfer(int from, int to,int amount) throws InterruptedException{ while (accounts[from] < amount) wait(); System.out.println(Thread.currentThread()); accounts[from] -= amount; System.out.printf(" %10.2f from %d to %d",amount,from,to); accounts[to] += amount; System.out.printf(" Total Balance: %10.2f%n",getTotalBalance()); notifyAll(); } public synchronized double getTotalBalance() { ... } }
2.3volatile域
同步包含两方面:互斥和内存可见性。volatile修饰的Field保证了内存可见性,但不保证互斥(原子性)。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
2.4 原子类
java.util.concurrent.atomic包中有许多类使用了很高效的机器级指令(而不是使用锁)来保证其操作的原子性。
应用程序员不应该使用这些类,它们仅供那些开发并发工具的系统程序员使用。
2.5 ThreadLocal类
有时候可能要避免共享变量,使用ThreadLocal类为各个线程提供各自的实例。
ThreadLocal类包装的字段通常是static变量,否则如果是实例变量则完全没必要。
package think.in.java.chap21.section3; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * private ThreadLocal<Integer> value = new ThreadLocal<Integer>(); * 该列子说明: * 如果去掉static,则每个ThreadLocalVariableHolder对象有一个value域,并且访问该对象的每个线程有一个本地value变量。 * */ public class ThreadLocalVariableHolder extends Thread{ private ThreadLocal<Integer> value = new ThreadLocal<Integer>(){ private Random random = new Random(47); protected synchronized Integer initialValue(){ return random.nextInt(10000); } }; public void increment() { value.set(value.get()+1); } public int get() { return value.get(); } public void run(){ increment(); System.out.println(Thread.currentThread()+" | "+this); } public String toString() { return "["+get()+",value=" + value + "]"; } public static void main(String[] args) throws Exception { ExecutorService exec = Executors.newFixedThreadPool(8); for (int i = 0; i < 2; i++) { ThreadLocalVariableHolder t = new ThreadLocalVariableHolder(); for (int j = 0; j < 4; j++) { exec.execute(t); } } TimeUnit.SECONDS.sleep(3); exec.shutdownNow(); } }
3. 阻塞队列BlockingQueue(同步的高层解决方案)
前面介绍了java并发编程的底层构件块,实际编程中应该尽量远离底层结构。多线程中的许多问题都是生产者-消费者模型,可以通过一个或者多个队列将其优雅地形式化。从5.0开始,JDK在java.util.concurrent包里提供了阻塞队列的官方实现。
阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列。
BlockingQueue 方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(null 或 false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。下表中总结了这些方法:
抛出异常 | 特殊值 | 阻塞 | 超时 | |
插入 | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
移除 | remove() |
poll() |
take() |
poll(time, unit) |
检查 | element() |
peek() |
不可用 | 不可用 |
BlockingQueue 不接受 null 元素。试图 add、put 或 offer 一个 null 元素时,某些实现会抛出 NullPointerException。null 被用作指示 poll 操作失败的警戒值。
有已知实现类:
ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue