关于java线程的6种状态
操作系统层面,进程的5种状态:
创建状态:进程正在被创建,尚未分配资源。
就绪:当进程被分配到除了CPU以外所有必要的资源后,只要再获得CPU的使用权,就可以立即运行,这就是就绪状态。
运行:获取到了cpu资源。
阻塞:进程因为某种原因,如:其他设备未就绪而无法继续执行,从而放弃CPU的状态称为阻塞状态;
终止状态:进程执行完毕或者被提前终止。
注意:java的阻塞,等待,计时等待,全都在操作系统层面全都算是阻塞状态。
JAVA层面,线程的6种状态:
注意:一个就绪状态线程,获取CPU资源后,会进入运行状态。
运行状态执行代码的过程中,如果需要访问共享资源,他会尝试获取锁,获取锁失败就会进入阻塞状态。
当其他线程唤醒了这个线程,这个线程会进先入就绪状态(而不是运行状态),再去获取CPU资源,进入运行状态,这时这个线程会再次尝试获取锁,获取失败再次进入阻塞状态。
- New(新建):新建一个线程对象,但这个对象还没有start();
- Runnable(运行):Java线程中将就绪和运行两种状态笼统的称为“运行”。
在java中,当线程对象的调用start()方法时,该线程就进入了可运行状态。
就绪状态:当进程被分配到除了CPU以外所有必要的资源后,只要再获得CPU的使用权,就可以立即运行,并且其他资源也都准备好了,只差CPU资源的状态,这是就绪状态。
运行状态:就绪状态的线程在获得CPU资源后变为运行中状态。
-
Blocked(阻塞):当线程获取锁失败时就会进入阻塞状态,被放到阻塞队列。等待获取锁成功的线程释放锁时会唤醒阻塞状态的线程,让他们重新争抢锁,争抢成功进入可运行状态。
-
Waiting(等待):获取锁成功后,线程可能需要等待其他线程完成某个任务后才能继续执行。(讲究配合)在这些情况下,可以使用 锁.wait() 方法来让线程进入等待队列。
-
Timed Waiting(计时等待):当线程在限定时间内等待另一个线程执行特定操作时的状态。
方式1.wait(long timeout) 释放锁,可被notify唤醒。属于Object的方法,只能在同步代码块或者同步方法中使用。
方式2.sleep(long millis) 不释放锁,不可被提前唤醒。属于Thread的方法,可以在任何地方使用 -
Terminated(终止):当线程执行完毕或者因异常退出时的状态。
JAVA线程的一些常用方法:
让线程进入无限期等待状态的方法:
锁.wait(),释放锁,并让线程进入等待队列。
让线程进入计时等待状态的方法。
1.wait(long timeout) 释放锁,并让线程进入计时等待状态。
可被notify唤醒。属于Object的方法,只能在同步代码块或者同步方法中使用。
2.sleep(long millis) 让线程进入计时等待状态。
不释放锁,不可被提前唤醒。属于Thread的方法,可以在任何地方使用
唤醒等待状态线程的方法:
1.锁.notify(),随机唤醒一个等待队列中的线程,但不会释放锁,防止其他线程获取锁,对接下来本线程的执行造成干扰。
2.锁.notifyAll()唤醒所有线程,并释放锁。
共同点:他们都只能唤醒wait(),或者wait(long timeout)方法进入阻塞状态的线程,对于sleep(long millis)这种无能为力。
怎么让t1,t2,t3三个线程按照顺序执行呢?
用join()方法,当在一个线程中调用另一个线程的 join() 方法时,调用线程会被阻塞,直到被调用线程执行完毕或者超时。
同步代码块内关于线程的方法
wait() 使当前线程进入阻塞状态,并释放锁。
notify()随机唤醒一个与锁对象关联Monitor内的阻塞的线程。
notifyAll()唤醒所有与锁对象关联的Monitor内的阻塞线程。
无论是notify()还是notifyAll()都不会释放锁。
Thread类的方法
注意,Thread的方法可以在任何地方使用,他的侧重点是在线程,与锁无关,不会对锁产生任何影响。
1、start():启动当前线程
2、run ():通常需要重写Thread类中的此方法,将创建的线程要执行的操作写在此方法中。
3、currentThread():静态方法,返回执行当前代码的线程
4、getName () :获取当前线程的名字,与3一起使用可得到正在执行的线程的名字
5、 setName () :设置当前线程的名字
6、 yield () :释放当前cpu的执行权
7、 join () :在线程a中调用线程b的join () ;此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
8、stop (): 已过时,强制结束当前线程。
9、sleep (long millitime) :休眠millitime毫秒
10、isAlive () :判断当前线程是否存活
wait方法和sleep方法的区别
共同点:都能让当前线程进入阻塞状态
他们都能被interrupt()打断
不同点:wait方法属于Object,sleep方法属于Thread
执行wait方法的线程可以被notify或notifyAll唤醒,执行sleep方法的线程不能被唤醒
wait方法必须在同步代码块或同步方法配合锁使用,sleep可以在任意位置使用。
synchronized底层
synchronized底层是Monitor
第二次解锁是因为防止异常情况无法释放锁
每一个lock对象都会关联一个Monitor,Monitor的结构如图:
Owner:存储当前获取锁的线程的,只能有一个线程可以获取
EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
CAS(Compare And Swap)
CAS思想:
CAS(比较并交换),比较内存位置的当前值和预期值是否相等,相等将新值写入内存位置,这个过程是原子的。
JAVA CAS数据交换流程
线程A从主内存中读到100,线程B种主内存中读到数据100,线程A先进行比较交换操作,成功后主内存中V的值变为101,这时线程B区比较交换,100和101不等,不能交换,需要重新从主内存中读取V的值101,然后进行比较交换,最终V中的值变为99。
JAVA CAS底层实现
在JAVA中,CAS 主要通过 Unsafe 类来实现,它提供了一系列的 CAS 相关方法,例如 compareAndSwapInt、compareAndSwapLong、compareAndSwapObject 等,在JDK8中这三个方法都是native方法,是由系统提供的方法
volatitle关键字
用于修饰变量。它的主要作用是保证该变量在多线程环境下的可见性,即当一个线程修改了该变量的值后,其他线程能够立即看到这个修改。
具体来说,使用 volatile 关键字修饰的变量会被存储在主内存中,而不是线程的本地内存中。这样,在一个线程修改了该变量的值后,会立即刷新到主内存中,并且其他线程在读取这个变量的值时会直接从主内存中获取,而不是从线程的本地内存中获取。
volatile 关键字主要用于以下两种情况:
保证变量的可见性: 当多个线程同时访问一个变量时,可以使用 volatile 关键字来确保每个线程都能够看到最新的值。
禁止指令重排序优化: 在一些情况下,JVM 会对指令进行重排序优化以提高性能,但这可能会导致多线程环境下出现意外的结果。使用 volatile 关键字可以禁止指令重排序优化,确保程序的正确性。
以上代码执行时第三个线程的while线程不会停止。
解决办法:在修饰stop变量的时候加上volatile,当前告诉 jit,不要对 volatile 修饰的变量做优化
AQS(AbstractQueuedSynchronizer)
全称是AbstractQueuedSynchronizer,即抽象队列同步器。很多锁会用到它。通常是通过Sync继承AQS然后使用他。
比如ReentrantLock 阻塞式锁 ,Semaphore 信号量 ,CountDownLatch 倒计时锁
AQS有一个volatile修饰的属性state,这个state可以表示很多东西。
如果是在ReentrantLock,它表示锁是否被获取(是否为0),被重复获取的次数。
如果在Semaphore中,表示当前许可剩余个数。
如果在CountDownLatch中,表示当前计数。
AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程。
AQS还有继承来的private transient Thread exclusiveOwnerThread
存储获取锁的线程
如何保证多个线程抢这个资源的原子性?
通过CAS比较交换设置state状态
ReentrantLock的实现原理
ReentrantLock锁,可重入锁,一个线程可以多次获取这个锁 ,底层原理为AQS队列
支持公平锁和非公平锁,构造器无参数为非公平锁,构造器有参为公平锁
工作流程
-
线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
-
假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
-
当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
-
公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
CountDownLatch(计数器)
创建一个计数器 new CountDownLatch(int count)
在一个线程中我们调用CountDownLatch的await()方法,该线程将会阻塞。
其他线程可以调用CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后,所有await的线程都将被唤醒。
底层原理就是,CountDownLatch类有一个内部类Sync继承AQS类,CountDownLatch会有一个Sync类的对象sync。
当我们调用await()方法,await()方法内部会用sync调用方法,把线程放入AQS的队列中排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。
public class CountDownLatch {
/**
* Synchronization control For CountDownLatch.
* Uses AQS state to represent count.
*/
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
Semaphore(信号量)
Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量。Semaphore可以控制某个方法的并发访问数量。
通过acquire()来获取许可,这时信号量-1,如果信号量为0时,线程获取再次获取许可,线程就会进入AQS的队列排队。
通过release()⽅法来释放许可,这时信号量+1,当某个线程释放了某个许可后,会去AQS的队列中唤醒第⼀个线程。