一,JAVA线程是如何实现的?

同步,涉及到多线程操作,那在JAVA中线程是如何实现的呢?

操作系统中讲到,线程的实现(线程模型)主要有三种方式:

①使用内核线程实现

②使用用户线程实现

③使用用户线程加轻量级线程实现

二,JAVA语言定义了哪几种线程状态?

JAVA语言定义了五种线程状态:①新建(New),当你 new 了一个Thread,但是并没有调用它的 start()方法时,就处于这种状态。

②运行(Run),这里包含了两种状态:一种是可运行状态,就是你调用了Thread的start()方法之后,但是该线程还未获得CPU(相当于操作系统中讲的就绪状态);另一种是运行状态,就是该线程被调度器分配了CPU,正在执行。

③等待(Waiting),处于这种状态的线程不会被分配CPU执行时间,它需要等待其他线程唤醒。等待又分成两种:无限等待和超时等待(限期等待)。

无限等待一般是执行等待的方法没有指定超时参数

比如Object类的wait()方法,会使线程进入无限等待状态,它还有一个带有 timeout(超时) 参数 的重载方法 Object.wait(long timeout),使线程超时等待。

调用Thread.sleep() 、 Object.wait()、Thread.join()方法都会使线程进入等待状态。

④阻塞(Blocked),个人感觉阻塞是与锁有关,而等待并不一定与锁有关。

比如,两个线程争夺对象锁(synchronized),未获得锁的那个线程将进入阻塞状态。而线程进入等待状态则有可能是因为③中提到的调用了Thread.sleep()方法,或者是线程读某个Socket端口上的数据,但是此时数据还未到达,则线程进入等待状态。

⑤结束(Terminated),线程的run方法运行完毕,进入结束状态。

三,同步(加锁)为什么会有代价?

《深入理解JVM》中讲到,在目前的JDK版本中,操作系统支持怎样的线程模型,在很大程序上决定了JAVA虚拟机的线程是怎样映射的。JAVA的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统帮忙完成,这就需要从用户态切换到核心态中,因此状态转换需要耗费很多的处理器时间

而在JAVA里面要实现同步,一种是使用对象锁,即synchronized关键字;另一种则是使用 java.util.ReentrantLock。获得锁的线程进入临界区执行、未获得锁的线程会阻塞,然后在某种条件下被唤醒。因此,同步(使用锁)是有代价的。

四,对象锁同步(synchronized同步) 与 ReentrantLock同步的区别

它们都是可重入锁,可用来互斥同步,但主要有三个方面的区别

①使用synchronized进行同步的线程在阻塞等待时,是不可中断的。而使用ReentrantLock进行同步的线程在阻塞等待时可中断。

如果临界区需要执行很长的时间,synchronized就只能一直阻塞等待了,而ReentrantLock 可以在阻塞等待一段时间之后,若还未获得锁,就可以被其他线程中断,从而去干其他事情。一个很好的示例可参考:ReentrantLock锁实现中断线程阻塞

通过调用 lock.lockInterruptibly()方法来实现线程在阻塞等待时 可被其他线程 中断。

②ReentrantLock可以实现公平锁

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。而非公平锁则按照抢占的方式来获得锁,synchronized中的锁是不公平的,而ReentrantLock可以在构造函数中指定创建的锁是否为公平锁。

  1. public ReentrantLock(boolean fair) {
  2. sync = fair ? new FairSync() : new NonfairSync();
  3. }

③使用ReentrantLock 锁可以同时绑定多个条件变量(Condition对象)

所谓 条件对象 就是说:线程好不容易获得了锁 进入临界区,却发现需要在满足某一条件之后,它才能执行。因此,使用一个条件对象来管理那些已经获得了锁但是却不能做有用工作的线程。

比如说:生产者--消费者模型,消费者获得了队列的锁,去队列中取产品,但是结果发现队列为空,它没有产品可消费。换句话说:它需要在队列不为空的条件下,才能消费产品。(没有产品,肯定无法消费产品啊!)

比如,消费者的代码一般是下面这样:进入consume()方法需要获得锁,但是却发现队列为空,故只能调用wait()方法 进入等待状态。(不是阻塞状态)

  1. public synchronized void consume(){
  2. while(queue.isEmpty())
  3. wait();//没有产品可消费,只能放弃锁,并等待生产者线程生产了产品之后,唤醒它
  4. //consume product
  5. notifyAll();
  6. //....
  7. }

那ReentrantLock呢?

  1. Condition emptyCondition = lock.newCondition();
  2. Condition fullCondition = lock.newCondition();
  3. .......
  4. public synchronized void consume(){
  5. try{
  6. lock.lock();//ReentrantLock lock
  7. while(queue.isEmpty())
  8. emptyCondition.await();
  9. //consume product
  10. emptyCondition.singalAll();
  11. //....
  12. }finally{
  13. lock.unlock();
  14. }
  15. }

对于同一把ReentrantLock,它可以 new多个Condition,即同一把锁可以关联多个条件变量。

Condition.awit()方法的JDK源码解释非常值得一读。部分摘录如下:

  1. Causes the current thread to wait until it is signalled or
  2. * {@linkplain Thread#interrupt interrupted}.
  3.  
  4. * <p>The lock associated with this {@code Condition} is atomically
         * released and the current thread becomes disabled for thread scheduling
         * purposes and lies dormant until <em>one</em> of four things happens

调用Condition.await()方法使线程放弃锁,并进入等待状态(不是阻塞状态),直到有下列四种情况发生 才从等待状态退出.....

.....

五,互斥同步 与 非阻塞同步 是什么?

所谓互斥同步,就是多个线程争夺一把锁时,未获得锁的那些线程将会阻塞。因此,互斥同步最主要的是进行线程阻塞和唤醒带来的性能问题,因此 互斥同步称为阻塞同步。从处理问题的方式上看,它是一种悲观的并发策略:它认为如果不采取正确的同步措施,那执行就可能出问题。也就是说:它总是对共享数据先进行加锁,然后再去访问,尽管在访问过程中 也许 并没有 其他线程 访问该共享数据。

而正如前面(三)中 提到,加锁是有代价的,如果对共享数据加了锁,但是在访问共享数据过程中,并没有其他线程来访问该共享数据(即并没有出现竞争),那这次加锁就感觉有点浪费了。(就相当于:花了大量的人力、物力应对某次可能发生的地震,但是最终地震没有发生)

于是,为了进一步的”优化“,就出现了基于冲突检测的乐观并发策略

乐观并发策略就是:先进行操作,如果没有其他线程争用共享数据,那么操作就成功了。如果数据有争用,产生了冲突,那再采用补救措施。

因此,乐观并发策略的很多实现都不需要把线程挂起(因为它是 先执行了 操作再说),因而称为:非阻塞同步。

乐观并发策略需要硬件指令集的支持。因为,它在操作的过程中需要检测是否发生了冲突,需要“操作”和“冲突检测”这两个步骤具备原子性。原子性如何保证?如果用互斥同步保证 那就又变回 悲观并发策略 了,因此只能靠硬件来保证原子性。

比如java.util.concurrent.atomic.AtomicInteger.java 中的自增加1方法getAndIncrement(),就是通过硬件原子指令:CAS指令(Compare and swap)来实现

  1. public final int getAndIncrement() {
  2. for (;;) {
  3. int current = get();
  4. int next = current + 1;
  5. if (compareAndSet(current, next))
  6. return current;
  7. }
  8. }
  9.  
  10. public final boolean compareAndSet(int expect, int update) {
  11. return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
  12. }

六,关于锁优化的一些知识

1)自旋锁 的 实现思想

在多核CPU下,一个线程获得锁进入临界区占用CPU执行时,我们有理由相信这个临界区代码很快就会执行完成,即: 共享数据的锁定状态只会持续很短的一段时间,而当另外一个线程刚好在这段时间去抢占锁,挂起这个抢占锁的线程有点 不值得。(毕竟占用锁的线程很快就会把锁释放了呀)

于是,就让请求抢占锁的那个线程“稍微等待一下”,但不放弃处理器的执行时间(一旦放弃,就意味着需要挂起和恢复线程了),看持有锁的线程是否很快就会释放锁。为了让线程等待一下,我们只需让线程执行一个忙循环(自旋),这就是:自旋锁。

2)自旋锁的“改进”---自适应自旋

自旋等待避免了线程切换的开销,但是它是要占用处理器时间的。因此,如果锁被占用的时间很短,那自旋等待的效果就会非常好;如何锁被占用的时间很长,执行忙循环(自旋)的线程就白白消耗处理器资源,造成性能上的浪费。默认情况下,自旋的次数是10次,但可以通过JVM参数进行修改。

为了应对锁被占用很长时间 而导致的长时间无效的自旋,自旋的时间必须有一定的限度。

那如何确定一个合适的限定呢?这就是自适应自旋的目标了。

自适应自旋对自旋的次数没有固定,比如说:在同一个锁对象,自旋等待刚刚成功获得过锁,那么这次也很有可能成功,进而允许自旋等待持续相对更长的时间。

再比如说,对于某个锁,自旋很少成功获得过锁,那在以后获取这个锁时可省略自旋过程,以避免处理器资源浪费。

3)轻量级锁和偏向锁

个人感觉轻量级锁和偏向锁 的功能 与 缓存 的思想有点像。直接同步互斥加锁的代价是很大的(重量级锁),那我们可以先来一个轻量级锁或偏向锁。

如果在加了 轻量级锁或偏向锁的过程中 没有发生其他线程来争抢锁(类似于缓存命中!)这意味着整个过程“几乎”不需要同步。

如果有其他线程争抢锁,那轻量级锁将不再有效(偏向锁的偏向模式失效),轻量级锁要膨胀为“重量级锁”,后面等待锁的线程要进入阻塞状态。这就是类似于缓存未命中!

关于轻量级锁和偏向锁的具体解释:可参考《深入理解JVM》

上面关于锁的优化是JVM的一些锁优化策略,在应用层进行锁优化方式有如下:

①尽量减少锁的持有时间。只对必要的需要同步的代码进行同步。

  1. synchronized{this}
  2. {
  3. methodA();// 把不需要同步的方法放到 sync 外面去
  4. mutex();
  5. //other method...// 把不需要同步的方法放到 sync 外面去
  6. }

②减少锁的粒度

ConcurrentHashMap就很好的应用了这种思想。它将整个HashMap分成了若干个段(Segment),每个段都有自己的锁,每个段负责管理HashMap中的一部分HashEntry,段与段之间的HashEntry互不干扰,多线程可以并行地操作不同的Segment管理下的HashEntry。

这样锁的粒度就减少了。如果整个HashMap只有一把锁管理,锁的粒度就很大。操作HashMap不同的区域都需要互斥同步。而ConcurrentHashMap将HashMap分解成段,每个段有一把锁,锁的粒度就少了。但是与此同时,锁的数量增多了。当需要访问ConcurrentHashMap的全局属性时(比如ConcurrentHashMap的size()方法),需要 获得 所有的段的锁。

  1. try {
  2. for (;;) {
  3. if (retries++ == RETRIES_BEFORE_LOCK) {
  4. for (int j = 0; j < segments.length; ++j)
  5. ensureSegment(j).lock(); // force creation

以上是size()方法的部分代码,size()的具体实现肯定也有相应的优化。

③锁分离

锁分离与锁分段有点相似,锁分离就是对不同的操作使用不同的锁。比如,java.util.concurrent.LinkedBlockingQueue 是一个线程安全的阻塞队列,take()方法从队列中取元素,put()方法向队列中添加元素。

这个队列是用链式存储结构实现,它的结点类如下:

  1. /**
  2. * Linked list node class
  3. */
  4. static class Node<E> {
  5. E item;
  6.  
  7. /**
  8. * One of:
  9. * - the real successor Node
  10. * - this Node, meaning the successor is head.next
  11. * - null, meaning there is no successor (this is the last node)
  12. */
  13. Node<E> next;
  14.  
  15. Node(E x) { item = x; }
  16. }

它的 put 操作 和 take 操作分别作用于队列的尾部和头部,并没有相互冲突。如果 take 和 put 都共享同一把锁,那么从队列中取走元素的同时,就不能向队列中添加元素,尽管它们互不“干扰”。

因此,为了提高并发效率,就使用了两把锁:一把 put 锁,一把 take 锁。这就是锁分离机制。

  1. /** Lock held by take, poll, etc */
  2. private final ReentrantLock takeLock = new ReentrantLock();
  3.  
  4. /** Wait queue for waiting takes */
  5. private final Condition notEmpty = takeLock.newCondition();
  6.  
  7. /** Lock held by put, offer, etc */
  8. private final ReentrantLock putLock = new ReentrantLock();
  9.  
  10. /** Wait queue for waiting puts */
  11. private final Condition notFull = putLock.newCondition();

take锁对应着 notEmtpy条件变量,putLock对应着一个 notFull条件变量。

在大部分情况下,多线程可以同时并行地向阻塞队列中添加元素和取出元素。比如线程A向队列添加元素的时候,并不阻塞线程B从队列中取出元素。

通过使用 take 锁 和 put 锁,将LinkedBlockingQueue的读写分离。优化了并发效率。

④锁粗化

这个与①中的尽量减少 锁的时间 思想相反。但它们适用的情况是不同的。

比如,当程序需要在 for 循环内部加锁时,每执行一次 for 循环中的操作就需要加锁一次,加锁之后执行临界区代码后又释放锁,这样加锁、释放锁的频率非常大,效率反而低了。

  1. public void syncMethod(){
  2. for(int i = 0; i < COUTN; i++)
  3. {
  4. synchronized(this){
  5. mutex();
  6. }
  7. //do something, do not need lock
  8. synchronized(this){
  9. mutex();
  10. }
  11. //do another thing which needs no lock
  12. }
  13. }

这样,直接用synchronized修饰方法反而要更好一点。

七,参考资料

《深入理解JVM》周志明

http://thrillerzw.iteye.com/blog/2055486

原文:http://www.cnblogs.com/hapjin/p/5765573.html

JAVA中关于并发的一些理解的更多相关文章

  1. java中容器的学习与理解

    以前一直对于java中容器的概念不理解,虽然学习过,但始终没有认真理解过,这几天老师提出了这样一个问题,你怎么理解java中的容器.瞬间就蒙了.于是各种搜资料学习了一下,下面是我学习后整理出来的的一些 ...

  2. Java 中的并发工具类

    Java 中的并发工具类 CountDownLatch public class JoinCountDownLatchTest { public static void main(String[] a ...

  3. java编程思想-java中的并发(二)

    二.共享受限资源 有了并发就可以同时做多件事情了.但是,两个或多个线程彼此互相干涉的问题也就出现了.如果不防范这种冲突,就可能发生两个线程同时试图访问同一个银行账户,或向同一个打印机打印,改变同一个值 ...

  4. Java中的并发库学习总结

    我们都知道,在JDK1.5之前,Java中要进行业务并发时,通常需要有程序员独立完成代码实现,当然也有一些开源的框架提供了这些功能,但是这些依然没有JDK自带的功能使用起来方便.而当针对高质量Java ...

  5. 谈谈java中的并发(一)

    一.并发的定义 并发:对于这个概念一直就是没怎么搞懂,就是感觉特别的生疏,(自己从从字面上理解就是多个东西,一起出发),所以就上网上查了一些资料: 同时拥有两个或多个线程,如果程序在单核处理器上运行, ...

  6. 关于Java中形参与实参的理解

    今天阅读了一个写的非常棒的博文,通过此博文再次复习了Java中参数传递的知识(即值传递与引用传递的区别).参考网站http://www.cnblogs.com/binyue/p/3862276.htm ...

  7. 浅谈对java中传参问题的理解

    之前用的c/c++比较多,在c/c++中对于传参类型,无外乎就是传值.传引用.传指针这几种.但在java中,由于没有指针类型,其传参的方式也发生了相应的变化.在网上找了找,按我之前的理解,java中传 ...

  8. Java中的不可变类理解

    一.Java中的不可变类 不可变类(Immutable Objects):当类的实例一经创建,其内容便不可改变,即无法修改其成员变量. 可变类(Mutable Objects):类的实例创建后,可以修 ...

  9. java中传值方式的个人理解

    前言 这几天在整理java基础知识方面的内容,对于值传递还不是特别理解,于是查阅了一些资料和网上相关博客,自己进行了归纳总结,最后将其整理成了一篇博客. 值传递 值传递是指在调用函数时将实际参数复制一 ...

随机推荐

  1. DevExpress更新至13.1.7

    DevExpress下的.NET界面组件 DXperience Universal Suite 最新发布13.1.7版,多个属性的定义方式发生变化,另外还有大量的bug修复.使用DevExpress朋 ...

  2. Atitit.url 汉字中文路径  404 resin4 resin  解决  v2 q329

    Atitit.url 汉字中文路径  404 resin4 resin  解决  v2 q329 1. Pluginx机制1 2. Code1 3. 参考4 1. 原理 过滤器  ,,拦截jpg  w ...

  3. Oracle function real_st_astext,解决ArcSDE中st_astext函数返回字符串结构异常问题

    项目过程中发现在Oracle中调用ArcSDE的st_astext函数返回ST_Geometry类型字段的WKT文本有时空间类型前缀没有返回,例如一个点的经度为113.4,纬度为30.6,调用st_a ...

  4. OC单例快速实现

    首先新建一个头文件,定义如下宏: 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 ...

  5. iOS 单例模式 浅叙

    单例模式作用 可以保证在程序运行过程中,一个类只有一个实例,而且该实例易于供外界使用 从而方便地控制了实例个数,并节约系统资源 单例模式使用场合 在整个引用程序中,共享一份资源(这份资源只需要创建初始 ...

  6. 【网络编程】TCP/IP、UDP、网络概…

    计算机刚刚发明出来的时候,两台计算机之间是无法通信的,为了使计算机之间能够进行数据的交流,制定了OSI(Open SystemInterconnection)开放系统互联模型,而TCP/IP(我们所使 ...

  7. 【读书笔记】iOS-程序进入到后台

    当一个iOS应用被送到后台,它的主线程会被暂停.你用NSThread的detachNewThreadSelector:toTar get:withObject:类方法创建的线程也被挂起了.如果你想在后 ...

  8. 【Android】不使用WebView来执行Javascript脚本(Rhino)

    前言 动态执行脚本能有效的降低重要功能硬编码带来的问题,尤其是依赖于第三方的应用,可以通过动态脚本+在线参数(例如友盟在线参数)再不更新应用的情况下升级功能. 声明 欢迎转载,但请保留文章原始出处:) ...

  9. 基于微软平台IIS/ASP.NET开发的大型网站有哪些呢?

    首先说明一下,本文绝不是要说Microsoft平台多么好,多么牛.只是要提醒一些LAMP/JAVA平台下的同志们,微软平台不至于像你们说的,和想象的那么不堪!只是你们自己不知道而已.同时,也希望广大M ...

  10. 从刚刚「简书」平台的短暂异常,谈Nginx An error occurred报错~

    09.26简书平台的短暂异常 An error occurred. Sorry, the page you are looking for is currently unavailable. Plea ...