Java并发编程的那些事-简版
synchronized关键字
synchronized关键字的作用是:解决在多线程的环境下,访问资源的同步性问题,也就是说被该关键字修饰的方法或代码块在任意时刻内只能被一个线程访问。
三种主要的用法:
修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
修饰静态方法 :给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁
修饰代码块 :指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁
构造方法不能使用 synchronized 关键字修饰
原理 : synchronized 的本质原理是获取对象监视器monitor的持有权。但是在修饰同步语句块时的具体操作是利用monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。而在修饰方法时是利用了ACC_SYNCHRONIZED标识进行标记该方法是同步方法。
volatile关键字
作用:防重排序、保证数据可见性、数据单次读写的原子性(也就是说不能保证完全的原子性)
可见性实现原理:利用特定类型的内存屏障。具体如果对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 lock 前缀的指令,是将当前处理器缓存行的数据写回到系统内存。写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效,这里是因为为了保证各个处理器的缓存是一致的,实现了缓存一致性协议(MESI)
缓存一致性协议:
缓存一致性协议有多种,但是日常处理的大多数计算机设备都属于 " 嗅探(snooping)" 协议。 所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线。 缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个 CPU 缓存可以读写内存)。 CPU 缓存不仅仅在做内存传输的时候才与总线打交道,而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。 当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。 只要某个处理器写内存,其它处理器马上知道这块内存在它们的缓存段中已经失效
有序性原理(防重排序):利用特定类型的内存屏障。在每个 volatile 写操作的前面插入一个 StoreStore 屏障。 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LQ8lDSxz-1641006131997)(https://secure1.wostatic.cn/static/kKKTyY42CYZ28VbiSm4TpN/~3E31D{5$IL16K1_OSNELJ4.png)]
volatile 的 happens-before 关系:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
ThreadLocal
作用:ThreadLocal变量,在不同的线程中都拥有属于线程自己的本地副本变量,从而实现广义上的线程安全(也就是说它不保证线程之间还存在共享关系的狭义上的安全性)
原理:
这里我们需要先了解Thread类中的两个变量 以及这两个变量的类型ThreadLocalMap
public class Thread implements Runnable {
//......
//属于本线程的ThreadLocalMap变量,只有本线程调用TreadLocal的set/get方法时才会被创建并初始化
ThreadLocal.ThreadLocalMap threadLocals = null;
//属于本线程的InheritableThreadLocal变量,由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}
TheadLocalMap 是ThreadLocal类中的静态内部类,是专门为ThreadLocal类实现的定制化的hashmap。每个Thread
中都具备一个ThreadLocalMap
,而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。然后我们再来看一段代码这是ThreadLocal类中的set方法。
public void set(T value) {
Thread t = Thread.currentThread(); //获得当前线程对象
ThreadLocalMap map = getMap(t); //获得当前对象中的ThreadLocalMap对象
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//为了方便理解将getMap一并给出
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
由上面的代码,我们可以知道,其实我们调用的ThreadLocal中的set()方法只是封装了ThreadLocalMap中的set()方法,所以我们不难得出 最终的变量是存放在了ThreadLocalMap当中。而ThreadLocal是属于当前线程的,在同一线程中不同的ThreadLocal对象共享同一个ThreadLocalMap。
TreadLocal内存泄露问题及解决方法:hreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
Java线程池
使用Java线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
线程池的创建
通过构造方法实现
ThreadPoolExecutor类中提供了四种构造方法。其中以最长参数列表的那个构造函数为主,其余构造函数均是在其基础上默认给了一些参数值,从而减少了用户提供的参数数量。
-
corePoolSize
: 核心线程数定义了最小可以同时运行的线程数量。 -
maximumPoolSize
: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -
workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor
其他常见参数:
-
keepAliveTime
:当线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁; -
unit
:keepAliveTime
参数的时间单位。 -
threadFactory
:executor 创建新线程的时候会用到。 -
handler
:饱和策略
ThreadPoolExecutor
** 饱和策略定义:**
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor
定义一些策略:
-
ThreadPoolExecutor.AbortPolicy
****: 抛出RejectedExecutionException
来拒绝新任务的处理。 -
ThreadPoolExecutor.CallerRunsPolicy
****: 调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 -
ThreadPoolExecutor.DiscardPolicy
****: 不处理新任务,直接丢弃掉。 -
ThreadPoolExecutor.DiscardOldestPolicy
****: 此策略将丢弃最早的未处理的任务请求。
使用Executor框架的工具类Executors来实现
- FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
上一篇: 字符串乘法问题总结
下一篇: 关于JVM直接内存触发Full GC