1.java并发基本概念
进程与线程
- 进程:
- 系统进行资源分配和调度的资本单位
- 进程在执行过程中用于独立的内存单元
- 一个进程崩溃,在保护模式下不会对其他进程产生影响
- 进程的主内存有:线程A、B的内存(通过JVM控制),共享变量
- 线程:
- CPU调度和分派的基本单位
- 多线程间共享进程的全部资源,但是拥有各自的栈和寄存器
- 一个线程奔溃基本上对应进程就挂了
- 线程A的内存有:本地内存变量A,自己栈上的内存变量,主存变量副本,线程B的内存有:本地内存变量B,自己栈上的内存变量,主存变量副本, 多进程的程序要比多进程的程序健壮,但是资源消耗大,同时切换效率低
- 进程创建方法:
- 使用Runtime的exec()方法(Runtime要学习下)
- 使用ProcessBuilder的start()方法
示例
线程中断
- 中断是一种协作机制,一个线程不能破事另一个线程停止
- 响应中断的两种方式: (1)传递InterrupedExecption (2)恢复中断,Thread.currentThread().interrupt()
- thread.interrupt():仅仅设置标志位true,调用可以抛出Interruption异常的方法才静行待决中断执行
- thread.isInterrupted():判断中断状态,返回boolean
- Thread.interrupted():静态方法,判断中断状态,并隐式充值未false
线程阻塞
- Thread.sleep():阻塞一定毫秒之后,或者被中断
- wait()语句:一直阻塞,直到接到通知(Notify()),或者被中断,或者超过指定毫秒数
- I/O阻塞:无限阻塞,直到I/O完成,不可被中断
- 锁阻塞:不可被中断
两个关键字
- volatile:轻量级同步,某些情况下可以用来做同步
- 该修饰词修饰的变量,多线程条件下,不保存变量私有拷贝,直接内存更新
- 多任务下,共享变量都应该加修饰词(加了volatile就是共享变量了,线程A、B上就没有该变量的主内存的副本了)
- 内存可见性(线程A对共享变量写入了,线程B再去读,B是知道A写入了,读取的是写入后的值),不保证原子性(线程A修改volatile变量时,机器执行有好几步,另外一个线程读取可能发生在这些步骤之间,从而不能保证原子性)
- synchronized
- 方法级别,对象级别,类级别
- 既可以内存可见性,又可以保证原子性,性能要比volatile差一些
- 使用volatile场景:
- 对变量的写入操作不依赖变量的当前值(i++就依赖),或者你能确保只有单个线程更新变量的值,也是ok的
- 该变量没有包含在具有其他变量的不变式中(eg:int j= i*2,j就包含在变量i的不变式中,这种情况就不行)
锁
- 死锁
- 持有并循环等待
- 可重入内置锁
- 同一线程对同一对象的锁是可以重入的,第二次获取锁时如果已经获得锁了,可以直接认为获得
- 互斥锁
- 会引起调用者阻塞
线程安全集合
- Vector和Hashtable
- 是多线程安全的,十个线程同时去获取,只有一个线程获得
- 实现代价高,多hashtable(比如100万数据)全部数据加锁,无竞争的同步会导致极大的性能代价
- Collection.synchronizedMap
- 通过提供一个不同步的基类和一个同步的包装器,这个Collection.synchronizedMap解决了线程安全性问题
- 允许需要同步的用户可以拥有同步,而不需要同步的用户可以不必同步,并没有解决性能问题
- ConcurrentHashMap和 CopyOnWriteArrayList
- 读几乎不加锁,写使用的是细粒度的分段锁
- 提供put-if-absen,remove-if-equal,replace-if-equal操作(这些操作能让插入、移除、替换操作变成原子性操作,从而保证线程安全,而hashtable没有这些操作,需要自己实现)
- 使用size()、isEmpty()等方法时(这些方法是对整体Map的操作),性能比较低
- CopyOnWriteArrayList适用于读操作频率远远大于写操作的场景
线程间协作
- wait()、wait(long)、wait(long,int)
- 将当前线程进入休眠状态(不是阻塞哦),知道接到通知或被中断为止
- notify()
- 通知那些可能等待该对象的对象锁的其他线程,唤醒的线程需要去竞争CPU。这个方法需要避免通知遗漏
- notifyAll()
- 唤醒所有原来在该对象上wait的线程
- join()
- 如果一个线程A中调用另外一个线程B的join方法,线程A将会在B执行完之后执行
- yield()
- 可以直接用Thread类调用,肯定是静态方法,yield让出CPU执行权交给同等级的线程,如果没有同等级的线程,那么该线程继续执行(就是让CPU重新分配一次)
- 单例工厂中的,双锁检测机制(DCL问题:http://blog.csdn.net/u014108122/article/details/38352005)
Lock锁和条件变量
- Lock接口实现类
- ReentranLock可重入锁、ReentranLockReadWriteLock.ReadLock读锁、ReentranLockReadWriteLock.WriteLock写锁
- 为保证锁最终一定会被释放,要把互斥去放在try语句块内,并在finally中释放锁,尤其当有return语句时,return语句必须放在try中
- Lock与synchronized区别
- jdk1.6之后,两者性能已经差不多了
- synchronized 使用阻塞同步,在CPU转换线程阻塞是会引起线程上下文切换
- locsk使用并阻塞同步,基于冲突检测的乐观并发策略,很多情况下都不需要把线程拉起
- 使用场景:可定时的,可轮询的或可中断的锁获取操作(锁,可中断)、公平队列,或者非块结构的锁,否则请使用synchronized
线程变量
- ThreadLocal
- 线程局部变量,其他线程访问不到,常用来作为线程传参
- inheritableTHreadLocal
- 创建一个线程时如果保存了所有inheritableTHreadLocal对象的值,那些这些值也将自动传递给子线程
- 使用场景:
- To keep state with a thread(user-id,logging-id),x6的日志打印应用到了这个特性
- to cache Objects which you need frequently(先在线程内存存储部分数据(cache),后续处理任务时,不需要重新加载)
2.线程池及ExecutorService相关类
Execcutor框架和线程池
Executor框架:
-
包括线程池、Executor,Executors,ExecutorService,CompletionService,Future,Callable
- Executor:
-
接口,定义execute(Runnable cmd)方法
- ExecutorService:
-
继承Executor接口
- ExeCutors:
- 提供一些类工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口
-
eg:newCachedThreadPool():不限定大小的线程池,newFixedThreadPool(int):限定大小的线程池,newScheduledThreadPool(int),定时调度的线程池,spring里用得比较多,newSingleThreadExecutor():创建一个单线程的线程池,用于部分任务需要按照FIFO执行的场景 https://www.cnblogs.com/ljp-sun/p/6580147.html 此外,ThreadPoolExecutor可以创建自定义线程池,new ThreadPoolExecutor(in corePoolSize,int maxPoolSize,long KeepAliveTime(线程空闲了回收时间),TimeUnit unit,BlockingQueue
workQueue,handler(拒绝任务的策略,自己实现过)) -
corePoolSize: 核心线程数,会一直存活,即使没有任务,线程池也会维护线程的最少数量(推荐corePoolSize = CPU核数+1)
-
maximumPoolSize: 线程池维护线程的最大数量
-
keepAliveTime: 线程池维护线程所允许的空闲时间,当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。
-
unit: 线程池维护线程所允许的空闲时间的单位、可选参数值为:TimeUnit中的几个静态属性:NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。
- workQueue: 线程池所使用的缓冲队列,常用的是:java.util.concurrent.ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue
1 2 3
1. 直接传递:这也是缺省的实现使用SynchronousQueue,直接将队列中的任务转交给线程。如果将任务提交给队列时没有足够的线程处理这个任务,新的线程会被创建。一般来说需要将maximumPoolSize设置为最大以避免出现拒绝新提交来的任务的情况出现。当然了如果任务提交的速度大过了处理任务的速度会引起线程池中线程无限增长的问题(PM项目中目前使,是可以的,因为每个指标项对应一个任务,都会添加一个对应的线程到线程池中,核心线程池大小为2)。 2. 无限队列:使用不限制容量的队列LinkedBlockingQueue。当所有corePoolSize的线程都在忙碌时新提交进来的任务在队列中等待。也就是说,即使线程池中的线程数小于maximumPoolSize,也不会有新的线程被创建(maximumPoolSize参数失效)。这种策略适用于提交来的各个任务相互独立的场景。例如,一个网页服务来说,使用这种策略能平滑瞬间突发的访问请求。 3. 有限队列:使用有限队列防止将maximumPoolSize设置为最大时,资源耗尽的问题。调整队列大小和maximumPoolSize之间关系比较就变得重要了。使用大容量队列和较小的线程数可以降低CPU和资源的使用但会导致效率低下。小容量的队列需要相对较大的maximumPoolSize配合,增加了CPU调度线程的负担。
- 任务拒绝
- handler: 线程池中的数量大于maximumPoolSize,对拒绝任务的处理策略,默认值ThreadPoolExecutor.AbortPolicy()。
1 2
在当队列达到极限导致任务执行阻塞时执行的处理策略。handler有四个选择:ThreadPoolExecutor.AbortPolicy(抛出java.util.concurrent.RejectedExecutionException异常)。ThreadPoolExecutor.CallerRunsPolicy(重试添加当前的任务,他会自动重复调用execute方法)。ThreadPoolExecutor.DiscardOldestPolicy(抛弃旧的任务)。 ThreadPoolExecutor.DiscardPolicy(抛弃当前的任务)。
- Executor执行给Callable任务
- Callable的call()方法只能通过ExecutorService的submit(Callable
task)方法执行,并返回一个 Future ,是表示任务等待完成的Future - 如果Future返回尚未完成,则get()方法会阻塞等待,直到Future完成返回,可以用isDone()方法判断Future是否完成了返回(x6的parallel并发执行、waterfall串行执行基础)