Java线程(篇外篇):线程和锁
最后更新于:2022-04-01 07:00:48
# Java线程(篇外篇):线程和锁
## 前言
本文翻译自JLS7(Java Language Specification)第17章,与大家分享。文中的英文还不知道怎么译,持续更新。
英文原文:[http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html](http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html)。
## 概述
前面章节大部分讨论的是只关心代码在同一时间执行一条语句或表达式的行为,也就是单线程执行,Java虚拟机同时可以支持多个线程执行。多个线程能够独立的执行代码,代码通常会操作共享主内存中的值和对象。多线程可以被多硬件处理器、时间分片单硬件处理器、时间分片多硬件处理器支持。
Java中线程由Thread类来代表。用户创建线程唯一的方式就是创建该类的对象;每一个线程都与这样的对象关联。当相应Thread对象的start()方法被调用时,一个线程就启动了。
线程的执行会产生不可预知的行为,尤其是在没有正确同步的情况下。本章描述了多线程程序的语义;它包含了被多线程更新的共享内存的值是否可见的规则。为了规范不同硬件架构中相似的内存模型,这些语义被称为Java程序语言内存模型。当没有争论出现时,我们将这些规则简称为"内存模型"。
这些语义没有规定多线程程序如何被执行。相反,它们描述了多线程程序能够展现出的行为。产生唯一允许行为的任何一个执行策略都是一个可接受的执行策略。
## 同步(Synchronization)
Java程序语言提供了多种机制用于线程通信。这些方法中最基本的就是同步,同步使用监视器实现。Java中每个对象都与一个监视器关联,线程基于某个对象来实现持有锁或者释放锁。同一时间只有一个线程可以持有监视器上的锁。此时,其它线程在尝试持有该监视器上的锁时,都将被阻塞,直到原来的线程释放该监视器上的锁。一个线程t也许会锁定特定的监视器多次; 每次解锁反转一个锁定操作的效果。
synchronized语句([§14.19](http://docs.oracle.com/javase/specs/jls/se7/html/jls-14.html#jls-14.19))使用一个对象的引用作为监视器,在执行其代码块之前,会先尝试对该监视器对象进行锁定操作。锁定操作成功完成后,会进一步执行代码块。如果代码块执行完,包括正常的或者异常的,那么该监视器上的锁会自动的释放。
synchronized方法([§8.4.3.6](http://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.4.3.6 "8.4.3.6. synchronized Methods"))被调用时,会自动进行锁定操作;直到锁定操作成功的完成,方法体才被执行。如果该方法是一个非静态方法,监视器对象是该方法所在类的一个实例。如果该方法是一个静态方法,监视器对象是该方法所在类的Class对象。如果方法体执行完,包括正常的或者异常的,那么该监视器上的锁会自动的释放。
Java程序语言既不阻止也不需要检测死锁条件。当多线程程序直接或间接的持有多个对象上的锁时应该使用常规的技术来避免死锁问题,如果必要的话,创建更高级别的不会死锁的锁定原语。
还有其它的机制,如volatile变量的读写和使用java.util.concurrent包中的类,提供了同步的替代方法。
## 等待集合和通知(Wait Sets and Notification)
每一个对象,除了有关联的监视器,还有一个关联的等待集合。等待集合中存储的是线程。
当一个对象被创建时,它的等待集合是空的。等待集合中线程的增加和删除这两个基本操作是原子的。对等待集合的操作只有通过Object.wait、Object.notify、Object.notifyAll三个方法。
等待集合操作也可以被线程的中断状态、线程类处理中断的方法所影响。另外,线程类的睡眠和合并方法拥有那些等待和通知操作的衍生属性。
## 等待(Wait)
wait()或带时间参数的wait(long millisecs)和wait(long millisecs, int nanosecs)被调用时会触发等待操作。
*调用wait(0)或wait(0, 0)是和wait()等价的。*
一个线程从等待中正常返回如果它没有抛出InterruptedException。
设线程t为执行m对象wait方法的线程,and let n be the number of lock actions by t on m that have not been matched by unlock actions。则会发生下面这些情况之一:
* 如果n=0(也就是线程t已经不再持有对象m的锁),将会抛出IllegalMonitorStateException。
* 如果是带时间参数的wait方法,并且nanosecs参数不在0-999999范围内或者millisecs是负数,将会抛出IllegalArgumentException。
* 如果线程t被中断,将会抛出InterruptedException并且线程t的中断状态设为false。
* 否则,以下情况会顺序发生:
1. 线程t被添加到m对象的等待集合中,并释放m上的锁。
2. 线程t直到从对象m的等待集合中移除后,才会执行后面的代码。直到下列的情况发生任意一种时,线程才会从等待队列中移除,并重新进行线程调度:
* 其他某个线程调用对象m的 notify 方法,并且线程t碰巧被任选为被唤醒的线程。
* 其他某个线程调用对象m的 notifyAll 方法。
* 其他某个线程中断了线程t。
* 如果是带时间参数的wait方法,从wait操作时间开始,millisecs毫秒数+nanosecs纳秒数指定的超时时间已用完。
* 虚假唤醒(spurious wake-ups)。*为了防止虚假唤醒,wait操作必须放在循环中。*
每个线程对于能引起它从等待集合中移除的事件都必须有一个确定的执行顺序。该顺序不必与其它的排序相一致,但是该线程必须表现得好像这些事件是按照这个顺序发生的。
例如,假设一个线程t在对象m的等待集合中,线程t的中断和对象m的通知两个事件同时发生,那么这两个事件必须有一个执行顺序。如果中断事件先执行,线程t会抛出一个InterruptedException并从等待中退出,并且对象m的等待集合中的其它线程会接收到通知。如果通知事件先执行,线程t从等待中正常退出,中断事件被挂起。
6. 线程t在m上执行锁操作。
7. 如果线程t是在第2部的时候从m的等待集合中移除,t的中断状态设置为false并且wait方法抛出InterruptedException。
## 通知(Notification)
notify和notifyAll被调用时会触发通知操作。
设线程t为执行m对象通知方法的线程,and let n be the number of lock actions by t on m that have not been matched by unlock actions。则会发生下面这些情况之一:
* 如果n=0,将会抛出IllegalMonitorStateException。这是线程t已经不再持有对象m的锁的情况。
* 如果n>0并且这是一个notify操作,如果m的等待集合非空,m的等待集合中一个线程u被选中,从等待集合中移除。等待集合中哪一个线程被选中是没有规则的。从等待集合中移除使u从等待状态恢复。然而,在u恢复后,直到线程t释放了在监视器的所之后,u才能持有锁。
* 如果n>0并且这是一个notifyAll操作,所有的线程都会从m的等待集合中移除,重新开始竞争。然而,恢复后,它们之中只有一个会持有监视器的锁。
## 中断(Interruptions)
Thread.interrupt、ThreadGroup.interrupt被调用时会触发中断操作。
设t为执行u.interrupt的线程,该操作会将线程u的中断状态设为true。
另外,如果m的等待集合中包含u,u会被移出。This enables u to resume in a wait action, in which case this wait will, after re-locking m's monitor, throw InterruptedException。
调用Thread.isInterrupted能确定一个线程的中断状态。静态方法Thread.interrupted也许会被一个守护线程调用,并清除它自己的中断状态。
## 等待、通知、中断的相互影响(Interactions of Waits, Notification, and Interruption)
待译。
## 睡眠和让步(Sleep and Yield)
待译。
Java线程(篇外篇):线程本地变量ThreadLocal
最后更新于:2022-04-01 07:00:46
# Java线程(篇外篇):线程本地变量ThreadLocal
首先说明ThreadLocal存放的值是线程内共享的,线程间互斥的,主要用于线程内共享一些数据,避免通过参数来传递,这样处理后,能够优雅的解决一些实际问题,比如Hibernate中的OpenSessionInView,就是使用ThreadLocal保存Session对象,还有我们经常用ThreadLocal存放Connection,代码如:
~~~
/**
* 数据库连接管理类
* @author 爽
*
*/
public class ConnectionManager {
/** 线程内共享Connection,ThreadLocal通常是全局的,支持泛型 */
private static ThreadLocal threadLocal = new ThreadLocal();
public static Connection getCurrConnection() {
// 获取当前线程内共享的Connection
Connection conn = threadLocal.get();
try {
// 判断连接是否可用
if(conn == null || conn.isClosed()) {
// 创建新的Connection赋值给conn(略)
// 保存Connection
threadLocal.set(conn);
}
} catch (SQLException e) {
// 异常处理
}
return conn;
}
/**
* 关闭当前数据库连接
*/
public static void close() {
// 获取当前线程内共享的Connection
Connection conn = threadLocal.get();
try {
// 判断是否已经关闭
if(conn != null && !conn.isClosed()) {
// 关闭资源
conn.close();
// 移除Connection
threadLocal.remove();
conn = null;
}
} catch (SQLException e) {
// 异常处理
}
}
}
~~~
这样处理的好处:
1. 统一管理Connection;
2. 不需要显示传参Connection,代码更优雅;
3. 降低耦合性。
ThreadLocal有四个方法,分别为:
### initialValue
~~~
protected T initialValue()
~~~
返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 [`get()`](http://blog.csdn.net/ghsau/article/details/15732053) 方法访问变量的时候。如果线程先于 get 方法调用 [`set(T)`](http://blog.csdn.net/ghsau/article/details/15732053) 方法,则不会在线程中再调用 initialValue 方法。
该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。
**返回:**
返回此线程局部变量的初始值
* * *
### get
~~~
public T get()
~~~
返回此线程局部变量的当前线程副本中的值。如果这是线程第一次调用该方法,则创建并初始化此副本。
**返回:**
此线程局部变量的当前线程的值
* * *
### set
~~~
public void set(T value)
~~~
将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 [`initialValue()`](http://blog.csdn.net/ghsau/article/details/15732053) 方法来设置线程局部变量的值。
**参数:**
`value` - 存储在此线程局部变量的当前线程副本中的值。
* * *
### remove
~~~
public void remove()
~~~
移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其initialValue。
很多人对ThreadLocal存在一定的误解,说ThreadLocal中有一个全局的Map,set时执行map.put(Thread.currentThread(), value),get和remove时也同理,但SUN的大师们是否是如此实现的,我们只能去看源码了。
set方法:
~~~
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取当前线程本地变量Map
ThreadLocalMap map = getMap(t);
// map不为空
if (map != null)
// 存值
map.set(this, value);
else
// 创建一个当前线程本地变量Map
createMap(t, value);
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
// 获取当前线程的本地变量Map
return t.threadLocals;
}
~~~
这里注意,ThreadLocal中是有一个Map,但这个Map不是我们平时使用的Map,而是ThreadLocalMap,ThreadLocalMap是ThreadLocal的一个内部类,不对外使用的。当使用ThreadLocal存值时,首先是获取到当前线程对象,然后获取到当前线程本地变量Map,最后将当前使用的ThreadLocal和传入的值放到Map中,也就是说ThreadLocalMap中存的值是[ThreadLocal对象, 存放的值],这样做的好处是,每个线程都对应一个本地变量的Map,所以**一个线程可以存在多个线程本地变量**。
get方法:
~~~
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
// 如果值为空,则返回初始值
return setInitialValue();
}
~~~
有了之前set方法的分析,get方法也同理,需要说明的是,如果没有进行过set操作,那从ThreadLocalMap中拿到的值就是null,这时get方法会返回初始值,也就是调用initialValue()方法,ThreadLocal中这个方法默认返回null。当我们有需要第一次get时就能得到一个值时,可以继承ThreadLocal,并且覆盖initialValue()方法。
(完)
Java线程(篇外篇):阻塞队列BlockingQueue
最后更新于:2022-04-01 07:00:43
# Java线程(篇外篇):阻塞队列BlockingQueue
好久没有写文章了,这段时间事情比较杂,工作也比较杂乱,上周日刚搬完家,从自建房搬到了楼房,提升了一层生活品质,哈哈!不过昨天晚上在公交车上钱包被偷了,前段时间还丢个自行车,不得不感叹,京城扒手真多,还无人处理。言归正传,这一段时间我的工作主要是改进公司的调度器,调度器调度线程池执行任务,生产者生产任务,消费者消费任务,那么这时就需要一个任务队列,生产者向队列里插入任务,消费者从队列里提取任务执行,调度器里是通过BlockingQueue实现的队列,随后小查一下,下面看看BlockingQueue的原理及其方法。
BlockingQueue最终会有四种状况,抛出异常、返回特殊值、阻塞、超时,下表总结了这些方法:
> | -- | *抛出异常* | *特殊值* | *阻塞* | *超时* |
|---|---|---|
> | **插入** | [`add(e)`](http://blog.csdn.net/ghsau/article/details/8108292) | [`offer(e)`](http://blog.csdn.net/ghsau/article/details/8108292) | [`put(e)`](http://blog.csdn.net/ghsau/article/details/8108292) | [`offer(e, time, unit)`](http://blog.csdn.net/ghsau/article/details/8108292) |
> | **移除** | [`remove()`](http://blog.csdn.net/ghsau/article/details/8108292) | [`poll()`](http://blog.csdn.net/ghsau/article/details/8108292) | [`take()`](http://blog.csdn.net/ghsau/article/details/8108292) | [`poll(time, unit)`](http://blog.csdn.net/ghsau/article/details/8108292) |
> | **检查** | [`element()`](http://blog.csdn.net/ghsau/article/details/8108292) | [`peek()`](http://blog.csdn.net/ghsau/article/details/8108292) | *不可用* | *不可用* |
BlockingQueue是个接口,有如下实现类:
1\. ArrayBlockQueue:一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。创建其对象必须明确大小,像数组一样。
2\. LinkedBlockQueue:一个可改变大小的阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。创建其对象如果没有明确大小,默认值是Integer.MAX_VALUE。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。
3. PriorityBlockingQueue:类似于LinkedBlockingQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序。
4. SynchronousQueue:同步队列。同步队列没有任何容量,每个插入必须等待另一个线程移除,反之亦然。
下面使用ArrayBlockQueue来实现之前实现过的[生产者消/费者模式](http://blog.csdn.net/ghsau/article/details/7433673),代码如下:
~~~
/** 定义一个盘子类,可以放鸡蛋和取鸡蛋 */
public class BigPlate {
/** 装鸡蛋的盘子,大小为5 */
private BlockingQueue eggs = new ArrayBlockingQueue(5);
/** 放鸡蛋 */
public void putEgg(Object egg) {
try {
eggs.put(egg);// 向盘子末尾放一个鸡蛋,如果盘子满了,当前线程阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
// 下面输出有时不准确,因为与put操作不是一个原子操作
System.out.println("放入鸡蛋");
}
/** 取鸡蛋 */
public Object getEgg() {
Object egg = null;
try {
egg = eggs.take();// 从盘子开始取一个鸡蛋,如果盘子空了,当前线程阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
// 下面输出有时不准确,因为与take操作不是一个原子操作
System.out.println("拿到鸡蛋");
return egg;
}
/** 放鸡蛋线程 */
static class AddThread extends Thread {
private BigPlate plate;
private Object egg = new Object();
public AddThread(BigPlate plate) {
this.plate = plate;
}
public void run() {
plate.putEgg(egg);
}
}
/** 取鸡蛋线程 */
static class GetThread extends Thread {
private BigPlate plate;
public GetThread(BigPlate plate) {
this.plate = plate;
}
public void run() {
plate.getEgg();
}
}
public static void main(String[] args) {
BigPlate plate = new BigPlate();
// 先启动10个放鸡蛋线程
for(int i = 0; i 10; i++) {
new Thread(new AddThread(plate)).start();
}
// 再启动10个取鸡蛋线程
for(int i = 0; i 10; i++) {
new Thread(new GetThread(plate)).start();
}
}
}
~~~
执行结果:
~~~
放入鸡蛋
放入鸡蛋
放入鸡蛋
放入鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
拿到鸡蛋
拿到鸡蛋
放入鸡蛋
放入鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
拿到鸡蛋
拿到鸡蛋
拿到鸡蛋
拿到鸡蛋
~~~
从结果看,启动10个放鸡蛋线程和10个取鸡蛋线程,前5个放入鸡蛋的线程成功执行,到第6个,发现盘子满了,阻塞住,这时切换到取鸡蛋线程执行,成功实现了生产者/消费者模式。java.util.concurrent包是个强大的包!
Java线程(十一):Fork/Join-Java并行计算框架
最后更新于:2022-04-01 07:00:41
# Java线程(十一):Fork/Join-Java并行计算框架
并行计算在处处都有大数据的今天已经不是一个新鲜的词汇了,现在已经有单机多核甚至多机集群并行计算,注意,这里说的是并行,而不是并发。严格的将,**并行是指系统内有多个任务同时执行**,而**并发是指系统内有多个任务同时存在**,不同的任务按时间分片的方式切换执行,由于切换的时间很短,给人的感觉好像是在同时执行。
Java在JDK7之后加入了并行计算的框架Fork/Join,可以解决我们系统中大数据计算的性能问题。Fork/Join采用的是分治法,Fork是将一个大任务拆分成若干个子任务,子任务分别去计算,而Join是获取到子任务的计算结果,然后合并,这个是递归的过程。子任务被分配到不同的核上执行时,效率最高。伪代码如下:
~~~
Result solve(Problem problem) {
if (problem is small)
directly solve problem
else {
split problem into independent parts
fork new subtasks to solve each part
join all subtasks
compose result from subresults
}
}
~~~
Fork/Join框架的核心类是[ForkJoinPool](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html),它能够接收一个[ForkJoinTask](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinTask.html),并得到计算结果。ForkJoinTask有两个子类,[RecursiveTask](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/RecursiveTask.html)(有返回值)和[RecursiveAction](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/RecursiveAction.html)(无返回结果),我们自己定义任务时,只需选择这两个类继承即可。类图如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-12_5694e16198f41.jpg)![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-12_5694e167336d2.jpg)
下面来看一个实例:计算一个超大数组所有元素的和。代码如下:
~~~
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
/**
* @author: shuang.gao Date: 2015/7/14 Time: 8:16
*/
public class SumTask extends RecursiveTask<Integer> {
private static final long serialVersionUID = -6196480027075657316L;
private static final int THRESHOLD = 500000;
private long[] array;
private int low;
private int high;
public SumTask(long[] array, int low, int high) {
this.array = array;
this.low = low;
this.high = high;
}
@Override
protected Integer compute() {
int sum = 0;
if (high - low <= THRESHOLD) {
// 小于阈值则直接计算
for (int i = low; i < high; i++) {
sum += array[i];
}
} else {
// 1\. 一个大任务分割成两个子任务
int mid = (low + high) >>> 1;
SumTask left = new SumTask(array, low, mid);
SumTask right = new SumTask(array, mid + 1, high);
// 2\. 分别计算
left.fork();
right.fork();
// 3\. 合并结果
sum = left.join() + right.join();
}
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
long[] array = genArray(1000000);
System.out.println(Arrays.toString(array));
// 1\. 创建任务
SumTask sumTask = new SumTask(array, 0, array.length - 1);
long begin = System.currentTimeMillis();
// 2\. 创建线程池
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 3\. 提交任务到线程池
forkJoinPool.submit(sumTask);
// 4\. 获取结果
Integer result = sumTask.get();
long end = System.currentTimeMillis();
System.out.println(String.format("结果 %s 耗时 %sms", result, end - begin));
}
private static long[] genArray(int size) {
long[] array = new long[size];
for (int i = 0; i < size; i++) {
array[i] = new Random().nextLong();
}
return array;
}
}
~~~
我们通过调整阈值(THRESHOLD),可以发现耗时是不一样的。实际应用中,如果需要分割的任务大小是固定的,可以经过测试,得到最佳阈值;如果大小不是固定的,就需要设计一个可伸缩的算法,来动态计算出阈值。如果子任务很多,效率并不一定会高。
未完待续。。。
Java线程(十):CAS
最后更新于:2022-04-01 07:00:39
# Java线程(十):CAS
## 前言
在Java并发包中有这样一个包,java.util.concurrent.atomic,该包是对Java部分数据类型的原子封装,在原有数据类型的基础上,提供了原子性的操作方法,保证了线程安全。下面以AtomicInteger为例,来看一下是如何实现的。
~~~
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
~~~
~~~
public final int decrementAndGet() {
for (;;) {
int current = get();
int next = current - 1;
if (compareAndSet(current, next))
return next;
}
}
~~~
以这两个方法为例,incrementAndGet方法相当于原子性的++i,decrementAndGet方法相当于原子性的--i(根据[第一章](http://blog.csdn.net/ghsau/article/details/7421217)和[第二章](http://blog.csdn.net/ghsau/article/details/7424694)我们知道++i或--i不是一个原子性的操作),这两个方法中都没有使用阻塞式的方式来保证原子性(如Synchronized),那它们是如何保证原子性的呢,下面引出CAS。
## Compare And Swap
CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。这一系列的操作是原子的。它们虽然看似复杂,但却是 Java 5 并发机制优于原有锁机制的根本。简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”。(这段描述引自《Java并发编程实践》)
简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。**当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V**。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。下面来看一下AtomicInteger是如何利用CAS实现原子性操作的。
## volatile变量
` private volatile int value; `
首先声明了一个volatile变量value,在[第二章](http://blog.csdn.net/ghsau/article/details/7424694)我们知道volatile保证了变量的内存可见性,也就是所有工作线程中同一时刻都可以得到一致的值。
~~~
public final int get() {
return value;
}
~~~
## Compare And Set
~~~
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;// 注意是静态的
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));// 反射出value属性,获取其在内存中的位置
} catch (Exception ex) { throw new Error(ex); }
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
~~~
比较并设置,这里利用Unsafe类的JNI方法实现,使用CAS指令,可以保证读-改-写是一个原子操作。compareAndSwapInt有4个参数,this - 当前AtomicInteger对象,Offset - value属性在内存中的位置(需要强调的是不是value值在内存中的位置),expect - 预期值,update - 新值,根据上面的CAS操作过程,当内存中的value值等于expect值时,则将内存中的value值更新为update值,并返回true,否则返回false。在这里我们有必要对Unsafe有一个简单点的认识,从名字上来看,不安全,确实,这个类是用于执行低级别的、不安全操作的方法集合,这个类中的方法大部分是对内存的直接操作,所以不安全,但当我们使用反射、并发包时,都间接的用到了Unsafe。
## 循环设置
现在在来看开篇提到的两个方法,我们拿incrementAndGet来分析一下其实现过程。
~~~
public final int incrementAndGet() {
for (;;) {// 这样优于while(true)
int current = get();// 获取当前值
int next = current + 1;// 设置更新值
if (compareAndSet(current, next))
return next;
}
}
~~~
循环内,获取当前值并设置更新值,调用compareAndSet进行CAS操作,如果成功就返回更新至,否则重试到成功为止。这里可能存在一个隐患,那就是循环时间过长,总是在当前线程compareAndSet时,有另一个线程设置了value(点子太背了),这个当然是属于小概率时间,目前Java貌似还不能处理这种情况。
## 缺点
虽然使用CAS可以实现非阻塞式的原子性操作,但是会产生ABA问题,关于ABA问题,计划单拿出一章来整理。
(完)
Java线程(九):Condition-线程通信更高效的方式
最后更新于:2022-04-01 07:00:36
# Java线程(九):Condition-线程通信更高效的方式
接近一周没更新《Java线程》专栏了,主要是这周工作上比较忙,生活上也比较忙,呵呵,进入正题,上一篇讲述了并发包下的Lock,Lock可以更好的解决线程同步问题,使之更面向对象,并且ReadWriteLock在处理同步时更强大,那么同样,线程间仅仅互斥是不够的,还需要通信,本篇的内容是基于上篇之上,使用Lock如何处理线程通信。
那么引入本篇的主角,Condition,Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。下面将之前写过的一个线程通信的例子替换成用Condition实现(Java线程(三)),代码如下:
~~~
public class ThreadTest2 {
public static void main(String[] args) {
final Business business = new Business();
new Thread(new Runnable() {
@Override
public void run() {
threadExecute(business, "sub");
}
}).start();
threadExecute(business, "main");
}
public static void threadExecute(Business business, String threadType) {
for(int i = 0; i 100; i++) {
try {
if("main".equals(threadType)) {
business.main(i);
} else {
business.sub(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Business {
private boolean bool = true;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public /*synchronized*/ void main(int loop) throws InterruptedException {
lock.lock();
try {
while(bool) {
condition.await();//this.wait();
}
for(int i = 0; i 100; i++) {
System.out.println("main thread seq of " + i + ", loop of " + loop);
}
bool = true;
condition.signal();//this.notify();
} finally {
lock.unlock();
}
}
public /*synchronized*/ void sub(int loop) throws InterruptedException {
lock.lock();
try {
while(!bool) {
condition.await();//this.wait();
}
for(int i = 0; i 10; i++) {
System.out.println("sub thread seq of " + i + ", loop of " + loop);
}
bool = false;
condition.signal();//this.notify();
} finally {
lock.unlock();
}
}
}
~~~
在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
这样看来,Condition和传统的线程通信没什么区别,Condition的强大之处在于它可以为多个线程间建立不同的Condition,下面引入API中的一段代码,加以说明。
~~~
class BoundedBuffer {
final Lock lock = new ReentrantLock();//锁对象
final Condition notFull = lock.newCondition();//写线程条件
final Condition notEmpty = lock.newCondition();//读线程条件
final Object[] items = new Object[100];//缓存队列
int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)//如果队列满了
notFull.await();//阻塞写线程
items[putptr] = x;//赋值
if (++putptr == items.length) putptr = 0;//如果写索引写到队列的最后一个位置了,那么置为0
++count;//个数++
notEmpty.signal();//唤醒读线程
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)//如果队列为空
notEmpty.await();//阻塞读线程
Object x = items[takeptr];//取值
if (++takeptr == items.length) takeptr = 0;//如果读索引读到队列的最后一个位置了,那么置为0
--count;//个数--
notFull.signal();//唤醒写线程
return x;
} finally {
lock.unlock();
}
}
}
~~~
这是一个处于多线程工作环境下的缓存区,缓存区提供了两个方法,put和take,put是存数据,take是取数据,内部有个缓存队列,具体变量和方法说明见代码,这个缓存区类实现的功能:有多个线程往里面存数据和从里面取数据,其缓存队列(先进先出后进后出)能缓存的最大数值是100,多个线程间是互斥的,当缓存队列中存储的值达到100时,将写线程阻塞,并唤醒读线程,当缓存队列中存储的值为0时,将读线程阻塞,并唤醒写线程,下面分析一下代码的执行过程:
1\. 一个写线程执行,调用put方法;
2\. 判断count是否为100,显然没有100;
3\. 继续执行,存入值;
4\. 判断当前写入的索引位置++后,是否和100相等,相等将写入索引值变为0,并将count+1;
5\. 仅唤醒**读线程阻塞队列**中的一个;
6\. 一个读线程执行,调用take方法;
7\. ……
8\. 仅唤醒**写线程阻塞队列**中的一个。
这就是多个Condition的强大之处,假设缓存队列中已经存满,那么阻塞的肯定是写线程,唤醒的肯定是读线程,相反,阻塞的肯定是读线程,唤醒的肯定是写线程,那么假设只有一个Condition会有什么效果呢,缓存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。
Java线程(八):锁对象Lock-同步问题更完美的处理方式
最后更新于:2022-04-01 07:00:34
# Java线程(八):锁对象Lock-同步问题更完美的处理方式
Lock是[java.util.concurrent.locks](http://blog.csdn.net/ghsau/article/details/7461369)包下的接口,Lock 实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作,它能以更优雅的方式处理线程同步问题,我们拿[Java线程(二)](http://blog.csdn.net/ghsau/article/details/7424694)中的一个例子简单的实现一下和sychronized一样的效果,代码如下:
~~~
public class LockTest {
public static void main(String[] args) {
final Outputter1 output = new Outputter1();
new Thread() {
public void run() {
output.output("zhangsan");
};
}.start();
new Thread() {
public void run() {
output.output("lisi");
};
}.start();
}
}
class Outputter1 {
private Lock lock = new ReentrantLock();// 锁对象
public void output(String name) {
// TODO 线程输出方法
lock.lock();// 得到锁
try {
for(int i = 0; i
System.out.print(name.charAt(i));
}
} finally {
lock.unlock();// 释放锁
}
}
}
~~~
这样就实现了和sychronized一样的同步效果,需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内。
如果说这就是Lock,那么它不能成为同步问题更完美的处理方式,下面要介绍的是读写锁(ReadWriteLock),我们会有一种需求,在对数据进行读写的时候,为了保证数据的一致性和完整性,需要读和写是互斥的,写和写是互斥的,但是读和读是不需要互斥的,这样读和读不互斥性能更高些,来看一下不考虑互斥情况的代码原型:
~~~
public class ReadWriteLockTest {
public static void main(String[] args) {
final Data data = new Data();
for (int i = 0; i 3; i++) {
new Thread(new Runnable() {
public void run() {
for (int j = 0; j 5; j++) {
data.set(new Random().nextInt(30));
}
}
}).start();
}
for (int i = 0; i 3; i++) {
new Thread(new Runnable() {
public void run() {
for (int j = 0; j 5; j++) {
data.get();
}
}
}).start();
}
}
}
class Data {
private int data;// 共享数据
public void set(int data) {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
}
public void get() {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
}
}
~~~
部分输出结果:
~~~
Thread-1准备写入数据
Thread-3准备读取数据
Thread-2准备写入数据
Thread-0准备写入数据
Thread-4准备读取数据
Thread-5准备读取数据
Thread-2写入12
Thread-4读取12
Thread-5读取5
Thread-1写入12
~~~
我们要实现写入和写入互斥,读取和写入互斥,读取和读取互斥,在set和get方法加入sychronized修饰符:
~~~
public synchronized void set(int data) {...}
public synchronized void get() {...}
~~~
部分输出结果:
~~~
Thread-0准备写入数据
Thread-0写入9
Thread-5准备读取数据
Thread-5读取9
Thread-5准备读取数据
Thread-5读取9
Thread-5准备读取数据
Thread-5读取9
Thread-5准备读取数据
Thread-5读取9
~~~
我们发现,虽然写入和写入互斥了,读取和写入也互斥了,但是读取和读取之间也互斥了,不能并发执行,效率较低,用读写锁实现代码如下:
~~~
class Data {
private int data;// 共享数据
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public void set(int data) {
rwl.writeLock().lock();// 取到写锁
try {
System.out.println(Thread.currentThread().getName() + "准备写入数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + "写入" + this.data);
} finally {
rwl.writeLock().unlock();// 释放写锁
}
}
public void get() {
rwl.readLock().lock();// 取到读锁
try {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "读取" + this.data);
} finally {
rwl.readLock().unlock();// 释放读锁
}
}
}
~~~
部分输出结果:
~~~
Thread-4准备读取数据
Thread-3准备读取数据
Thread-5准备读取数据
Thread-5读取18
Thread-4读取18
Thread-3读取18
Thread-2准备写入数据
Thread-2写入6
Thread-2准备写入数据
Thread-2写入10
Thread-1准备写入数据
Thread-1写入22
Thread-5准备读取数据
~~~
从结果可以看出实现了我们的需求,这只是锁的基本用法,锁的机制还需要继续深入学习。
Java线程(七):Callable和Future
最后更新于:2022-04-01 07:00:32
# Java线程(七):Callable和Future
接着上一篇继续并发包的学习,本篇说明的是Callable和Future,它俩很有意思的,一个产生结果,一个拿到结果。
Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值,下面来看一个简单的例子:
~~~
public class CallableAndFuture {
public static void main(String[] args) {
Callable callable = new Callable() {
public Integer call() throws Exception {
return new Random().nextInt(100);
}
};
FutureTask future = new FutureTask(callable);
new Thread(future).start();
try {
Thread.sleep(5000);// 可能做一些事情
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
~~~
FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值,那么这个组合的使用有什么好处呢?假设有一个很耗时的返回值需要计算,并且这个返回值不是立刻需要的话,那么就可以使用这个组合,用另一个线程去计算返回值,而当前线程在使用这个返回值之前可以做其它的操作,等到需要这个返回值时,再通过Future得到,岂不美哉!这里有一个Future模式的介绍:[](http://caterpillar.onlyfun.net/Gossip/DesignPattern/FuturePattern.htm)[http://openhome.cc/Gossip/DesignPattern/FuturePattern.htm](http://openhome.cc/Gossip/DesignPattern/FuturePattern.htm)。
下面来看另一种方式使用Callable和Future,通过ExecutorService的submit方法执行Callable,并返回Future,代码如下:
~~~
public class CallableAndFuture {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future future = threadPool.submit(new Callable() {
public Integer call() throws Exception {
return new Random().nextInt(100);
}
});
try {
Thread.sleep(5000);// 可能做一些事情
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
~~~
代码是不是简化了很多,ExecutorService继承自Executor,它的目的是为我们管理Thread对象,从而简化并发编程,Executor使我们无需显示的去管理线程的生命周期,是JDK 5之后启动任务的首选方式。
执行多个带返回值的任务,并取得多个返回值,代码如下:
~~~
public class CallableAndFuture {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();
CompletionService cs = new ExecutorCompletionService(threadPool);
for(int i = 1; i 5; i++) {
final int taskID = i;
cs.submit(new Callable() {
public Integer call() throws Exception {
return taskID;
}
});
}
// 可能做一些事情
for(int i = 1; i 5; i++) {
try {
System.out.println(cs.take().get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}
~~~
其实也可以不使用CompletionService,可以先创建一个装Future类型的集合,用Executor提交的任务返回值添加到集合中,最后遍历集合取出数据,代码略。
Java线程(六):线程池
最后更新于:2022-04-01 07:00:29
# Java线程(六):线程池
自JDK5之后,Java推出了一个并发包,[java.util.concurrent](http://write.blog.csdn.net/postedit),在Java开发中,我们接触到了好多池的技术,String类的对象池、Integer的共享池、连接数据库的连接池、Struts1.3的对象池等等,池的最终目的都是节约资源,以更小的开销做更多的事情,从而提高性能。
我们的web项目都是部署在服务器上,浏览器端的每一个request就是一个线程,那么服务器需要并发的处理多个请求,就需要线程池技术,下面来看一下Java并发包下如何创建线程池。
1. 创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。
` ExecutorService threadPool = Executors.newFixedThreadPool(3);// 创建可以容纳3个线程的线程池 `
2. 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
` ExecutorService threadPool = Executors.newCachedThreadPool();// 线程池的大小会根据执行的任务数动态分配 `
3. 创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
` ExecutorService threadPool = Executors.newSingleThreadExecutor();// 创建单个线程的线程池,如果当前线程在执行任务时突然中断,则会创建一个新的线程替代它继续执行任务 `
4\. 创建一个可安排在给定延迟后运行命令或者定期地执行的线程池。
` ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(3);// 效果类似于Timer定时器 `
每种线程池都有不同的使用场景,下面看一下这四种线程池使用起来有什么不同。
1\. FixedThreadPool
~~~
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
for(int i = 1; i 5; i++) {
final int taskID = i;
threadPool.execute(new Runnable() {
public void run() {
for(int i = 1; i 5; i++) {
try {
Thread.sleep(20);// 为了测试出效果,让每次任务执行都需要一定时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第" + taskID + "次任务的第" + i + "次执行");
}
}
});
}
threadPool.shutdown();// 任务执行完毕,关闭线程池
}
}
~~~
输出结果:
~~~
第1次任务的第1次执行
第2次任务的第1次执行
第3次任务的第1次执行
第2次任务的第2次执行
第3次任务的第2次执行
第1次任务的第2次执行
第3次任务的第3次执行
第1次任务的第3次执行
第2次任务的第3次执行
第3次任务的第4次执行
第2次任务的第4次执行
第1次任务的第4次执行
第4次任务的第1次执行
第4次任务的第2次执行
第4次任务的第3次执行
第4次任务的第4次执行
~~~
上段代码中,创建了一个固定大小的线程池,容量为3,然后循环执行了4个任务,由输出结果可以看到,前3个任务首先执行完,然后空闲下来的线程去执行第4个任务,在FixedThreadPool中,有一个固定大小的池,如果当前需要执行的任务超过了池大小,那么多于的任务等待状态,直到有空闲下来的线程执行任务,而当执行的任务小于池大小,空闲的线程也不会去销毁。
2\. CachedThreadPool
上段代码其它地方不变,将newFixedThreadPool方法换成newCachedThreadPool方法。
输出结果:
~~~
第3次任务的第1次执行
第4次任务的第1次执行
第1次任务的第1次执行
第2次任务的第1次执行
第4次任务的第2次执行
第3次任务的第2次执行
第2次任务的第2次执行
第1次任务的第2次执行
第2次任务的第3次执行
第3次任务的第3次执行
第1次任务的第3次执行
第4次任务的第3次执行
第2次任务的第4次执行
第4次任务的第4次执行
第3次任务的第4次执行
第1次任务的第4次执行
~~~
可见,4个任务是交替执行的,CachedThreadPool会创建一个缓存区,将初始化的线程缓存起来,如果线程有可用的,就使用之前创建好的线程,如果没有可用的,就新创建线程,终止并且从缓存中移除已有60秒未被使用的线程。
3\. SingleThreadExecutor
上段代码其它地方不变,将newFixedThreadPool方法换成newSingleThreadExecutor方法。
输出结果:
~~~
第1次任务的第1次执行
第1次任务的第2次执行
第1次任务的第3次执行
第1次任务的第4次执行
第2次任务的第1次执行
第2次任务的第2次执行
第2次任务的第3次执行
第2次任务的第4次执行
第3次任务的第1次执行
第3次任务的第2次执行
第3次任务的第3次执行
第3次任务的第4次执行
第4次任务的第1次执行
第4次任务的第2次执行
第4次任务的第3次执行
第4次任务的第4次执行
~~~
4个任务是顺序执行的,SingleThreadExecutor得到的是一个单个的线程,这个线程会保证你的任务执行完成,如果当前线程意外终止,会创建一个新线程继续执行任务,这和我们直接创建线程不同,也和newFixedThreadPool(1)不同。
4.ScheduledThreadPool
~~~
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ThreadPoolTest {
public static void main(String[] args) {
ScheduledExecutorService schedulePool = Executors.newScheduledThreadPool(1);
// 5秒后执行任务
schedulePool.schedule(new Runnable() {
public void run() {
System.out.println("爆炸");
}
}, 5, TimeUnit.SECONDS);
// 5秒后执行任务,以后每2秒执行一次
schedulePool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("爆炸");
}
}, 5, 2, TimeUnit.SECONDS);
}
}
~~~
ScheduledThreadPool可以定时的或延时的执行任务。
Java的并发包很强大,上面所说只是入门,随着学习深入,会有更多记录在博客里。
Java线程(五):Timer和TimerTask
最后更新于:2022-04-01 07:00:27
# Java线程(五):Timer和TimerTask
Timer和TimerTask可以做为实现线程的第三种方式,前两中方式分别是继承自Thread类和实现Runnable接口。
Timer是一种线程设施,用于安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行,可以看成一个定时器,可以调度TimerTask。TimerTask是一个抽象类,实现了Runnable接口,所以具备了多线程的能力。
一个Timer可以调度任意多个TimerTask,它会将TimerTask存储在一个队列中,顺序调度,如果想两个TimerTask并发执行,则需要创建两个Timer。下面来看一个简单的例子:
~~~
import java.util.Timer;
import java.util.TimerTask;
public class TimerTest {
static class MyTimerTask1 extends TimerTask {
public void run() {
System.out.println("爆炸!!!");
}
}
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new MyTimerTask1(), 2000);// 两秒后启动任务
}
}
~~~
schedule是Timer调度任务的方法,Timer重构了四个schedule方法,具体可以查看JDK API。
看一个稍复杂的例子,假设有这样一种需求,实现一个连环炸弹,2秒后爆炸一次,3秒后爆炸一次,如此循环下去,这就需要创建两个任务,互相调度,代码如下:
~~~
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class TimerTest {
static class MyTimerTask1 extends TimerTask {
public void run() {
System.out.println("爆炸!!!");
new Timer().schedule(new MyTimerTask2(), 2000);
}
}
static class MyTimerTask2 extends TimerTask {
public void run() {
System.out.println("爆炸!!!");
new Timer().schedule(new MyTimerTask1(), 3000);
}
}
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new MyTimerTask2(), 2000);
while(true) {
System.out.println(new Date().getSeconds());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
~~~
自JDK5之后,可以用ScheduledThreadPoolExecutor来替代Timer。
Java线程(四):线程中断、线程让步、线程睡眠、线程合并
最后更新于:2022-04-01 07:00:25
# Java线程(四):线程中断、线程让步、线程睡眠、线程合并
最近在Review线程专栏,修改了诸多之前描述不够严谨的地方,凡是带有Review标记的文章都是修改过了。本篇文章是插进来的,因为原来没有写,现在来看传统线程描述的不太完整,所以就补上了。理解了线程同步和线程通信之后,再来看本文的知识点就会简单的多了,本文是做为传统线程知识点的一个补充。有人会问:JDK5之后有了更完善的处理多线程问题的类(并发包),我们还需要去了解传统线程吗?答:需要。在实际开发中,无外乎两种情况,一个是开发新内容,另一个是维护原有程序。开发新内容可以使用新的技术手段,但是我们不能保证原有程序是用什么实现的,所以我们需要了解原有的。另外一点,了解传统线程的工作原理,使我们在使用并发包时更加得心应手。
## 线程中断
线程中断涉及到三个方法,如下:
| `void` | `[interrupt](http://blog.csdn.net/ghsau/article/details/17560467)**()`中断线程。 |
|---|---|---|
| `static boolean` | `[interrupted](http://blog.csdn.net/ghsau/article/details/17560467)**()`测试当前线程是否已经中断。 |
| `boolean` | `[isInterrupted](http://blog.csdn.net/ghsau/article/details/17560467)**()`测试线程是否已经中断。 |
interrupt()方法用于中断线程,通常的理解来看,只要某个线程启动后,调用了该方法,则该线程不能继续执行了,来看个小例子:
~~~
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread("MyThread");
t.start();
Thread.sleep(100);// 睡眠100毫秒
t.interrupt();// 中断t线程
}
}
class MyThread extends Thread {
int i = 0;
public MyThread(String name) {
super(name);
}
public void run() {
while(true) {// 死循环,等待被中断
System.out.println(getName() + getId() + "执行了" + ++i + "次");
}
}
}
~~~
运行后,我们发现,线程t一直在执行,没有被中断,原来interrupt()是骗人的,汗!其实interrupt()方法并不是中断线程的执行,而是为调用该方法的线程对象打上一个标记,设置其中断状态为true,通过isInterrupted()方法可以得到这个线程状态,我们将上面的程序做一个小改动:
~~~
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread("MyThread");
t.start();
Thread.sleep(100);// 睡眠100毫秒
t.interrupt();// 中断t线程
}
}
class MyThread extends Thread {
int i = 0;
public MyThread(String name) {
super(name);
}
public void run() {
while(!isInterrupted()) {// 当前线程没有被中断,则执行
System.out.println(getName() + getId() + "执行了" + ++i + "次");
}
}
}
~~~
这样的话,线程被顺利的中断执行了。很多人实现一个线程类时,都会再加一个flag标记,以便控制线程停止执行,其实完全没必要,通过线程自身的中断状态,就可以完美实现该功能。如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。 我们可以捕获该异常,并且做一些处理。另外,Thread.interrupted()方法是一个静态方法,它是判断当前线程的中断状态,需要注意的是,线程的中断状态会由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。
## 线程让步
线程让步,其方法如下:
| `static void` | `[yield](http://blog.csdn.net/ghsau/article/details/17560467)**()`暂停当前正在执行的线程对象,并执行其他线程 |
|---|---|---|
线程让步用于正在执行的线程,在某些情况下让出CPU资源,让给其它线程执行,来看一个小例子:
~~~
public class YieldTest {
public static void main(String[] args) throws InterruptedException {
// 创建线程对象
YieldThread t1 = new YieldThread("t1");
YieldThread t2 = new YieldThread("t2");
// 启动线程
t1.start();
t2.start();
// 主线程休眠100毫秒
Thread.sleep(100);
// 终止线程
t1.interrupt();
t2.interrupt();
}
}
class YieldThread extends Thread {
int i = 0;
public YieldThread(String name) {
super(name);
}
public void run() {
while(!isInterrupted()) {
System.out.println(getName() + "执行了" + ++i + "次");
if(i % 10 == 0) {// 当i能对10整除时,则让步
Thread.yield();
}
}
}
}
~~~
输出结果略,从输出结果可以看到,当某个线程(t1或者t2)执行到10次、20次、30次等时,就会马上切换到另一个线程执行,接下来再交替执行,如此往复。**注意,如果存在synchronized线程同步的话,线程让步不会释放锁(监视器对象)**。
## 线程睡眠
线程睡眠涉及到两个方法,如下:
| `static void` | `[sleep](http://blog.csdn.net/ghsau/article/details/17560467)**(long millis)`在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。 |
|---|---|---|
| `static void` | `[sleep](http://blog.csdn.net/ghsau/article/details/17560467)**(long millis, int nanos)`在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行)。 |
**线程睡眠的过程中,如果是在synchronized线程同步内,是持有锁(监视器对象)的**,也就是说,线程是关门睡觉的,别的线程进不来,来看一个小例子:
~~~
public class SleepTest {
public static void main(String[] args) {
// 创建共享对象
Service service = new Service();
// 创建线程
SleepThread t1 = new SleepThread("t1", service);
SleepThread t2 = new SleepThread("t2", service);
// 启动线程
t1.start();
t2.start();
}
}
class SleepThread extends Thread {
private Service service;
public SleepThread(String name, Service service) {
super(name);
this.service = service;
}
public void run() {
service.calc();
}
}
class Service {
public synchronized void calc() {
System.out.println(Thread.currentThread().getName() + "准备计算");
System.out.println(Thread.currentThread().getName() + "感觉累了,开始睡觉");
try {
Thread.sleep(10000);// 睡10秒
} catch (InterruptedException e) {
return;
}
System.out.println(Thread.currentThread().getName() + "睡醒了,开始计算");
System.out.println(Thread.currentThread().getName() + "计算完成");
}
}
~~~
输出结果:
~~~
t1准备计算
t1感觉累了,开始睡觉
t1睡醒了,开始计算
t1计算完成
t2准备计算
t2感觉累了,开始睡觉
t2睡醒了,开始计算
t2计算完成
~~~
## 线程合并
线程合并涉及到三个方法,如下:
| ` void` | `[join](http://blog.csdn.net/ghsau/article/details/17560467)**()`等待该线程终止。 |
|---|---|---|
| ` void` | `[join](http://blog.csdn.net/ghsau/article/details/17560467)**(long millis)`等待该线程终止的时间最长为 `millis` 毫秒。 |
| ` void` | `[join](http://blog.csdn.net/ghsau/article/details/17560467)**(long millis, int nanos)`等待该线程终止的时间最长为 `millis` 毫秒 + `nanos` 纳秒。 |
线程合并是优先执行调用该方法的线程,再执行当前线程,来看一个小例子:
~~~
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
JoinThread t1 = new JoinThread("t1");
JoinThread t2 = new JoinThread("t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("主线程开始执行!");
}
}
class JoinThread extends Thread {
public JoinThread(String name) {
super(name);
}
public void run() {
for(int i = 1; i 10; i++)
System.out.println(getName() + getId() + "执行了" + i + "次");
}
}
~~~
t1和t2都执行完才继续主线程的执行,所谓合并,就是等待其它线程执行完,再执行当前线程,执行起来的效果就好像把其它线程合并到当前线程执行一样。
## 线程优先级
线程最低优先级为1,最高优先级为10,看起来就有10个级别,但这10个级别能不能和CPU对应上,还未可知,Thread类中提供了优先级的三个常量,如下:
| java.lang.[Thread](http://blog.csdn.net/ghsau/article/details/17560467 "java.lang 中的类") |
|---|---|---|---|
| `public static final int``[MAX_PRIORITY](http://blog.csdn.net/ghsau/article/details/17560467)` | `10` |
| `public static final int``[MIN_PRIORITY](http://blog.csdn.net/ghsau/article/details/17560467)` | `1` |
| `public static final int``[NORM_PRIORITY](http://blog.csdn.net/ghsau/article/details/17560467)` | `5` |
我们创建线程对象后,如果不显示的设置优先级的话,默认为5。优先级可以看成一种特权,优先级高的,获取CPU调度的机会就大,优先级低的,获取CPU调度的机会就小,这个和我们现实生活很一样啊,优胜劣汰。线程优先级的示例就不写了,比较简单。
## wait()和sleep()区别
区别太大了,但是在Java线程面试题中是很常见的问题,相信你阅读过本专栏后,能够轻松的解答,这里不再赘述。
Java线程(三):线程协作-生产者/消费者问题
最后更新于:2022-04-01 07:00:23
# Java线程(三):线程协作-生产者/消费者问题
上一篇讲述了线程的互斥(同步),但是在很多情况下,仅仅同步是不够的,还需要线程与线程协作(通信),生产者/消费者问题是一个经典的线程同步以及通信的案例。该问题描述了两个共享固定大小缓冲区的线程,即所谓的“生产者”和“消费者”在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者,通常采用线程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。本文讲述了JDK5之前传统线程的通信方式,更高级的通信方式可参见[Java线程(九):Condition-线程通信更高效的方式](http://blog.csdn.net/ghsau/article/details/7481142)和[Java线程(篇外篇):阻塞队列BlockingQueue](http://blog.csdn.net/ghsau/article/details/8108292)。
假设有这样一种情况,有一个盘子,盘子里只能放一个鸡蛋,A线程专门往盘子里放鸡蛋,如果盘子里有鸡蛋,则一直等到盘子里没鸡蛋,B线程专门从盘子里取鸡蛋,如果盘子里没鸡蛋,则一直等到盘子里有鸡蛋。这里盘子是一个互斥区,每次放鸡蛋是互斥的,每次取鸡蛋也是互斥的,A线程放鸡蛋,如果这时B线程要取鸡蛋,由于A没有释放锁,B线程处于等待状态,进入阻塞队列,放鸡蛋之后,要通知B线程取鸡蛋,B线程进入就绪队列,反过来,B线程取鸡蛋,如果A线程要放鸡蛋,由于B线程没有释放锁,A线程处于等待状态,进入阻塞队列,取鸡蛋之后,要通知A线程放鸡蛋,A线程进入就绪队列。我们希望当盘子里有鸡蛋时,A线程阻塞,B线程就绪,盘子里没鸡蛋时,A线程就绪,B线程阻塞,代码如下:
~~~
import java.util.ArrayList;
import java.util.List;
/** 定义一个盘子类,可以放鸡蛋和取鸡蛋 */
public class Plate {
/** 装鸡蛋的盘子 */
List eggs = new ArrayList();
/** 取鸡蛋 */
public synchronized Object getEgg() {
while (eggs.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object egg = eggs.get(0);
eggs.clear();// 清空盘子
notify();// 唤醒阻塞队列的某线程到就绪队列
System.out.println("拿到鸡蛋");
return egg;
}
/** 放鸡蛋 */
public synchronized void putEgg(Object egg) {
while (eggs.size() > 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
eggs.add(egg);// 往盘子里放鸡蛋
notify();// 唤醒阻塞队列的某线程到就绪队列
System.out.println("放入鸡蛋");
}
static class AddThread implements Runnable {
private Plate plate;
private Object egg = new Object();
public AddThread(Plate plate) {
this.plate = plate;
}
public void run() {
plate.putEgg(egg);
}
}
static class GetThread implements Runnable {
private Plate plate;
public GetThread(Plate plate) {
this.plate = plate;
}
public void run() {
plate.getEgg();
}
}
public static void main(String args[]) {
Plate plate = new Plate();
for(int i = 0; i 10; i++) {
new Thread(new AddThread(plate)).start();
new Thread(new GetThread(plate)).start();
}
}
}
~~~
输出结果:
~~~
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
~~~
程序开始,A线程判断盘子是否为空,放入一个鸡蛋,并且唤醒在阻塞队列的一个线程,阻塞队列为空;假设CPU又调度了一个A线程,盘子非空,执行等待,这个A线程进入阻塞队列;然后一个B线程执行,盘子非空,取走鸡蛋,并唤醒阻塞队列的A线程,A线程进入就绪队列,此时就绪队列就一个A线程,马上执行,放入鸡蛋;如果再来A线程重复第一步,在来B线程重复第二步,整个过程就是生产者(A线程)生产鸡蛋,消费者(B线程)消费鸡蛋。
前段时间看了张孝祥老师线程的视频,讲述了一个其学员的面试题,也是线程通信的,在此也分享一下。
题目:子线程循环10次,主线程循环100次,如此循环100次,好像是空中网的笔试题。
~~~
public class ThreadTest2 {
public static void main(String[] args) {
final Business business = new Business();
new Thread(new Runnable() {
@Override
public void run() {
threadExecute(business, "sub");
}
}).start();
threadExecute(business, "main");
}
public static void threadExecute(Business business, String threadType) {
for(int i = 0; i 100; i++) {
try {
if("main".equals(threadType)) {
business.main(i);
} else {
business.sub(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Business {
private boolean bool = true;
public synchronized void main(int loop) throws InterruptedException {
while(bool) {
this.wait();
}
for(int i = 0; i 100; i++) {
System.out.println("main thread seq of " + i + ", loop of " + loop);
}
bool = true;
this.notify();
}
public synchronized void sub(int loop) throws InterruptedException {
while(!bool) {
this.wait();
}
for(int i = 0; i 10; i++) {
System.out.println("sub thread seq of " + i + ", loop of " + loop);
}
bool = false;
this.notify();
}
}
~~~
大家注意到没有,在调用wait方法时,都是用while判断条件的,而不是if,在wait方法说明中,也推荐使用while,因为在某些特定的情况下,线程有可能被假唤醒,使用while会循环检测更稳妥。wait和notify方法必须工作于synchronized内部,且这两个方法只能由锁对象来调用。另附这两种方法的JavaDoc说明:
### notify
~~~
public final void notify()
~~~
唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个`wait` 方法,在对象的监视器上等待。
直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
此方法只应由作为此对象监视器的所有者的线程来调用。通过以下三种方法之一,线程可以成为此对象监视器的所有者:
* 通过执行此对象的同步 (Sychronized) 实例方法。
* 通过执行在此对象上进行同步的 `synchronized` 语句的正文。
* 对于 `Class` 类型的对象,可以通过执行该类的同步静态方法。
一次只能有一个线程拥有对象的监视器。
**抛出:**
`[IllegalMonitorStateException](http://blog.csdn.net/ghsau/article/details/7433673 "java.lang 中的类")` - 如果当前的线程不是此对象监视器的所有者。
### notifyAll
~~~
public final void notifyAll()
~~~
唤醒在此对象监视器上等待的所有线程。线程通过调用其中一个 `wait` 方法,在对象的监视器上等待。
直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
此方法只应由作为此对象监视器的所有者的线程来调用。请参阅 `notify` 方法,了解线程能够成为监视器所有者的方法的描述。
**抛出:**
[IllegalMonitorStateException](http://blog.csdn.net/ghsau/article/details/7433673 "java.lang 中的类") 如果当前的线程不是此对象监视器的所有者。
### wait
~~~
public final void wait(long timeout)
throws InterruptedException
~~~
导致当前的线程等待,直到其他线程调用此对象的 [`notify()`](http://blog.csdn.net/ghsau/article/details/7433673) 方法或[`notifyAll()`](http://blog.csdn.net/ghsau/article/details/7433673) 方法,或者超过指定的时间量。
当前的线程必须拥有此对象监视器。
此方法导致当前线程(称之为 T)将其自身放置在对象的等待集中,然后放弃此对象上的所有同步要求。出于线程调度目的,线程 T 被禁用,且处于休眠状态,直到发生以下四种情况之一:
* 其他某个线程调用此对象的 notify 方法,并且线程 T 碰巧被任选为被唤醒的线程。
* 其他某个线程调用此对象的 notifyAll 方法。
* 其他某个线程[`中断`](http://blog.csdn.net/ghsau/article/details/7433673)线程 T。
* 已经到达指定的实际时间。但是,如果 timeout 为零,则不考虑实际时间,该线程将一直等待,直到获得通知。
然后,从对象的等待集中删除线程 T,并重新进行线程调度。然后,该线程以常规方式与其他线程竞争,以获得在该对象上同步的权利;一旦获得对该对象的控制权,该对象上的所有其同步声明都将被还原到以前的状态 - 这就是调用wait 方法时的情况。然后,线程T 从wait方法的调用中返回。所以,从wait 方法返回时,该对象和线程T 的同步状态与调用wait 方法时的情况完全相同。
在没有被通知、中断或超时的情况下,线程还可以唤醒一个所谓的*虚假唤醒* (spurious wakeup)。虽然这种情况在实践中很少发生,但是应用程序必须通过以下方式防止其发生,即对应该导致该线程被提醒的条件进行测试,如果不满足该条件,则继续等待。换句话说,等待应总是发生在循环中,如下面的示例:
~~~
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
}
~~~
(有关这一主题的更多信息,请参阅 Doug Lea 撰写的《Concurrent Programming in Java (Second Edition)》(Addison-Wesley, 2000) 中的第 3.2.3 节或 Joshua Bloch 撰写的《Effective Java Programming Language Guide》(Addison-Wesley, 2001) 中的第 50 项。
如果当前线程在等待时被其他线程[`中断`](http://blog.csdn.net/ghsau/article/details/7433673),则会抛出InterruptedException。在按上述形式恢复此对象的锁定状态时才会抛出此异常。
注意,由于 wait 方法将当前的线程放入了对象的等待集中,所以它只能解除此对象的锁定;可以同步当前线程的任何其他对象在线程等待时仍处于锁定状态。
此方法只应由作为此对象监视器的所有者的线程来调用。请参阅 `notify` 方法,了解线程能够成为监视器所有者的方法的描述。
**参数:**
`timeout` - 要等待的最长时间(以毫秒为单位)。
**抛出:**
`[IllegalArgumentException](http://blog.csdn.net/ghsau/article/details/7433673 "java.lang 中的类")` - 如果超时值为负。
`[IllegalMonitorStateException](http://blog.csdn.net/ghsau/article/details/7433673 "java.lang 中的类")` - 如果当前的线程不是此对象监视器的所有者。
`[InterruptedException](http://blog.csdn.net/ghsau/article/details/7433673 "java.lang 中的类")` - 如果在当前线程等待通知之前或者正在等待通知时,另一个线程中断了当前线程。在抛出此异常时,当前线程的*中断状态* 被清除。
* * *
### wait
~~~
public final void wait(long timeout,
int nanos)
throws InterruptedException
~~~
导致当前的线程等待,直到其他线程调用此对象的 [`notify()`](http://blog.csdn.net/ghsau/article/details/7433673) 方法或[`notifyAll()`](http://blog.csdn.net/ghsau/article/details/7433673) 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。
此方法类似于一个参数的 `wait` 方法,但它允许更好地控制在放弃之前等待通知的时间量。用毫微秒度量的实际时间量可以通过以下公式计算出来:
> ~~~
> 1000000*timeout+nanos
> ~~~
在其他所有方面,此方法执行的操作与带有一个参数的 [`wait(long)`](http://blog.csdn.net/ghsau/article/details/7433673) 方法相同。需要特别指出的是,wait(0, 0) 与wait(0) 相同。
当前的线程必须拥有此对象监视器。该线程发布对此监视器的所有权,并等待下面两个条件之一发生:
* 其他线程通过调用 `notify` 方法,或 `notifyAll` 方法通知在此对象的监视器上等待的线程醒来。
* `timeout` 毫秒值与 `nanos` 毫微秒参数值之和指定的超时时间已用完。
然后,该线程等到重新获得对监视器的所有权后才能继续执行。
对于某一个参数的版本,实现中断和虚假唤醒是有可能的,并且此方法应始终在循环中使用:
~~~
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout, nanos);
... // Perform action appropriate to condition
}
~~~
此方法只应由作为此对象监视器的所有者的线程来调用。请参阅 `notify` 方法,了解线程能够成为监视器所有者的方法的描述。
**参数:**
`timeout` - 要等待的最长时间(以毫秒为单位)。
`nanos` - 额外时间(以毫微秒为单位,范围是 0-999999)。
**抛出:**
`[IllegalArgumentException](http://blog.csdn.net/ghsau/article/details/7433673 "java.lang 中的类")` - 如果超时值是负数,或者毫微秒值不在 0-999999 范围内。
`[IllegalMonitorStateException](http://blog.csdn.net/ghsau/article/details/7433673 "java.lang 中的类")` - 如果当前线程不是此对象监视器的所有者。
`[InterruptedException](http://blog.csdn.net/ghsau/article/details/7433673 "java.lang 中的类")` - 如果在当前线程等待通知之前或者正在等待通知时,其他线程中断了当前线程。在抛出此异常时,当前线程的*中断状态* 被清除。
### wait
~~~
public final void wait()
throws InterruptedException
~~~
导致当前的线程等待,直到其他线程调用此对象的 [`notify()`](http://blog.csdn.net/ghsau/article/details/7433673) 方法或[`notifyAll()`](http://blog.csdn.net/ghsau/article/details/7433673) 方法。换句话说,此方法的行为就好像它仅执行wait(0)调用一样。
当前的线程必须拥有此对象监视器。该线程发布对此监视器的所有权并等待,直到其他线程通过调用 `notify` 方法,或 `notifyAll` 方法通知在此对象的监视器上等待的线程醒来。然后该线程将等到重新获得对监视器的所有权后才能继续执行。
对于某一个参数的版本,实现中断和虚假唤醒是可能的,而且此方法应始终在循环中使用:
~~~
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
~~~
此方法只应由作为此对象监视器的所有者的线程来调用。请参阅 `notify` 方法,了解线程能够成为监视器所有者的方法的描述。
**抛出:**
`[IllegalMonitorStateException](http://blog.csdn.net/ghsau/article/details/7433673 "java.lang 中的类")` - 如果当前的线程不是此对象监视器的所有者。
`[InterruptedException](http://blog.csdn.net/ghsau/article/details/7433673 "java.lang 中的类")` - 如果在当前线程等待通知之前或者正在等待通知时,另一个线程中断了当前线程。在抛出此异常时,当前线程的*中断状态* 被清除。
Java线程(二):线程同步synchronized和volatile
最后更新于:2022-04-01 07:00:20
# Java线程(二):线程同步synchronized和volatile
[上篇](http://blog.csdn.net/ghsau/article/details/7421217)通过一个简单的例子说明了线程安全与不安全,在例子中不安全的情况下输出的结果恰好是逐个递增的(其实是巧合,多运行几次,会产生不同的输出结果),为什么会产生这样的结果呢,因为建立的Count对象是线程共享的,一个线程改变了其成员变量num值,下一个线程正巧读到了修改后的num,所以会递增输出。
要说明线程同步问题首先要说明Java线程的两个特性,可见性和有序性。多个线程之间是不能直接传递数据交互的,它们之间的交互只能通过共享变量来实现。拿上篇博文中的例子来说明,在多个线程之间共享了Count类的一个对象,这个对象是被创建在主内存(堆内存)中,每个线程都有自己的工作内存(线程栈),工作内存存储了主内存Count对象的一个副本,当线程操作Count对象时,首先从主内存复制Count对象到工作内存中,然后执行代码count.count(),改变了num值,最后用工作内存Count刷新主内存Count。当一个对象在多个内存中都存在副本时,如果一个内存修改了共享变量,其它线程也应该能够看到被修改后的值,**此为可见性**。多个线程执行时,CPU对线程的调度是随机的,我们不知道当前程序被执行到哪步就切换到了下一个线程,一个最经典的例子就是银行汇款问题,一个银行账户存款100,这时一个人从该账户取10元,同时另一个人向该账户汇10元,那么余额应该还是100。那么此时可能发生这种情况,A线程负责取款,B线程负责汇款,A从主内存读到100,B从主内存读到100,A执行减10操作,并将数据刷新到主内存,这时主内存数据100-10=90,而B内存执行加10操作,并将数据刷新到主内存,最后主内存数据100+10=110,显然这是一个严重的问题,我们要保证A线程和B线程有序执行,先取款后汇款或者先汇款后取款,**此为有序性**。本文讲述了JDK5.0之前传统线程的同步方式,更高级的同步方式可参见[Java线程(八):锁对象Lock-同步问题更完美的处理方式](http://blog.csdn.net/ghsau/article/details/7461369)。
下面同样用代码来展示一下线程同步问题。
TraditionalThreadSynchronized.java:创建两个线程,执行同一个对象的输出方法。
~~~
public class TraditionalThreadSynchronized {
public static void main(String[] args) {
final Outputter output = new Outputter();
new Thread() {
public void run() {
output.output("zhangsan");
}
}.start();
new Thread() {
public void run() {
output.output("lisi");
}
}.start();
}
}
class Outputter {
public void output(String name) {
// TODO 为了保证对name的输出不是一个原子操作,这里逐个输出name的每个字符
for(int i = 0; i
System.out.print(name.charAt(i));
// Thread.sleep(10);
}
}
}
~~~
运行结果:
1. zhlainsigsan
显然输出的字符串被打乱了,我们期望的输出结果是zhangsanlisi,这就是线程同步问题,我们希望output方法被一个线程完整的执行完之后再切换到下一个线程,Java中使用synchronized保证一段代码在多线程执行时是互斥的,有两种用法:
1\. 使用synchronized将需要互斥的代码包含起来,并上一把锁。
~~~
{
synchronized (this) {
for(int i = 0; i
System.out.print(name.charAt(i));
}
}
}
~~~
这把锁必须是需要互斥的多个线程间的共享对象,像下面的代码是没有意义的。
~~~
{
Object lock = new Object();
synchronized (lock) {
for(int i = 0; i
System.out.print(name.charAt(i));
}
}
}
~~~
每次进入output方法都会创建一个新的lock,这个锁显然每个线程都会创建,没有意义。
2\. 将synchronized加在需要互斥的方法上。
~~~
public synchronized void output(String name) {
// TODO 线程输出方法
for(int i = 0; i
System.out.print(name.charAt(i));
}
}
~~~
这种方式就相当于用this锁住整个方法内的代码块,如果用synchronized加在静态方法上,就相当于用××××.class锁住整个方法内的代码块。使用synchronized在某些情况下会造成死锁,死锁问题以后会说明。**使用synchronized修饰的方法或者代码块可以看成是一个原子操作**。
每个锁对(JLS中叫monitor)都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,当一个线程被唤醒(notify)后,才会进入到就绪队列,等待CPU的调度,反之,当一个线程被wait后,就会进入阻塞队列,等待下一次被唤醒,这个涉及到线程间的通信,下一篇博文会说明。看我们的例子,当第一个线程执行输出方法时,获得同步锁,执行输出方法,恰好此时第二个线程也要执行输出方法,但发现同步锁没有被释放,第二个线程就会进入就绪队列,等待锁被释放。一个线程执行互斥代码过程如下:
1\. 获得同步锁;
2\. 清空工作内存;
3\. 从主内存拷贝对象副本到工作内存;
4\. 执行代码(计算或者输出等);
5\. 刷新主内存数据;
6\. 释放同步锁。
所以,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。
volatile是第二种Java多线程同步的机制,根据[JLS(Java LanguageSpecifications)](http://docs.oracle.com/javase/specs/jls/se7/jls7.pdf)的说法,一个变量可以被volatile修饰,在这种情况下内存模型(主内存和线程工作内存)确保所有线程可以看到一致的变量值,来看一段代码:
~~~
class Test {
static int i = 0, j = 0;
static void one() {
i++;
j++;
}
static void two() {
System.out.println("i=" + i + " j=" + j);
}
}
~~~
一些线程执行one方法,另一些线程执行two方法,two方法有可能打印出j比i大的值,按照之前分析的线程执行过程分析一下:
1\. 将变量i从主内存拷贝到工作内存;
2\. 改变i的值;
3\. 刷新主内存数据;
4\. 将变量j从主内存拷贝到工作内存;
5\. 改变j的值;
6\. 刷新主内存数据;
这个时候执行two方法的线程先读取了主存i原来的值又读取了j改变后的值,这就导致了程序的输出不是我们预期的结果,要阻止这种不合理的行为的一种方式是在one方法和two方法前面加上synchronized修饰符:
~~~
class Test {
static int i = 0, j = 0;
static synchronized void one() {
i++;
j++;
}
static synchronized void two() {
System.out.println("i=" + i + " j=" + j);
}
}
~~~
根据前面的分析,我们可以知道,这时one方法和two方法再也不会并发的执行了,i和j的值在主内存中会一直保持一致,并且two方法输出的也是一致的。另一种同步的机制是在共享变量之前加上volatile:
~~~
class Test {
static volatile int i = 0, j = 0;
static void one() {
i++;
j++;
}
static void two() {
System.out.println("i=" + i + " j=" + j);
}
}
~~~
one方法和two方法还会并发的去执行,但是加上volatile可以将共享变量i和j的改变直接响应到主内存中,这样保证了**主内存中i和j的值一致性**,然而在执行two方法时,在two方法获取到i的值和获取到j的值中间的这段时间,one方法也许被执行了好多次,导致j的值会大于i的值。所以volatile可以保证内存可见性,不能保证并发有序性。
没有明白JLS中为什么使用两个变量来阐述volatile的工作原理,这样不是很好理解。volatile是一种弱的同步手段,相对于synchronized来说,某些情况下使用,可能效率更高,因为它不是阻塞的,尤其是读操作时,加与不加貌似没有影响,处理写操作的时候,可能消耗的性能更多些。但是volatile和synchronized性能的比较,我也说不太准,多线程本身就是比较玄的东西,依赖于CPU时间分片的调度,JVM更玄,还没有研究过虚拟机,从顶层往底层看往往是比较难看透的。在JDK5.0之前,如果没有参透volatile的使用场景,还是不要使用了,尽量用synchronized来处理同步问题,线程阻塞这玩意简单粗暴。另外**volatile和final不能同时修饰一个字段**,可以想想为什么。
Java线程(一):线程安全与不安全
最后更新于:2022-04-01 07:00:18
# Java线程(一):线程安全与不安全
作为一个Java web开发人员,很少也不需要去处理线程,因为服务器已经帮我们处理好了。记得大一刚学Java的时候,老师带着我们做了一个局域网聊天室,用到了AWT、Socket、多线程、I/O,编写的客户端和服务器,当时做出来很兴奋,回学校给同学们演示,感觉自己好NB,呵呵,扯远了。上次在百度开发者大会上看到一个提示语,自己写的代码,6个月不看也是别人的代码,自己学的知识也同样如此,学完的知识如果不使用或者不常常回顾,那么还不是自己的知识。大学零零散散搞了不到四年的Java,我相信很多人都跟我一样,JavaSE基础没打牢,就急忙忙、兴冲冲的搞JavaEE了,然后学习一下前台开发(html、css、javascript),有可能还搞搞jquery、extjs,再然后是Struts、hibernate、spring,然后听说找工作得会linux、oracle,又去学,在这个过程中,是否迷失了,虽然学习面很广,但就像《神雕侠侣》中黄药师评价杨过,博而不精、杂而不纯,这一串下来,感觉做Java开发好难,并不是学着难,而是知识面太广了,又要精通这个,又要精通那个,这只是我迷茫时候的想法,现在我已经找到方向了。
回归正题,当我们查看JDK API的时候,总会发现一些类说明写着,线程安全或者线程不安全,比如说StringBuilder中,有这么一句,“将`StringBuilder` 的实例用于多个线程是不安全的。如果需要这样的同步,则建议使用`[StringBuffer](http://blog.csdn.net/ghsau/article/details/7421217 "java.lang 中的类")`。 ”,那么下面手动创建一个线程不安全的类,然后在多线程中使用这个类,看看有什么效果。
Count.java:
~~~
public class Count {
private int num;
public void count() {
for(int i = 1; i 10; i++) {
num += i;
}
System.out.println(Thread.currentThread().getName() + "-" + num);
}
}
~~~
在这个类中的count方法是计算1一直加到10的和,并输出当前线程名和总和,我们期望的是每个线程都会输出55。
ThreadTest.java:
~~~
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
Count count = new Count();
public void run() {
count.count();
}
};
for(int i = 0; i 10; i++) {
new Thread(runnable).start();
}
}
}
~~~
这里启动了10个线程,看一下输出结果:
~~~
Thread-0-55
Thread-1-110
Thread-2-165
Thread-4-220
Thread-5-275
Thread-6-330
Thread-3-385
Thread-7-440
Thread-8-495
Thread-9-550
~~~
只有Thread-0线程输出的结果是我们期望的,而输出的是每次都累加的,这里累加的原因以后的博文会说明,那么要想得到我们期望的结果,有几种解决方案:
1\. 将Count中num变成count方法的局部变量;
~~~
public class Count {
public void count() {
int num = 0;
for(int i = 1; i 10; i++) {
num += i;
}
System.out.println(Thread.currentThread().getName() + "-" + num);
}
}
~~~
2\. 将线程类成员变量拿到run方法中,这时count引用是线程内的局部变量;
~~~
public class ThreadTest4 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
public void run() {
Count count = new Count();
count.count();
}
};
for(int i = 0; i 10; i++) {
new Thread(runnable).start();
}
}
}
~~~
3\. 每次启动一个线程使用不同的线程类,不推荐。
上述测试,我们发现,存在成员变量的类用于多线程时是不安全的,不安全体现在这个成员变量可能发生**非原子性的操作**,而变量定义在方法内也就是局部变量是线程安全的。想想在使用struts1时,不推荐创建成员变量,因为action是单例的,如果创建了成员变量,就会存在线程不安全的隐患,而struts2是每一次请求都会创建一个action,就不用考虑线程安全的问题。所以,日常开发中,通常需要考虑成员变量或者说全局变量在多线程环境下,是否会引发一些问题。
前言
最后更新于:2022-04-01 07:00:16
> 原文出处:[Java线程](http://blog.csdn.net/column/details/java-thread.html)
> 作者:[高爽](http://blog.csdn.net/ghsau)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# Java线程
> 主要介绍线程生命周期、线程同步、线程锁、线程通信、线程池、并发包等相关知识。