0


Java多线程(2)

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


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

  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 是可重入锁

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就消失啦.

标签: java jvm 面试

本文转载自: https://blog.csdn.net/m0_58154870/article/details/127542526
版权归原作者 魚小飛 所有, 如有侵权,请联系我们删除。

“Java多线程(2)”的评论:

还没有评论