当前位置: 首页 > article >正文

javaEE-线程的常用方法-4

目录

一.start():启动一个线程

调用start()方法

start()方法只能调用一次:

java中的API:

start()和run()的区别:

二.中断一个线程

中断线程方法1:引入标志位

中断线程方法2:调⽤interrupt()⽅法

抛出的异常:

三.等待一个线程 join()

四、获取线程引用

五。线程的状态

六、线程安全(重点,难点)

引起线程不安全的原因:

解决方法:

“加锁”

“可重入”性

死锁

java标准库中的线程安全类:

内存可见性



一.start():启动一个线程

之前我们已经看到了如何通过重写run⽅法创建⼀个线程对象但线程对象被创建出来并不意味着线程就开始运⾏了。重写run⽅法是提供给线程要做的事情的指令清单.

调用start()方法

调⽤start⽅法,才真的在操作系统的底层创建出⼀个线程.

start()方法只能调用一次:

对于同一个线程对象来说,start()方法只能调用一次.

若调用多次,除第一次的调用,之后的线程就会出现 illegalThreadStateException(非法线程状态异常)异常终止,而第一次调用的线程还能正常运行.

要想启动更多的线程,就要创建新的线程对象.

这里的main线程,和两个t1,t2 线程都是每隔一秒执行一次,可以看出打印结果 main和thread也是正如分析的一样.

在jconsloe.exe上也能看到三个线程的执行.

java中的API:

API 是应用程序编程接口(Application Programming Interface)的缩写。

 API 的结构:

在 Java 中,API(应用程序编程接口)指的是整个包、接口、类、方法等以及它们之间的关系和规范。API 是开发者用来构建应用程序的工具集,它包括了所有这些元素.

Java API ,指的是整个 Java 标准库,包括所有的包、接口、类、方法和异常等。这些元素共同构成了 Java 语言的核心功能,使得开发者能够构建各种应用程序,从简单的命令行工具到复杂的企业级应用。

它是一套预定义的函数、方法或类的集合,允许应用程序访问某些功能或数据,而无需关心底层的实现细节。API 为开发者提供了构建软件应用的积木。

start()和run()的区别:

start()方法:

调用start()是创建了一个新的线程:main线程,和t线程,两个线程同时工作,互不干扰。(并行执行)

通过Thread类调用start,开启一个线程,此时该线程处于就绪状态,并没有执行,一旦得到cpu的时间片,就自动调用run方法,开始执行。注意:无需等run方法结束,即可执行下面的代码。

run()方法

而若单调用run()方法,run()方法只是类的一个普通方法而已。只是在main()线程中,去执行了一个run()方法,该方法执行完后,再去执行后面的代码,属于串行执行。注意:这里不会创建新线程。

总结:run()就是一个普通的方法,而start()会创建一个新线程去执行run()的代码。

1、start方法用来启动相应的线程

2、run方法只是thread的一个普通方法,在主线程里执行;

3、需要并行处理的代码放在run方法中,start方法启动线程后自动调用run方法

4、run方法必去是public的访问权限,返回类型为void。

此处,调用run()方法后,就只能去执行run()方法中的代码,而main()方法调用run()方法后面的代码,只有执行完run()方法后,才能去执行。(属于一条路线)

参考博客:

线程中start方法和run方法的区别_线程start和run区别-CSDN博客

二.中断一个线程

一个线程在执行过程中,因某些需要,要让该线程中断,不再执行.就需要对该线程进行中断处理。

(就好像你在打游戏,突然来了个电话,就要先中断你的游戏,去接听电话)

中断线程方法1:引入标志位

通过共享的标记来进⾏沟通(这需要线程之间的代码逻辑的配合执行)

设置静态变量,通过对其修改,来实现中断线程的功能

注意:这里的isQuite是设置在全局变量处的,而不能设置在main线程中,

原因是run()方法是通过使用lambda表达式(匿名内部类)来实现的,但lambda函数中的变量要遵循变量捕获原则,就是内部用到的局部变量不能是可以修改的,而此处的isQuite又需要对其修改,因此不能设置成fianl类型的。

但lambda表达式可以访问到方法外定义的任意变量,因此, 就只能设置成全局变量了.

lambda表达式中,不允许存在可能被修改的变量的原因是:

这里结果,执行完"3s后 Thread线程结束",在将isQuite设置为true之前,又执行了一次t线程,才结束t线程.

这里执行完"3s后 Thread线程结束",直接将isQuite设置为true,结束t线程。

可见线程的执行顺序和执行时间是随机的.

中断线程方法2:调⽤interrupt()⽅法

isInterrupted():判定标志位

interrupt():设置标志位

将run方法内的循环条件设置为判定标志位,再在调用标志位,使其改变,达到中断线程的效果。

但这样在执行的时候会抛出一个异常

在sleep()函数,当主动让t线程结束(修改interrupted标志位)的时候,此时sleep()的执行还未结束,当sleep()被提前唤醒的时候,会自动清除interrupted标志位.就会出现矛盾:到底是让该线程结束,还是继续执行.

要不想让异常终止,只需要修改异常内容就可以.

抛出的异常:

旧版的idea是执行try-catah后,catch里的代码是自动打印调用栈.

新版的idea是执行try-catah后,catch中再抛出一个异常.

但是在实际开发过程中,catch对以上两种方法都不用,idea生成的这两种方法都不用,这只是一个站位的作用.

在实际开发中,catch代码块中实际可能会进行如下操作:

在java中.程序的终止,是一种"软性"操作.就是说,需要线程中的代码配合,才能达到中断的效果.

三.等待一个线程 join()

因为多线程是随机调度的,有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作。

为了实现这种效果,该方法就能解决这样的问题的。等待一个线程,指的是让一个线程执行结束,再进行之后的执行.

多用于一个线程不确定执行时间,且要等待该线程结束,再进行别的线程操作.

下面的代码实现这样的功能:在t线程中执行1到5的相加运算,再在main线程中将结果打印

package Thread_;

public class Thread11 {
    private static int count;
    public static void main(String[] args) throws InterruptedException {
        //计算1--5相加,再在main线程中打印结果
        Thread t=new Thread(()->{
            for(int i=0;i<5;i++){
                count+=i;
            }
        System.out.println("Thread线程执行结束!");
        });
     t.start();
     t.join();//线程等待
     System.out.println("结果为: "+count);
    }
}

join()的功能:在哪个线程中调用被调用,就暂停该线程(进入阻塞状态),哪个线程调用该方法,就先执行哪个线程.

join()方法有一个受查异常InterruptedException,使用时需要处理。

上面的代码中,join方法在main线程中被调用,则main就进入阻塞状态。t线程调用了该方法,则要等t线程执行结束,才继续执行main线程。(就是说:main线程要等t线程执行结束之后,main才能继续执行)

就是因为阻塞,使这两个线程结束产生了先后关系。

//计算1--10000相加,分成两个线程执行,再在main线程中打印结果
private static int count;

    public static void main(String[] args) throws InterruptedException {
        //计算1--10000相加,分成两个线程执行,再在main线程中打印结果
        Thread t1=new Thread(()->{
            int n=0;
            //t1: 计算1到5000的相加
            for(int i=1;i<=5000;i++){
                n+=i;
            }
            count+=n;
            System.out.println("Thread线程执行结束!");
        });
        Thread t2=new Thread(()->{
            int n=0;
            for(int i=5001;i<=10000;i++){
                n+=i;
            }
            count+=n;
            System.out.println("Thread线程执行结束!");
        });
        t1.start();
        t2.start();
        
        t1.join();//线程等待
        t2.join();//线程等待
        System.out.println("结果为: "+count);
    }

此时的结果是正确的,但是若进行更大数字的相加时,

让其计算1到100亿数字的相加,算一下执行时间。

一个线程完成计算:

 private static long count;

    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            for(long i=0;i<100_0000_0000L;i++){
                count+=i;
            }
        });
        t.start();
        long beg=System.currentTimeMillis();
        t.join();
        long end=System.currentTimeMillis();
        System.out.println("count= "+count);
        System.out.println("运算时间为: "+(end-beg));

    }

运行时间为3074ms.

 两个线程完成计算:t1负责完成前50亿的计算,t2负责完成后50亿的计算。

private static long count;

    public static void main(String[] args) throws InterruptedException {
        //计算1--100_0000_0000(100亿)相加,分成两个线程执行,再在main线程中打印结果
        Thread t1=new Thread(()->{
            long n=0;
            //t1: 计算1到5000的相加
            for(long i=1;i<=50_0000_0000L;i++){
                n+=i;
            }
            count+=n;
            System.out.println("Thread线程执行结束!");
        });
        Thread t2=new Thread(()->{
            long n=0;
            for(long i=50_0000_0001L;i<=100_0000_0000L;i++){
                n+=i;
            }
            count+=n;
            System.out.println("Thread线程执行结束!");
        });
        t1.start();
        t2.start();

        t1.join();//线程等待
        t2.join();//线程等待
        System.out.println("结果为: "+count);
    }

 

运行时间为1827ms.

双线程运算时间缩短,但此时也会存在线程安全的问题.

join()方法还有别的重载方法:

join():属于死等,t2线程不结束,就不会向下执行,

join(long millis):超时等待。在millis时间内,若t2线程没有结束,就不再等待,进行正常的代码流程。

第三个是设置一个ns级的时间,过于精确,用处不大。

要想把被join()阻塞的进程提前唤醒,也是可以通过interrupt()方法,将其唤醒。

四、获取线程引用

Thread.currenThread():获取当前线程的引用(Thread的引用)

如果是继承Thread,则可直接是由this调用;

若是实现Runnable接口或lambda表达式,此时this就不能代替Thread了,只能使用Thread.currendThread().

五。线程的状态

就绪状态:线程正在执行,或者随时准备着CPU的调用,执行的状态。

阻塞状态:线程暂时不方便去CPU上执行。

java中,线程有以下这几种状态:

1、NEW:Thread线程创建好了,但是还未调用start()方法。且直有处于NEW状态的线程才能调用start().

2、TERMINATED:Thread对象仍然存在,但是该线程已经执行完毕

3、RUNNABLE:就绪状态,线程正在执行,或者随时准备着CPU的调用,去CPU上执行。

4、TIMED_WAITING:指定时间的阻塞状态,达到一定时间后,自动解除阻塞

5、WAITING无时间限制的阻塞(死等),直有满足指定条件,才会结束阻塞。(join()/wain()都会进入WAITING状态)

6、BLOCK:由于锁竞争引起的阻塞。(存在线程安全的问题)

各状态的转换关系:

了解这些状态后,对代码的调试起到非常大的帮助

在jconsloe.exe中,也能看到线程的状态:

六、线程安全(重点,难点)

某个代码,若不论是在单线程下执行,还是在多线程下执行,都不会出现bug,这样的线程称为“线程安全”

若在单线程下运行正确,在多线程下,就可能产生bug,这样的线程就是称为“线程不安全”的,或叫存在“线程安全”问题

1.用一个线程计算1到10000的和,main线程打印结果:

结果正确.

2.用两个线程 t1计算1-5000的和,t2计算5001-10000的和,main打印结果:

该方法运行几次,发现每次的执行结果不确定,并且结果还是错误的。这就属于存在“线程安全问题”的代码。

这里的count++,在系统的底层其实是执行的三个cpu指令

1、load:从内存读取数据到cpu寄存器上。

2、add:将寄存器中的值+1.

3、save:将寄存器中的值写回内存中。

两个线程执行的三个cpu指令可能有各种顺序。

列出几种情况:

但是无数种情况中,只有在一个线程从load到save执行完毕后,再去执行下一个线程的load,才能得到正确结果。

在5万次的自增过程中,也不知道多少次是正确的执行顺序.这也是为啥采用两个线程计算时,每次的结果不但错误,且不一样.

引起线程不安全的原因:

1.操作系统上的线程是“抢占式线程”,“随即调度”的。这给线程之间的执行顺序带来了很多变数。(根本原因)

2、代码结构上:代码中存在多个线程同时修改一个变量

(一个线程修改一个变量,或多个线程读取一个变量,或是多个线程修改多个变量,这些都不会引起线程安全问题)

3.上面的线程修改操作(load->add->save),不是“原子的”操作(要莫不执行,要么执行完)(直接原因)

不是“原子的“指的是,一个线程上的这些指令,执行到一半,可能会被调度走,让其他线程继续执行。而每个cpu指令(load,add,save....单个来看)都是原子的(要不不执行,要不执行完)。

4、内存可见性问题。

5、指令重排序问题。

解决方法:

1、针对线程的“抢占式线程”,“随即调度”。

2.代码结构上:可以不让多个线程同时修改一个变量,但这个要分情况,有时可以调整,但有时是无法实现调整的。

3、不是“原子的”操作:可以将count++生成的几个指令,通过一些方法,将其打包,使其成为一个“整体”。

“加锁”

可以通过“加锁”,来实现这样的效果。锁具有“互斥”,“排他”这样的特性。

在java中,加锁的方式有好多种,最主要使用的方式是通过加synchronized关键字,来加锁。

加锁的目的是,把count++的三个操作(load,add,save)打包成一个原子操作。

但这里进行锁操作,需要先准备一个“锁对象”,加锁,解锁的操作都是依托锁对象来执行的。

synchronized(对象){ }

进入{}后,会进行加锁(lock),出{}后,进行解锁(unlock)。

在java中,任何一个对象都可以成为锁对象。也就是说()中的内容可以是随意的,但必须为对象。

如果一个线程,针对一个对象加上锁之后,若别的线程也想对这个对象上锁,该线程就会产生阻塞(BLOCKED),直到上一个线程解锁为止,该线程才能继续操作。

解析:每次count++之前,进行上锁,count++之后,进行解锁。

若两个对象针对不同的对象加锁,则就不会有锁竞争,也不会产生阻塞。此时还是会存在线程安全问题。

这里能否通过上锁解决线程安全问题,最主要的就是是否对同一个对象上锁

synchronized是调用系统的api进行加锁的,系统的api本质上是靠cpu上的特定指令完成加锁的。

通过锁竞争,让第二个线程的指令无法插入到第一个线程的执行指令之间,而不是禁止第一个线程被调度出cpu.

若一个线程加锁,另一个线程不加锁,又会怎样呢?

通过结果可以看出,仅对一个线程加锁,是无法解决线程安全问题的。未加锁的线程中的count++操作,仍然会被另一个线程插队。

synhrionzed的别的加锁方式:

class Test{
    public int count1;
    synchronized  public void add(){
        count1++;
    }
}
public class Thread13 {
    //线程安全问题
    public static void main(String[] args) throws InterruptedException {
//        Object block = new Object();
        Test test = new Test();
        Thread t1=new Thread(()->{
            for(int i=1;i<=10000;i++){
                test.add();//将count++放到类的方法中,对该类进行加锁
            }
        });
        Thread t2=new Thread(()->{
            for(int i=1;i<=10000;i++){
                test.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count="+test.count1);
    }
}

或者是以这种形式:

这都是对同一个对象加锁,这等价于将锁加到方法上。类似于上面的代码

若是synchronized加到静态方法上,则相当于对类进行了加锁

后两个原因之后再解释。

关于String:

“可重入”性

若加两层锁,会怎样呢?

感觉这样写,会进入一个死等的状态,但结果:

正常输出!!!

这又是什么原因呢?

是因为加的两次锁,是在同一个线程中进行的,在第二次加锁的时候,知道该线程已经加过锁了,就不会进行阻塞,会继续执行代码,这个特性称为“可重入”

使用“可重入”性,就会避免类似上面的代码出现阻塞状态。

如果没有“可重入”性,当写的代码非常复杂时,就非常容易出现这样的阻塞状态。因为加锁的代码可能会非常隐蔽。

底层内部实现可重入行:

有一个计数器,最初为0,在第一次加锁的时候,计数器+1,同时记录是哪个线程加的锁;

在第二次加锁的时候,此时计数器为1,判定持有锁线程和加锁线程是否为同一个线程,若为同一个线程,说明该线程已经加过锁了,就不再加锁了,仅对计数器+1;若不为同一线程,则加锁线程就会进入阻塞状态。

解锁的时候,是从内层向外层以此解锁,每到 },计数器就-1,当计数器为0,就真正实现解锁了。

注意:整个过程只有一把锁,

死锁

加锁能够解决线程安全问题,但是若加锁处理不当,就可能产生死锁。

产生死锁的四个必要条件(全部具备,才会产生死锁):

1、互斥使用:一个线程获取到了这把锁,另一个线程也想获取这把锁,就进入了阻塞状态。

2、不可抢占:一个线程拿到这把锁之后,只能主动解锁,别的线程不能强行把锁抢走。

3、请求和保持:一个线程获取锁A之后,尝试获取锁B.

4、循环等待:该线程尝试获取锁,进入阻塞状态,未获取到,就一直处于阻塞状态。

死锁的三种场景:

1、一个线程,一把锁:就像上面的在一个线程内,两次获取同一把锁,若没有可重入性,则该线程就会进入死锁状态。

2、两个线程,两把锁:线程1获取到了锁A,线程2获取到了锁B;接下来,线程1尝试获取锁B,线程2尝试获取锁A。两个线程都不能获取到,都进入了阻塞状态,就产生了死锁。

3.M各线程N把锁:

最经典的问题:哲学家就餐问题:假设5个哲学家就餐,但直有5根筷子,

针对上述问题,解决死锁,有几种方法:

1、加一个筷子(加一把锁)。

2、减少一个哲学家(减少一个线程)。

3、让线程获取琐时,按规定顺序获取。(给锁编一个号,让线程从小到大获取锁)(这种方法比较常用)

4、银行家算法。(比较复杂,先不讨论)

java标准库中的线程安全类:

Java标准库中很多都是线程不安全的.这些类可能会涉及到多线程修改共享数据,⼜没有任何加锁措 施:

这些在java标准库中,都准备弃用了。

内存可见性

如果一个线程写,一个线程读,是否会引起线程安全问题呢?

在t1线程中,设置循环条件:判断flag是否被修改,在mian线程中通过控制台输入,修改flag:

输入1后,t1线程并没有按照预期结果结束执行!

说明引起了线程安全问题。

这是因为和内存可见性问题有关。

在t1线程的while循环中会执行两条核心指令:

1、load:读取内存中flag的值到cpu寄存器上。

2.字节跳转指令:将cup寄存器上的值和0进行比较。

在用户输入值之前,t1线程已经进行过多次循环了(上亿次),其中load每次从内存中读取的值都是相同的,并且load操作的开销远超过字节跳转(访问寄存器的速度远远超过访问内存)

这之后,再修改flag的值,就没有作用了。

想让自己写的代码,无论什么情况都不会出现内存可见性问题,可以用volatile关键字来修饰,这样就可以使上述优化强制关闭,保证每次循环都是从内存中读取数据的。(同时,也降低了代码执行的效率)

引入volatile关键字,相当于把主动权交给了程序员自己。


http://www.kler.cn/a/447830.html

相关文章:

  • Jenkins持续集成部署——jenkins安装
  • 车载网关性能 --- 缓存buffer划分要求
  • Unity 3D饼状图效果
  • Redis篇--常见问题篇5--热Key(Hot Key,什么是热Key,服务降级,一致性哈希)
  • 国家认可的人工智能从业人员证书如何报考?
  • Spring Cloud Gateway 源码
  • GIT与github的链接(同步本地与远程仓库)
  • 深入理解 Java 中的 ArrayList 和 List:泛型与动态数组
  • (2024.12)Ubuntu20.04安装ZED-SDK
  • 图解HTTP-HTTP报文
  • 硬盘接口模式sata与ahci区别, U盘UEFI GPT与Legacy 启动项区别,硬盘格式MBR和gpt的区别
  • JavaEE 导读与环境配置
  • 【Windows版】opencv 和opencv_contrib配置
  • 大模型+安全实践之春天何时到来?
  • CSS系列(30)-- 逻辑属性详解
  • AI 在商旅产品中的应用
  • 天地图接口Python代码详解
  • 概率论基础知识点公式汇总
  • 基于微信小程序的乡村旅游系统
  • 聊一聊 C#前台线程 如何阻塞程序退出
  • OpenAI 发布会 9 天技术总结
  • springboot中责任链模式之简单应用
  • 《开启微服务之旅:Spring Boot Web开发》(一)
  • Numpy数组索引,切片
  • 2025年西安市科技创新奖励补贴政策一览
  • Android10 rk3399 隐藏截屏功能