多线程学习(2)

线程的生命周期:

 

一共有五种状态,分别是新建,就绪,运行,阻塞和死亡。

新建状态:当程序new了一个thread之后,那么这个线程就属于新建状态,此时JVM会给它分配内存,并初始化。

就绪状态:当线程调用了start方法之后,该线程就处于就绪状态。java虚拟机会给他创建方法调用栈和程序计数器,等待调度运行。

运行状态:如果处于就绪状态的线程获得了cpu的时间片,那么就进入了运行状态,开始执行run方法。

阻塞状态:阻塞状态是指线程因为某些原因放弃了cpu的使用权,暂时停止了运行。知道重新进入就绪状态,才能获得cpu时间片,以便进入运行状态。

阻塞的情况分为三种情况:

1. 等待阻塞(wait):执行wait方法,JVM会把线程放入等待队列。

2. 同步阻塞(使用锁):运行的线程如果在获取对象的时候需要获取同步锁,而恰好当时锁被别的线程占据了,那么JVM会把线程放进锁池中。

3. 其他阻塞(sleep/join):

运行的线程执行sleep或者是join方法的时候,或者发出了io的请求,JVM会把线程设置为阻塞状态,等到sleep时间到达,join等待线程终止,或者IO完毕了之后,线程可以重新进入就绪状态。

线程死亡状态:三种情况可以结束线程:1. 正常结束,当run方法执行完成之后,线程就结束了 2. 异常结束,抛出异常,结束线程。 3. 调用stop结束线程,直接调用stop方法可以直接结束线程,但是该方法容易造成死锁,不推荐使用。

 

Sleep方法和 Wait的区别?

1. Sleep是Thread类中的方法,而Wait是object类中的方法。

2. sleep方法会让线程暂停设定的时间,暂止让出cpu时间片,但是他依旧监控cpu的状态,当指定的时间到达之后又会恢复运行状态。

3. sleep方法,不会释放锁。而wait方法会释放锁。

4. 调用wait方法的时候,回释放锁,然后线程进入等待池中,进入阻塞状态,只有针对这个对象调用notify()方法才能让该线程进入锁池等待获取锁进入运行状态。

Start方法和 Run的区别?

start 方法会启动一个线程,这是真正的实现了多线程的运行。这时候程序不需要等待run方法体里的代码执行结束就可以执行下面的代码。 通过start来启动一个线程,线程是属于就绪状态的,当线程得到cpu时间片之后才会执行run方法里的代码进入运行状态。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

后台线程:

也称为守护线程,服务线程,它是为用户线程提供服务的线程,没有用户服务的时候会自动离开。 优先级较低。我们可以通过setDaemon(true) 来设置守护线程。

GC线程就是一个典型的守护线程,当我们的程序中不运行任何的线程,那么就没有垃圾回收。所以当GC线程是JVM上唯一的线程的时候,那么它会就自动离开。他始终在低级别的状态下,监控系统可以回收的垃圾。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了,如果还有一个或以上的非守护线程则 JVM 不会退出。

JAVA 锁:

乐观锁:

乐观锁是一种思想,不是一种锁。 我们乐观的认为在读多写少的情况下,遇到并发写入的可能性比较低,所以每次去写数据的时候并不会加锁,但是在更新数据的时候需要去判断一下别人在此期间有没有去更新这个数据。一般的方法是在写数据的时候,先读取出当前的版本号,然后在更新的时候对比一下当前的版本号和之前的版本号是否一致,一致就更新,不一致就重复读-比较-写的操作。

java中的乐观锁是通过CAS来实现的。CAS是一种原子的操作,比较当前的值和传入的值是否一样,一样就更新成功,否则就失败。

悲观锁:

顾名思义就是悲观的认为写的情况多于读,遇到并发写的可能性非常高,每次去取数据的时候,数据都会被别的线程修改,所以在每次读写数据的时候都会加锁。这样的话,别的线程想要访问数据就会被block直到拿到锁为止。java中有两种悲观锁:synchronized 和 retreenlock。

自旋锁:

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁
的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),
等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程
也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁
的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
synchronized锁:
独占式的悲观锁,可以锁住任意一个非null的对象。
1. 作用于方法上的时候,锁住的是对象的实例(this)。
2. 作用于静态方法的时候,锁住的是Class实例,相当于是一个类的全局锁。
3. synchronized 作用于一个对象实例时(this),锁住的是所有以该对象为锁的代码块。我们一般使用这种方法来锁代码块。
Synchronized 核心组件
1) Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
2) Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
3) Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
4) OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
5) Owner:当前已经获取到所资源的线程被称为 Owner;
6) !Owner:当前释放锁的线程。
Synchronized 实现:
通过反编译可与看到synchronized是通过一个叫做monitor的东西实现的同步,有两个指令:monitorenter和monitorexit,每一个对象都有一个monitor(监视器)存储在对象头中。在内存中,对象一般由三个部分组成:对象头,实例数据和对其填充。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现。

monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;保证了一定会自动释放锁。

synchronized修饰方法,是通过acc_sychronized 标识符来完成对于monitor的获取的。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

  1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
  2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
  3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因。

Synchronized 在JDK6之前是重量级的锁。因为在给monitor加锁的解锁的过程中需要调用内核函数park() 和 unpark(), 需要从用户态切换到内核态,这是一种系统调用,需要耗费的资源较多。
Synchronized JDK6之后的优化, 锁升级:
无锁-》偏向锁-〉轻量级锁 - 》 重量级锁
偏向锁: 多线程来竞争一个锁,锁总是倾向于给一个线程获得,为了让这个线程获得锁的代价更低(不用频繁的加锁解锁),那么就引入了偏向锁。
在第一个线程获取到锁的时候,会在对象头来里存储一个偏向锁的id,然后还会存储获得锁的线程。然后这个线程再次进入同步代码块的时候就只需要看偏向锁的id和线程id是否正确即可。

适用于一个线程反复进入同步代码块。偏向锁的想法是 一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。

所以,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID即可,处理流程如下:

  1. 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
  2. 若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
  3. 如果测试线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
  4. 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
  5. 执行同步代码块;

轻量级锁:
当我们关闭了偏向锁或者有多个线程竞争就会导致偏向锁变成轻量级锁。轻量级锁一般适用于多个线程交替进入同步代码块的情况下。如果出现了多个线程竞争的情况,那么就需要锁升级,升级为重量级锁。
当一个线程访问一个同步块的时候,首先在线程的栈帧里创建一个lock record,然后把对象头里mark word复制到lock record中,接着用mark word 里的lock word指针指向当前获得锁的线程,然后线程里的record 指针指向 mark word。

概括:

偏向锁:不需要加锁,只需要对比thread id

轻量级锁:CAS+自旋

重量级锁:依靠monitor对象,实际上是依赖于计算机操作系统的底层内核函数。mutex lock

可重入锁 rentrantlock:
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完
成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。 可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
一些主要方法:
lock() 执行这个方法的时候,如果当前锁处于空闲状态,那么直接获得锁。如果锁已经被别的线程占有,那么当前的线程就被禁用了直到获取锁。
trylock() 尝试获取锁,如果锁可用就返回true,反之false。该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用,当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
unlock() 解锁操作,释放所占有的锁。
可重入锁默认是非公平锁,但是可以通过在构造函数中设置为公平。但是非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。
CAS:
CAS全称是compare and swap 比较再交换。CAS操作可以保证数据修改的原子性。
CAS的原理? 
CAS操作需要三个值,内存中最新的值,旧的预估值,要修改的值。如果内存中韩最新的值等于旧的预估值,那么就修改成功,不然就自旋再次比较。
通过CAS+自旋可以保证数据修改的原子性,atmoicInteger里的加一方法就是通过这个来实现的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值