Java 多线程并发编程一览笔录
知识体系图:
1、线程是什么?
线程是进程中独立运行的子任务。
2、创建线程的方式
方式一:将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法
方式二:声明实现 Runnable 接口的类。该类然后实现 run 方法
推荐方式二,因为接口方式比继承方式更灵活,也减少程序间的耦合。
3、获取当前线程信息?
Thread.currentThread()
4、线程的分类
线程分为守护线程、用户线程。线程初始化默认为用户线程。
setDaemon(true) 将该线程标记为守护线程或用户线程。
特性:设置守护线程,会作为进程的守护者,如果进程内没有其他非守护线程,那么守护线程也会被销毁,即使可能线程内没有运行结束。
5、线程间的关系?
某线程a 中启动另外一个线程 t,那么我们称 线程 t是 线程a 的一个子线程,而 线程a 是 线程t 的 父线程。
最典型的就是我们在main方法中 启动 一个 线程去执行。其中main方法隐含的main线程为父线程。
6、线程API一览:如何启动、停止、暂停、恢复线程?
(1)start() 使线程处于就绪状态,Java虚拟机会调用该线程的run方法;
(2)stop() 停止线程,已过时,存在不安全性:
一是可能请理性的工作得不得完成;
二是可能对锁定的对象进行“解锁”,导致数据不同步不一致的情况。
推荐 使用 interrupt() +抛异常 中断线程。
(3)suspend() 暂停线程,已过时。
resume() 恢复线程,已过时。
suspend 与resume 不建议使用,存在缺陷:
一是可能独占同步对象;
二是导致数据不一致。
(4)yield() 放弃当前线程的CPU资源。放弃时间不确认,也有可能刚刚放弃又获得CPU资源。
(5)t.join() 等待该线程t 销毁终止。
7、synchronized关键字用法
一 原子性(互斥性):实现多线程的同步机制,使得锁内代码的运行必需先获得对应的锁,运行完后自动释放对应的锁。
二 内存可见性:在同一锁情况下,synchronized锁内代码保证变量的可见性。
三 可重入性:当一个线程获取一个对象的锁,再次请求该对象的锁时是可以再次获取该对象的锁的。
如果在synchronized锁内发生异常,锁会被释放。
总结:
(1)synchronized方法 与 synchronized(this) 代码块 锁定的都是当前对象,不同的只是同步代码的范围
(2)synchronized (非this对象x) 将对象x本身作为“对象监视器”:
a、多个线程同时执行 synchronized(x) 代码块,呈现同步效果。
b、当其他线程同时执行对象x里面的 synchronized方法时,呈现同步效果。
c、当其他线程同时执行对象x里面的 synchronized(this)方法时,呈现同步效果。
(3)静态synchronized方法 与 synchronized(calss)代码块 锁定的都是Class锁。Class 锁与 对象锁 不是同一个锁,两者同时使用情况可能呈异步效果。
(4)尽量不使用 synchronized(string),是因为string的实际锁为string的常量池对象,多个值相同的string对象可能持有同一个锁。
8、volatile关键字用法
一 内存可见性:保证变量的可见性,线程在每次使用变量的时候,都会读取变量修改后的最的值。
二 不保证原子性。
9、线程间的通信方式
线程间通信的方式主要为共享内存、线程同步。
线程同步除了synchronized互斥同步外,也可以使用wait/notify实现等待、通知的机制。
(1)wait/notify属于Object类的方法,但wait和notify方法调用,必须获取对象的对象级别锁,即synchronized同步方法或同步块中使用。
(2)wait()方法:在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,或者其他某个线程中断当前线程,导致当前线程一直阻塞等待。等同wait(0)方法。
wait(long timeout) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待。 单位为毫秒。
void wait(long timeout, int nanos) 与 wait(long timeout) 不同的是增加了额外的纳秒级别,更精细的等待时间控制。
(3)notfiy方法:唤醒在此对象监视器上等待的单个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。
(4)notifyAll方法:唤醒在此对象监视器上等待的所有线程。
需要:wait被执行后,会自动释放锁,而notify被执行后,锁没有立刻释放,由synchronized同步块结束时释放。
应用场景:简单的生产、消费问题。
1
2
3
4
5
6
7
8
|
synchronized (lock) { //获取到对象锁lock
try {
lock.wait(); //等待通信信号, 释放对象锁lock
} catch (InterruptedException e) {
e.printStackTrace();
}
//接到通信信号
} |
1
2
3
4
5
|
synchronized (lock) { //获取到对象锁lock
lock.notify(); //通知并唤醒某个正等待的线程
//其他操作
} //释放对象锁lock |
10、ThreadLocal与InheritableThreadLocal
让每个线程都有自己独立的共享变量,有两种方式:
一 该实例变量封存在线程类内部;如果该实例变量(非static)是引用类型,存在可能逸出的情况。
二 就是使用ThreadLocal在任意地方构建变量,即使是静态的(static)。具有很好的隔离性。
(1)重写initialValue()方法: 初始化ThreadLocal变量,解决get()返回null问题(
(2)InheritableThreadLocal 子线程可以读取父线程的值,但反之不行
11、ReentrantLock的使用
一个简单的示例:
1
2
3
4
5
6
7
8
9
|
private java.util.concurrent.locks.Lock lock = new ReentrantLock();
public void method() {
try {
lock.lock();
//获取到锁lock,同步块
} finally {
lock.unlock(); //释放锁lock
}
} |
ReentrantLock 比 synchronized 功能更强大,主要体现:
(1)ReentrantLock 具有公平策略的选择。
(2)ReentrantLock 可以在获取锁的时候,可有条件性地获取,可以设置等待时间,很有效地避免死锁。
如 tryLock() 和 tryLock(long timeout, TimeUnit unit)
(3)ReentrantLock 可以获取锁的各种信息,用于监控锁的各种状态。
(4)ReentrantLock 可以灵活实现多路通知,即Condition的运用。
————————————————————————————–
一、公平锁与非公平锁
ReentrantLock 默认是非公平锁,允许线程“抢占插队”获取锁。公平锁则是线程依照请求的顺序获取锁,近似FIFO的策略方式。
二、锁的使用:
(1)lock() 阻塞式地获取锁,只有在获取到锁后才处理interrupt信息
(2)lockInterruptibly() 阻塞式地获取锁,立即处理interrupt信息,并抛出异常
(3)tryLock() 尝试获取锁,不管成功失败,都立即返回true、false,注意的是即使已将此锁设置为使用公平排序策略,tryLock()仍然可以打开公平性去插队抢占。如果希望遵守此锁的公平设置,则使用 tryLock(0, TimeUnit.SECONDS),它几乎是等效的(也检测中断)。
(4)tryLock(long timeout, TimeUnit unit)在timeout时间内阻塞式地获取锁,成功返回true,超时返回false,同时立即处理interrupt信息,并抛出异常。
如果想使用一个允许闯入公平锁的定时 tryLock,那么可以将定时形式和不定时形式组合在一起:
if (lock.tryLock() || lock.tryLock(timeout, unit) ) { … }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
private java.util.concurrent.locks.ReentrantLock lock = new ReentrantLock();
public void testMethod() {
try {
if (lock.tryLock( 1 , TimeUnit.SECONDS)) {
//获取到锁lock,同步块
} else {
//没有获取到锁lock
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) //如果当前线程持有锁lock,则释放锁lock
lock.unlock();
}
} } |
三、条件Condition的使用
条件Condition可以由锁lock来创建,实现多路通知的机制。
具有await、signal、signalAll的方法,与wait/notify类似,需要在获取锁后方能调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
private final java.util.concurrent.locks.Lock lock = new ReentrantLock();
private final java.util.concurrent.locks.Condition condition = lock.newCondition();
public void await() {
try {
lock.lock();
//获取到锁lock
condition.await(); //等待condition通信信号,释放condition锁
//接到condition通信
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); //释放对象锁lock
}
} |
12、ReentrantReadWriteLock的使用
ReentrantReadWriteLock是对ReentrantLock 更进一步的扩展,实现了读锁readLock()(共享锁)和写锁writeLock()(独占锁),实现读写分离。读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
读锁示例:
1
2
3
4
5
6
7
8
9
|
private final java.util.concurrent.locks.ReadWriteLock lock = new ReentrantReadWriteLock();
public void method() {
try {
lock.readLock().lock();
//获取到读锁readLock,同步块
} finally {
lock.readLock().unlock(); //释放读锁readLock
}
} |
写锁示例:
1
2
3
4
5
6
7
8
9
|
private final java.util.concurrent.locks.ReadWriteLock lock = new ReentrantReadWriteLock();
public void method() {
try {
lock.writeLock().lock();
//获取到写锁writeLock,同步块
} finally {
lock.writeLock().unlock(); //释放写锁writeLock
}
} |
13、同步容器与异步容器概览
(1)同步容器
包括两部分:
一个是早期JDK的Vector、Hashtable;
一个是它们的同系容器,JDK1.2加入的同步包装类,使用Collections.synchronizedXxx工厂方法创建。
1
|
Map<String, Integer> hashmapSync = Collections.synchronizedMap( new HashMap<String, Integer>());
|
同步容器都是线程安全的,一次只有一个线程访问容器的状态。
但在某些场景下可能需要加锁来保护复合操作。
复合类操作如:新增、删除、迭代、跳转以及条件运算。
这些复合操作在多线程并发的修改容器时,可能会表现出意外的行为,
最经典的便是ConcurrentModificationException,
原因是当容器迭代的过程中,被并发的修改了内容,这是由于早期迭代器设计的时候并没有考虑并发修改的问题。
其底层的机制无非就是用传统的synchronized关键字对每个公用的方法都进行同步,使得每次只能有一个线程访问容器的状态。这很明显不满足我们今天互联网时代高并发的需求,在保证线程安全的同时,也必须有足够好的性能。
(2)并发容器
与Collections.synchronizedXxx()同步容器等相比,util.concurrent中引入的并发容器主要解决了两个问题:
1)根据具体场景进行设计,尽量避免synchronized,提供并发性。
2)定义了一些并发安全的复合操作,并且保证并发环境下的迭代操作不会出错。
util.concurrent中容器在迭代时,可以不封装在synchronized中,可以保证不抛异常,但是未必每次看到的都是”最新的、当前的”数据。
1
|
Map<String, Integer> concurrentHashMap = new ConcurrentHashMap<String, Integer>()
|
ConcurrentHashMap 替代同步的Map即(Collections.synchronized(new HashMap()))。众所周知,HashMap是根据散列值分段存储的,同步Map在同步的时候会锁住整个Map,而ConcurrentHashMap在设计存储的时候引入了段落Segment定义,同步的时候只需要锁住根据散列值锁住了散列值所在的段落即可,大幅度提升了性能。ConcurrentHashMap也增加了对常用复合操作的支持,比如”若没有则添加”:putIfAbsent(),替换:replace()。这2个操作都是原子操作。注意的是ConcurrentHashMap 弱化了size()和isEmpty()方法,并发情况尽量少用,避免导致可能的加锁(当然也可能不加锁获得值,如果map数量没有变化的话)。
CopyOnWriteArrayList和CopyOnWriteArraySet分别代替List和Set,主要是在遍历操作为主的情况下来代替同步的List和同步的Set,这也就是上面所述的思路:迭代过程要保证不出错,除了加锁,另外一种方法就是”克隆”容器对象。—缺点也明显,占有内存,且数据最终一致,但数据实时不一定一致,一般用于读多写少的并发场景。
ConcurrentSkipListMap可以在高效并发中替代SoredMap(例如用Collections.synchronzedMap包装的TreeMap)。
ConcurrentSkipListSet可以在高效并发中替代SoredSet(例如用Collections.synchronzedSet包装的TreeMap)。
ConcurrentLinkedQuerue是一个先进先出的队列。它是非阻塞队列。注意尽量用isEmpty,而不是size();
14、CountDownLatch闭锁的使用
CountDownLatch是一个同步辅助类。
通常运用场景:
(1)作为启动信号:将计数 1 初始化的 CountDownLatch 用作一个简单的开/关锁存器,或入口。
通俗描述:田径赛跑运动员等待(每位运动员为一个线程,都在await())的”发令枪”,当发令枪countDown(),喊0的时候,所有运动员跳过await()起跑线并发跑起来了。
(2)作为结束信号:在通过调用 countDown() 的线程打开入口前,所有调用 await 的线程都一直在入口处等待。用 N 初始化的 CountDownLatch 可以使一个线程在 N 个线程完成某项操作之前一直等待,或者使其在某项操作完成 N 次之前一直等待。
通俗描述:某裁判,在终点等待所有运动员都跑完,每个运动员跑完就计数一次(countDown())当为0时,就可以往下继续统计第一人到最后一个撞线的时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
public long timeTasks( int nThreads, final Runnable task) throws InterruptedException {
/**
*一个启动信号,在 driver 为继续执行 worker 做好准备之前,它会阻止所有的 worker 继续执行。
*/
final CountDownLatch startSignal = new CountDownLatch( 1 );
/**
* 一个完成信号,它允许 driver 在完成所有 worker 之前一直等待。
*/
final CountDownLatch doneSignal = new CountDownLatch(nThreads);
for ( int i = 0 ; i < nThreads; i++) {
Thread t = new Thread() {
public void run() {
try {
startSignal.await(); /** 阻塞于此,一直到startSignal计数为0,再往下执行 */
try {
task.run();
} finally {
doneSignal.countDown(); /** doneSignal 计数减一,直到最后一个线程结束 */
}
} catch (InterruptedException ignored) {
}
}
};
t.start();
}
long start = System.currentTimeMillis();
startSignal.countDown(); /** doneSignal 计数减一,为0,所有task开始并发执行run */
doneSignal.await(); /** 阻塞于此,一直到doneSignal计数为0,再往下执行 */
long end = System.currentTimeMillis();
return end - start;
} public static void main(String[] args) throws InterruptedException {
final Runnable task = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(( long ) (Math.random() * 1000 ));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end" );
}
};
long time = new CountDownLatchTest().timeTasks( 10 , task);
System.out.println( "耗时:" + time + "ms" );
} |
更多的api:
boolean await(long timeout, TimeUnit unit) 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
15、CyclicBarrier关卡的使用
CyclicBarrier是一个同步辅助类。
CyclicBarrier让一个线程达到屏障时被阻塞,直到最后一个线程达到屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行
CyclicBarrier(int parties, Runnable barrierAction)构造函数,用于在所有线程都到达屏障后优先执行barrierAction的run()方法
使用场景:
可以用于多线程计算以后,最后使用合并计算结果的场景;
通俗描述:某裁判,在终点(await()阻塞处)等待所有运动员都跑完,所有人都跑完就可以做吃炸鸡啤酒(barrierAction),但是只要一个人没跑完就都不能吃炸鸡啤酒,当然也没规定他们同时跑(当然也可以,一起使用CountDownLatch)。
————————————————————————————–
CyclicBarrier与CountDownLatch的区别:
CountDownLatch强调的是一个线程等待多个线程完成某件事,只能用一次,无法重置;
CyclicBarrier强调的是多个线程互相等待完成,才去做某个事情,可以重置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
public static class WorkerThread implements Runnable {
private final CyclicBarrier cyclicBarrier;
public WorkerThread(CyclicBarrier cyclicBarrier) {
this .cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " pre-working" );
/**
* 线程在这里等待,直到所有线程都到达barrier。
*/
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " working" );
} catch (Exception e) {
e.printStackTrace();
}
}
} public static void main(String[] args) {
int THREAD_NUM = 5 ;
final CyclicBarrier cyclicBarrier = new CyclicBarrier(THREAD_NUM, new Runnable() {
/**
* 当所有线程到达barrier时执行
*/
@Override
public void run() {
System.out.println( "--------------Inside Barrier--------------" );
}
});
for ( int i = 0 ; i < THREAD_NUM; i++) {
new Thread( new WorkerThread(cyclicBarrier)).start();
}
} |
更多api:
int await(long timeout, TimeUnit unit) 在所有参与者都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。
16、Semaphore信号量的使用
Semaphore信号量是一个计数信号量。
可以认为,Semaphore维护一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。
通俗描述:某个车库只有N个车位,车主们要泊车,请向车库保安处阻塞 acquire()等待获取许可证,当获得许可证,车主们才可以去泊车。当某个车主离开车位的时候,交还许可证release() ,从而其他阻塞等待的车主有机会获得许可证。
另外:
Semaphore 默认是非公平策略,允许线程“抢占插队”获取许可证。公平策略则是线程依照请求的顺序获取许可证,近似FIFO的策略方式。
17、Executors框架(线程池)的使用
(1)线程池是什么?
线程池是一种多线程的处理方式,利用已有线程对象继续服务新的任务(按照一定的执行策略),而不是频繁地创建销毁线程对象,由此提供服务的吞吐能力,减少CPU的闲置时间。具体组成部分包括:
a、线程池管理器(ThreadPool)用于创建和管理线程池,包括创建线程池、销毁线程池,添加新任务。
b、工作线程(Worker)线程池中的线程,闲置的时候处于等待状态,可以循环回收利用。
c、任务接口(Task)每个任务必须实现的接口类,为工作线程提供调用,主要规定了任务的入口、任务完成的收尾工作、任务的状态。
d、等待队列(Queue)存放等待处理的任务,提供缓冲机制。
(2)Executors框架常见的执行策略
Executors框架提供了一些便利的执行策略。
1
|
java.util.concurrent.ExecutorService service = java.util.concurrent.Executors.newFixedThreadPool( 100 );
|
- newSingleThreadExecutor:创建一个单线程的线程池。
这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool:创建固定大小的线程池。
每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- newCachedThreadPool:创建一个可缓存的线程池。
如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。
此线程池支持定时以及周期性执行任务的需求。
- newSingleThreadScheduledExecutor:创建一个单线程的线程池。
此线程池支持定时以及周期性执行任务的需求。
(3)ExecutorService线程池管理
ExecutorService的生命周期有3个状态:运行、关闭(shutting down)、停止。
提交任务submit(xxx)扩展了基本方法 Executor.execute(java.lang.Runnable)。
<T> Future<T> submit(Callable<T> task) 提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。
Future<?> submit(Runnable task) 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
<T> Future<T> submit(Runnable task, T result) 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
shutdown() 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
List<Runnable> shutdownNow() 试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
一个简单的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool( 10 );
for ( int i = 0 ; i < 100 ; i++) {
executorService.submit( new Runnable() {
@Override
public void run() {
System.out.println( "哈哈" );
}
});
}
/**
* 如果不再需要新任务,请适当关闭executorService并拒绝新任务
*/
executorService.shutdown();
} |
(3)ThreadPoolExecutor机制
ThreadPoolExecutor为Executors的线程池内部实现类。
构造函数详解:
ThreadPoolExecutor线程池管理机制:
1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
一个简单的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public static void main(String[] args) {
java.util.concurrent.ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor( 10 , //corePoolSize 核心线程数
100 , //maximumPoolSize 最大线程数
30 , //keepAliveTime 线程池中超过corePoolSize数目的空闲线程最大存活时间;
// TimeUnit keepAliveTime时间单位
TimeUnit.SECONDS,
//workQueue 阻塞任务队列
new LinkedBlockingQueue<Runnable>( 1000 ),
//threadFactory 新建线程的工厂
Executors.defaultThreadFactory(),
//RejectedExecutionHandler当提交任务数超过maxmumPoolSize+workQueue之和时,
// 任务会交给RejectedExecutionHandler来处理
new ThreadPoolExecutor.AbortPolicy()
);
for ( int i = 0 ; i < 100 ; i++) {
threadPoolExecutor.submit( new Runnable() {
@Override
public void run() {
System.out.println( "哈哈" );
}
});
}
/**
* 如果不再需要新任务,请适当关闭threadPoolExecutor并拒绝新任务
*/
threadPoolExecutor.shutdown();
} |
18、可携带结果的任务Callable 和 Future / FutureTask
(1)为解决Runnable接口不能返回一个值或受检查的异常,可以采用Callable接口实现一个任务。
1
2
3
4
5
6
7
8
9
|
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/ V call() throws Exception;
} |
(2)Future表示异步计算的结果,可以对于具体的Runnable或者Callable任务进行查询是否完成,查询是否取消,获取执行结果,取消任务等操作。
V get() throws InterruptedException, ExecutionException 如有必要,等待计算完成,然后获取其结果。
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException 如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。
(3)FutureTask
FutureTask则是一个RunnableFuture<V>,而RunnableFuture实现了Runnbale又实现了Futrue<V>这两个接口。
简单示例一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public static void main(String[] args) throws InterruptedException {
FutureTask<Integer> future = new FutureTask<Integer>( new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 返回一个值或受检查的异常
//throw new Exception();
return new Random().nextInt( 100 );
}
});
new Thread(future).start();;
/**
* 模拟其他业务逻辑
*/
Thread.sleep( 1000 );
//Integer result = future.get(0, TimeUnit.SECONDS);
Integer result = null ;
try {
result = future.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println( "result========" + result);
}
|
简单示例二,采用Executors:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public static void main(String[] args) throws InterruptedException {
java.util.concurrent.ExecutorService threadPoolExecutor =
java.util.concurrent.Executors.newCachedThreadPool();
Future<Integer> future = threadPoolExecutor.submit(
new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 返回一个值或受检查的异常
//throw new Exception();
return new Random().nextInt( 100 );
}
});
/**
* 如果不再需要新任务,请适当关闭threadPoolExecutor并拒绝新任务
*/
threadPoolExecutor.shutdown();
/**
* 模拟其他业务逻辑
*/
Thread.sleep( 1000 );
//Integer result = future.get(0, TimeUnit.SECONDS);
Integer result = null ;
try {
result = future.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println( "result========" + result);
}
|
简单示例三,采用Executors+CompletionService:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
static class MyCallable implements Callable<Integer> {
private final int i;
public MyCallable( int i) {
super ();
this .i = i;
}
@Override
public Integer call() throws Exception {
// 返回一个值或受检查的异常
//throw new Exception();
return new Integer(i);
}
} public static void main(String[] args) throws InterruptedException {
java.util.concurrent.ExecutorService threadPoolExecutor =
java.util.concurrent.Executors.newCachedThreadPool();
java.util.concurrent.CompletionService<Integer> completionService =
new java.util.concurrent.ExecutorCompletionService<Integer>(threadPoolExecutor);
final int threadNum = 10 ;
for ( int i = 0 ; i < threadNum; i++) {
completionService.submit( new MyCallable(i + 1 ));
}
/**
* 如果不再需要新任务,请适当关闭threadPoolExecutor并拒绝新任务
*/
threadPoolExecutor.shutdown();
/**
* 模拟其他业务逻辑
*/
Thread.sleep( 2000 );
for ( int i = 0 ; i < threadNum; i++) {
try {
System.out.println( "result========" + completionService.take().get());
} catch (ExecutionException e) {
e.printStackTrace();
}
}
} |
注意的是提交到CompletionService中的Future是按照完成的顺序排列的,而不是按照添加的顺序排列的。
19、Atomic系列-原子变量类
其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。其中的类可以分成4组
基本类:AtomicInteger、AtomicLong、AtomicBoolean;
引用类型:AtomicReference、AtomicStampedRerence、AtomicMarkableReference;–AtomicStampedReference 或者 AtomicMarkableReference 解决线程并发中,导致的ABA问题
数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray —数组长度固定不可变,但保证数组上每个元素的操作绝对安全的
属性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
Updater使用限制:
限制1:操作的目标不能是static类型,前面说到unsafe的已经可以猜测到它提取的是非static类型的属性偏移量,如果是static类型在获取时如果没有使用对应的方法是会报错的,而这个Updater并没有使用对应的方法。
限制2:操作的目标不能是final类型的,因为final根本没法修改。
限制3:必须是volatile类型的数据,也就是数据本身是读一致的。
限制4:属性必须对当前的Updater所在的区域是可见的,也就是private如果不是当前类肯定是不可见的,protected如果不存在父子关系也是不可见的,default如果不是在同一个package下也是不可见的。
简单示例:
1
2
3
4
5
|
static class A {
volatile int intValue = 100 ;
} private AtomicIntegerFieldUpdater<A> atomicIntegerFieldUpdater
= AtomicIntegerFieldUpdater.newUpdater(A. class , "intValue" );
|
20、总结
什么叫线程安全?
线程安全就是每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的。
线程安全就是说多线程访问同一代码,不会产生不确定的结果。
线程安全问题多是由全局变量和静态变量引起的,当多个线程对共享数据只执行读操作,不执行写操作时,一般是线程安全的;当多个线程都执行写操作时,需要考虑线程同步来解决线程安全问题。
什么叫线程同步?
多个线程操作一个资源的情况下,导致资源数据前后不一致。这样就需要协调线程的调度,即线程同步。 解决多个线程使用共通资源的方法是:线程操作资源时独占资源,其他线程不能访问资源。使用锁可以保证在某一代码段上只有一条线程访问共用资源。
有两种方式实现线程同步:
1、synchronized
2、同步锁(Lock)
什么叫线程通信?
有时候线程之间需要协作和通信。
有两种方式实现线程通信:
1、synchronized 实现内存可见性,满足线程共享变量
2、wait/notify\notifyAll(synchronized同步方法或同步块中使用) 实现内存可见性,及生产消费模式的相互唤醒机制
3、同步锁(Lock)的Condition(await\signal\signalAll)
4、管道,实现数据的共享,满足读写模式
浅谈Java多线程机制
浅谈Java多线程机制
(-----文中重点信息将用红色字体凸显-----)
一、话题导入
在开始简述Java多线程机制之前,我不得不吐槽一下我国糟糕的IT界技术分享氛围和不给力的互联网技术解答深度。当一个初学java的小哥向我请教Java多线程机制相关问题时,我让他去寻求度娘的帮助,让他先学会自己尝试解决问题;但是他告诉我在网上找不到他想要的信息,我也尝试性的在网上收刮了半天,也确实找不到内容详尽、表述清晰的文献。更遗憾的是某些也许有一定参考价值的文档都需要通过非正常手段下载,比如注册、回复甚至是花钱购买,这难免会让不少人望而却步,最后不了了之。
我并不是蓄意抨击,而是希望更多的人能够向LINUX*之父Stallman一样,学会奉献;如果大家都能够尝试去奉献,最终每个人也将更易于索取。
(以后得空将会陆续将Java各知识点归类总结,并放在CSDN个人博客中;出Java之外还考虑介绍下其他方面的内容,届时请保持关注哟^( 。。)^)
二、现实中的类似问题
假设你是某快餐店的老板,随着自己的苦心经营,终于让快餐店门庭若市、生意兴隆;为了拓展销路,你决定增加送餐上门服务,公司财务告诉你你可以为拓展此业务支配12万元,这个时候你会怎么支配这笔钱呢?
当然有很多种支配方式,并且在支配上需要考虑到人员数量、送餐范围、送餐形式等多个问题;这里我们集中讨论下送餐形式这个细节:
1)买一辆雪弗兰赛欧;
2)买15辆电瓶车;
除去员工工资等基本成本过后剩余的钱用于购买送餐工具,上面我给出了两种送餐交通工具,他们都有各自的优点:首先,雪弗兰赛欧能够达到更快的送餐速度,而且可以供应的送餐范围更广;其次,用电瓶车作为送餐交通工具可以同时为多个顾客派送,并且运送成本显然更加低廉。在这两者之间,你会作何选择呢?
显然是第二种送餐交通工具更加实用:相较之下,后者可以处理的顾客数量更多,靠后的顾客等待时间明显缩短。试想一下,如果你打了电话定了午饭,就因为你是第25个顾客,晚上六点才给你送来,你会是什么心情?
其实,快餐店老板选择多辆电瓶车进行送餐的考虑同进程选择多线程控制的思想是如出一辙的,单线程的程序往往功能非常有限,在某些特定领域甚至不能达到我们所期望的效能。例如,当你想让服务器数据能够被多个客户同时访问时,单线程将让这一设想化为泡影;单线程情况下,多个客户的需求将存入一个栈队,并且依次执行,靠后的客户很难有较好的访问体验。
Java语言提供了非常优秀的多线程支持,多线程的程序可以包含多个顺序执行流,且多个顺序执行流之间互不干扰。总的来说,使用多线程编程有如下多个优点:
1)多个线程之间可以共享内存数据;
2)多个线程是并发执行的,可以同时完成多个任务;
3)Java语言内置了多线程功能支持,从而简化了Java的多线程编程。
三、线程的创建和启动
Java使用Thread类代表线程,所有线程对象都是Thread类或者其子类的实例。创建线程的方式有三种,分别是:
1)继承Thread类创建线程;
2)实现Runnable接口创建线程;
3)使用Callable和Future创建线程。
以上三种方式均可以创建线程,不过它们各有优劣,我将在分别叙述完每一种创建线程的方式后总结概括。
3.1 继承Thread类创建线程
主要步骤为:
① 定义一个类并继承Thread类,此类中需要重写Thread类中的run()方法,这个run()方法就是多线程都需要执行的方法;整个run()方法也叫做线程执行体;
② 创建此类的实例(对象),这时就创建了线程对象;
③ 调用线程对象的start()方法来启动该线程。
举例说明:
- <span style="font-size:12px;">
- public class MyThread extends Thread
- {
- public static void main(String[] args)
- {
- MyThread m1 = new MyThread();
- MyThread m2 = new MyThread();
- m1.start();//调用start()方法来开启线程
- m2.start();</span>
- }
- private int a;
- public void run()//重写run()方法
- {
- for ( ; a<100 ; a++ )
- {
- System.out.println(getName()+"-----"+a);
- //通过继承Thread类来创建线程时,可以通过getName()方法来获取线程的名称
- }
- }
- }
- </span>
上面通过一个简单的例子演示了创建线程的第一种方法(通过继承Thread类创建线程);通过运行以上代码发现有两个线程在并发执行,它们各自分别打印出0-99。由于没有对线程进行显示的命名,所以系统默认这两个线程的名称为Thread-0和Thread-1,num会跟随线程的个数依次递增。具体怎样定义线程名称,我将在后面提及。
那么在上述例子中一共有多少个线程在运行呢?答案是三个!
分别是main(主线程)、Thread-0和Thread-1;我们在多线程编程时一定不要忘记Java程序运行时默认的主线程,main()方法的方法体就是主线程的线程执行体;同理,run()方法就是新建线程的线程执行体。
PS: 其实上述例子中创建线程的代码(标红)可以简化,使用匿名对象来创建线程:
- <span style="font-family:Microsoft YaHei;font-size:12px;"><span style="font-size:12px;">new MyThread().start();
- new MyThread().start();
- </span></span>
------------------------------------------------------------------------------------------------------------------------------------------
在程序中如果想要获取当前线程对象可以使用方法:Thread.currentThread();
如果想要返回线程的名称,则可以使用方法:getName();
故如果想要获取当前线程的名称可以使用以上二者的搭配形式:Thread.currentThread().getName();
此外,还可以通过setName(String name)方法为线程设置名字;具体操作步骤是在定义线程后用线程对象调用setName()方法:
- <span style="font-family:Microsoft YaHei;font-size:12px;">MyThread m1 = new MyThread();
- m1.setName("xiancheng1");</span>
如此便能将线程对象m1的名称由Thread-0改变成xiancheng1。
------------------------------------------------------------------------------------------------------------------------------------------
在讨论完设置线程名称及获取线程名称的话题后,我们来分析下变量的共享。从以上代码运行结果来看,线程Thread0和线程Thread1分别输出0-99,由此可以看出,使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。
3.2 实现Runnable接口创建线程类
主要步骤为:
① 定义一个类并实现Runnable接口,重写该接口的run()方法,run()方法的方法体依旧是该线程的线程执行体;
② 创建定义的类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象;
③ 调用线程的start()方法来启动该线程。
举例说明:
- public class MyThread implements Runnable
- {
- public static void main(String[] args)
- {
- MyThread m1 = new MyThread();
- Thread t1 = new Thread(m1,"线程1");
- Thread t2 = new Thread(m1,"线程2");
- t1.start();
- t2.start();
- }
- private int i;
- public void run()
- {
- for ( ; i<100 ; i++ )
- {
- System.out.println(Thread.currentThread().getName()+" "+i);
- }
- }
- }
运行上面的程序可以看出:两个子线程的i变量是连续的,也就是说采用Runnable接口的方式创建的两个线程可以共享线程类的实例属性,这是因为我们创建的两个线程共用同一个target(m1),所以多个线程可以共享同一个线程类的实例属性。
通过对以上两种创建新线程的方法进行比较分析,可以知道两种创建并启动多线程方式的区别是:通过继承Thread类创建的对象即是线程对象,而通过实现Runnable接口创建的类对象只能作为线程对象的target。
3.3 通过Callable和Future创建线程
Callable接口是在Java5才提出的,它是Runnable接口的增强版;它提供了一个call()方法作为线程执行体,且call()方法比run()方法更为强大,主要体现在:
① call()方法可以有返回值;
② call()方法可以申明抛出异常。
Java5提供了Future接口来代表Callable接口里call()方法的返回值,并为Futrue接口提供一个FutureTask实现类,此实现类实现了Future接口,并且实现了Runnable接口,可以作为Thread类的target。不过需要提出的是,Callable接口有泛型限制,Callable接口里的泛型形参类型于call()方法返回值类型相同。
主要步骤为:(创建并启动有返回值的线程)
① 创建Callable接口的实现类,并实现call()方法作为线程的执行体,且该call()方法有返回值;
//不再是void
② 创建Callable接口实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
③ 使用FutureTask对象作为Thread对象的target创建并启动新线程;
④ 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
举例说明:
- public class MyThread implements Callable<Integer>//泛型
- {
- public static void main(String[] args)
- {
- MyThread m1 = new MyThread();//创建Callable对象
- //使用FutureTask来包装Callable对象
- FutureTask<Integer> task = new FutureTask<Integer>(m1);
- Thread t1 = new Thread(task,"有返回值的线程");
- t1.start();//启动线程
- //获取线程返回值
- try
- {
- System.out.println("子线程的返回值:"+task.get());
- }
- catch (Exception ex)
- {
- ex.printStackTrace();
- }
- }
- public Integer call()//返回值类型为Integer
- //泛型在集合框架部分会详细介绍
- {
- int i = 0;
- for ( ; i<100 ; i++ )
- {
- System.out.println(Thread.currentThread().getName()+" "+i);
- }
- return i;//call()可以有返回值
- }
- }
其实,创建Callable实现类与创建Runnable实现类没有太大区别,只是Callable的call()方法允许声明抛出异常,而且允许带返回值。
3.4 三种创建线程方法的对比
由于实现Runnable接口和实现Callable接口创建新线程方法基本一致,这里我们姑且把他们看作是同一类型;这种方式同继承Thread方式相比较,优劣分别为:
1.采用实现Runnable接口和Callable接口的方式创建多线程
① 优点:
1)实现类只是实现了接口,所以它还可以继承其他类;
2)多个线程可以共享一个target,所以适合多线程处理同一资源的模式,从而可以将CPU、代码和数据分开,较好的体现了面向对象的思想。
② 缺点:
1)编程比较复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
2.采用继承Thread类的方式来创建新线程
① 优点:
1)编写简单,如果需要访问当前线程,则只需要使用this即可。
② 缺点:
1)因为线程已经继承了Thread类,所以不能再继承其他类。
3.总结
① 综合分析,我们一般采用实现Runnable接口和实现Callable接口的方式来创建多线程。
四、线程的生命周期
4.1 CPU运行机制简介
一般情况下,计算机在一段时间内同时处理着多个进程,然而多个线程的执行顺序并不是依次进行的,而是“同时”在进行;其实这是我们产生的“错觉”。CPU有自己的工作频率,我们称之为主频;它的意思是CPU单位时间(一般定义为1s)内处理单元运算的次数。一般来说,频率越高,CPU的性能就更加优越。正是因为CPU有着很高的工作频率,才能在不同进程之间进行快速的切换,才会给我们造成一种多个任务在同时进行的假象。可以这么说,计算机在某一时刻只能处理单个进程的某一段运算单元(多核处理器的计算机除外)。
4.2 线程的状态
当新线程被创建后,他并不是一建立就进入运行状态,也不是一直在运行;由于CPU工作时是在多个进程间不停的切换运行,所以线程会处于多种运行状态,它们包括:新建、就绪、运行、阻塞和死亡(不同的人可能对线程状态的分类持不同意见,这里我们就不深究了)。
1. 新建和就绪状态
当程序使用了new关键字创建了一个线程之后,该线程就处于新建状态。当线程对象调用了start()方法之后,该线程便处于就绪状态,处于这个状态的线程并没有开始运行,而只是表示它可以运行了;不过该线程具体什么时候开始运行,完全取决于JVM里线程调度器的调度,这是具有随机性的,这是一种抢占式的调度策略。
需要注意的是:我们启动一个线程使用的是start()方法,而不是调用线程对象的run()方法。调用start()方法来启动线程,系统会把该run()方法当作线程执行体来处理,但如果直接调用线程对象的run()方法,则run()方法会直接被执行,并且在run()方法返回之前其他线程无法并发执行;此时系统会把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。
2. 运行和阻塞状态
如果处于就绪状态的线程获得了CPU执行权,开始执行run()方法的线程执行体,则该线程便处于运行状态。
阻塞状态只能由运行状态进入,而处于阻塞状态的线程只有重新回到就绪状态才能开始下一次运行;换句话说:进入阻塞状态的线程不能直接再运行。当然,运行状态的线程并不是只能通过“运行—》阻塞—》就绪—》运行”方法才能重新运行,它可以直接从运行状态恢复到就绪状态,这里要用到yield()方法。
线程进入阻塞状态的情况有:
① 线程调用sleep()方法,主动放弃了可执行资格;
② 当前线程想获取的锁被另一个线程所持有;
③ 线程在等待被唤醒;
④ 线程调用了suspend()方法将该线程挂起。(此方法容易产生死锁,不推荐使用)
线程从阻塞状态进入就绪状态的情况有:
① 线程sleep()时间已到;
② 线程成功获取了锁;
③ 线程被唤醒;
④ 处于挂起状态的线程被调用了resume()恢复方法。
可以看出线程进入阻塞状态和线程进入就绪状态的方法或途径是一一对应的。
3. 线程死亡状态
线程死亡的情况有:
① run()或call()方法执行完成,线程正常结束;
② 线程抛出异常或者错误;
③ 调用线程的stop()方法结束线程。(此方法容易导致死锁,故不推荐使用)
主线程和其他线程之间的关系:
一旦我们建立新线程,它将独立于主线程运行,不受主线程的约束和影响,他们拥有相同的地位;当主线程结束时,其他线程不会受其影响而结束。后面会介绍另外一种线程—后台线程,只要前台线程全部结束,后台线程也会自动结束;后台线程充当的是辅助前台线程的角色,所以后台线程也叫“守护线程”。
为了测试某线程是否已经死亡,可以调用其isAlive()方法,当线程处于就绪、运行和阻塞三种状态时,方法返回值为true,当线程处于其他两种状态时,此方法返回值为false。
需要注意的是:
① 不要试图对一个已经死亡的线程调用start()方法,否则会抛出“IllegalThreadStateException”异常;
② 不要试图对一个线程进行多次start()方法调用,否则也会抛出“IllegalThreadStateException”异常。
4. 线程状态转换关系
关于线程多个状态之间的转换关系,可以用以下转换图来表示:
五、控制线程
5.1 join线程
Thread提供线程“插队”的方法,就是让一个线程等待另一个线程完成的方法-----join()方法,目的是让当下线程等待插入线程运行完成后再继续运行。它一般用于将大问题划分成许多小问题,每个小问题用一个小线程完成;这有点像“坐公交车”,公共汽车就是主线线程,乘客就是插入辅助小线程,小线程在“某站”上车,待到目的地就下车,许多小线程为完成自己的目的在特定时间插入又在特定时间结束。
举例说明:
- public class JoinThreadTest extends Thread//也可通过实现接口来定义
- {
- public JoinThreadTest(String name)
- {
- super(name);
- }
- public void run()
- {
- int i=0;
- for ( ; i<200 ; i++ )
- {
- System.out.println(getName()+"--第--"+i+"--次");
- //由于这里是继承Thread类,所以可以直接使用getName()方法来获取线程名称
- }
- }
- public static void main(String[] args) throws Exception
- {
- for ( int a = 0 ; a<200 ; a++ )
- {
- if (a==20)
- {
- JoinThreadTest jt1 = new JoinThreadTest("插入线程");
- jt1.start();//启动子线程
- jt1.join();
- }
- System.out.println(Thread.currentThread().getName()+"--第--"+a+"--次");
- }
- }
- }
上例*有两个线程存在,分别是主线程和新建线程jt1,由于虚拟机首先从main()主函数开始读取,所以主函数开始执行,等到变量a等于20时,开始执行if内部代码块,此段代码新建一个线程jt1并启动该线程。由于jt1线程使用了join()方法,则主线程会等待jt1线程执行完成后才能继续执行。需要指出的是,通过继承Thread()类建立新线程,获取线程名称可以直接使用getName()方法,但是由于此方法是非静态方法,所以在主函数执行体中获取主线程名称不能直接使用getName()方法,必须使用完整的获取线程名称的方法-----Thread.currentThread().getName(),否则会报错。
join()方法有一定的灵活性。由于它的“强制性”,我们在调用此方法后插入线程需要执行完成后原线程才能继续执行,但是有的时候我们并不需要这样的效果,我们可能希望设定插入线程执行一定的时间然后返回原线程继续执行。join()方法的重载形式便应运而生了:
① join():等待被插入的线程执行完成;
② join(long millis):等待被插入的线程的时间最长为millis毫秒。
对于第二种方法,会出现两种情况:如果在特定时间内插入线程提前完成,则原线程还是需要等待直到特定时间后才能继续执行;第二种情况是如果插入线程在特定的时间内没有完成执行任务,则原线程不再等待并开始继续执行,如此原线程和插入线程又处于并列运行状态。
还有另外一种重载形式,但是对时间精度要求过高,几乎没有“用武之地”,这里就不细说了。
5.2 后台线程
顾名思义,后台线程就是在后台运行的线程,它是为其他线程提供服务的,所以后台线程也叫做“守护线程”。Java的垃圾回收线程就是典型的后台线程。
后台线程较为特殊,如果所有的前台线程都死亡,则后台线程也会随之自动死亡;就如无本之木,没有了实际的意义和存在的必要。
调用Thread对象的setDaemon(true)方法可以将指定线程设置成后台线程。
举例说明:
- public class DaemonThreadTest extends Thread
- {
- static int i=0;
- public void run()
- {
- for ( ; i<100 ; i++ )
- {
- System.out.println(getName()+"----"+i);
- }
- }
- public static void main(String[] args)
- {
- DaemonThreadTest dtt = new DaemonThreadTest();
- //设置此线程为后台线程
- dtt.setDaemon(true);
- dtt.start();
- int a=0;
- while (a<10)
- {
- System.out.println(Thread.currentThread().getName()+"~~~~"+a);
- a++;
- }
- if (i<99)
- {
- System.out.println("后台线程dtt没有执行完成就退出了!"+i);
- }
- }
- }
笔者在运行以上代码的时候会出现想要的结果:主线程main执行完成后,新建线程Thread-0还没有执行完,最后if语句中的文字”后台线程dtt没有执行完就退出了!“输出;可以证明辅助线程在主线程执行完成后就随之死亡,哪怕自己还没有执行完成。但是笔者在运行上述代码的时候发生了一件看似诧异的事情:if语句输出的i的值比run()方法里的i值要小一点,这其实是容易理解的----前台线程死亡后,JVM会通知后台线程死亡,但是从后台线程接收指令并做出反应需要一定的时间,所以导致run()方法在这个时间差里继续运行,才导致了两个i值不同。
在这里笔者要重申一点:由于上述代码中选用的i变量范围较小,故有的时候可能看到的情况是dtt线程执行完成了,这并不是代码错误了,而是由于执行内容少代码瞬间就执行完成了,这是由于处理器性能和随机性决定的。如果我们把变量i的范围调整到1000,出现想要结果的可能性就会大很多。
Thread类提供了一个判断线程是否为后台线程的方法----isDaemon()方法。
需要指出的是,前台线程如果创建子线程依旧默认是前台线程;同理,后台线程创建的子线程默认是后台线程。此外,setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadStateException异常。
5.3 线程等待---sleep()
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态方法sleep()来实现,sleep()方法主要形式为:
- <span style="font-family:Microsoft YaHei;font-size:12px;"> static void sleep(long millis);</span>
括号里的参数表示的是线程等待的时间。当线程进入等待状态,在暂停时间范围内线程无法”提前“开始执行,哪怕系统中没有其他线程运行。下面的例子将说明这一点:
- public class SleepTest
- {
- public static void main(String[] args) throws Exception
- {
- for (int a=0 ;a<200 ; a++ )
- {
- System.out.println("a的值是: "+a);
- if (a==50)
- {
- Thread.sleep(2000);
- //括号中参数的单位是毫秒
- }
- }
- }
- }
运行上面代码我们可以发现,程序中只有一个线程----主线程,当for循环执行到a等于50的时候会停顿两秒然后再接着执行直到进程结束。
5.4 线程让步----yield()
其实yield()方法同sleep()方法比较类似,它们的共同点就是放弃当前执行权。但是它们也有明显的区别:sleep()方法是让线程放弃当前执行权并转入阻塞状态,而yield()方法是让当前线程放弃执行权后进入就绪状态;此外,前者是规定了线程等待的具体时间,而后者只是让当前线程暂停一下,让线程调度器重新调度,完全可能发生的情况是:当某个线程调用了yield()方法暂停后,线程调度器又将其调度出来重新执行,而期间没有其他线程插入。
需要指出的是,某个线程执行了yield()方法后,只有优先级大于或等于当前线程优先级的线程才会获得执行机会,等待会介绍了设置线程优先级后笔者会用例子加以说明。
总结两方法的异同,sleep()方法和yield()方法区别如下:
① sleep()方法暂停当前线程后将执行权让出给其他线程,而yield()方法只会把执行权让给优先级大于或等于自己优先级的线程;
② sleep()方法将当前线程转入阻塞状态,而yield()方法则把当前线程转入就绪状态;
③ sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时需要对异常进行相应,要么处理要么抛出,而yield()方法没有声明抛出任何异常;
④ sleep()方法比yield()方法有更好的移植性,通常不建议使用yield()方法来控制并发线程的执行。
5.5 改变线程的优先级
简单的说,线程有一定的优先级,优先级从1到10不等;而主线程和新建线程默认优先级为普通,用数字表示就是优先级为5。优先级越高,或的执行权的可能也就越大。Thread类提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级;一般情况下,线程优先级如果用数字表示相差不大的情况下效果不是很明显,而且由于用数字表示优先级移植性不佳,故我们一般只取三种优先级,并赋予特殊的名称:
① MAX_PRIORITY:其值是10;
② MIN_PRIORTY:其值是1;
③ NORM_PRIORITY:其值是5。
结合yield()方法和线程优先级知识,笔者举例加以巩固:
- public class YieldTest extends Thread
- {
- public YieldTest(String name)
- {
- super(name);
- }
- static int a=0;
- public void run()
- {
- for ( ; a<100 ; a++ )
- {
- System.out.println(getName()+"----"+a);
- if (a==50)
- {
- Thread.yield();
- }
- }
- }
- public static void main(String[] args)
- {
- YieldTest yt1 = new YieldTest("高级线程");
- yt1.setPriority(Thread.MAX_PRIORITY);
- yt1.start();
- YieldTest yt2 = new YieldTest("低级线程");
- yt2.setPriority(Thread.MIN_PRIORITY);
- yt2.start();
- }
- }
上述代码共创建了两个新线程,两个线程共用一个变量a;当运行以上代码时可以清楚的看见,在for循环100次的执行过程中,线程yt1(也就是高级线程)获得执行的次数要多余yt2(也就是低级线程)所执行的次数。此外,由于yield()方法的特殊性,我们几乎感觉不到调用了yield()方法带来的线程切换。
六、线程同步
6.1 线程安全问题分析
使用多线程可以提高进程的执行效率,但是它也伴随着一定的风险;这是由系统的线程调度具有一定的随机性造成的,我们首先通过一个大家耳熟能详的例子来说明多线程引发的同步问题----银行取钱。
我们按照生活中正常的取、存钱操作编写如下代码:
- public class DrawTest
- {
- public static void main(String[] args)
- {
- //创建账户
- Account acct=new Account("公共账户",1000);
- //模拟两个线程对同一个账户取钱
- new DrawThread("客户甲",acct,800).start();//匿名对象
- new DrawThread("客户乙",acct,800).start();
- }
- }
- class Account
- {
- //建立并封装用户编号和账户余额两个变量
- private String number;
- private double balance;
- //建立构造器进行初始化
- public Account(String number,double balance)
- {
- this.number = number;
- this.balance = balance;
- }
- public void setNumber(String number)
- {
- this.number=number;
- }
- /*
- public void setBalance(double balance)
- {
- this.balance=balance;
- }
- */
- public String getNumber()
- {
- return number;
- }
- public double getBalance()
- {
- return balance;
- }
- //为了判断用户是否是同一个用户,我们重写hashCode()和equals()方法来进行判断
- public int hashCode()
- {
- return number.hashCode();
- }
- public boolean equals(Object obj)
- {
- if (this==obj)
- {
- return true;
- }
- if (obj!=null&&obj.getClass()==Account.class)
- {
- Account target = (Account)obj;
- return target.getNumber().equals(number);
- }
- else
- return false;
- }
- }
- class DrawThread extends Thread
- {
- private Account account;//模拟用户账户
- private double drawAmount;//希望取钱的数目
- public DrawThread(String name,Account account,double drawAmount)
- {
- super(name);
- this.account=account;
- this.drawAmount=drawAmount;
- }
- //当多个线程操作同一个数据时,将涉及数据安全问题
- public void run()
- {
- if (account.getBalance()>=drawAmount)//判断余额是否大于取钱数
- {
- System.out.println(getName()+"取钱成功!"+drawAmount);
- try
- {
- Thread.sleep(10);
- }
- catch (InterruptedException ex)
- {
- ex.printStackTrace();//打印异常信息
- }
- //修改余额
- account.setBalance(account.getBalance()-drawAmount);
- System.out.println("余额为:"+account.getBalance());
- }
- else
- {
- System.out.println(getName()+"取钱失败,余额不足!");
- }
- }
- }
运行上面代码会发现不符合实际的情况发生:账户余额只有1000却取出了1600,而且账户余额出现了负值,这不是银行希望的结果。这种滑稽的错误是因为线程调度的不确定性,run()方法的方法体不具有同步安全性;程序中有两个并发线程在修改Account对象。
6.2 同步代码块
由银行取钱“风波”可以了解到,当有两个进程并发修改同一个文件时就有可能造成异常。为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块,同步代码块的语法格式如下:
- <span style="font-family:Microsoft YaHei;font-size:12px;"> synchronized (对象)
- {
- 需要被同步的代码;
- }
- </span>
上面代码中,synchronized后括号中的对象就是同步监视器,线程在执行同步代码块之前需要先获得同步监视器的锁。同步代码块的同步监视器为对象,我们一般选用Object类来创建对象,这个对象就是锁。
- <span style="font-family:Microsoft YaHei;font-size:12px;"> Object obj = new Object();</span>
任何时刻只能有一个线程获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放锁。如此,我们将银行取钱问题的代码稍加修改,就能达到我们想要的运算结果:
- public void run()
- {
- synchronized(account)
- {
- if (account.getBalance()>=drawAmount)//判断余额是否大于取钱数
- {
- System.out.println(getName()+"取钱成功!"+drawAmount);
- try
- {
- Thread.sleep(10);
- }
- catch (InterruptedException ex)
- {
- ex.printStackTrace();//打印异常信息
- }
- //修改余额
- account.setBalance(account.getBalance()-drawAmount);
- System.out.println("余额为:"+account.getBalance());
- }
- else
- {
- System.out.println(getName()+"取钱失败,余额不足!");
- }
- }
- }
上面程序使用synchronized将run()方法里的方法体修改为同步代码块,该同步代码块的同步监视器就是account对象,这样的做法符合“加锁-修改-解锁”的逻辑;通过这种方式可以保证并发线程在任意时刻只有一个线程可以进入修改共享资源的代码区,从而保证了线程的安全性。
6.3 同步函数
同步函数就是使用synchronized关键字修饰的方法,同步函数的同步监视器是this,也就是调用方法的对象本身。需要指出的是,synchronized关键字可以修饰方法和代码块,但是不能修饰构造器、属性等。
同步的前提:
① 必须要有两个或两个以上的线程;
② 必须是多个线程使用同一个锁;
③ 必须保证同步中只有一个线程在运行;
为了减少保证线程安全而带来的负面影响(例如更加消耗资源),程序可以进行优化和控制:
① 只对那些会改变竞争资源的方法或代码进行同步;
② 如果可变类有两种运行环境:单线程和多线程环境,则应该为该可变类提供两种版本,即线程安全版本和线程不安全版本。
如果同步函数为静态同步,则其同步监视器就是:类名.class。
6.4 同步在单例设计模式中的应用
单例设计模式,顾名思义就是一个类只能创建一个对象;单例设计模式一共分为两种,分别是饿汉式和懒汉式。由于饿汉式在一开始就建立了对象并初始化提供了调用的方法,故饿汉式在多线程情况下没有安全隐患,不会引起多线程异常;而懒汉式由于需要对对象是否为空进行判断,所以可能导致多线程异常。
饿汉式单例设计模式:
- class single
- {
- private static single s = new single;
- private single(){}
- public static single getInstance()
- {
- return s;
- }
- }
懒汉式单例设计模式:
- class single
- {
- private static single s = null;
- private single(){}
- public static single getInstance{}
- {
- if (s==null)
- {
- synchronized(single.class)
- {
- if (s==null)//二次判断
- {
- s=new single();
- }
- }
- }
- return s;
- }
- }
6.5 释放同步监视器的锁定
线程会在如下几种情况释放对同步监视器的锁定:
① 当前线程的同步方法或者同步代码块执行完毕;
② 当前线程的同步方法或者同步代码块中遇到break、return终止了该代码块或该方法的继续执行;
③ 当前线程的同步方法或者同步代码块中遇到未处理的error或Exception,导致了该代码块或该方法异常而结束;
④ 程序执行了同步监视器对象的wait()方法。
线程在如下情况下不会释放同步监视器:
① 程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行;
② 其他线程调用了当前线程的suspend()方法将当前线程挂起。
6.6 同步锁
从Java5开始,Java提供了一种功能更加强大的线程同步机制-----通过显示定义同步锁对象来实现同步,在这种机制下,同步锁采用Lock对象充当。
Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现了允许更灵活的结构,Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁),使用该Lock对象可以显示的加锁、释放锁。
举例说明:
- class LockTest
- {
- //用ReentrantLock类定义锁对象
- private final ReentrantLock lock= new ReentrantLock();
- //将此锁应用在需要保证线程安全的方法上
- public void test()
- {
- //加锁
- lock.lock();
- try
- {
- //需要保证线程安全的代码
- }
- catch (Exception e)
- {
- System.out.println("发生错误信息,请重新确认代码!");
- }
- finally
- {
- //释放锁
- lock.unlock();
- }
- }
- }
使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。前面介绍的银行存取钱例子中,可以使用ReentrantLock类定义的锁来保证线程安全,而且相较于synchronized代码块或synchronized方法更加简洁方便。
使用Lock与使用同步方法有点类似,只是使用Lock时显示使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。此外,Lock提供了同步方法和同步代码块所没有的其他功能:
① 用于非结构块的tryLock()方法;
② 试图获取可中断锁的lockInterruptibly()方法;
③ 获取超时失效锁的tryLock(long,TimeUnit)方法。
ReentrantLock锁具有可重入性,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套使用,线程在每次调用lock()加锁后,必须显示调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
6.7 死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应采取措施避免死锁出现;一旦出现死锁,整个程序既不会发生任何异常,也不会给出提示,只是所有线程处于阻塞状态,无法继续。
举例说明:
- class A
- {
- public synchronized void foo(B b)
- {
- System.out.println("当前线程名称为:"+Thread.currentThread().getName()+"进入了A实例的foo方法");
- try
- {
- Thread.sleep(200);
- }
- catch (InterruptedException e)
- {
- e.printStackTrace();
- }
- System.out.println("当前线程名称为:"+Thread.currentThread().getName()+"企图调用B实例的last方法");
- b.last();
- }
- public synchronized void last()
- {
- System.out.println("进入了A类的last方法内部");
- }
- }
- class B
- {
- public synchronized void bar(A a)
- {
- System.out.println("当前线程名称为:"+Thread.currentThread().getName()+"进入了B实例的bar方法");
- try
- {
- Thread.sleep(200);
- }
- catch (InterruptedException e)
- {
- e.printStackTrace();
- }
- System.out.println("当前线程名称为:"+Thread.currentThread().getName()+"企图调用A实例的last方法");
- a.last();
- }
- public synchronized void last()
- {
- System.out.println("进入了B类的last方法内部");
- }
- }
- public class DeadLockTest implements Runnable
- {
- A a=new A();
- B b=new B();
- public void init()
- {
- Thread.currentThread().setName("主线程");
- a.foo(b);
- System.out.println("进入了主线程之后");
- }
- public void run()
- {
- Thread.currentThread().setName("副线程");
- b.bar(a);
- System.out.println("进入了副线程之后");
- }
- public static void main(String[] args)
- {
- DeadLockTest d1=new DeadLockTest();
- new Thread(d1).start();
- d1.init();
- }
- }
6.8 线程通信
线程间通信方法:
① wait():导致当前线程等待,括号中可以定义等待时间,若不定义等待时间,则需要等待至被唤醒;
② notify():唤醒在此同步监视器上等待的单个线程,如果多个线程在等待,则随机唤醒其中一个线程;
③ notifyAll():唤醒在此同步监视器上的所有线程。
需要注意的是,以上三个方法并不属于Thread类,而是属于Object类。对于使用synchronized修饰的同步方法,因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法;而同步代码块中同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法。
如果程序不是通过synchronized关键字来保证同步,而是使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用上述三个方法来进行线程间通信了,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。Condition实例被绑定在一个Lock对象上,要活的特定Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。同样的,Condition类提供了如下3个方法:
① await():类似于wait()方法,导致当前线程等待;
② signal():唤醒在此Lock对象上等待的单个线程;
③ signalAll():唤醒在此Lock对象上等待的所有线程。
这里还是以取钱的例子来说明:
- public class Account
- {
- private final Lock lock=new ReentrantLock();
- private final Condition cond=lock.newCondition();
- private String accountNo;
- private double balance;
- private boolean flag=false;
- public Account(){}
- public Account(String accountNo,double balance)
- {
- this.accountNo=accountNo;
- this.balance=balance;
- }
- public void setAccountNo(String accountNo)
- {
- this.accountNo=accountNo;
- }
- public String getAccountNo(String accountNo)
- {
- return accountNo;
- }
- public double getBalance(double balance)
- {
- return balance;
- }
- public void draw(double drawAmount)
- {
- lock.lock();
- try
- {
- if (!flag)
- {
- cond.await();
- }
- else
- {
- System.out.println(Thread.currentThread().getName()+"取钱:"+drawAmount);
- balance -= drawAmount;
- System.out.println("账户余额为:"+balance);
- flag=false;
- cond.signalAll();
- }
- }
- catch (InterruptedException e)
- {
- e.printStackTrace();
- }
- finally
- {
- lock.unlock();
- }
- }
- public void deposit(double depositAmount)
- {
- lock.lock();
- try
- {
- if (flag)
- {
- cond.await();
- }
- else
- {
- System.out.println(Thread.currentThread().getName()+"存款:"+depositAmount);
- balance+=depositAmount;
- System.out.println("账户余额为:"+balance);
- flag=true;
- cond.signalAll();
- }
- }
- catch (InterruptedException e )
- {
- e.printStackTrace();
- }
- finally
- {
- lock.unlock();
- }
- }
- }
七、其他内容
以上是关于多线程机制的基础内容,除此之外,还有关于"线程组和未处理的异常"、"线程池"等基础概念及内容,在这里笔者就不详细阐述了,读者打好多线程机制的基础后,可以自行学习这些拓展内容。
多线程处理机制,就介绍到这里啦!