先明确一下几个概念:
1.
- 多线程:一个程序(进程)在运行的时候产生了不止一个线程。
- 并行:多个CUP或多台机器同时运行一段逻辑,真正的同时进行。
- 并发:通过CPU调度算法(如 时间片),让用户看上去是在同时进行。
- 线程安全:在并发的情况下,线程调度不会影响程序执行的结果。反过来,线程不安全时,线程调度可能会导致变量等修改混乱,影响最终执行结果的正确性。
- 同步:java中的同步是指通过人为的控制,使多线程运行正确,得出正确的结果。
2.volatile:
首先,多线程模型分为:main memory,working memory。
多线程是会缓存的,所以多线程中的变量最可能遇到的问题是:变量被不同线程修改的读值问题。(如:a变量已经被b线程修改了,但是因为a变量之前已经被缓存过,所以之后c线程读取到的a还是缓存的旧值)。
基于这种情况,volatile关键字的作用就是:不去缓存,直接取值。
更详细的说就是:通常在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去,如图。而volatile关键词的作用:每次针对该变量的操作都激发一次load and save。
3.基本线程类指的是:Thread类,Runnable接口,Callable接口。
Thread类实现了Runnable接口
- 启动一个线程的方法:
MyThread mythead = new MyThread()
mythread.start();
- 中断:中断会影响到线程的wait状态,sleep状态,join状态。被打断后的线程抛出InterruptedException异常。
Thread.interrupted()方法检查当前线程是否发生中断,返回boolean。
线程在synchronized获锁过程中不能被中断。
中断是一个状态!interrupt()方法只是将这个状态置为true而已。所以说正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted()) ,则同样可以在中断后离开代码体。
- 不能在try catch结构的catch来获取线程的异常,可以在try结构中用thread.setUncaughtExceptionHandler(),在这个方法中重写uncaughtException()方法。
例: - Runnable:
与Thread类似。 - Callable:
future模式:并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是isDone和get。其中Future对象用来存放该线程的返回值以及状态
ExecutorService e = Executors.newFixedThreadPool(3);
//submit方法有多重参数版本,及支持callable也能够支持runnable接口类型.
Future future = e.submit(new myCallable());
future.isDone() //return true,false 无阻塞
future.get() // return 返回值,阻塞直到该线程运行结束
- 高级多线程类
接下来是实际项目中常用到的工具了,Java1.5提供了一个非常高效实用的多线程包:java.util.concurrent, 提供了大量高级工具,可以帮助开发者编写高效、易维护、结构清晰的Java多线程程序。
1.ThreadLocal类
作用:保存线程的独立变量。
2.原子类(AtomicInteger、AtomicBoolean……)
使用atomic wrapper class如atomicInteger,或者使用自己保证原子的操作,则等同于synchronized。
如:
//返回值为boolean
AtomicInteger.compareAndSet(int expect,int update)
AtomicReference
对于AtomicReference 来讲,也许对象会出现,属性丢失的情况,即oldObject == current,但是oldObject.getPropertyA != current.getPropertyA。
这时候,AtomicStampedReference就派上用场了。这也是一个很常用的思路,即加上版本号。
3.Lock类
在java.util.concurrent里有三个实现:
ReentrantLock
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock
主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。
lock更灵活,可以自由定义多把锁的枷锁解锁顺序(synchronized要按照先加的后解顺序)
提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有trylock的带超时时间版本。
本质上和监视器锁(即synchronized是一样的)
能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。
和Condition类的结合性能更强大。
ReentrantLock
可重入的意义在于持有锁的线程可以继续持有,并且要释放对等的次数后才真正释放该锁。
使用方法是:
1.先new一个实例
ReentrantLock r=new ReentrantLock();
2.加锁
r.lock()或r.lockInterruptibly();
此处也是个不同,后者可被打断。当a线程lock后,b线程阻塞,此时如果是lockInterruptibly,那么在调用b.interrupt()之后,b线程退出阻塞,并放弃对资源的争抢,进入catch块。(如果使用后者,必须throw interruptable exception 或catch)
3.释放锁
r.unlock()
必须做!何为必须做呢,要放在finally里面。以防止异常跳出了正常流程,导致灾难。这里补充一个小知识点,finally是可以信任的:经过测试,哪怕是发生了OutofMemoryError,finally块中的语句执行也能够得到保证。
ReentrantReadWriteLock
可重入读写锁(读写锁的一个实现)
ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
ReadLock r = lock.readLock();
WriteLock w = lock.writeLock();
两者都有lock,unlock方法。写写,写读互斥;读读不互斥。可以实现并发读的高效线程安全代码
4.容器类
这里就讨论比较常用的两个:
BlockingQueue
ConcurrentHashMap
BlockingQueue
阻塞队列。该类是java.util.concurrent包下的重要类,通过对Queue的学习可以得知,这个queue是单向队列,可以在队列头添加元素和在队尾删除或取出元素。类似于一个管 道,特别适用于先进先出策略的一些应用场景。普通的queue接口主要实现有PriorityQueue(优先队列),有兴趣可以研究
BlockingQueue在队列的基础上添加了多线程协作的功能:
BlockingQueue
除了传统的queue功能(表格左边的两列)之外,还提供了阻塞接口put和take,带超时功能的阻塞接口offer和poll。put会在队列满的时候阻塞,直到有空间时被唤醒;take在队 列空的时候阻塞,直到有东西拿的时候才被唤醒。用于生产者-消费者模型尤其好用,堪称神器。
常见的阻塞队列有:
ArrayListBlockingQueue
LinkedListBlockingQueue
DelayQueue
SynchronousQueue
ConcurrentHashMap
高效的线程安全哈希map。请对比hashTable , concurrentHashMap, HashMap。