现在的计算机,cpu在计算的时候,并不总是从内存读取数据,它的数据读取顺序优先级是:寄存器-高速缓存-内存。线程耗费的是CPU,线程计算的时候,原始的数据来自内存,在计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。当多个线程同时读写某个内存数据时,就会产生多线程并发问题,涉及到三个特性:原子性,有序性,可见性。 支持多线程的平台都会面临 这种问题,运行在多线程平台上支持多线程的语言应该提供解决该问题的方案。
线程安全
线程安全以及如何保证
线程的working memory是cpu的寄存器和高速缓存的抽象描述:
现在的计算机,cpu在计算的时候,并不总是从内存读取数据,它的数据读取顺序优先级是:寄存器-高速缓存-内存。线程耗费的是CPU,线程计算的时候,原始的数据来自内存,在计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。当多个线程同时读写某个内存数据时,就会产生多线程并发问题,涉及到三个特 性:原子性,有序性,可见性。 支持多线程的平台都会面临 这种问题,运行在多线程平台上支持多线程的语言应该提供解决该问题的方案。
JVM是一个虚拟的计算机,它也会面临多线程并发问题,java程序运行在java虚拟机平台上,java程序员不可能直接去控制底层线程对寄存器高速缓存内存之间的同步,那么java从语法层面,应该给开发人员提供一种解决方案,这个方案就是诸如 synchronized, volatile,锁机制(如同步块,就绪队 列,阻塞队列)等等。这些方案只是语法层面的,但我们要从本质上去理解它;
每个线程都有自己的执行空间(即工作内存),线程执行的时候用到某变量,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作:读取,修改,赋值等,这些均在工作内存完成,操作完成后再将变量写回主内存;
各个线程都从主内存中获取数据,线程之间数据是不可见的;打个比方:主内存变量A原始值为1,线程1从主内存取出变量A,修改A的值为2,在线程1未将变量A写回主内存的时候,线程2拿到变量A的值仍然为1;
这便引出“可见性”的概念:当一个共享变量在多个线程的工作内存中都有副本时,如果一个线程修改了这个共享变量的副本值,那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见性问题。
普通变量情况:如线程A修改了一个普通变量的值,然后向主内存进行写回,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量的值才会对线程B可见。
编写线程安全的代码,本质上就是管理对状态(state)的访问,而且通常都是共享的、可变的状态。这里的状态就是对象的变量(静态变量和实例变量)
线程安全的前提是该变量是否被多个线程访问, 保证对象的线程安全性需要使用同步来协调对其可变状态的访问;若是做不到这一点,就会导致脏数据和其他不可预期的后果。无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。Java中首要的同步机制是synchronized关键字,它提供了独占锁。除此之外,术语“同步”还包括volatile变量,显示锁和原子变量的使用。
在没有正确同步的情况下,如果多个线程访问了同一个变量,你的程序就存在隐患。有3种方法修复它:
- 不要跨线程共享变量;
- 使状态变量为不可变的;或者 *
- 在任何访问状态变量的时候使用同步。
通俗的讲一个对象的状态就是它的数据,存储在状态变量中,比如实例域或者静态域;无论何时,只要多于一个的线程访问给定的状态变量。而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问.
synchronized
其实是操作系统管程的实现,每个JAVA对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁。
当一个线程试图访问带有synchronized(this)标记的代码块时,必须获得 this关键字引用的对象的锁,在以下的两种情况下,本线程有着不同的命运。
- 假如这个锁已经被其它的线程占用,JVM就会把这个线程放到本对象的锁池中。本线程进入阻塞状态。锁池中可能有很多的线程,等到其他的线程释放了锁,JVM就会从锁池中随机取出一个线程,使这个线程拥有锁,并且转到就绪状态。
- 假如这个锁没有被其他线程占用,本线程会获得这把锁,开始执行同步代码块。
(一般情况下在执行同步代码块时不会释放同步锁,但也有特殊情况会释放对象锁
如在执行同步代码块时,遇到异常而导致线程终止,锁会被释放;在执行代码块时,执行了锁所属对象的wait()方法,这个线程会释放对象锁,进入对象的等待池中)
Synchronized关键字保证了数据读写一致和可见性等问题,但是他是一种阻塞的线程控制方法,在关键字使用期间,所有其他线程不能使用此变量,这就引出了一种叫做非阻塞同步的控制线程安全的需求
关于volatile关键字
volatile关键字保证变量对其它线程的可见性,对volatile变量所有的写操作都能立刻反映到其它线程之中,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前可以立即从内存刷新,即一个线程修改了某个变量的值,其它线程读取的话肯定能看到新的值;
有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情
- 将当前处理器缓存行的数据会写回到系统内存。
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
关键点:其实相当于线程 像缓存行写数据的时候,会锁住缓存行,是其他线程不能读,写完后失效缓存行,其他线程便可以从内存读到共享变量的最新值了;
处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存;如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
Java并发编程实践写道:“一个理解volatile变量好的方法:想想它们的行为与SynchrosizedInteger类相似,只不过用get和set方法取代了对volatile变量的读写操作。然而访问volatile变量的操作不会加锁,也就不会引起线程的阻塞,这使volatile相对于synchronized而言,只是轻量级的同步机制”
线程池使用
一般用法
线程池将任务的提交和执行解耦开来,另外由于线程的创建和销毁均由系统调用实现,系统调用需要在用户态和内核态之间来回切换,具有一定的开销,所以将线程放入线程池,以备继续使用,可以提高系统效率
ThreadPoolExecutor创建线程池时需要指定基本大小,最大大小、存活时间等,这些因素共同负责线程的创建与销毁:
1 2 3 4 5 6 7 |
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue |
基本大小就是线程池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出了这个数量的线程。线程池的最大大小标识可同时活动的线程数量的上限。如果某个线程的空闲时间超过了存活时间,那么被标记为可回收的,并且当前线程池的大小超过了基本大小时,这个线程将被终止。
通过调节线程池的基本大小和存活时间,可以帮助线程池回收空闲线程战友的资源,从而使得这些资源可以执行其他工作。
执行时间较长的任务不仅会造成线程池的阻塞,甚至还会增加执行较短任务的服务时间。如果线程中的线程的数量远小于稳定状态下执行较长任务的数量,那么最后可能所有的线程都会运行这些执行较长的任务,从而影响整体性能。
需要注意的:
- 只有当工作队列满了才会创建核心线程以外的线程,如果核心线程设置为1,然后工作队列很大的话,由于不满足新建线程的条件,那么在队列里面排队的时间可能会很长,只有一个线程处理任务,导致响应时间变慢。
- 线程池最大线程个数一定要大于核心线程数,避免犯低级错误
- 工作队列通常是一个阻塞队列,如果希望获取最大的相应延时,避免任务排队,以采用同步移交的方式,即将工作队列设置成SynchronousQueue,它并不是一个真正的队列,只起到一个移交的作用,当提交任务时,必须有另一个线程在等待接受这个元素,如果没有现成正在等待,并且线程池当前大小小于最大值,就创建一个线程来响应这个提交,否则根据饱和策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给处理线程,而不是首先放在队列里。
- 某些场景下,如果由于并发太高等原因,可能超出了线程池的处理能力,就会触发饱和策略逻辑,可以通过setRejectedExecutorHandler来设置饱和逻辑,默认情况采用AbortPolicy抛出RejectExecutionException。
与ThreadLocal结合
在某些情下,如HBase客户端HTable是费线程安全的,但是又想支持多线程访问。
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 48 49 |
//线程本地存储 private ThreadLocal new ThreadLocal protected Map |
一些总结
- 无论何种方式,启动一个线程,就要给它一个名字!这对排错诊断系统监控有帮助。否则诊断问题时,无法直观知道某个线程的用途。
- 使用ThreadLocal,一般都是声明在静态变量中,如果不断的创建ThreadLocal而且没有调用其remove方法,将会导致内存泄露。如果是static的ThreadLocal,一般不需要调用remove。
- 为了方便并发执行任务,出现了一种专门用来执行任务的实现,也就是Executor。将任务的提交和执行解耦开来,任务提交者不需要再创建管理线程,使用更方便,也减少了开销。
- Synchronized是Lock的一种简化实现,一个Lock可以对应多个Condition,而synchronized把Lock和Condition合并了,一个synchronized Lock只对应一个Condition,可以说Synchronized是Lock的简化版本。
- 一定要使用锁的时候,注意获得锁的顺序,相反顺序获得锁,就容易产生死锁。