深入理解Java多执行绪与并发程式设计
一、课程目标
多执行绪三大特性Java内存模型
Volatile
ThreadLoca
执行绪池
二、执行绪三大特性
多执行绪有三大特性,原子性、可见性、有序性2.1 什么是原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。
我们操作资料也是如此,比如i = i+1;其中就包括,读取i的值,计算i,写入i。这行程式码在Java中是不具备原子性的,则多执行绪执行肯定会出问题,所以也需要我们使用同步和lock这些东西来确保这个特性了。
原子性其实就是保证资料一致、执行绪安全一部分,
2.2 什么是可见性
当多个执行绪访问同一个变数时,一个执行绪修改了这个变数的值,其他执行绪能够立即看得到修改的值。
若两个执行绪在不同的cpu,那么执行绪1改变了i的值还没重新整理到主存,执行绪2又使用了i,那么这个i值肯定还是之前的,执行绪1对变数的修改执行绪没看到这就是可见性问题。
2.3什么是有序性
程式执行的顺序按照程式码的先后顺序执行。
一般来说处理器为了提高程式执行效率,可能会对输入程式码进行优化,它不保证程式中各个语句的执行先后顺序同程式码中的顺序一致,但是它会保证程式最终执行结果和程式码顺序执行的结果是一致的。如下:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
则因为重排序,他还可能执行顺序为 2-1-3-4,1-3-2-4
但绝不可能 2-1-4-3,因为这打破了依赖关系。
显然重排序对单执行绪执行是不会有任何问题,而多执行绪就不一定了,所以我们在多执行绪程式设计时就得考虑这个问题了。
三、Java内存模型
共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个执行绪对共享变数的写入时,能对另一个执行绪可见。从抽象的角度来看,JMM定义了执行绪和主内存之间的抽象关系:执行绪之间的共享变数储存在主内存(main memory)中,每个执行绪都有一个私有的本地内存(local memory),本地内存中储存了该执行绪以读/写共享变数的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了快取,写缓冲区,暂存器以及其他的硬件和编译器优化。
从上图来看,执行绪A与执行绪B之间如要通讯的话,必须要经历下面2个步骤:
首先,执行绪A把本地内存A中更新过的共享变数重新整理到主内存中去。然后,执行绪B到主内存中去读取执行绪A之前已更新过的共享变数。下面通过示意图来说明这两个步骤: 
如上图所示,本地内存A和B有主内存中共享变数x的副本。假设初始时,这三个内存中的x值都为0。执行绪A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当执行绪A和执行绪B需要通讯时,执行绪A首先会把自己本地内存中修改后的x值重新整理到主内存中,此时主内存中的x值变为了1。随后,执行绪B到主内存中去读取执行绪A更新后的x值,此时执行绪B的本地内存的x值也变为了1。从整体来看,这两个步骤实质上是执行绪A在向执行绪B传送讯息,而且这个通讯过程必须要经过主内存。JMM通过控制主内存与每个执行绪的本地内存之间的互动,来为java程序员提供内存可见性保证。总结:什么是Java内存模型:java内存模型简称jmm,定义了一个执行绪对另一个执行绪可见。共享变数存放在主内存中,每个执行绪都有自己的本地内存,当多个执行绪同时访问一个数据的时候,可能本地内存没有及时重新整理到主内存,所以就会发生执行绪安全问题。
四、 Volatile
4.1、什么是VolatileVolatile 关键字的作用是变数在多个执行绪之间可见。
程式码:
class ThreadVolatileDemo extends Thread {
public boolean flag = true;
@Override
public void run() {
System.out.println("开始执行子执行绪....");
while (flag) {
}
System.out.println("执行绪停止");
}
public void setRuning(boolean flag) {
this.flag = flag;
}
}
public class ThreadVolatile {
public static void main(String[] args) throws InterruptedException {
ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
threadVolatileDemo.start();
Thread.sleep(3000);
threadVolatileDemo.setRuning(false);
System.out.println("flag 已经设定成false");
Thread.sleep(1000);
System.out.println(threadVolatileDemo.flag);
}
}
执行结果:

已经将结果设定为fasle为什么?还一直在执行呢。
原因:执行绪之间是不可见的,读取的是副本,没有及时读取到主内存结果。
解决办法使用Volatile关键字将解决执行绪之间可见性, 强制执行绪每次读取该值的时候都去“主内存”中取值
4.2、 Volatile非原子性
public class VolatileNoAtomic extends Thread {
private static volatile int count;
// private static AtomicInteger count = new AtomicInteger(0);
private static void addCount() {
for (int i = 0; i count++;
// count.incrementAndGet();
}
System.out.println(count);
}
public void run() {
addCount();
}
public static void main(String[] args) {
VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
for (int i = 0; i arr[i] = new VolatileNoAtomic();
}
for (int i = 0; i arr[i].start();
}
}
}
执行结果:

结果发现 资料不同步,因为Volatile不用具备原子性。
4.3、使用AtomicInteger原子类
public class VolatileNoAtomic extends Thread {
static int count = 0;
private static AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i //等同于i++
atomicInteger.incrementAndGet();
}
System.out.println(count);
}
public static void main(String[] args) {
// 初始化10个执行绪
VolatileNoAtomic[] volatileNoAtomic = new VolatileNoAtomic[10];
for (int i = 0; i // 建立
volatileNoAtomic[i] = new VolatileNoAtomic();
}
for (int i = 0; i volatileNoAtomic[i].start();
}
}
}
4.4、volatile与synchronized区别
仅靠volatile不能保证执行绪的安全性。(原子性)
①volatile轻量级,只能修饰变数。synchronized重量级,还可修饰方法
②volatile只能保证资料的可见性,不能用来同步,因为多个执行绪并发访问volatile修饰的变数不会阻塞。
synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的执行绪才能进入临界区,从而保证临界区中的所有语句都全部执行。多个执行绪争抢synchronized锁物件时,会出现阻塞。
执行绪安全性
执行绪安全性包括两个方面,①可见性。②原子性。
从上面自增的例子中可以看出:仅仅使用volatile并不能保证执行绪安全性。而synchronized则可实现执行绪的安全性。
五、ThreadLocal
5.1、什么是ThreadLocalThreadLocal提高一个执行绪的区域性变数,访问某个执行绪拥有自己区域性变数。
当使用ThreadLocal维护变数时,ThreadLocal为每个使用该变数的执行绪提供独立的变数副本,所以每一个执行绪都可以独立地改变自己的副本,而不会影响其它执行绪所对应的副本。
ThreadLocal的界面方法
ThreadLocal类界面很简单,只有4个方法,我们先来了解一下:
void set(Object value)设定当前执行绪的执行绪区域性变数的值。
public Object get()该方法返回当前执行绪所对应的执行绪区域性变数。
public void remove()将当前执行绪区域性变数的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当执行绪结束后,对应该执行绪的区域性变数将自动被垃圾回收,所以显式呼叫该方法清除执行绪的区域性变数并不是必须的操作,但它可以加快内存回收的速度。
protected Object initialValue()返回该执行绪区域性变数的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟呼叫方法,线上程第1次呼叫get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的预设实现直接返回一个null。
案例:建立三个执行绪,每个执行绪生成自己独立序列号。
程式码:
class Res {
// 生成序列号共享变数
public static Integer count = 0;
public static ThreadLocal threadLocal = new ThreadLocal() {
protected Integer initialValue() {
return 0;
};
};
public Integer getNum() {
int count = threadLocal.get() + 1;
threadLocal.set(count);
return count;
}
}
public class ThreadLocaDemo2 extends Thread {
private Res res;
public ThreadLocaDemo2(Res res) {
this.res = res;
}
@Override
public void run() {
for (int i = 0; i System.out.println(Thread.currentThread().getName() + "---" + "i---" + i + "--num:" + res.getNum());
}
}
public static void main(String[] args) {
Res res = new Res();
ThreadLocaDemo2 threadLocaDemo1 = new ThreadLocaDemo2(res);
ThreadLocaDemo2 threadLocaDemo2 = new ThreadLocaDemo2(res);
ThreadLocaDemo2 threadLocaDemo3 = new ThreadLocaDemo2(res);
threadLocaDemo1.start();
threadLocaDemo2.start();
threadLocaDemo3.start();
}
}
5.2、ThreadLocal实现原理
ThreadLocal通过map集合
Map.put(“当前执行绪”,值);
六、执行绪池
1.1、什么是执行绪池?执行绪池是指在初始化一个多执行绪应用程序过程中建立一个执行绪集合,然后在需要执行新的任务时重用这些执行绪而不是新建一个执行绪。执行绪池中执行绪的数量通常完全取决于可用内存数量和应用程序的需求。然而,增加可用执行绪数量是可能的。执行绪池中的每个执行绪都有被分配一个任务,一旦任务已经完成了,执行绪回到池子中并等待下一次分配任务。
1.2、执行绪池作用
基于以下几个原因在多执行绪应用程序中使用执行绪是必须的:
执行绪池改进了一个应用程序的响应时间。由于执行绪池中的执行绪已经准备好且等待被分配任务,应用程序可以直接拿来使用而不用新建一个执行绪。执行绪池节省了CLR 为每个短生存周期任务建立一个完整的执行绪的开销并可以在任务完成后回收资源。执行绪池根据当前在系统中执行的程序来优化执行绪时间片。执行绪池允许我们开启多个任务而不用为每个执行绪设定属性。执行绪池允许我们为正在执行的任务的程式引数传递一个包含状态资讯的物件引用。执行绪池可以用来解决处理一个特定请求最大执行绪数量限制问题。1.3 、 执行绪池四种建立方式
Java通过Executors(jdk1.5并发包)提供四种执行绪池,分别为:
newCachedThreadPool建立一个可快取执行绪池,如果执行绪池长度超过处理需要,可灵活回收空闲执行绪,若无可回收,则新建执行绪。
newFixedThreadPool 建立一个定长执行绪池,可控制执行绪最大并发数,超出的执行绪会在伫列中等待。
newScheduledThreadPool 建立一个定长执行绪池,支援定时及周期性任务执行。
newSingleThreadExecutor 建立一个单执行绪化的执行绪池,它只会用唯一的工作执行绪来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
1.1 newCachedThreadPool
建立一个可快取执行绪池,如果执行绪池长度超过处理需要,可灵活回收空闲执行绪,若无可回收,则新建执行绪。示例程式码如下:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i final int index = i;
// try {
// Thread.sleep(index * 1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "---" + index);
}
});
}
总结: 执行绪池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的执行绪,而不用每次新建执行绪。
1.2 newFixedThreadPool
建立一个定长执行绪池,可控制执行绪最大并发数,超出的执行绪会在伫列中等待。示例程式码如下:
// 建立一个定长执行绪池,可控制执行绪最大并发数,超出的执行绪会在伫列中等待
final ExecutorService newCachedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i final int index = i;
newCachedThreadPool.execute(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("i:" + index);
}
});
}
总结:因为执行绪池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。
定长执行绪池的大小最好根据系统资源进行设定。如Runtime.getRuntime().availableProcessors()
1.3 newScheduledThreadPool
建立一个定长执行绪池,支援定时及周期性任务执行。延迟执行示例程式码如下:
// 建立一个定长执行绪池,支援定时及周期性任务执行。延迟执行示例程式码如下:
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
newScheduledThreadPool.schedule(new Runnable() {
public void run() {
System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);
表示延迟3秒执行。
1.4 newSingleThreadExecutor
建立一个单执行绪化的执行绪池,它只会用唯一的工作执行绪来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例程式码如下:
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i final int index = i;
newSingleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("index:" + index);
try {
Thread.sleep(200);
} catch (Exception e) {
// TODO: handle exception
}
}
});
}
注意: 结果依次输出,相当于顺序执行各个任务。