16.1线程概述
Process
Thread
进程:系统进行资源分配和调度的独立单位
独立性 动态性 并发性
线程是进程的执行单元
主线程
一个线程必须有一个父进程
线程的执行是抢占式的
进程间的线程,共享内存、文件句柄等
使用多线程,比使用多进程,实现并发的性能高的多
Java虚拟机在后台内置了一个超级线程来进行垃圾回收
16.2 线程的创建和启动
两种方法:
1继承Thread类:创建的子类,可以直接代表线程对象
this 来获取当前对象
2实现Runnable接口:只能作为线程对象的target
Thread.currentThread() 静态方法,获取当前对象
run()方法,线程执行体
Thread(Runnable target)
分配新的 Thread 对象
Thread(Runnable target, String name)
分配新的 Thread 对象。
3使用Callable和Furture创建线程
使用Callable和Furture创建线程
Callable接口中提供了一个call() 方法,才可以作为线程执行体,类似于run()。
但是有返回值,可以抛出异常
因此Callable完全可以作为Thread的一个target
但是Callable接口不是Runnable的子接口,call()还有返回值,怎样才能拿到call方法的返回值呢?
—>Furture接口代表该返回值
—>其实现类 :FurtureTask类
FurtureTask实现了Furture和Runnable接口
使用步骤:
1、创建Callable类,实现call()方法—>创建Callable对象
2、使用FurtureTask来包装Callable类
3、new Thread(furtureTask,“新线程”).start
4、get方法可以获取call()的返回值
一般推荐用Runnable接口或Callable接口来创建多线程:
优点:
支持多线程共享一个Target对象
还可以继承其他类
Thread.currentThread()
继承Thread:简单,this即可获取当前线程对象
但是不能再继承了其他父类了
16.3 线程的生命周期 P725 一共5个状态 新建 就绪 运行 阻塞 死亡
1、新建 new对象
2、就绪 new ThreadTwo().start() 调用start()以后
启动线程 用start(),
不能用run(),用run()会被当成一个普通对象,而不是一个线程执行体
run()程序执行过程中,其他线程无法并发执行
就绪后,等待执行,不会立即执行
Thread.sleep(1) 线程休息1ms 进入阻塞状态
3、运行
抢占式调度策略—所有现代桌面和服务器OS
协作式策略—某些手机OS;需要主动放弃所占用的CPU资源
4、阻塞
进入阻塞的几种情况:
sleep()
IO阻塞
等待同步锁
等待通知
suspend() 尽量少用
阻塞后只能进入就绪状态
resume() —>返回就绪状态
线程调度器
5、死亡
isAlive()
true 就绪、运行、阻塞
false 新建/死亡
start() 方法 只有新建状态的线程才能使用
16.4 控制线程的方法
join线程 让一个线程阻塞,等待另一个线程完成
x.join() 在主线程中调用这个方法,将等该x线程执行完成,然后在回到主线程。再次之前主线程阻塞。
后台线程/守护线程 Daemon Thread
垃圾回收机制,是典型的后台线程
当所有前台线程死亡时,后台线程自动死亡
前台进程的子线程默认前台进程,后台进程一样
setDaemon(true) 必须在进程start之前使用
线程睡眠
sleep()
会阻塞进程,进入到阻塞状态。sleep()结束后,又会回到就绪状态
线程让步
yield()
不会阻塞进程,会让当前进程进入到就绪状态,让系统的线程调度去重新调度一次
完全可能的情况是:刚刚yield完成,线程调度器立马就将其调度出来重新执行
能否被调度出来,与该线程的优先级有关
sleep()方法比yield()有更好的移植性
改变线程优先级
每个线程的默认优先级与创建它的父线程优先级相同
void setPriority(int newPriority)
更改线程的优先级
16.5 线程同步P731
同步监视器
同步代码块synchronized Block
任何时刻只有一个线程可以获得对同步监视器的锁定,
同步代码块执行完毕后,该线程会释放对同步监视器的锁定
推荐使用可能被并发访问的共享资源充当同步监视器
放在run方法里面,对一片代码块进行修饰
synchronized (account){
…
执行该代码之前,一定要先获取到同步监视器的锁定
同步监视器,同时只有一个线程能获取到
}
同步方法synchronized Method
线程安全的类具有如下特征:
- 该类的对象可以被多个线程安全的访问;
- 每个线程调用该对象的任意方法之后都将得到正确的结果;
- 每个线程调用该对象的任意方法后,该对象状态依然保持合理状态
不可变类总是线程安全的,因为它的对象状态不可改变,但可变对象需要额外的方法来保证其线程安全。
synchronized 关键词 修饰方法
同步监视器是this,即调用该方法的对象
synchronized的用法(Java中每一个对象都可以作为锁,这是synchronized实现同步的基础)
1、普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
2、静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
3、同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
加锁-修改-释放锁
面向对象中流行的设计方法:DDD,即Domain Driven Design 领域驱动设计
可变类的线程安全是以降低程序的运行效率为代价的,可采用如下策略减少线程安全所带来的负面影响:
- 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步;
- 如果可变类有两种运行环境:单线程和多线程,则应该为该可变类提供两种版本,线程安全与线程不安全版本
(如StringBuilder保证性能,StringBuffer保证线程安全)
同步监视器,一般是可能被并发访问的共享资源
如下情况,线程不会释放同步监视器:
- 线程执行同步代码块或同步方法时,程序调用sleep、yield方法来暂停当前线程的执行,当前线程不会释放同步监视器;
- 线程执行同步代码快时,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器。
尽量避免使用suspend和resume方法来控制线程
同步锁Lock
某些锁允许对共享资源并发访问,如ReadWriteLock
Lock、ReadWriteLock是Java5新提供大的两个根接口;
为Lock提供了ReentrantLock(可重入锁)实现类;ReentrantLock较常用
为ReadWriteLock提供了ReentrantReadWriteLock实现类
ReentrantLock锁具有可重入性,一个线程可以对已被加锁的ReentrantLock锁再次加锁
同步代码块/同步方法: 隐式使用当前对象作为同步监视器 更好用 简单
锁Lock: 显式使用Lock对象作为同步锁 更灵活
同步代码块,一般写在线程Thread里面的run()方法里面,synchronized(Obj) {}
同步方法,写在对象中,synchronized 作为关键词,修饰方法 DDD
Lock是定义在对象里面的,使用在线程安全的方法里面
ReentrantLock具有可重入性,即可以对已经加锁的ReentrantLock重新加锁
死锁dead lock:JVM无法识别,不处理,所有线程处于阻塞状态,无法继续
锁等待
http://blog.csdn.net/qq_20641565/article/details/53208909
Lock、synchronized和ReadWriteLock的区别和联系
Lock和synchronized最大的区别就是当使用synchronized,一个线程抢占到锁资源,其他线程必须像SB一样得等待;
而使用Lock,一个线程抢占到锁资源,其他的线程可以不等待或者设置等待时间,实在抢不到可以去做其他的业务逻辑(tryLock()方法)
- ReadWriteLock
Java.util.concurrent.locks.ReadWriteLock有一种高级的线程锁机制,它允许多个线程读某个资源,但每次只允许一个线程来写
ReadWriteLock Locking规则
下面是一个线程允许锁住ReadWriteLock然后对保护的资源进行读或写操遵循的原则
- Read Lock
如果没有写入线程锁住ReadWriteLock,并且没有线程需要获得写入锁进行写入操作。那么多个线程可以获得锁来进行读操作。
- Write Lock
如果没有线程在写或者读操作,那么一次仅有一个线程可以获得锁以进行写操作。
16.6线程通信
1、传统的线程通信 使用同步方法
这些方法与进程的5个状态无关,不会改变进程状态?
wait方法,导致线程等待 —> 调用wait方法当前线程会释放对于该同步监视器的锁定;
notify唤醒在此同步监视器上等待的单个线程,任意选择一个唤醒,只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程;
notifyAll唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
这三个方法属于Object类,只能由同步监视器对象来调用
sleep是线程类(Thread)的方法,执行此方法会导致当前此线程暂停指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法或notifyAll后本线程才获得对象锁进入运行状态
2、使用Condition控制线程通信 使用Lock
只有使用synchonized关键字的情况下,才能用传统的线程通信方式 wait notify notify
若使用Lock对象来实现同步,那么就需要使用Condition控制线程通信
lock.newCondition
await:类似于隐式同步监视器上的wait方法,导致当前线程等待,直到其他线程调用该Condition的signal方法或signalAll方法来唤醒该线程,await方法有更多变体。。。
signal:唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程使用await方法放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
singalAll:唤醒在此Lock对象上等待的所有线程,只有当前线程使用await方法放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
3、使用阻塞队列控制线程通信 BlockingQueue接口
BlockingQueue是Queue的子接口,作为线程同步的工具
当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;
当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞
原来格式为表格(table),转换较复杂,未转换,需要手动复制一下
{"cells":[{},{"value":"抛出异常"},{"value":"特殊值"},{"value":"阻塞","inlineStyles":{"bold":[{"from":0,"to":2,"value":true}]}},{"value":"超时"},{"value":"插入"},{"value":"add(e)"},{"value":"offer(e)"},{"value":"put(e)","inlineStyles":{"bold":[{"from":0,"to":6,"value":true}]}},{"value":"offer(e, time, unit)"},{"value":"移除"},{"value":"remove()"},{"value":"poll()"},{"value":"take()","inlineStyles":{"bold":[{"from":0,"to":6,"value":true}]}},{"value":"poll(time, unit)"},{"value":"检查"},{"value":"element()"},{"value":"peek()"},{"value":"不可用","inlineStyles":{"bold":[{"from":0,"to":3,"value":true}]}},{"value":"不可用"}],"heights":[23,23,23,23],"widths":[70,70,70,70,117]}
16.7线程组和未处理的异常
ThreadGroup 线程组
setUncaughtExceptionHandlers
16.8 线程池
http://www.importnew.com/19011.html
深入理解Java之线程池
http://ifeve.com/java-threadpool/
聊聊并发(三)Java线程池的分析和使用
new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
keepAliveTime, milliseconds,runnableTaskQueue, threadFactory,handler);
该线程池正常维护的线程池大小就是corePoolSize
除非线程数达到corePoolSize且任务队列也满了,才会继续增加线程数
最后线程池的线程数也有一个最大限度maximumPoolSize
如果maximumPoolSize已经超出且任务队列已经满了,此时将报错,无法处理了
CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。
IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*N cpu。
16.9 线程相关类 P757
1ThreadLocal
该类提供了线程局部 (thread-local) 变量
ThreadLocal和其他同步机制一样,也是为了解决多进程对同一变量的访问冲突
2包装线程不安全的集合 ArrayList HashSet等
3线程安全的集合类
Concurrent开头的集合类
CopyOnwrite开头的集合类
Concurrent开头的集合类 支持多个线程并发写入访问
ConcurrentHashMap 支持16个线程并发写入,超过16,有一些线程需要等待
可修改构造函数的concurrencyLevel参数,其默认值是16。
CopyOnWriteArrayList
写入:每次均复制一个新数组操作
适合读取操作远多于写入的场景,如缓存
Java 并发实践 — ConcurrentHashMap 与 CAS
http://www.importnew.com/26035.html
CAS:Compare And Swap
深入并发包 ConcurrentHashMap
http://www.importnew.com/26049.html !!!
HashMap 容易出现闭环,get方法容易出现死循环;所以HashMap是线程不安全的。
HashTable 它是线程安全的,它在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,在多线程的环境下,它是安全的,但是无疑是效率低下的
其实HashTable有很多的优化空间,锁住整个table这么粗暴的方法可以变相的柔和点,比如在多线程的环境下,对不同的数据集进行操作时其实根本就不需要去竞争一个锁,因为他们不同hash值,不会因为rehash造成线程不安全,所以互不影响,这就是锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table,这就是这篇文章的主角ConcurrentHashMap JDK1.7版本的核心思想
从ConcurrentHashMap代码中可以看出,它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中
在ConcurrentHashMap中,就是把Map分成了N个Segment,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中
通过把整个Map分为N个Segment(类似HashTable)ConcurrentHashMap可以提供相同的线程安全,但是效率提升N倍,默认提升16倍
HashMap与ConcurrentHashMap的区别
人很聪明,真的很聪明。既然不能全锁(HashTable)又不能不锁(HashMap),所以就搞个部分锁,只锁部分,用到哪部分就锁哪部分。
一个大仓库,里面有若干个隔间,每个隔间都有锁,同时只允许一个人进隔间存取东西。
但是,在存取东西之前,需要有一个全局索引(Hash),告诉你要操作的资源在哪个隔间里,然后当你看到隔间空闲时,就可以进去存取,如果隔间正在占用,那你就得等着。聪明!!
ConcurrentHashMap的遍历是从后向前历遍的.因为如果有另一个线程B在执行clear操作时,会把table中的所有slot都置为null,这个操作是从前向后执行
如果线程A在遍历Map时也是从前向后,则有可能会出现追赶现象。
Map的遍历
Iterator
30 while (it.hasNext()) {
31 Map.Entry
32 System.out.println(“key= “ + entry.getKey() + “ and value= “ + entry.getValue());
33 }
http://www.importnew.com/19685.html
Java中的几个HashMap/ConcurrentHashMap实现分析
JDK8 有全新的实现: