Java多线程(2)
创始人
2024-03-31 15:47:36
0

1.jconsole.exe

在介绍多线程状态之前,我们先来认识一个Java JDK自带的工具"jconsole.exe"

它可以让我们很好地观察Java线程的状态.

首先我们需要找到自己安装jdk的目录

然后进入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());}
}

打开工具后会是如下页面,我们运行程序后,就可以在本地进程中观察到我们运行的进程.

在这里插入图片描述

双击这个进程

就会进入观察页面,会提醒一个是否以不安全状态连接,我们直接点击"不安全的连接"就好
在这里插入图片描述

点击"不安全的连接"之后

就正式进入到了监视窗口,点击线程栏,就可以在左下角的线程栏中观察到我们这个程序的线程.

选择想查看的线程,就可以看到线程的状态以及一些信息

在这里插入图片描述

2.Java中线程的状态

Java中线程的状态是Java内部的状态.与其他操作系统的状态会有所差异.

官方文档中有6种状态,也可以细分为7种状态

  • NEW: 安排了工作,还未开始行动

  • RUNNABLE: 可工作的.又可以分成正在工作中和即将开始工作

    可以细分为运行态和就绪态

  • BLOCKED: 这几个都表示排队等着其他事情

  • WAITING: 这几个都表示排队等着其他事情

  • TIMED_WAITING: 这几个都表示排队等着其他事情

  • TERMINATED: 工作完成了


先认识一个方法:getState(),获取线程的状态(谁调用获取谁的)

//此处就是获取当前运行代码的线程的状态.
Thread.currentThread().getState();

NEW:安排了工作还没开始行动

把Thread对象创建好了,但是还没有调用start运行

public class Test {public static void main(String[] args) {//NEWThread t = new Thread(()->{});System.out.println(t.getState());}
}

RUNNABLE:可工作的,又可以分为正在工作和即将开始工作

就绪状态:处于这个状态的线程,就是在就绪队列中,随时可以被调度到CPU上(或者正在运行中)

如果代码没有进行sleep,也没有其他导致阻塞的操作,代码大概率是在这个状态

public class Test {public static void main(String[] args){//RUNNABLESystem.out.println(Thread.currentThread().getState());}
}

TERMINATED:工作完成了

操作系统中的线程已经执行完毕,销毁了.但是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:表示排队等着其他事情(和下一个一起观察)

当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待.此时对应的线程(尝试加锁的线程)就处于BLOCKED状态.(等待获取锁)

TIMED_WAITING

表示线程在等待等待其他线程发来通知.(下列代码中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() {@Overridepublic void run() {synchronized (object) {System.out.println("hehe");}}}, "t2");t2.start();}
}

运行起来代码以后,使用 jconsole工具就可以观察到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED

WAITING同样表示等待其他线程发来通知.

与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() {@Overridepublic void run() {synchronized (object) {System.out.println("hehe");}}}, "t2");t2.start();}
}

此时使用 jconsole 工具可以看到 t1 的状态是 WAITING

3.了解方法:yield():大公无私,让出 CPU

谁调用Thread.yield()方法谁就让出CPU,不会改变线程的状态,会重新进入就绪队列排队

public class Test {public static void main(String[] args) {Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {while (true) {System.out.println("张三");Thread.yield();}}}, "t1");t1.start();Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {while (true) {System.out.println("李四");}}}, "t2");t2.start();}
}

通过上述代码,我们可以发现,张三的数量远远少于李四,说明"张三"总在给"李四"让行.这就是yield的作用

4.多线程带来的的风险-线程安全问题

我们先来观察下面代码

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呢?


类似于此类多线程安全问题的具体的原因有以下几点

  1. 线程是抢占式,进程间的调度充满随机性.
  2. 多个线程对同一个变量进行修改操作
  3. 针对变量的操作不是原子的
  4. 内存可见性(属于编译器优化)
  5. 指令重排序(属于编译器优化)

我们图示一个可能发生的状态来描述.

在这里插入图片描述

我们发现,此时虽然执行了两次++操作,但是最后的结果却只是++了一次的结果.

类似于这种状态还有多种.

在这里插入图片描述

认真想想,只要不是这种每次的操作是原子性的执行,就都会产生上述不安全的情况.

什么是原子性.

在我们这里,原子性就是保证一段代码是不可分割执行的.

synchronized关键字

对于上述安全问题的解决,我们就需要了解这个关键字.

synchronized的底层是使用操作系统的mutex lock实现的


synchronized 的特性

1.互斥

synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待.

  • 进入 synchronized 修饰的代码块,相当于加锁
  • 退出 synchronized 修饰的代码块,相当于解锁

2.刷新内存

synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

所以 synchronized 也能保证内存可见性(简单理解就是保证每次得到的数据都是最新的).

3.可重入

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的时候,本质上是在针对某个"对象"进行加锁

1. 直接修饰普通方法

当修饰普通方法的时候,锁对象就是this:加锁操作就是在设置this的对象头的标志位

2. 修饰一个代码块

修饰一个代码块的时候,就需要显示指定针对哪个对象加锁(Java中的任意对象都可以作为锁对象)

3. 修饰一个静态方法

修饰一个静态方法的时候,就是针对当前类的类对象(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关键字,就是对此方法加了锁,此时我们再去运行这段代码.无论运行几次,最终结果都会达到我们的预期.

5.内存可见性

我们看一个编译器优化的问题.

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关键字就会保证了内存可见性,保证程序每次读取的值,都是内存中真实的值.

volatile不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

我们给isQuit变量加上volatile关键字,在运行程序,输入1之后马上就会退出循环,bug就消失啦.

相关内容

热门资讯

新华社快讯:韩国检方对尹锡悦、... 新华社快讯:负责调查韩国前第一夫人金建希案件的特检组29日发布最终调查结果,对包括前总统尹锡悦、金建...
巩固国家通用语言文字法律地位 本报记者 朱宁宁 我国第一部有关语言文字的专门法律——国家通用语言文字法完成首次大修。 2025年1...
甘肃“十五五”规划建议:加快构... 中共甘肃省委关于制定国民经济和社会发展第十五个五年规划的建议发布,其中提到,加快构建 房地产发展新模...
部署六大重点工作 2026年积... 来源:经济参考报 12月27日至28日在京召开的全国财政工作会议为2026年的财政工作划定了重点。会...
权威抚养权律师推荐:家理(深圳... 在抚养权纠纷中,当事人急需专业且靠谱的律师来维护自身权益。那么,资深抚养权律师哪个好,经验丰富的抚养...
四川拓宽法律援助范围 今年办理... “终于胜诉了!要是按以前的规定,我这种情况属于合同纠纷,不符合法律援助申请条件。”近日,来自自贡市的...
汽车早报|零跑汽车发布首款MP... 重庆追加汽车置换、汽车报废更新补贴 据重庆日报,重庆市商务委消息,为贯彻落实国家部委相关要求,扎实...
自贸试验区昆明片区发布一批区域... 12月26日,中国(云南)自贸试验区昆明片区举行制度创新专题新闻发布会,联合昆明综合保税区发布一批改...
原创 存... “钱存银行,50万以内绝对安全”。 这句话你一定听过,但很多人只知其一,不知其二。 2015年《存款...
美银CEO判断:特朗普关税政策... 智通财经获悉,美国银行首席执行官Brian Moynihan表示,尽管2025年的关税措施曾冲击美国...