线程的生命周期:
一共有五种状态,分别是新建,就绪,运行,阻塞和死亡。
新建状态:当程序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。
自旋锁:
monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;保证了一定会自动释放锁。
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:
- 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
- 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
- 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因。
适用于一个线程反复进入同步代码块。偏向锁的想法是 一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。
所以,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID即可,处理流程如下:
- 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
- 若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
- 如果测试线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
- 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
- 执行同步代码块;
概括:
偏向锁:不需要加锁,只需要对比thread id
轻量级锁:CAS+自旋
重量级锁:依靠monitor对象,实际上是依赖于计算机操作系统的底层内核函数。mutex lock