首页 > 编程语言 >并发编程基础

并发编程基础

时间:2024-08-18 20:37:52浏览次数:10  
标签:Thread 编程 基础 System 并发 线程 println new out

并发编程基础

什么是线程

进程是操作系统中的一个实体,是操作系统资源分配的基本单位,在Java中,一个进程必然至少有一个线程,这个线程被称为主线程。进程下的多个线程共享进程的资源。
操作系统分配CPU资源是以进程下的线程为基本单位而分配的,因为线程才是主要执行任务的。
undefined。每个线程都有一个相对应的栈区,用于存储当前线程所用到的局部变量,以及线程内函数调用的栈帧数据。
如下图所示,我们的一个main方法,输出HelloWorld会创建以下几个线程:

public class Main {
    public static void main(String[] args) {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();

        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println(threadInfo.getThreadId() + "\t" + threadInfo.getThreadName());
        }
    }
}

image

一个CPU核心只能同时运行一个线程,但是在现代CPU中由于使用了超线程技术,使得可以将一个物理核心拆成两个逻辑核心来使用,也就是一个逻辑核心可以使用一个线程。
如下图所示内核中标识的就是我们的物理核心数,而逻辑核心数则是20个。
image
在Java中,如果想要获取计算机系统中的逻辑核心数可以使用Runtime类中的availableProcessors方法获取到,如下代码所示:


public class Main {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        int i = runtime.availableProcessors();
        System.out.println(i);
    }
}

image

上下文切换

线程的创建

继承Thread

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hello world");
    }
}
public class Main {

    private static void isCompletedByTaskCount(ThreadPoolExecutor threadPool) {
        while (threadPool.getTaskCount() != threadPool.getCompletedTaskCount()) {
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

通过继承Thread类并重写run方法的方式来实现线程类的创建,在run方法中编写具体的业务代码。在main方法中创建线程对象并调用start方法就可以将线程执行起来。调用start方法之后线程处于就绪状态,如果CPU为线程分配了时间片,线程才会进行之后执行,执行完毕之后线程处于终止状态。
优点是是如果想要在run方法中获取当前线程的话,无需调用Thread.currentThread()来获取线程,直接使用this关键字就可以获取当前线程。
缺点是线程业务代码与线程耦合度较高且无非继承其他类,如果其他的业务功能需要多线程,需要重新继承Thread并重写run方法。

实现runnable

Java虽然不能不能对类进行多继承,但是可以继承多个接口。Runnable接口可以优化继承Thread无法继承其他类的,与Thread类似,在run方法中编写业务代码,然后新建一个Thread对象,并调用start方法即可,两个线程可以同时引用同一个Runnable来完成业务功能。

class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println("Hello world");
        System.out.println(this);
    }
}

public class Main {

    public static void main(String[] args) {
        Runnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();;
        Thread thread1 = new Thread(runnable);
        thread1.start();;
    }
}

futureTask

通过futureTask创建的线程可以获取线程执行后的回调数据,如下所示,定义了一个task,返回值类型为string,执行线程调用后调用FutureTask.get()方法可以获取线程的执行结果。

class CallerTask implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("call me");

        return "test";
    }
}
public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> stringFutureTask = new FutureTask<>(new CallerTask());
        new Thread(stringFutureTask).start();
        String s = stringFutureTask.get();
        System.out.println(s);
    }
}

总结

  • 继承Thread的方式可以很方便的在类中定义成员变量并使用set方法对成员变量进行设置,并在执行多线程中使用。缺点是无法实现多线程且业务代码与线程耦合严重。
  • 使用Runnable的好处是,解决了Thread无法继承的问题。缺点是大多数使用Runnable的时候是只能new Thread(() -> {}), 这种lambda表达式的方式创建对象使得匿名对象无法引用外部的非final的成员变量。如果使用实现runnable接口的方式的话就与继承Thread方式类似了。
  • 前两种创建线程的方式都无法获取到线程的回调结果,而futureTask可以获取线程的回调。

线程通知与等待

wait

监视器锁: 每个对象都有一个当线程进入被synchronized修饰的语句块,得先获取当前对象的监视器锁才能继续执行,否则一直会被阻塞。
如下所示, 以下狱中中三个线程,只有获取到a的线程锁才能执行synchronized语句内容,a的监视器锁只有一个,所以这三个线程执行是互斥的,只有一个线程执行完毕,其他线程才能继续执行。

        Integer a = 100;

        new Thread(() -> {
            synchronized (a) {
                System.out.println("thread a");
            }
        }).start();

        new Thread(() -> {
            synchronized (a) {
                System.out.println("thread b");
            }
        }).start();
        
        new Thread(() -> {
            synchronized (a) {
                System.out.println("thread c");
            }
        }).start();

只有获取到监视器锁才能调用wait方法,否则会抛出异常,在调用线程中的使用的共享变量的wait()方法时,这个线程就会被挂起,如下代码所示


        new Thread(() -> {
            synchronized (a) {
                System.out.println("Hello world");
                try {
                    a.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();

只有在其他线程中调用共享变量的notify或notifyAll方法才能唤醒线程继续执行。

        Integer a = 100;

        new Thread(() -> {
            synchronized (a) {
                System.out.println("Hello world");
                try {
                    a.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (a) {
                a.notifyAll();
            }
        }).start();

或是直接调用线程的interrupt方法直接抛出异常也会使线程中断,解除阻塞,继续往下执行。

        Integer a = 100;

        Thread thread = new Thread(() -> {
            synchronized (a) {
                System.out.println("Hello world");
                try {
                    a.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        thread.start();


        thread.interrupt();

需要注意的是, 有时会出现虚假唤醒的问题,虚假唤醒就是没有被唤醒,但是无缘无故被唤醒了,为了解决这个问题,我们需要循环使线程一直wait(), 等满足条件之后再由其他线程进行唤醒操作。

synchronized(a) {
    while (xxx == ture) {
        a.wait();
    }
}

如下代码所示,创建两个线程,分别是消费者和生产者,如果队列满了生产者会挂起当前线程,如果队列空了消费者会挂起当前线程。

        Queue<String> queue = new ArrayDeque<>();

        AtomicInteger i = new AtomicInteger();

        // 生产者线程
        new Thread(() -> {
            synchronized (queue) {
                while (true) {
                    // 如果队列满了则阻塞该线程
                    while (queue.size() >= 50) {
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    i.getAndIncrement();
                    queue.add(i.get() + "");
                    System.out.println("生产者生产数据");
                    // 如果未满则释放监视器锁供其他线程使用
                    queue.notify();
                }
            }

        }).start();

        // 消费者线程
        new Thread(() -> {
            while (true) {
                // 获取贡献变量queue的监视器锁
                synchronized (queue) {
                    // 如果队列为空则阻塞该线程
                    while (queue.isEmpty()) {
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    // 如果线程不为空则释放共享变量的监视器锁
                    queue.notify();
                    String poll = queue.poll();
                    System.out.println("消费者消费数据," + poll);
                }
            }
        }).start();

调用共享变量的wait方法只会释放当前当前变量的锁,其他变量的锁则依旧保留,如下所示,线程A中拿到了资源A和资源B的锁,只会调用资源A的wait方法释放了资源A的锁,资源B的锁并未释放,所以线程B获取资源B的锁会一直阻塞。

    private static volatile Object resourceA = new Object();
    private static volatile Object resourceB = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            // 获取资源A的锁
            synchronized (resourceA) {
                System.out.println("thread get A lock.");
                synchronized (resourceB) {
                    System.out.println("thread get B lock");
                    try {
                        // 释放资源A的锁 但未释放资源B的锁
                        resourceA.wait();
                        System.out.println("thread B relase A lock");
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });

        Thread t2 = new Thread(() -> {
            // 休眠一秒 避免线程2先执行
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 由于资源B的锁未释放,所以进入不到语句中
            synchronized (resourceB) {
                System.out.println("get B lock");
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }

如果直接调用interrupt来打断等待状态的线程则会抛异常并直接返回。

    private static volatile Object resourceA = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("begin");
            synchronized (resourceA) {
                try {
                    resourceA.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("end");
            }
        });

        thread.start();

        thread.interrupt();
    }

img

notify

唤醒调用共享变量的wait方法的线程,由于一个共享变量可以被多个线程引用,所以具体唤醒哪个线程,这个是随机的。调用notify方法的前提是,获取到该贡献变量的监视器锁。被唤醒的线程也不一定立即能执行,首先得等调用notify方法的线程释放锁,其次得和其他使用到共享变量锁的线程竞争成功才可以执行。

notifyAll

唤醒所有被共享变量等待的线程,如下所示线程1和线程2获取到资源A的锁之后进行等待,在线程3中将线程1和线程2全部唤醒。

    private static volatile Object resourceA = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (resourceA) {
                try {
                    System.out.println("get a lock begin");
                    resourceA.wait();
                    System.out.println("get a lock end");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (resourceA) {
                try {
                    System.out.println("get b lock begin");
                    resourceA.wait();
                    System.out.println("get b lock end");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        Thread t3 = new Thread(() -> {
            synchronized (resourceA) {
                resourceA.notifyAll();
            }
        });

        t1.start();
        t2.start();

        Thread.sleep(1000);
        t3.start();

        t1.join();
        t2.join();
        t3.join();

    }

join

如果调用了线程的join方法,那么将会阻塞,等待线程执行完毕才会继续向下执行,如下代码所示,三个线程都调用了join方法,等待三个线程执行完毕才会输出最后一句。

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("线程1");
        });


        Thread t2 = new Thread(() -> {
            System.out.println("线程2");
        });


        Thread t3 = new Thread(() -> {
            System.out.println("线程3");
        });

        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println("joined");
    }

线程停止

  1. 业务代码执行完成
  2. 业务代码出现异常
  3. 调用中断方法
    调用中断方法时,通知线程当前线程要中断,具体是否中断,还要看实际情况,不是一点会发生中断。为了避免直接调用stop方法引发的一系列问题,那么可以使用线程中断来保证线程可以正常停止。
    当线程需要停止时,调用线程的interrupt方法发送中断信号,将中断标志位设置成ture;当中断业务操作执行完成时调用,如下代码所示,在main方法中调用调用t1线程的中断方法,然后在具体的业务处理中判断当前线程是否发生中断,如果发生中断则进行业务处理,可以在业务处理中将线程获取的资源释放掉,这样就解决了直接stop无法释放资源的问题了。
package com.lyra;

import javax.imageio.plugins.tiff.TIFFImageReadParam;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;

public class Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // 系统业务代码
            System.out.println("Hello world");

            // 获取当前线程
            Thread thread = Thread.currentThread();
            while (!thread.isInterrupted()) {
                System.out.println(thread.getName() + "\t" + thread.isInterrupted());
            }
            // 处理中断 关闭资源等等
            System.out.println(thread.getName() + "\t" + thread.isInterrupted());

        });

        t1.start();

        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        t1.interrupt();
    }
}

需要注意的是,不建议使用取消标志位来中止线程,如下代码所示,在线程类中有个cancel方法,当cancel为true就表示发生中断了,然后可以进行业务处理,但是这种方法如果在线程中sleep的话,没办法感知到,只能等待线程下次唤醒才能处理中断,这样的中断不及时。

package com.lyra;

import javax.imageio.plugins.tiff.TIFFImageReadParam;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;

public class Main {
    public static void main(String[] args) {
        Runnable helloWorld = new Runnable() {
            public void setCancel(Boolean cancel) {
                this.cancel = cancel;
            }

            public Boolean getCancel() {
                return cancel;
            }

            private Boolean cancel = true;

            @Override
            public void run() {
                System.out.println("Hello World");
                try {
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                while (cancel) {
                    System.out.println("处理异常");
                    cancel = false;
                }
            }
        };

        Thread t1 = new Thread(helloWorld);

        t1.start();



    }
}

而使用interrupt方法时,如果线程中中断了且睡眠了,则会直接幻想抛出异常,这样就保证了异常中断的及时处理。

package com.lyra;

import javax.imageio.plugins.tiff.TIFFImageReadParam;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;

public class Main {
    public static void main(String[] args) {
        Runnable helloWorld = new Runnable() {
            public void setCancel(Boolean cancel) {
                this.cancel = cancel;
            }

            public Boolean getCancel() {
                return cancel;
            }

            private Boolean cancel;

            @Override
            public void run() {
                System.out.println("Hello World");
                Thread thread = Thread.currentThread();
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                while (!thread.isInterrupted()) {
                    System.out.println(thread.getName() + "\t" + thread.isInterrupted());
                }
                System.out.println(thread.getName() + "\t" + thread.isInterrupted());
            }
        };

        Thread t1 = new Thread(helloWorld);

        t1.start();

        t1.interrupt();
    }
}

image

  1. 手动停止线程
    线程类提供了stop对象,但是已经过时了。
    过期原因官方文档: https://docs.oracle.com/en%2Fjava%2Fjavase%2F11%2Fdocs%2Fapi%2F%2F/java.base/java/lang/doc-files/threadPrimitiveDeprecation.html
    当执行stop方法后,操作系统会将线程进行中止,如果占用的资源未被释放则会导致资源异常。所以方法就被弃用了。

标签:Thread,编程,基础,System,并发,线程,println,new,out
From: https://www.cnblogs.com/lyraHeartstrings/p/17863240.html

相关文章

  • Java基础语法学习笔记
    1.注释:在程序中用于解释说明的文字,不影响程序执行使用快捷键也可以进行注释2.字面量:数据在程序中的书写格式,字面量可以理解为值特殊的字符:\n,\t3.变量:变量是用来记住程序要处理的数据。(其实可以理解为一个装数据的盒子)(1)定义格式:数据类型变量名称=数据(2)变量......
  • c++--基础语法
    frompixiv参考博客ChatgptC++基础-知识点修饰符const在C++中,const关键字用于定义不可修改的变量、指针、函数参数和返回值等。它可以增强代码的安全性和可读性,防止意外修改数据。1.常量变量使用const定义的变量是不可更改的常量。一旦赋值,就不能再修改。cons......
  • 028、Vue3+TypeScript基础,使用路由功能实现页面切换效果
    01、在main.js中引入路由并使用路由,代码如下://引入createApp用于创建Vue实例import{createApp}from'vue'//引入App.vue根组件importAppfrom'./App.vue'//引入路由importrouterfrom'./router'constapp=createApp(App);//使用路由app.use(router);//App......
  • 光纤基础 -- ITU-T G.65X单模光纤
            ITU-T定义了7种通信光纤:G.651~G.657,其中G.651是多模光纤,G.652至G.657都是单模光纤,本文介绍传送网相关的光纤以及应用场景。ITU-TG.65X单模光纤子类ITU-TG.652ITU-TG.652.A,ITU-TG.652.B,ITU-TG.652.C,ITU-TG.652.DITU-TG.653ITU-TG.653.A,ITU-TG......
  • 高并发内存池项目
    高并发内存池项目文章目录高并发内存池项目项目介绍技术栈需求浅谈池化技术内存池重新认识malloc和free第一步!设计一个定长内存池重要原理高并发内存池整体框架CurrentMemoryPool主要由三层结构组成ThreadCacheCentralCachePageCache多线程并发环境下,malloc和Conc......
  • 027、Vue3+TypeScript基础,使用自定义hook,把功能计算都放到hook中精简代码
    01、在view中创建myhook文件夹,并创建2个文件。usesDog.ts代码如下:import{onMounted,reactive}from"vue";importaxiosfrom"axios";exportdefaultfunction(){//抓取图片letdogList=reactive(['https://images.dog.ceo/breeds/pembro......
  • 英伟达训练营RAG机器人(基础版)
    概述:        该RAG对话机器人可利用Rag技术对使用者的提问生成答案,可以文字的形式回答提问者。同时,该机器人采用微软phi-3-small-128k-instruct的模型,其具有高性能,低延迟,低成本的特点。模型选择:     此机器人采用微软的phi-3-small-128k-instruct模型,Phi-......
  • 026、Vue3+TypeScript基础,使用async和await来异步读取axios的网络图片
    01、App.vue代码如下:<template><divclass="app"><h2>App.Vue</h2><Person/></div></template><scriptlang="ts"setupname="App">//JS或TSimportPersonfrom'./......
  • 【Linux操作系统】——操作命令基础入门
    ......
  • python基础语法01
    (1)编写环境介绍在下载安装好python后:打开命令提示符界面,进入python解释器界面(即为一个执行python代码的程序):编写python代码时,还是建议用editor编辑器或者IDE集成环境,IDE时editor的扩展,包括了autocompletion,debugging,testing等功能。这里暂时使用vscode编写学习,因为mosh......