探索Java多线程的核心概念与实践技巧,带你从入门到精通!

各位看官早安午安晚安呀

如果您觉得这篇文章对您有帮助的话

欢迎您一键三连,小编尽全力做到更好
欢迎您分享给更多人哦

今天我们来学习多线程编程-"掌握线程创建、管理与安全"

上一节课程我们铺垫了一系列的东西,引出来了我们的多线程,接下来就让小编带领大家一起进入多线程的世界吧!!!

目录

上一节课程我们铺垫了一系列的东西,引出来了我们的多线程,接下来就让小编带领大家一起进入多线程的世界吧!!!

1.创建线程的方法:

1.1通过继承Thread类,然后重写run()方法

1.2.实现Runnnable接口,重写run方法

1.3.二者区别

2.(通过jconsole观察进程里面的多线程情况)

2.1.首先我们要知道每一个线程都是独立的执行流

2.1.jconsole

3.Thread的一些其他方法

4.中断一个线程(interrupt)

4.1.手动设置标志位

4.2.Thread内置的标志位

5.线程等待(join方法)

6.线程状态

7.线程安全问题(最重要)

8.synchronized(原子性)


1.创建线程的方法:

线程实话说是操作系统的概念,**程序员想要操作线程肯定就需要操作系统提供给我们一些API,**但是不同的操作系统提供的API又不同 (实话说这就让小编想起了这个)

=> java就针对上述的系统API进行了封装(跨平台嘛,(^-^)V 我们java太厉害啦)

=> 我们程序员只需要了解这一套API就够啦~~

java提供给我们的API就是Thread类,我们创建Thread对象就可以操作系统内部的线程啦

老规矩:学习一个类,先看他的构造方法

我们大概先学这几种,还有一种线程分组的,小编现在也不了解,后续再给大家介绍吧~~

Runnable表示一个 以运行的任务而已,这个任务是交给线程执行还是其他用图我们不关心

1.1通过继承Thread类,然后重写run()方法

java 复制代码
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("我创建的一个新线程"); 
        //  这个run方法里面描述了我这个线程要干啥
    }
}

这是我们自己创建的线程,但是一个java程序跑起来, 还有一个跟随进程一起创建的线程,这个线程叫做主线程,这个主线程的入口方法是main方法。

这个run方法呢是我们创建的这个线程的入口方法!!!

一个程序跑起来,从哪个方法开始执行,哪个方法就是他的入口方法。

run方法是入口方法没有错,但是这个线程我们想要跑起来,肯定要启动呀

java 复制代码
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("我创建的一个新线程");
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();

        t.start();  // 启动线程,start方法才是真正的调用系统的API ,
        // 创建一个线程然后这个线程通过run方法跑起来
        System.out.println("这就是主线程");
        
    }

1.2.实现Runnnable接口,重写run方法

java 复制代码
class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("我实现Runnable,重写run方法实现的任务");
        }
    }
}


public class Test {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();

        while (true) {
            System.out.println("主线程");
        }
    }

}

当然我们通过匿名内部类lambda表达式 实现也完全没问题(上面我们继承Thread类重写run方法当然也可以使用匿名内部类的方式,但是lambda表达式不可以 (函数式接口定义(就是lambda表达式):一个接口有且只有一个抽象方法))

java 复制代码
Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("通过匿名内部类实现");
            }
        });
        Thread t2 = new Thread(() -> {
            System.out.println("通过lambda表达式实现");
        });

1.3.二者区别

首先我们要明确创建线程的两个关键操作

1.首先要明确要执行的任务(就是想要通过这个线程干啥)

2.调用系统API创建出线程。

好,现在我们就可以明确知道他俩的区别了,耦合性不同

1.第一种方法(继承Thread的)把任务嵌套在了线程里面(后面想要修改任务,就要修改线程的源代码(大工程))

2.第二种方法:**我要让线程执行其他任务,直接构造方法变成其他任务就好了。**并且我这个任务又不是只给一个线程使用,其他线程想用的话直接传过去就行了。

总之:把任务分离出来,耦合性更低了,效率更高!!!

2.(通过jconsole观察进程里面的多线程情况

2.1.首先我们要知道每一个线程都是独立的执行流

就拿这个代码来说(但是这个代码打印的太快了不好观察,我们可以让他休眠一下,慢一点打印)

java 复制代码
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("我创建的一个新线程");
    }
}

public class Test {
    public static void main(String[] args){
        Thread t = new MyThread();
        t.start(); 

        while (true) {
            System.out.println("主线程");
        }
    }

休眠的方法:sleep方法是Thread类的静态方法**(这个异常的处理很有讲究的,大家尽量不要犯这样的错误呀)**

java 复制代码
class MyThread extends Thread{
    @Override
    public void run() {
        while(true){
            try {
                System.out.println("我创建的一个新线程");
                Thread.sleep(1000);//  sleep 这里就是一个类方法,我们直接调用就好了
                //  这里不能抛出异常,如果子类方法抛出额外的异常,调用者(可能只了解父类方法)可能不知道如何正确处理这些新的异常。
                //这里 父类的 run方法并没有抛出异常
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();

        while (true) {
            Thread.sleep(1000);
            System.out.println("主线程");
        }
    }

可以看到两个线程的正在交替打印日志,并且不是一种规律进行打印。

你俩都是休眠1000ms,休眠后并且你你俩谁先执行都不一定(随机的),这个取决于操作系统对于调度器的的具体实现。

通过并发执行,更加充分的的利用CPU资源

2.1.jconsole

jconsole(观察多线程属性的一种方式,还有IDEA的一种一个)

jdk里面的一个可执行程序,jdk的位置:

如何启动:

1.启动之前保证idea的程序已经跑起来了

2.有的兄弟需要管理员方式运行(正常情况不行的话)

=>

3.Thread的一些其他方法

java 复制代码
    public static void main(String[] args) throws InterruptedException {
       Thread t = new Thread(() -> {
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               throw new RuntimeException(e);
           }
           System.out.println("创建的线程");
       },"主线程");


        System.out.println(t.getId());  //java 给这个线程分配的id
        System.out.println(t.getName()); // 我设置的线程名字  -> 主线程
        System.out.println(t.isDaemon()); // 后台线程? false
        System.out.println(t.isAlive());// 线程是否存活  false 还没开始运行
        t.setDaemon(true);  // 设置t线程为后台线程
        //这四行一下就执行完了
        t.start();
        System.out.println(t.isAlive());  //true ,不过就存活这一下就结束了,其他线程都结束了
        //后台线程肯定结束了
        // 最后发现没打印创建的线程
    }

4.中断一个线程(interrupt)

java里面就是让一个线程快点结束(而不是直接让一个线程中间断掉(这样一个线程会导致残留数据,不太科学))

4.1.手动设置标志位

我们用一个isQuit作为标志位,让主线程5秒后修改isQuit的值

java 复制代码
 public static  boolean isQuit = false;  // 类属性
    public static void main(String[] args) throws InterruptedException {
        //这里就涉及到了变量的捕获的语法,但是
        Thread t = new Thread(() ->{
            while(!isQuit){
                //我这里捕获到的其实是拷贝外部变量的一份
                System.out.println("我新创建的一个线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("线程工作完毕");
        },"t线程");

        t.start();

        Thread.sleep(5000); // 五秒之后设置isQuit = true

        isQuit = true;

    }

给大家提一个问题,毕竟当时小编在写这个代码的时候也忘了

这里不涉及是涉及匿名内部类的变量捕获吗?为什么这里不报错呢?

答:这个时候isQuit是成员变量,此时lambda访问这个成员,就不再是变量捕获的语法了,变成了"内部类访问外部类"的语法,就没有final(实际上没有被修改过的也算)的限制了

另外: 对于基本数据类型,捕获的是它们的值的副本(实际上就是复制了一份)

如果是局部变量的话就会报错,因为修改了

如果一个局部变量不能解决问题就可以考虑把这个局部变量换成成员变量

但是这样写还是不太舒服

1.还要我们自己创建

2.并且如果另一个线程正在sleep就不能够及时响应

java有没有已经设置好的标志位呢?当然!!!

4.2.Thread内置的标志位

t.interrupt的作用:

java 复制代码
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("我新建的一个线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                   e.printStackTrace();   //  这里扔出一个异常,也仅仅只是打印出异常的位置信息而已,也没真正的处理什么
                }
            }
            System.out.println("我的循环即将终止");
        });
        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("让线程终止");
        t.interrupt();// 把线程内部的标志位设置为true
    }
    

我们发现线程确实还是在运行,一张图搞懂

还有一个静态方法,那大家都用的是一个标志位实在是不太科学(但是如果是你想让两个东西抵消好像还可以)(先记住吧)

5.线程等待(join方法)

线程等待:一个线程等待另一个线程结束再执行**(控制线程结束的顺序)**

java 复制代码
    public static void main(String[] args) throws InterruptedException {  //线程阻塞抛出的异常
        Thread t =new Thread(() ->{
            while(true){
                System.out.println("我新建的一个线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        System.out.println("等待线程开始");
        t.join(5000);  // 让主线程等待5s,也会造成线程阻塞,也要抛出异常
        System.out.println("join 等待结束");
    }
java 复制代码
   public static void main(String[] args) throws InterruptedException {
       Thread t = new Thread(() -> {
           System.out.println("我新建的一个线程");
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               throw new RuntimeException(e);
           }
       });

       long t1 = System.currentTimeMillis();
       t.start();
       t.join();  // 也会造成线程阻塞,也要抛出异常
       long t2 = System.currentTimeMillis(); // 调度,还有创建线程的开销
       System.out.println(t2 - t1);  //
   }

6.线程状态

java 复制代码
    public static void main(String[] args) {
        for(Thread.State state : Thread.State.values()){
            System.out.println(state);
        }
    }
java 复制代码
//观察状态
   public static void main(String[] args) throws InterruptedException {
       Thread t = new Thread(() -> {
               try {
                   Thread.sleep(1000);
                   System.out.println("11111111");
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
       });
       System.out.println(t.getState()); //  NEW
       t.start();

       System.out.println(t.getState()); //RUNNABLE
       Thread.sleep(500);
       System.out.println(t.getState()); //TIMED_WAITING: 由于sleep这种固定时间的方式产生的阻塞

       System.out.println(t.getState());  // terminated,Thread对象还在,但是我创建的对象已经跑完了
   }

7.线程安全问题(最重要)

简述:

单个线程下执行可以,但是多个线程执行就出现bug,这种就是线程安全问题

(写出的代码和预想的结果不同就是bug!!!)

我们先看一个代码:两个线程同时修改一个静态成员变量

java 复制代码
   public static int count;
    public static void main(String[] args) throws InterruptedException {
        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();
        System.out.println(count);

    }

竟然不是10000

但是我们修改一下代码:结果就是10000了

java 复制代码
   public static int count;
    public static void main(String[] args) throws InterruptedException {
        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();
        t1.join();
        t2.start();
        t2.join();
        System.out.println(count);

    }

到底是什么原因呢?

count++这个操作其实是分成三步进行的(CPU通过三个指令来实现的)

这种也可能(所以说这排列组合是无数种)

归根到底:

这是因为线程的抢占式运行,两个线程不是往上累加而是独立运行。

如果这个count++变成一条指令(原子的),那么线程随便执行都没问题了

所有的都变成了样子,这个时候我们就想到给这个count++进行加锁,把他变成一条指令!!!

8.synchronized(原子性)

我们就需要使用synchronized关键字给一个代码块进行加锁

java 复制代码
   public static int count;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 5000; i++){
                synchronized (locker){
                    count++;
                }
                //synchronized,
                // 我们把要加锁的代码放在这个代码块里面就行了
                //我们如果对两个线程加相同的锁,就会造成"锁竞争/锁冲突",毕竟我们就只有这一把锁,线程2必须等线程1解锁然后再加锁

                //    由于我们是对一个变量进行操作,我们还是尽量加同一把锁,两把锁,还是会并发执行
                //导致:那么一个线程对 count 的修改可能不会被另一个线程立即看到.

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

之后就变成这么执行了,变成了串行执行

t2线程由于锁的竞争(拿不到locker的监视器锁)就只能阻塞等待,一直等待到t1线程unlock之后才能获得这把锁,这样就避免了load,add,save的穿插执行。

如果我们两个线程分别给两个线程进行加锁,就还是会穿插执行。因为没有了锁竞争!!!

后面的线程安全问题后面再讲吧~~~(上一次看铠甲勇士还是在上一次~~~哈哈哈)

上述就是进程的"黑匣子":PCB如何记录着任务的一生

的全部内容了,线程的出现,我们的效率又得到了很大的提升~~~,但是管理和安全也是一个很大的问题。预知后事如何,请听下回分解~~~

能看到这里相信您一定对小编的文章有了一定的认可。

有什么问题欢迎各位大佬指出
欢迎各位大佬评论区留言修正~~
您的支持就是我最大的动力​​​!!!

相关推荐
白晨并不是很能熬夜33 分钟前
【JVM】字节码指令集
java·开发语言·汇编·jvm·数据结构·后端·javac
火烧屁屁啦38 分钟前
【JavaEE进阶】Spring AOP详解
java·spring·java-ee
卡布奇诺-海晨1 小时前
JVM之Arthas的dashboard命令以及CPU飙高场景
java·spring boot
学c真好玩1 小时前
Spring
java·后端·spring
沉默王二1 小时前
更快更强!字节满血版DeepSeek在IDEA中真的爽!
java·前端·程序员
2301_807449201 小时前
字符串相乘——力扣
java·算法·leetcode
小五Z1 小时前
RabbitMQ高级特性--消息确认机制
java·rabbitmq·intellij-idea
Kevinyu_1 小时前
Maven
java·maven
nickxhuang1 小时前
【基础知识】回头看Maven基础
java·maven
日月星辰Ace2 小时前
jwk-set-uri
java·后端