1.jconsole.exe
在介绍多线程状态之前,我们先来认识一个Java JDK自带的工具"jconsole.exe"
它可以让我们很好地观察Java线程的状态.
首先我们需要找到自己安装jdk的目录
然后进入bin目录下.找到名为"jconsole.exe"的程序文件
然后打开程序
这里我们使用一段代码来帮助观察
publicclassTest1{publicstaticvoidmain(String[] args)throwsInterruptedException{Thread t1 =newThread(()->{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运行
publicclassTest{publicstaticvoidmain(String[] args){//NEWThread t =newThread(()->{});System.out.println(t.getState());}}
RUNNABLE:可工作的,又可以分为正在工作和即将开始工作
就绪状态:处于这个状态的线程,就是在就绪队列中,随时可以被调度到CPU上(或者正在运行中)
如果代码没有进行sleep,也没有其他导致阻塞的操作,代码大概率是在这个状态
publicclassTest{publicstaticvoidmain(String[] args){//RUNNABLESystem.out.println(Thread.currentThread().getState());}}
TERMINATED:工作完成了
操作系统中的线程已经执行完毕,销毁了.但是Thread对象还在,获取到的状态.
publicclassTest{publicstaticvoidmain(String[] args)throwsInterruptedException{//TERMINATED,Thread t =newThread(()->{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唤醒)
publicclassTest{publicstaticvoidmain(String[] args){finalObject object =newObject();Thread t1 =newThread(newRunnable(){@Overridepublicvoidrun(){synchronized(object){while(true){try{Thread.sleep(1000);}catch(InterruptedException e){
e.printStackTrace();}}}}},"t1");
t1.start();Thread t2 =newThread(newRunnable(){@Overridepublicvoidrun(){synchronized(object){System.out.println("hehe");}}},"t2");
t2.start();}}
运行起来代码以后,使用 jconsole工具就可以观察到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED
WAITING同样表示等待其他线程发来通知.
与TIMED_WAITING的区别在于,TIMED_WAITING线程在等待唤醒,但设置了时限;
而WAITING 线程在无限等待唤醒
publicclassTest{publicstaticvoidmain(String[] args){finalObject object =newObject();Thread t1 =newThread(newRunnable(){@Overridepublicvoidrun(){synchronized(object){while(true){try{// [与上述代码相同,只是修改了这里]// Thread.sleep(1000);
object.wait();}catch(InterruptedException e){
e.printStackTrace();}}}}},"t1");
t1.start();Thread t2 =newThread(newRunnable(){@Overridepublicvoidrun(){synchronized(object){System.out.println("hehe");}}},"t2");
t2.start();}}
此时使用 jconsole 工具可以看到 t1 的状态是 WAITING
3.了解方法:yield():大公无私,让出 CPU
谁调用Thread.yield()方法谁就让出CPU,不会改变线程的状态,会重新进入就绪队列排队
publicclassTest{publicstaticvoidmain(String[] args){Thread t1 =newThread(newRunnable(){@Overridepublicvoidrun(){while(true){System.out.println("张三");Thread.yield();}}},"t1");
t1.start();Thread t2 =newThread(newRunnable(){@Overridepublicvoidrun(){while(true){System.out.println("李四");}}},"t2");
t2.start();}}
通过上述代码,我们可以发现,张三的数量远远少于李四,说明"张三"总在给"李四"让行.这就是yield的作用
4.多线程带来的的风险-线程安全问题
我们先来观察下面代码
classCounter{publicint count =0;publicvoidincrease(){
count++;}}publicclassTest{privatestaticCounter counter =newCounter();publicstaticvoidmain(String[] args)throwsInterruptedException{Thread t1 =newThread(()->{for(int i =0; i <50000; i++){
counter.increase();}});Thread t2 =newThread(()->{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关键字
对于上述安全问题的解决,我们就需要了解这个关键字.
synchronized的底层是使用操作系统的mutex lock实现的
synchronized 的特性
1.互斥
synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待.
- 进入 synchronized 修饰的代码块,相当于加锁
- 退出 synchronized 修饰的代码块,相当于解锁
2.刷新内存
synchronized 的工作过程:
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
所以 synchronized 也能保证内存可见性(简单理解就是保证每次得到的数据都是最新的).
3.可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题.(也就是在自己加锁后,在没有释放锁的状态下,自己是可以再次进入这段加锁的代码的.)
在下面的代码中,
increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释 放, 相当于连续加两次锁)
对于synchronized来说,这个代码是完全没问题的. 因为 synchronized 是可重入锁
staticclassCounter{publicint count =0;synchronizedvoidincrease(){
count++;}synchronizedvoidincrease2(){increase();}}
实现方式: 在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.
如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取 到锁, 并让计数器自增.
解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
synchronized关键字 本质就是对代码加锁
加锁之后,可以保证指令的原子性,同时保证内存可见性
使得多个线程不可以同时对同一个变量进行修改操作,也就保证了安全.
synchronized 有三种使用方式
使用synchronized的时候,本质上是在针对某个"对象"进行加锁
1. 直接修饰普通方法
当修饰普通方法的时候,锁对象就是this:加锁操作就是在设置this的对象头的标志位
2. 修饰一个代码块
修饰一个代码块的时候,就需要显示指定针对哪个对象加锁(Java中的任意对象都可以作为锁对象)
3. 修饰一个静态方法
修饰一个静态方法的时候,就是针对当前类的类对象(xxx.class)加锁
当一个线程加锁成功的时候,其他线程如果尝试加锁,就会触发阻塞等待.等待到锁释放时对线程的唤醒.此时对应的线程(尝试加锁的线程)就处于BLOCKED状态
那我们该怎么解决上述问题呢
当然是使用synchronized加锁啦(对获取变量并修改的操作进行加锁,把操作打包成原子的)
正确的加锁之后,多线程代码就变成安全的了.
classCounter{publicint count =0;synchronizedpublicvoidsIncrease(){//加锁
count++;}}publicclassTest2{privatestaticCounter counter =newCounter();publicstaticvoidmain(String[] args)throwsInterruptedException{Thread t1 =newThread(()->{for(int i =0; i <50000; i++){
counter.sIncrease();}});Thread t2 =newThread(()->{for(int i =0; i <50000; i++){
counter.sIncrease();}});
t1.start();
t2.start();
t1.join();
t2.join();System.out.println(counter.count);}}
我们对要操作变量的方法,加上synchronized关键字,就是对此方法加了锁,此时我们再去运行这段代码.无论运行几次,最终结果都会达到我们的预期.
5.内存可见性
我们看一个编译器优化的问题.
publicclassTest{// private static volatile int isQuit = 0;privatestaticint isQuit =0;publicstaticvoidmain(String[] args){Thread t =newThread(()->{while(isQuit ==0){//不进行操作,使得程序不停地访问isQuit}System.out.println("循环结束 t线程退出");});
t.start();Scanner sc =newScanner(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就消失啦.
版权归原作者 魚小飛 所有, 如有侵权,请联系我们删除。