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

JavaEE 初阶:线程(2)

等待一个线程--join()

多线程之间并发执行,随机调度,但是join()的作用可以帮助我们控制线程结束的先后顺序。

join() 的优点

虽然我们可以通过sleep休眠来控制线程结束的顺序,但是我们设置时间来进行休眠等待的时候,由于无法确定时间线程执行的时间比我们设置的时间的长和短,导致出现没有必要的等待,或者线程没有完成,休眠会指定CPU等待的时间,导致CPU时间的浪费,而join()只会使当前的线程阻塞,直到任务的完成。

public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("结束");
        });
        t.start();
        //意思为等到thread全部执行完,才开始执行main
        t.join();

        //得到一定的时间如果还没有等到thread执行完的话,就执行main
        //t.join(2000);

        System.out.println("main");
        //Thread.sleep(3000);

    }
}

我们可以观察一下线程

在main线程中t.join意思是让main线程等t线程先结束,当执行到t.join()使,main线程就会阻塞等待,一直到t线程执行完毕,join才会继续执行。

注:如果t线程一直不结束,join就会一直不执行

所以我们就要进行优化,给join加上最大的等待时间

如果2000毫秒之后,t线程还没有结束的话,那么就不进行等待开始执行main线程,(2000)就是最大的等待时间。

获得当前线程的引用

public static Thread currentThread()-获得当前线程的引用

 public class ThreadDemo {
 public static void main(String[] args) {
 Thread thread = Thread.currentThread();
 System.out.println(thread.getName());
 }
}

休眠当前的线程

在设置休眠的过程中,我们需要保证的一点是我们设置的休眠时间,实际上是要大于参数设置的休眠时间的。

实质:代码调用sleep,相当于让出了CPU资源,等到时间到了的时候,需要操作系统内核,把线程重新调度到CPU上,才能继续执行。

sleep(0)的作用:

让出了当前的线程,放弃了CPU资源,等待操作系统重新调度。(把CPU让出,给别人更多的执行机会)

进程状态

1.就绪   2.阻塞

NEW:表示安排了工作还未开始执行。(new了Thread对象,还没start)

TERMINATED:工作完成了(内核中的线程已经结束了,但是Thread对象还在)

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

  • 指定时间阻塞
  • 线程阻塞(不参与CPU调度,不继续执行了)
  • 阻塞的时间是有上线的
public class Demo13 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        System.out.println(t.getState());
        t.start();
        Thread.sleep(500);
        System.out.println(t.getState());

    }
}

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

        就绪

  • 线程正在CPU上执行   
  • 线程随时去CPU上执行

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

  • 死等,没有超过时间的阻塞等待

 

多线程带来的风险-线程安全

main 在t1.join处阻塞,等t1结束,在t2.join处阻塞,等t2结束,main继续执行。

最终打印的值,使t1和t2都执行完的值。

public class Demo15 {
    public static int count;
    public static void main(String[] args) throws InterruptedException {
        Object locker1=new Object();
        Object locker2=new Object();
        Thread t1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });

        Thread t2=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

//        t1.start();
//        t1.join();
//        t2.start();
//        t2.join();

        System.out.println(count);
    }
}

运行的结果

我们可以发现每次运行的结果都是不一样的。

这样的代码。很明显就是有bug,是多线程引起的bug,这样的bug,就称为“线程安全问题”

如果我们把两个线程改为串行执行的话

代码运行结果与预期效果相符,所以很明显,当前的bug是由于多线程并发执行代码引起的bug,这样的bug,我们称为线程安全问题,或者是线程不安全。

我们可以站在CPU执行指令的角度上

  • load,把内存中的值(count变量),读取到CPU寄存器上
  • add,把指定寄存器中的值,进行+1操作(结果还是在这个寄存器中)
  • save,把寄存器中的值,写回到内存中。

操作系统对线程的调度是随机的

我们可以画几种情况(但是不只是这几种情况,还有别的情况)

 

注:实际上每个线程调走,都可能有其他的更多的线程,甚至是背的进行的线程占用cpu执行

线程安全问题的产生原因

1.根本原因:操作系统对于线程的调度是随机的,抢占式执行

2.多个线程同时修改同一个变量

3.修改的操作不是原子的

4.内存可见性问题。引起的线程不安全

5.指令的重排序,引起的线程不安全

通过加锁操作,让不是原子的操作,打包成一个原子的操作。

对于上面的代码,我们可以进行加锁的操作,count++之前,先进行加锁,然后进行count++,计算完毕之后,再进行解锁。

加锁/解锁本身都是操作系统的api,很多操作系统都对这样的api进行了封装

synchronized关键词

Java中采用synchronized这样的关键字,搭配代码块,来实现类似的效果

注:在Java中,任何一个对象都可以用做"锁"

主要的是,是否有多个对象在竞争一个锁

public class Demo15 {
    public static int count;
    public static void main(String[] args) throws InterruptedException {
        Object locker1=new Object();
        Thread t1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (locker1){
                    count++;
                }
            }
        });

        Thread t2=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (locker1){
                    count++;
                }
            }
        });
        t1.start();
        t1.join();
        t2.start();
        t2.join();



        System.out.println(count);
    }
}

注:两个线程争对同一个对象加锁,才会产生互斥的效果(一个线程加上锁了,另一个线程就得阻塞等待,等到第一个线程释放锁,才有机会进行加锁)

可以使用synchronized的修饰方法

class Counter{
    private int count;
    public /*synchronized*/ void add(){
        count++;
    }
    public int get(){
        return count;
    }
    public synchronized void fun(){//加锁
        //synchronized (Counter.class);
    }//解锁(只要是出了大括号)
}
public class Demo18 {
    public static void main(String[] args) throws InterruptedException {
       Counter counter=new Counter();
       Object locker=new Object();
        Thread thread=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    counter.add();
                }
            }
        });

        Thread thread2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    counter.add();
                }
            }
        });
        thread.start();
        thread2.start();
        thread.join();
        thread2.join();
        System.out.println("count="+counter.get());
    }
}

synchronized修饰普通方法,相当于给this加锁

synchronized修饰静态方法,相当于给类对象加锁 

可重复锁

class Counter2{
    private int count;

    public void add(){
        count++;
    }
    public int get(){
        return count;
    }
}
public class Demo19 {
    public static void main(String[] args) throws InterruptedException {
        Counter2 counter2=new Counter2();
        Thread t=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (counter2){
                    synchronized (counter2){
                        synchronized (counter2){
                            counter2.add();
                        }
                    }
                }
            }
        });

        t.start();
        t.join();

        System.out.println("counter2="+counter2.get());
    }
}

阻塞等待: 

一旦方法调用的太深就会出现阻塞等待,想要解除阻塞,就需要往下执行,等到第一次的锁被释放,这样的问题,称为"死锁"。

为了解决上述的问题,Java的synchronized就引入可重入的概念(当线程针对一个锁,加锁成功之后,后续该线程再次针对这个锁进行加锁,不会触发阻塞,而是直接的往下走)

可重入锁的原理,关键是让锁对象内部保存,当前是哪个线程持有这把锁,后续的有线程争对这把锁加锁的时候,对比一下,是否是同一个锁。

关于死锁的出现可能

一个线程,一把锁,连续的加锁两次。

两个线程两把锁,每一个线程获得一把锁之后,都尝试获得对方的锁

//死锁
//eg:线程1和线程2都有自己的一把锁,但是都想去争对方的锁,导致死锁
public class Demo20 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1=new Object();
        Object locker2=new Object();
        Thread t1=new Thread(()->{
            try {
                Thread.sleep(1000);
                synchronized (locker1){
//                    synchronized (locker2){
//                        System.out.println("线程1争抢线程2的锁成功");
//                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //把嵌套的锁,改为并列的锁,就可以避免死锁
            synchronized (locker2){
                System.out.println("线程1争抢线程2的锁成功");
            }
        });

        Thread t2=new Thread(()->{
            try {
                Thread.sleep(1000);
//                synchronized (locker2){
//                    synchronized (locker1){
//                        System.out.println("线程2争抢线程1的锁成功");
//                    }
//                }
                //约定好加锁的顺序,就可以避免死锁
                synchronized (locker1){
                    synchronized (locker2){
                        System.out.println("线程2争抢线程1的锁成功");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
    //由于发生了死锁,所以无法成功输出
}

注:如果不加sleep(),很可能t1把locker1和locker2都拿到了,t2还没开始拿呢。

死锁的第三种情况,哲学家就餐问题

构成死锁的四个必要条件

1.锁是互斥的(锁的基本性质),一个线程拿到锁之后,另一个线程尝试拿到锁,必须要阻塞等待

2.锁是不可以抢占的,线程1拿到锁,线程2想要拿到锁,必须阻塞等待。

3.请求和保持(一个线程拿到锁1之后,不释放锁1,就去拿锁2)

4.循环等待,多个线程,多把锁之间的等待过程,构成了循环等待。(约定好加锁的顺序就可以破除循环等待了)

内存的可见性

造成线程安全的问题之一

public class Demo21 {
    private   static int flg=0;
    public static void main(String[] args) throws InterruptedException {

        Thread t1=new Thread(()->{
            while (flg==0){
            }
            System.out.println("线程结束");
        });


        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入flg的值");
            flg=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

运行结果 

发现没有执行到   System.out.println("线程结束");

很明显,这也是一个bug,线程的安全问题。

产生的原因:编译器会对代码进行自动的优化,对你的代码进行调整,是程序效率更高,但是虽然是声称优化操作,保证逻辑不变,但是尤其在多线程的程序中,编译器的判断可能会出现错误,导致优化后的逻辑,与实际的逻辑发生偏差。

修改的方式引入sleep,直接占用1ms,此时优化flag无足轻重

也可以使用volatile关键词

通过这个关键词的来修饰某个变量,编译器对这个变量的读取操作,都不会优化成读寄存器。

public class Demo21 {
    //采用关键字volatile
    private  volatile static int flg=0;
    public static void main(String[] args) throws InterruptedException {

        Thread t1=new Thread(()->{
            while (flg==0){
                //sleep也可以解决优化的问题,但是很影响效率
//                try {
//                    Thread.sleep(1000);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
            }
            System.out.println("线程结束");
        });


        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入flg的值");
            flg=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

成功运行 

希望能对大家有所帮助!!!!


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

相关文章:

  • 【情感】程序人生之情感关系中的平等意识(如何经营一段长期稳定的关系 沸羊羊舔狗自查表)
  • 数据分享:空气质量数据--哈尔滨
  • SpringBoot中实现拦截器和过滤器
  • Springboot 下载附件
  • DeepSeek v3为何爆火?如何用其集成Milvus搭建RAG?
  • 设计形成从业务特点到设计模式的关联
  • RabbitMQ的常见面试题及其答案的总结
  • 美团商家端 字符验证码 分析
  • 使用npm 插件[mmdc]将.mmd时序图转换为图片
  • VuePress2配置unocss的闭坑指南
  • 适配器模式(类适配器,对象适配器)
  • 高频java面试题
  • 用语言模型 GLM-Zero-Preview 来驱动战场推演
  • 数据挖掘——支持向量机分类器
  • Centos源码安装MariaDB 基于GTID主从部署(一遍过)
  • Redis面试相关
  • vue2框架配置路由设计打印单
  • 【Axios使用手册】如何使用axios向后端发送请求并进行数据交互
  • 利用PHP爬虫获取1688按关键字搜索商品:技术解析与实践指南
  • 【C语言程序设计——循环程序设计】枚举法换硬币(头歌实践教学平台习题)【合集】
  • 【HTTP和gRPC的区别】协议类型/传输效率/性能/语义/跨语言支持/安全性/使用场景/易用性对比
  • Kafka详解 ③ | Kafka集群操作与API操作
  • 常用的聚合函数
  • TCPDump参数详解及示例
  • 组合模式——C++实现
  • UniApp | 从入门到精通:开启全平台开发的大门