在介绍多线程状态之前,我们先来认识一个Java JDK自带的工具"jconsole.exe"
它可以让我们很好地观察Java线程的状态.
然后进入bin目录下.找到名为"jconsole.exe"的程序文件
这里我们使用一段代码来帮助观察
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
try {
Thread.sleep(30000);//t1线程睡眠30秒
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1");
t1.start();
Thread.sleep(60000);//main线程睡眠1分钟
System.out.println(t1.getState());
}
}
打开工具后会是如下页面,我们运行程序后,就可以在本地进程中观察到我们运行的进程.
就会进入观察页面,会提醒一个是否以不安全状态连接,我们直接点击"不安全的连接"就好
就正式进入到了监视窗口,点击线程栏,就可以在左下角的线程栏中观察到我们这个程序的线程.
选择想查看的线程,就可以看到线程的状态以及一些信息
Java中线程的状态是Java内部的状态.与其他操作系统的状态会有所差异.
官方文档中有6种状态,也可以细分为7种状态
NEW: 安排了工作,还未开始行动
RUNNABLE: 可工作的.又可以分成正在工作中和即将开始工作
可以细分为运行态和就绪态
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了
先认识一个方法:getState(),获取线程的状态(谁调用获取谁的)
//此处就是获取当前运行代码的线程的状态.
Thread.currentThread().getState();
把Thread对象创建好了,但是还没有调用start运行
public class Test {
public static void main(String[] args) {
//NEW
Thread t = new Thread(()->{});
System.out.println(t.getState());
}
}
就绪状态:处于这个状态的线程,就是在就绪队列中,随时可以被调度到CPU上(或者正在运行中)
如果代码没有进行sleep,也没有其他导致阻塞的操作,代码大概率是在这个状态
public class Test {
public static void main(String[] args){
//RUNNABLE
System.out.println(Thread.currentThread().getState());
}
}
操作系统中的线程已经执行完毕,销毁了.但是Thread对象还在,获取到的状态.
public class Test {
public static void main(String[] args) throws InterruptedException {
//TERMINATED,
Thread t = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
Thread.sleep(3000);
System.out.println(t.getState());
}
}
当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待.此时对应的线程(尝试加锁的线程)就处于BLOCKED状态.(等待获取锁)
表示线程在等待等待其他线程发来通知.(下列代码中t1等待sleep唤醒)
public class Test {
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("hehe");
}
}
}, "t2");
t2.start();
}
}
运行起来代码以后,使用 jconsole工具就可以观察到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED
与TIMED_WAITING的区别在于,TIMED_WAITING线程在等待唤醒,但设置了时限;
而WAITING 线程在无限等待唤醒
public class Test {
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
while (true) {
try {
// [与上述代码相同,只是修改了这里]
// Thread.sleep(1000);
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("hehe");
}
}
}, "t2");
t2.start();
}
}
此时使用 jconsole 工具可以看到 t1 的状态是 WAITING
谁调用Thread.yield()方法谁就让出CPU,不会改变线程的状态,会重新进入就绪队列排队
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("张三");
Thread.yield();
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("李四");
}
}
}, "t2");
t2.start();
}
}
通过上述代码,我们可以发现,张三的数量远远少于李四,说明"张三"总在给"李四"让行.这就是yield的作用
我们先来观察下面代码
class Counter{
public int count = 0;
public void increase(){
count++;
}
}
public class Test {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();//main线程等待t1和t2线程运行结束,输出最终结果.
System.out.println(counter.count);
}
}
多次运行后,我们会发现结果总会在50_000-100_000之间.
为什么会有这样的现象发生呢?为什么不是每次都是100_000呢?
我们图示一个可能发生的状态来描述.
我们发现,此时虽然执行了两次++操作,但是最后的结果却只是++了一次的结果.
类似于这种状态还有多种.
认真想想,只要不是这种每次的操作是原子性的执行,就都会产生上述不安全的情况.
在我们这里,原子性就是保证一段代码是不可分割执行的.
对于上述安全问题的解决,我们就需要了解这个关键字.
synchronized的底层是使用操作系统的mutex lock实现的
synchronized 的特性
synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待.
synchronized 的工作过程:
所以 synchronized 也能保证内存可见性(简单理解就是保证每次得到的数据都是最新的).
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题.(也就是在自己加锁后,在没有释放锁的状态下,自己是可以再次进入这段加锁的代码的.)
在下面的代码中,
increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释 放, 相当于连续加两次锁)
对于synchronized来说,这个代码是完全没问题的. 因为 synchronized 是可重入锁
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
实现方式: 在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.
如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取 到锁, 并让计数器自增.
解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
synchronized关键字 本质就是对代码加锁
加锁之后,可以保证指令的原子性,同时保证内存可见性
使得多个线程不可以同时对同一个变量进行修改操作,也就保证了安全.
synchronized 有三种使用方式
使用synchronized的时候,本质上是在针对某个"对象"进行加锁
当修饰普通方法的时候,锁对象就是this:加锁操作就是在设置this的对象头的标志位
修饰一个代码块的时候,就需要显示指定针对哪个对象加锁(Java中的任意对象都可以作为锁对象)
修饰一个静态方法的时候,就是针对当前类的类对象(xxx.class)加锁
当一个线程加锁成功的时候,其他线程如果尝试加锁,就会触发阻塞等待.等待到锁释放时对线程的唤醒.此时对应的线程(尝试加锁的线程)就处于BLOCKED状态
当然是使用synchronized加锁啦(对获取变量并修改的操作进行加锁,把操作打包成原子的)
正确的加锁之后,多线程代码就变成安全的了.
class Counter{
public int count = 0;
synchronized public void sIncrease(){
//加锁
count++;
}
}
public class Test2 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.sIncrease();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.sIncrease();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
我们对要操作变量的方法,加上synchronized关键字,就是对此方法加了锁,此时我们再去运行这段代码.无论运行几次,最终结果都会达到我们的预期.
我们看一个编译器优化的问题.
public class Test {
// private static volatile int isQuit = 0;
private static int isQuit = 0;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while(isQuit == 0){
//不进行操作,使得程序不停地访问isQuit
}
System.out.println("循环结束 t线程退出");
});
t.start();
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个isQuit的值");
if(sc.hasNextInt()){
isQuit = sc.nextInt();
}
System.out.println("main 执行完毕");
}
}
上述代码,当输入1之后t线程可能是不会退出的.这就属于编译器优化了.
因为在不停高速地访问isQuit变量,编译器直接 (进行优化) 将isQuit变量的值一次读取并保存副本.后续读取副本的值.不会访问实际内存,大量节省了程序读的时间.(也因此产生了bug)
volatile关键字就会保证了内存可见性,保证程序每次读取的值,都是内存中真实的值.
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.
我们给isQuit变量加上volatile关键字,在运行程序,输入1之后马上就会退出循环,bug就消失啦.