精华内容
下载资源
问答
  • 多线程高并发】-为什么要使用多线程?创建多少个线程合适呢? 我们为什么要选择多线程?大多数我们选择多线程的原因就是因为快。但 并发场景在所有场景下都是快的吗? 这个快能否度量呢? 使用多线程就是在正确...

    【多线程高并发】-为什么要使用多线程?创建多少个线程合适呢?

    我们为什么要选择多线程?大多数我们选择多线程的原因就是因为快。但

    • 并发场景在所有场景下都是快的吗?
    • 这个快能否度量呢?

    使用多线程就是在正确的场景下通过设置正确的线程来最大化程序的运行速度。在硬件级别就是充分利用CPU和I/O的利用率。

    在具体场景下,通过是以下两个场景:

    • CPU密集型程序;
    • I/O密集型程序;

    一、CPU密集型程序

    一个完整请求,I/O操作可以在很短时间内完成,但是CPU还有很多运算要处理,也就是说CPU计算的比例占很大一部分。

    比如我们要计算1+2+…100亿的总和,很明显,这就是一个CPU密集型程序。

    假如我们在【单核】CPU下,如果我们创建4个线程来分段计算,即:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Po2zyRm-1617943429303)(multi_thread\00/1.png)]

    由于是单核CPU,所有线程都在等待CPU时间片。按照理想情况来看,四个线程执行的时间总和与一个线程独自完成是相等的,实际上我们还忽略了四个线程上下文切换的开销;

    综上,单核CPU处理CPU密集型程序,这种情况并不适合使用多线程。

    此时如果在4核CPU下,同样创建四个线程来分段计算,看看会发生什么?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UgXGIfRf-1617943429308)(multi_thread\00\2.png)]

    每个线程都有CPU来运行,并不会发生等待CPU时间片的情况,也没有线程切换的开销。理论情况来看效率提升了4倍。

    如果是多核CPU处理CPU密集型程序,完全可以最大化利用CPU核心数,应用并发线程来提高效率。

    二、I/O密集型程序

    与CPU密集型程序相对,一个完整请求,CPU运算操作完成之后还有很多I/O操作要做, 也就是说I/O操作占比很大部分。

    在进行I/O操作时,CPU是空闲状态,所以要最大化利用CPU,不能让其是空闲状态,同样在单核CPU的情况下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hfJlEULM-1617943429310)(multi_thread\00\3.png)]

    从上图中可以看出,每个线程都执行了相同长度的CPU耗时和I/O耗时,如果将上面的图多画几个周期,CPU操作耗时固定,将I/O操作耗时变为CPU耗时的3倍。这时候会发现,CPU又有空闲,这是你就可以新建线程4,来继续最大化利用CPU。

    综上:

    线程等待时间所占比例越高,需要越多线程;线程CPU时间所占比较越高,需要越少线程。

    三、那创建多少个线程合适呢?

    首先是场景问题,从上面知道我们有CPU密集型和I/O密集型两个场景,不同的场景需要的线程数也就不一样。

    CPU密集型程序创建多少个线程合适呢?

    对于CPU密集型程序来说,理论上线程数量=CPU核数(逻辑)就可以了,但是实际上,数量一般会设置为CPU核数+1,为什么呢?

    计算机CPU密集型任务的线程恰好在某时因为发生一个页面错误或者因为其它原因而暂停,刚好有一个额外的线程,可以确保在这种情况下CPU周期不会中断工作。

    I/O密集型程序创建多少个线程合适呢?

    一个CPU核心的最佳线程数=(1/CPU利用率)=1+(I/O耗时/CPU耗时)。

    多个CPU核心的最佳线程数=CPU核心数*(1+(I/O耗时/CPU耗时))

    按照上面的公式,假如几乎全是I/O耗时,纯理论上就可以说是2N(N=CPU核数)。

    如果理论都好用,那就用不着实践了,也就更不会有调优的事情出现了。不过在初始阶段,确实可以按照这个理论作为伪标准,毕竟差也不可能差太多,这样调优会更好一些。

    之后具体调优阶段可以利用一些工具SkyWalkingCATzipkin等调优工具来知道具体的I/O耗时和CPU耗时,以及CPU利用率。

    四、线程场景有关问题

    一、小问一

    假设要求一个系统的TPS(Transaction Per Second或者Task Per Second)至少为20,然后假设每一个Transaction由一个线程完成,继续假设平均每个线程处理一个Transaction的时间为4s。

    如何设计线程个数,使得其可以在1s内处理完这20个transaction?

    因:一个线程一个transaction时间为4s,那每秒一个线程只能处理1/4=0.25个TPS。

    所:1秒内要处理20个TPS,理论线程数=20/0.25=80个。

    但没有考虑到CPU数目,一般服务器的CPU核数为16或者32。如果有80个线程,肯定会带来太多不必要的线程上下文切换开销,这就需要调优,来达到最佳balance。

    二、小问二

    计算操作需要5ms,DB操作需要100ms,对于一台8核CPU的服务器,怎么设置线程数呢?

    线程数=8*(1+100/5)=168个。

    那么如果DB的QPS(Query Per Second)上限是1000,那么此时这个线程数又应该设置为多大呢?

    因为:1s=1000ms,当前一个任务5+100=105ms,那么一个线程每秒可以处理的任务数就是1000/105。

    那么168个线程每秒可以处理的任务数就是168*1000/105=1600QPS。

    而因为QPS的上限是1000,所以线程数就要等比减少为168*1000/1600=105个。

    同样,这也没用考虑CPU数目,接下来就是细节调优的阶段了。

    因为一次请求不仅仅包括CPU和I/O操作,具体的调优过程还要考虑内存资源、网络等具体情况。

    五、增加CPU核心一定能解决问题吗?

    即使算出了理论线程数,但实际上CPU核数不够,会带来线程上下文切换的开销。

    那么下一步就需要增加CPU核数,那盲目的增加CPU核数就一定能解决问题吗?

    有一个阿姆达尔定律,它代表了处理器并行计算后效率提升的能力。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F0p0l6Rp-1617943429314)(multi_thread\00\4.png)]

    这个公式告诉我们,假如我们的串行率是5%,那么无论采用什么技术,最高也就只能提高20倍的性能。

    临界区都是串行的,非临界区都是并行的,用单线程执行临界区的时间/用单线程执行(临界区+非临界区)的时间就是串行百分比。

    六、总结

    多线程一定就比单线程高校,比如大名鼎鼎的Redis,是基于内存操作的,这种情况下,单线程可以很高效的利用CPU。而多线程的使用场景一般是存在相当比例的I/O或网络操作。

    依据公式先计算,后续结合实际来调优(综合考虑CPU 内存 硬盘读写速度 网路状况等)

    线程一定就比单线程高校,比如大名鼎鼎的Redis,是基于内存操作的,这种情况下,单线程可以很高效的利用CPU。而多线程的使用场景一般是存在相当比例的I/O或网络操作。

    依据公式先计算,后续结合实际来调优(综合考虑CPU 内存 硬盘读写速度 网路状况等)

    盲目增加CPU核数也不一定能解决我们的问题。

    展开全文
  • java 多线程

    2021-03-15 22:44:10
    来源网址:...本文通过简单的编程示例来说明Java多线程编程多么直观。读完本文以后,用户应该能够编写简单的多线程程序。为什么会排队等待?下面的这个简单的 Java 程序完成四项不相关的任务。这样的...

    来源网址:http://developer.51cto.com/art/200907/140261.htm

    在 Java 程序中使用多线程要比在 C 或 C++ 中容易得多,这是因为Java 编程语言提供了语言级的支持。本文通过简单的编程示例来说明Java多线程编程多么直观。读完本文以后,用户应该能够编写简单的多线程程序。

    为什么会排队等待?

    下面的这个简单的 Java 程序完成四项不相关的任务。这样的程序有单个控制线程,控制在这四个任务之间线性地移动。此外,因为所需的资源 ? 打印机、磁盘、数据库和显示屏 -- 由于硬件和软件的限制都有内在的潜伏时间,所以每项任务都包含明显的等待时间。因此,程序在访问数据库之前必须等待打印机完成打印文件的任务,等等。如果您正在等待程序的完成,则这是对计算资源和您的时间的一种拙劣使用。改进此程序的一种方法是使它成为多线程的。

    四项不相关的任务

    classmyclass {

    staticpublicvoidmain(String args[]) {

    print_a_file();

    manipulate_another_file();

    access_database();

    draw_picture_on_screen();

    }

    }

    在本例中,每项任务在开始之前必须等待前一项任务完成,即使所涉及的任务毫不相关也是这样。但是,在现实生活中,我们经常使用多线程模型。我们在处理某些任务的同时也可以让孩子、配偶和父母完成别的任务。例如,我在写信的同时可能打发我的儿子去邮局买邮票。用软件术语来说,这称为多个控制(或执行)线程。

    可以用两种不同的方法来获得多个控制线程:

    多个进程

    在大多数操作系统中都可以创建多个进程。当一个程序启动时,它可以为即将开始的每项任务创建一个进程,并允许它们同时运行。当一个程序因等待网络访问或用户输入而被阻塞时,另一个程序还可以运行,这样就增加了资源利用率。但是,按照这种方式创建每个进程要付出一定的代价:设置一个进程要占用相当一部分处理器时间和内存资源。而且,大多数操作系统不允许进程访问其他进程的内存空间。因此,进程间的通信很不方便,并且也不会将它自己提供给容易的编程模型。

    线程

    线程也称为轻型进程 (LWP)。因为线程只能在单个进程的作用域内活动,所以创建线程比创建进程要廉价得多。这样,因为线程允许协作和数据交换,并且在计算资源方面非常廉价,所以线程比进程更可取。线程需要操作系统的支持,因此不是所有的机器都提供线程。Java 编程语言,作为相当新的一种语言,已将Java多线程编程支持与语言本身合为一体,这样就对线程提供了强健的支持。

    使用Java 编程语言实现线程

    Java编程语言使多线程如此简单有效,以致于某些程序员说它实际上是自然的。尽管在 Java 中使用线程比在其他语言中要容易得多,仍然有一些概念需要掌握。要记住的一件重要的事情是 main() 函数也是一个线程,并可用来做有用的工作。程序员只有在需要多个线程时才需要创建新的线程。

    Thread 类

    Thread 类是一个具体的类,即不是抽象类,该类封装了线程的行为。要创建一个线程,程序员必须创建一个从 Thread 类导出的新类。程序员必须覆盖 Thread 的 run() 函数来完成有用的工作。用户并不直接调用此函数;而是必须调用 Thread 的 start() 函数,该函数再调用 run()。下面的代码说明了它的用法:

    创建两个新线程

    importjava.util.*;

    classTimePrinterextendsThread {

    intpauseTime;

    String name;

    publicTimePrinter(intx, String n) {

    pauseTime = x;

    name = n;

    }

    publicvoidrun() {

    while(true) {

    try{

    System.out.println(name +":"+new

    Date(System.currentTimeMillis()));

    Thread.sleep(pauseTime);

    }catch(Exception e) {

    System.out.println(e);

    }

    }

    }

    staticpublicvoidmain(String args[]) {

    TimePrinter tp1 =newTimePrinter(1000,"Fast Guy");

    tp1.start();

    TimePrinter tp2 =newTimePrinter(3000,"Slow Guy");

    tp2.start();

    }

    }

    在本例中,我们可以看到一个简单的程序,它按两个不同的时间间隔(1 秒和 3 秒)在屏幕上显示当前时间。这是通过创建两个新线程来完成的,包括 main() 共三个线程。但是,因为有时要作为线程运行的类可能已经是某个类层次的一部分,所以就不能再按这种机制创建线程。虽然在同一个类中可以实现任意数量的接口,但 Java 编程语言只允许一个类有一个父类。同时,某些程序员避免从 Thread 类导出,因为它强加了类层次。对于这种情况,就要 runnable 接口。

    Runnable 接口

    此接口只有一个函数,run(),此函数必须由实现了此接口的类实现。但是,就运行这个类而论,其语义与前一个示例稍有不同。我们可以用 runnable 接口改写前一个示例。(不同的部分用黑体表示。)

    创建两个新线程而不强加类层次

    importjava.util.*;

    classTimePrinterimplementsRunnable {

    intpauseTime;

    String name;

    publicTimePrinter(intx, String n) {

    pauseTime = x;

    name = n;

    }

    publicvoidrun() {

    while(true) {

    try{

    System.out.println(name +":"+new

    Date(System.currentTimeMillis()));

    Thread.sleep(pauseTime);

    }catch(Exception e) {

    System.out.println(e);

    }

    }

    }

    staticpublicvoidmain(String args[]) {

    Thread t1 =newThread(newTimePrinter(1000,"Fast Guy"));

    t1.start();

    Thread t2 =newThread(newTimePrinter(3000,"Slow Guy"));

    t2.start();

    }

    }

    请注意,当使用 runnable 接口时,您不能直接创建所需类的对象并运行它;必须从 Thread 类的一个实例内部运行它。许多程序员更喜欢 runnable 接口,因为Java多线程编程从 Thread 类继承会强加类层次。

    synchronized 关键字

    到目前为止,我们看到的示例都只是以非常简单的方式来利用Java多线程编程。只有最小的数据流,而且不会出现两个线程访问同一个对象的情况。但是,在大多数有用的程序中,线程之间通常有信息流。试考虑一个金融应用程序,它有一个 Account 对象,如下例中所示:

    一个银行中的多项活动

    publicclassAccount {

    String holderName;

    floatamount;

    publicAccount(String name,floatamt) {

    holderName = name;

    amount = amt;

    }

    publicvoiddeposit(floatamt) {

    amount += amt;

    }

    publicvoidwithdraw(floatamt) {

    amount -= amt;

    }

    publicfloatcheckBalance() {

    returnamount;

    }

    }

    在此代码样例中潜伏着一个错误。如果此类用于单线程应用程序,不会有任何问题。但是,在多线程应用程序的情况中,不同的线程就有可能同时访问同一个 Account 对象,比如说一个联合帐户的所有者在不同的 ATM 上同时进行访问。在这种情况下,存入和支出就可能以这样的方式发生:一个事务被另一个事务覆盖。这种情况将是灾难性的。但是,Java 编程语言提供了一种简单的机制来防止发生这种覆盖。每个对象在运行时都有一个关联的锁。这个锁可通过为方法添加关键字 synchronized 来获得。这样,修订过的 Account 对象(如下所示)将不会遭受像数据损坏这样的错误:

    对一个银行中的多项活动进行同步处理

    publicclassAccount {

    String holderName;

    floatamount;

    publicAccount(String name,floatamt) {

    holderName = name;

    amount = amt;

    }

    publicsynchronizedvoiddeposit(floatamt) {

    amount += amt;

    }

    publicsynchronizedvoidwithdraw(floatamt) {

    amount -= amt;

    }

    publicfloatcheckBalance() {

    returnamount;

    }

    }

    deposit() 和 withdraw() 函数都需要这个锁来进行操作,所以当一个函数运行时,另一个函数就被阻塞。请注意, checkBalance() 未作更改,它严格是一个读函数。因为 checkBalance() 未作同步处理,所以任何其他方法都不会阻塞它,它也不会阻塞任何其他方法,不管那些方法是否进行了同步处理。

    Java 编程语言中的高级多线程支持

    线程组

    Java多线程编程是被个别创建的,但可以将它们归类到线程组中,以便于调试和监视。只能在创建线程的同时将它与一个线程组相关联。在使用大量线程的程序中,使用线程组组织线程可能很有帮助。可以将它们看作是计算机上的目录和文件结构。

    线程间发信

    当线程在继续执行前需要等待一个条件时,仅有 synchronized 关键字是不够的。虽然 synchronized 关键字阻止并发更新一个对象,但它没有实现线程间发信。Object 类为此提供了三个函数:wait()、notify() 和 notifyAll()。以全球气候预测程序为例。这些程序通过将地球分为许多单元,在每个循环中,每个单元的计算都是隔离进行的,直到这些值趋于稳定,然后相邻单元之间就会交换一些数据。所以,从本质上讲,在每个循环中各个线程都必须等待所有线程完成各自的任务以后才能进入下一个循环。这个模型称为 屏蔽同步,下例说明了这个模型:

    屏蔽同步

    publicclassBSync {

    inttotalThreads;

    intcurrentThreads;

    publicBSync(intx) {

    totalThreads = x;

    currentThreads =0;

    }

    publicsynchronizedvoidwaitForAll() {

    currentThreads++;

    if(currentThreads 

    try{

    wait();

    }catch(Exception e) {}

    }

    else{

    currentThreads =0;

    notifyAll();

    }

    }

    }

    当对一个线程调用 wait() 时,该线程就被有效阻塞,只到另一个线程对同一个对象调用 notify() 或 notifyAll() 为止。因此,在前一个示例中,不同的线程在完成它们的工作以后将调用 waitForAll() 函数,最后一个线程将触发 notifyAll() 函数,该函数将释放所有的线程。第三个函数 notify() 只通知一个正在等待的线程,当对每次只能由一个线程使用的资源进行访问限制时,这个函数很有用。但是,不可能预知哪个线程会获得这个通知,因为这取决于 Java 虚拟机 (JVM) 调度算法。

    将 CPU 让给另一个线程

    当线程放弃某个稀有的资源(如数据库连接或网络端口)时,它可能调用 yield() 函数临时降低自己的优先级,以便某个其他线程能够运行。

    守护线程

    有两类线程:用户线程和守护线程。用户线程是那些完成有用工作的线程。 守护线程是那些仅提供辅助功能的线程。Thread 类提供了 setDaemon() 函数。Java 程序将运行到所有用户线程终止,然后它将破坏所有的守护线程。在 Java 虚拟机 (JVM) 中,即使在 main 结束以后,如果另一个用户线程仍在运行,则程序仍然可以继续运行。

    避免不提倡使用的方法

    不提倡使用的方法是为支持向后兼容性而保留的那些方法,它们在以后的版本中可能出现,也可能不出现。Java 多线程支持在版本 1.1 和版本 1.2 中做了重大修订,stop()、suspend() 和 resume() 函数已不提倡使用。这些函数在 JVM 中可能引入微妙的错误。虽然函数名可能听起来很诱人,但请抵制诱惑不要使用它们。

    调试线程化的程序

    在线程化的程序中,可能发生的某些常见而讨厌的情况是死锁、活锁、内存损坏和资源耗尽。

    死锁

    死锁可能是多线程程序最常见的问题。当一个线程需要一个资源而另一个线程持有该资源的锁时,就会发生死锁。这种情况通常很难检测。但是,解决方案却相当好:在所有的线程中按相同的次序获取所有资源锁。例如,如果有四个资源 ?A、B、C 和 D ? 并且一个线程可能要获取四个资源中任何一个资源的锁,则请确保在获取对 B 的锁之前首先获取对 A 的锁,依此类推。如果“线程 1”希望获取对 B 和 C 的锁,而“线程 2”获取了 A、C 和 D 的锁,则这一技术可能导致阻塞,但它永远不会在这四个锁上造成死锁。

    活锁

    当一个线程忙于接受新任务以致它永远没有机会完成任何任务时,就会发生活锁。这个线程最终将超出缓冲区并导致程序崩溃。试想一个秘书需要录入一封信,但她一直在忙于接电话,所以这封信永远不会被录入。

    内存损坏

    如果明智地使用 synchronized 关键字,则完全可以避免内存错误这种气死人的问题。

    资源耗尽

    某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线程数远远超过了可用的资源数,则最好使用 资源池。一个最好的示例是数据库连接池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为 资源库。

    调试大量的线程

    有时一个程序因为有大量的线程在运行而极难调试。在这种情况下,下面的这个类可能会派上用场:

    publicclassProbeextendsThread {

    publicProbe() {}

    publicvoidrun() {

    while(true) {

    Thread[] x =newThread[100];

    Thread.enumerate(x);

    for(inti=0; i<100; i++) {

    Thread t = x[i];

    if(t ==null)

    break;

    else

    System.out.println(t.getName() +"\t"+ t.getPriority()

    +"\t"+ t.isAlive() +"\t"+ t.isDaemon());

    }

    }

    }

    }

    限制线程优先级和调度

    Java 线程模型涉及可以动态更改的线程优先级。本质上,线程的优先级是从 1 到 10 之间的一个数字,数字越大表明任务越紧急。JVM 标准首先调用优先级较高的线程,然后才调用优先级较低的线程。但是,该标准对具有相同优先级的线程的处理是随机的。如何处理这些线程取决于基层的操作系统策略。在某些情况下,优先级相同的线程分时运行;在另一些情况下,线程将一直运行到结束。请记住,Java 支持 10 个优先级,基层操作系统支持的优先级可能要少得多,这样会造成一些混乱。因此,只能将优先级作为一种很粗略的工具使用。最后的控制可以通过明智地使用 yield() 函数来完成。通常情况下,请不要依靠线程优先级来控制线程的状态。

    小结

    本文说明了如何进行Java多线程编程。像是否应该使用线程这样的更重要的问题在很大程序上取决于手头的应用程序。决定是否在应用程序中使用多线程的一种方法是,估计可以并行运行的代码量。并记住以下几点:

    使用多线程不会增加 CPU 的能力。但是如果使用 JVM 的本地线程实现,则不同的线程可以在不同的处理器上同时运行(在多 CPU 的机器中),从而使多 CPU 机器得到充分利用。

    如果应用程序是计算密集型的,并受 CPU 功能的制约,则只有多 CPU 机器能够从更多的线程中受益。

    当应用程序必须等待缓慢的资源(如网络连接或数据库连接)时,或者当应用程序是非交互式的时,多线程通常是有利的。

    基于 Internet 的软件有必要是多线程的;否则,用户将感觉应用程序反映迟钝。例如,当开发要支持大量客户机的服务器时,多线程可以使编程较为容易。在这种情况下,每个线程可以为不同的客户或客户组服务,从而缩短了响应时间。

    某些程序员可能在 C 和其他语言中使用过线程,在那些语言中对线程没有语言支持。这些程序员可能通常都被搞得对线程失去了信心。

    展开全文
  • 某些编程语言被设计为可以将并发任务彼此隔离,这些语言通常称为函数...与在任务操作系统中分叉外部进程不同,线程机制是在由执行程序表示的单一进程中创建任务。这种方式产生的一个好处是操作系统的透明性,这对J...

    某些编程语言被设计为可以将并发任务彼此隔离,这些语言通常称为函数型语言,其中每个函数调用都不会产生任何副作用(并因此而不能干涉其他函数),并因此可以当做独立的任务来驱动。Erlang就是这样的语言。

    Java采取了更加传统的方式,在顺序性语言的基础上提供对线程的支持。与在多任务操作系统中分叉外部进程不同,线程机制是在由执行程序表示的单一进程中创建任务。这种方式产生的一个好处是操作系统的透明性,这对Java而言,是一个重要的设计指标。

    Java的线程机制是抢占式的,这表示调度机制会周期性的中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动他的任务。

    当从Runnable导出一个类时,它必须具有run()方法,但是这个方法并无特殊之处——它不会产生任何内在的线程能力。要实现线程行为,你必须显式地将一个任务附着到线程上。

    将Runnable对象转变为工作任务的传统方式时把它提交给一个Thread构造器。Thread构造器只需要一个Runnable对象。调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()

    方法,以便在这个新线程中启动该任务。

    多线程交互模式:不进行交互的模式:线程间不需要处理共享的数据,也不需要进行动作协调,那么将会非常简单,就是多个独立的线程各自完成自己线程中的工作。

    基于共享容器协同的多线程模式:多个线程间对共享的数据进行处理。例如经典的生产者消费者例子,我们有一个队列用于生产和消费,那么这个队列就是多个线程会共享的一个容器或者数据对象,多个线程会并发地访问这个队列。

    在多线程环境下对同一份数据的访问,我们需要有所保护和控制以保证访问的正确性。对于存储数据的容器或者对象,有线程安全和线程不安全之分,而对于线程不安全的容器或对象,一般可以通过

    加锁或者通过Copy On Write的方式来控制并发访问。

    使用加锁方式时,如果数据在多线程中的读写比例很高,则一般会采用读写锁而非简单的互斥锁。

    对于线程安全的容器和对象,我们就可以在多线程环境下直接使用它们了。在这些线程安全的容器和对象中,有些是支持并发的,这种方式的效率会比简单的加互斥锁的实现更好,在JDK中的

    java.util.concurrent包中有很多这样的容器类。(注:比如HashMap改CurrentHashMap时,要注意之前的锁粒度,操作的原子性)

    通过事件协同的多线程模式:除了并发访问的控制,线程间会存在着协调的需求,例如A、B两个线程,B线程需要等到某个状态或事件发生后才能继续自己的工作,而这个状态改变或者事件产生需要和A线程相关。那么在这个场景

    下,就需要线程间的协调。

    避免死锁,一般来说,能够原子性的获取需要的多个锁,或者注意调整获取多个锁的获取顺序,就会比较好地避免死锁。

    此外,线程之间还会传递数据。因为这些线程共用进程的内存空间,所以线程间的传递数据就相对容易一些了。

    多进程模式:多进程相对于单进程下的多线程方式来说,资源控制会更容易实现,此外,多进程中的单个进程问题,不会造成整体的不可用。这两天是多进程区别于单进程多线程方式的两个特点。

    当然,使用多进程会比多线程稍微复杂一些。多进程间可以共享数据,但是其代价比多线程要大,会涉及序列化与反序列化的开销。

    我们的分布式系统是多机组成的系统,可以近似看作是把单机多进程变成了多机的多进程。

    可用性分析:

    单线程和单进程多线程的程序在遇到机器故障、OS问题或者自身的进程问题时,会导致整个功能不可用。

    对于多进程的系统,如果遇到机器故障或者OS问题,那么功能也会整体不可用,而如果是多进程中的某个进程问题,那么是有可能保持系统的部分功能正常的——当然这取决于多进程系统自身的实现方式。

    而在多机系统中,如果遇到某些机器故障、OS问题或者某些机器的进程问题,我们都有机会来保证整体的功能大体可用——可能是部分功能失效,也可能是不再能承担正常情况下那么大的系统压力了。

    综上,不同的方式可能造成的故障影响面也是不同的。

    节选《大型网站系统与Java中间件实践》

    展开全文
  • Java多线程(一文看懂!)

    2021-07-28 17:36:15
    具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中

    多线程

    一,多线程的介绍
    二,多线程的四种实现方式
    三,多线程的五大状态
    四,多线程的调度
    五,线程的同步(例:多口售票问题)
    六,线程的协作(例:生产者-消费者模型)
    七,线程的中断

    一,多线程的介绍

    >>百度中多线程的介绍(multithreading):
      是指从软件或者硬件上实现多个线程并发执行的技术。
    
    >>这里就需要了解一下什么是进程?
    是操作系统进行资源分配的最小单位;
    同一进程中的多个线程共享该进程中的全部系统资源,
    而进程和进程之间是相互独立的,是系统进行资源分配和调度的一个独立单位;
    进程是具有一定功能的程序关于某个数据集合上的一次运行活动;
    
    >>什么是线程?
    线程是cpu调度和分派的基本单位,
    同一进程中的多个线程共享该进程中的全部系统资源,
    线程是进程运行和执行的最小调度单位
     
    >>为什么要使用多线程?
      1. 从计算机底层来说:线程是程序执行的最小单元,线程间的切换,调度成本远远小于进程,
         另外多核CPU时代意味着多个线程可以同时运行,减少了线程上下文切换的开销;
         上下文切换:多线程的上下文切换是指CPU控制权由已经正在运行的线程切换到另一个
                    就绪并等待获取CPU执行权的线程的过程;
      2. 从互联网趋势来说:当下的系统频繁需要百万级,甚至是千万级的并发量,而多线程并发
          编程正是开发高并发的基础,利用多线程机制可以大大提高系统整体的并发能力和性能;
        
    >>多线程可能带来的问题:
      并发编程的目的是为了提高程序的执行效率,但并发编程并不总能提高程序运行效率
      而且还有可能带来其它问题,如:内存泄漏,上下文切换,死锁,还受限于硬件和软件的资源闲置问题
    

    二,多线程的四种实现方式

    方法一:继承Thead类
    1. 用关键字extends继承Thead类

    2. 重写run方法;(获得执行线程的线程名:Thread.currentThread().getName()

    3. 在测试类中创建类的实例,调用start方法;

      >>线程介绍:
         继承了Thread类,编写简单,
         如果需要访问当前线程,无需Thread.currentTread()方法,直接使用this,即可获得当前线程。
         但由于类的单继承原则,该类不能继承其他类,灵活度不高
      
    public class TheadDemo extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            TheadDemo demo=new TheadDemo();
            demo.start();
        }
    }
    
    方法二:实现Runnable接口
    1. 用关键字implements实现Runnable接口

    2. 重写run方法;

    3. 在测试类中创建类的实例

    4. 创建Thead类的实例,传入上面创建的实例对象;

    5. 用Thead类的实例,调用start方法;

      >>线程介绍:
        实现Runnable接口比较容易实现多个线程共享一个对象,
        非常适合多线程处理同一份资源的场景,
        因而可以将代码,数据分开,形成清晰的代码模型,体现着Java面向对象的思想
      
    public class RunnableDemo implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    
        public static void main(String[] args) {
            RunnableDemo demo=new RunnableDemo();
            Thread thread=new Thread(demo);
            thread.start();
        }
    }
    
    >>start方法和run方法的区别:
      调用start方法后,线程会被放到等待队列,等待JVM调度,并不一定马上开始执行,此时线程处于就绪状态
      该方法用来启动线程,调用后子线程就绪;
      
      run方法用来封装子线程所需要的业务代码,run方法运行结束,子线程终止
    
    方法三:实现Callable接口
    1. 用关键字implements实现Callable接口,并规定泛型;

    2. 重写call方法;

    3. 在测试类中创建类的实例

    4. 创建FutureTask的实例,并传入上面类的实例对象;

    5. 创建Thead的实例,传入FutureTask的实例对象;

    6. 用Thead的实例对象调用start方法;

      线程介绍:
        实现Runnable接口和实现Callable接口
        比较容易实现多个线程共享一个对象,非常适合,多线程处理同一份资源的场景,
        因而可以将代码,数据分开,形成清晰的代码模型,体现着Java面向对象的思想
        而实现Runnable接口中的call方法,可以有返回值,灵活度更高
      
    public class CallableTask implements Callable<Integer> {
        @Override
        public Integer call() throws InterruptedException {
            int sum = 0;
            for (int i = 0; i < 100; i++) {
                sum += i;
                System.out.println(Thread.currentThread().getName() + ":" + sum);
            }
            return sum;
        }
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            CallableTask callableTask = new CallableTask();
            FutureTask<Integer> futureTask = new FutureTask<>(callableTask);
            Thread thread = new Thread(futureTask, "子线程");
            futureTask.get();//得到CallableTask中call方法的返回数据
            thread.start();
        }
    }
    
    方法四:使用线程池实现

    来看看自定义的线程池构造函数(参数最多的)

    public ThreadPoolExecutor(
    int corePoolSize,                      //线程池中核心线程的数量
    int maximumPoolSize,                   //线程池可创建的最大线程数量
    long keepAliveTime,                    //线程最大等待时间
    TimeUnit unit,                         //指定keepAliveTime的单位
    BlockingQueue<Runnable> workQueue,     //指定存储未执行任务的队列
    ThreadFactory threadFactory,           //创建线程的工厂
    RejectedExecutionHandler handler       //指定拒绝策略
    )
    
    public class 自定义线程池 {
        private static final int CORE_POOL_SIZE = 5;
        private static final int MAX_POOL_SIZE = 10;
        private static final int QUEUE_CAPACITY = 100;
        private static final Long KEEP_ALIVE_TIME = 1L;
    
        public static void main(String[] args) {
    
            //通过ThreadPoolExecutor构造函数自定义参数创建
            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                    CORE_POOL_SIZE,
                    MAX_POOL_SIZE,
                    KEEP_ALIVE_TIME,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                    new ThreadPoolExecutor.CallerRunsPolicy());
    
            for (int i = 0; i < 10; i++) {
                //创建RunableTaskForPool对象(RunableTaskForPool类实现了Runnable 接口)
                Runnable worker = new RunableTaskForPool();
                //执行Runnable使用execute方法
                //执行Runnable和Callable类型任务用submit方法
                executor.execute(worker);
            }
            //终止线程池
            executor.shutdown();
        }
    }
    

    在这里插入图片描述

    >>线程池获取线程优势:
      Tread/Runnable/Callable创建线程如果频繁创建和关闭会消耗大量系统资源,影响性能;
      因此更建议使用线程池来创建线程;
      由于线程池创建并管理多个线程,当程序需要使用线程时,直接从线程池获取即可,使用完成会再次放回线程池
      不需要频繁创建和销毁线程,可以大大提高系统性能,项目开发主要使用线程池的方式
    
    >>线程池的执行流程:
      1. 提交一个线程任务,当线程池里存活的核心线程数小于线程数时,线程会创建一个核心线程去处理提交的任务
      2. 如果核心线程数已满,提交的线程就会放在任务队列中排队等待
      3. 当核心线程已满,任务队列已满,此时判断线程数是否到达最大数量,如果没有到达,则创建非核心线程执行提交的任务
      4. 如果核心线程数已到达,任务队列也已满,非核心线程数也到达,则采用拒绝策略处理
    
    >>线程池的几种工作队列:
      ArrayBlockingQueue(有界队列):
      是一个基于数组结构的有界阻塞队列,此队列按先进先出原则排序
      
      LinkedBlockingQueue(可设置容量队列):
      一个基于链表结构的阻塞队列,也按照先进先出原则排序,吞吐量高于ArrayBlockingQueue
      
      SynchronousQueue(同步队列):
      一个不存储元素的阻塞队列,每个插入操作,必须等到另一个线程调用移除操作 否则插入操作一直处于
      阻塞状态,吞吐量通常高于LinkedBlockingQueue,CachedTreadPool线程池使用该队列
      
      PriorityBlockingQueue(优先级队列):
      一个具有优先级的无限阻塞队列
      
      DelayQueue(延迟队列):
      是一个任务定时周期的延迟执行队列,根据执行时间从小到大排序,
      否则根据插入到队列的先后顺序,newScheduledTreadPool线程池使用该队列
    
    
    >>线程池的状态:
      RUNNING:线程池一旦被创建就处于该状态,任务数为0,能接收新任务,对以排队的业务进行处理
      SHUTDOWN:不接收新任务,但能处理已排队的业务;
             调用线程池的shutdown()方法,线程池由RUNNING转变为SHUTDOWN状态
      STOP:不接收新任务,不处理已排队业务,并且会中断正在处理的任务;
             调用线程池的shutdownNow()方法,线程池由RUNNING或SHUTDOWN转变为STOP状态
      TIDYING:线程在SHUTDOWN状态下任务数为空,其它所有任务已中止,线程池会变为TIDYING状态
             线程在STOP状态,线程池中执行任务为空时就会转变为TIDYING状态
      TERMINATED:线程池彻底中止,线程池在TIDYING执行完terminated()方法就会转变为TERMINATED状态
    
    >>线程池的四种拒绝策略:
      AbortPolicy:默认策略,丢弃任务并抛出异常RejectedExectionExecption
      DiscardPolicy:丢弃任务,不抛异常
      DiscardOldestPolicy:丢弃队列最前面的任务,然后从新提交被拒绝的任务
      CallerRunPolicy:交给调用线程池的线程处理
      
      (《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过
     ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规
     避资源耗尽的风险)
    >>Executors工具类可创建的几种常见线程池:
      newCachedThreadPool:可缓存线程池
      newFixedThreadPool:固定线程数量的线程池
      newScheduledThreadPool:定时线程池,定时任务和周期任务使用
      ThreadPoolExecutor:自定义线程池,可控制:核心线程数(核心线程数不会被释放)/最大线程数
                           /最大空闲时间(到达最大空闲时间会自动回收的线程)/时间单位/等待队列
    
    • 用实现Callable接口的方式完成一个线程
    public class CallableTaskForPool implements Callable<String> {
        @Override
        public String call() throws Exception {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+":线程任务被执行"+i);
            }
            return "Callable线程任务被执行";
        }
    } 
    
    • 运用Callable接口完成的线程实现两个线程池
    public class 带有缓冲区的线程池 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //创建一个带有缓冲区的线程池,返回一个ExecutorService对象,该对象用于对线程池进行管理
            //如:向线程池提交线程任务,关闭线程池等操作
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < 100; i++) {
                Future<String> future= executorService.submit(new CallableTaskForPool());//向线程池提交任务
            }
            executorService.shutdown();//关闭线程池
        }
    }
    
    public class 固定数量的线程池 {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(3);//传入线程数
            for (int i = 0; i < 20; i++) {
                executorService.submit(new CallableTaskForPool());
            }
            executorService.shutdown();
        }
    }
    
    • 用实现Runnable接口的方式完成一个线程池
    public class RunableTaskForPool implements Runnable {
        @Override
        public void run() {
            System.out.println("执行周期线程任务");
        }
    }
    
    • 运用Runnable接口完成的线程实现两个线程池
    public class 定时任务线程池 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
            //五个参数:线程任务/延迟时间/间隔时间/时间单位
            ScheduledFuture<?> scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(new RunableTaskForPool(), 0, 1000, TimeUnit.MILLISECONDS);
            scheduledFuture.get();//得到RunableTaskForPool()中的run方法返回的数据
            scheduledExecutorService.shutdown();
        }
    }
    
    public class 自定义线程池 {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            //创建等待队列
            ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
            //参数:核心线程数(核心线程数不会被释放)/最大线程数/最大空闲时间(到达最大空闲时间会自动回收的线程)/时间单位/等待队列
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 10, 20, TimeUnit.MILLISECONDS, queue);
            for (int i = 0; i < 100; i++) {
                Future<?> future = threadPoolExecutor.submit(new RunableTaskForPool());
                future.get();//获得返回结果
            }
            threadPoolExecutor.shutdown();
        }
    }
    

    三,多线程的五种状态

        新建:当一个Thread类或者其子类被调用时,新生的线程对象处于新建状态,
             此时它已经有了相应的内存空间和其他资源,在新建状态下的线程不会被执行;
        就绪:当线程被创建,并调用了start方法,该线程就进入了就绪状态,
             该状态下的线程位于可运行池(线程池)等待获得cpu的使用权;
        运行:处于该状态的线程专用cpu
             只有处于就绪状态的线程才有机会转为运行状态;
        阻塞:放弃cpu资源,让其他资源获取,
              五种阻塞原因:
                1.位于等待池:执行wait方法,jvm就会将该线程放于等待池;
                2.位于锁池:试图获得某个对象同步锁时,如果该对象的同步锁已经被其他线程占用,
                           jvm会将这个线程放在这个对象的锁池中;
                3.执行了sleep方法;
                4.调用其他线程join方法;
                5.发出IO请求时;
        死亡:run方法结束,线程结束;
    

    在这里插入图片描述

    四,多线程的常用调度

    • wait:线程等待(该线程会被放在等待池,等待被唤醒,在同步锁中使用);
    • notify:唤醒一个处于等待的线程,在同步锁中使用;
    • notifyAll:唤醒所有等待的线程,在同步锁中使用;
    • sleep(静态方法):线程休眠,指定休眠一定时间,休眠期间线程处于堵塞状态,休眠结束,线程进入就绪状态等待获取cpu资源;(传入数字,默认单位为毫秒);
    • join:在一个线程中调用另一个线程的该方法,则该线程进入等待状态,等待调用线程执行结束,该线程才会继续执行;
    • yield(静态方法):线程让步,中止本次线程执行,让出cpu资源,回到就绪状态继续抢占cpu资源;
    • setPriority:设置线程优先级,只能相对的增大抢到cpu资源的概率(传入数字1-10);
    • setDaemon:守护线程,又叫精灵或者幽灵线程,当其它线程结束后该线程自动结束,如果尚未结束,jvm强行终止。通常情况可能会超出当然也就i体现了jvm强行中止(传入boolean值);
    • interrupt:其实它的作用就是传出一个线程应该中断的信号,至于该怎样中断线程,设计者来实现

    五,线程的同步(多口售票问题)

    >>百度的解释就很好:
      线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。
      当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,
      即各位线之间要有个先来后到(抢占cpu资源),不能一窝蜂挤上去抢作一团。
      线程同步的真实意思和字面意思恰好相反。
      线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个共享资源进行操作,而不是同时进行操作。
           
    >>线程安全:
      当多个线程访问时,采用加锁机制,当一个线程访问某个类的数据时进行保护,其它线程不能访问
      直到该线程结束,其它线程才可访问,不会出现数据不一致和或者数据污染
    
    >>保证线程安全的方式:
      1. 使用线程安全的类,如:CopyOnWriteArrayList,ConcurrentHashMap等
      2. synchronized同步代码块
      3. 同步方法
      4. 使用LOCK锁
    
    >>线程死锁:
      多个线程同时被堵塞,他们中的一个或全部都在等待某个资源的释放,由于线程被无限期的阻塞,
      程序不能中止和运行的一种状态;
      例:A需要x,y两个资源,B也需要x,y两个资源;而抢占CPU时,A得到了x资源,B得到了y资源
            此时,A等待获取y资源,B等待获取x资源,线程未结束前又不能释放已获得的资源,
            因此造成了相互等待,形成死锁
      >线程死锁的四个条件:
      互斥条件:该资源任意一个时刻只能由一个线程占用
      请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
      不剥夺条件:线程已获得的资源在未使用前不能被其他线程强行剥夺,只有自己使用完毕后才能释放资源
      循环等待条件:若干线程之间形成一种头尾相接的循环等待资源的关系
      >避免线程死锁: 破坏以上四个条件即可
      破坏互斥条件:无法破坏,因为我们用锁本身就是为了互斥访问
      破坏请求与保持条件:一次申请所有资源
      破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
      破坏循环等待条件:使用按序申请资源来预防,按某一顺序申请资源,释放资源则反序释放,这样就可以破坏该条件
    
    两种同步的方法
    public class SynchronizedDemo {
        //方法1
        public void test1() {
            synchronized (this) {
                //使用this实现对象级同步
            }
        }
    
        //方法2(LOCK锁)
        private static final Object LOCK = new Object();
        public void test2() {
            synchronized (LOCK) {
                //方法级同步
            }
        }
    
        //方法3
        public synchronized void test3() {
            //对象级的方法同步
        }
    
        //方法4
        public static synchronized void test4() {
            //同步的静态方法,静态方法属于类,所以也可以实现对象级的同步
        }
    }
    

    例:多口售票(有助理解线程同步):

      描述:我只有10张票,给两个销售点去售卖,不能卖重复了,用多线程的思想解决
    
    public class SaleTask {
        private int ticketNum;//定义票数
    
        //构造器设置票数
        public SaleTask(int ticketNum) {
            this.ticketNum = ticketNum;
        }
    
        //卖票方式
        public void doSale() {
            if (ticketNum <= 0) {
                System.out.println("没票了");
                return;
            }
            System.out.println(Thread.currentThread().getName() + ":第" + ticketNum + "张票");
            ticketNum--;
        }
    
        public static void main(String[] args) {
            //创建对象
            SaleTask sale1 = new SaleTask(10);
            //销售点一,匿名内部类形式实现线程的创建的法一
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        sale1.doSale();
                        try {
                            Thread.sleep(10);//让线程睡一会,方便销售点2和它抢占cpu
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }, "销售点1").start();
            //销售点二,匿名内部类形式实现线程的创建的法二
            new Thread("销售点2") {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        sale1.doSale();
                        try {
                            Thread.sleep(20);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }.start();
        }
    }
    

    在这里插入图片描述
    很明显,票卖的五花八门; 所以就要实现线程同步

    多口售票解决法一(线程同步法一,互斥同步)
        public void doSale() {
            synchronized (this) {
                if (ticketNum <= 0) {
                    System.out.println("没票了");
                    return;
                }
                System.out.println(Thread.currentThread().getName() + ":第" + ticketNum + "张票");
                ticketNum--;
            }
        }
    

    多个对象时:

        //LOCK锁,指定了唯一对象
        private static final Object LOCK = new Object();
        //卖票方式
        public void doSale() {
            synchronized (LOCK) {
                if (ticketNum <= 0) {
                    System.out.println("没票了");
                    return;
                }
                System.out.println(Thread.currentThread().getName() + ":第" + ticketNum + "张票");
                ticketNum--;
            }
        }
    
    多口售票解决法二(线程同步法二)
        //优化卖票方法
        public synchronized void doSale() {
            if (ticketNum <= 0) {
                System.out.println("没票了");
                return;
            }
            System.out.println(Thread.currentThread().getName() + ":第" + ticketNum + "张票");
            ticketNum--;
        }
    

    多个对象时

        public static synchronized void doSale() {
                if (ticketNum <= 0) {
                    System.out.println("没票了");
                    return;
                }
                System.out.println(Thread.currentThread().getName() + ":第" + ticketNum + "张票");
                ticketNum--;
        }
    

    六,线程的协作(生产者-消费者模型)

    • 线程协作:线程协作是在多线程互斥同步的基础上,使线程之间依照一定条件,有目的、
      有计划地交互协同工作,这是一种较高级的线程同步方式
      Java中线程间的通信是通过:wait-notify机制实现的,即waitnotifynotifyAll三个方法的灵活使用
      这三个方法均定义在 Object 类中,是 final 修饰的实例方法。
      这三个方法必须在 synchronized 代码中调用,而且只有锁对象才能调用,即持有锁对象监视器的线程才能调用锁对象的这三个方法
    • wait()
      调用该方法的线程放弃同步锁并进入等待状态(堵塞状态),直到其他线程
      获得相同的同步锁并调用 notify( )notifyAll()方法来唤醒。
    • notify( )
      通知在相同同步锁上处于等待的一个线程结束等待,进入到就绪状态。
      即唤醒一个等待(当前线程所持有)同步锁的线程。
    • notifyAll()
      通知在相同同步锁上处于等待的所有线程结束等待,进入到就绪状态。
      即唤醒所有等待(当前线程所持有)同步锁的线程

    生产者消费者模型(助于理解线程协作)(传送门:阻塞队列实现生消模型

     描述:我有一个面包店,有一个师傅生产面包,有一个师傅销售面包,怎样达到一种产销平衡
    

    面包类

    public class Bread {
        private String bread;
        public Bread() {
        }
    }
    

    操作类

    public class BreadCabinet {
        private Bread[] breads;//定义一个面包数组,用于规定容器大小
        private int num;//用于统计当前面包数量
    
        public BreadCabinet(int initSize) {
            breads=new Bread[initSize];//初始化容器大小
        }
        public synchronized void doBread(Bread bread){
            while (breads.length==num){//说明面包箱满了
                System.out.println("面包箱满了,快卖");
                try {
                    wait();//容器装满了,就让该线程进入等待状态
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            breads[num]=bread;
            num++;//做出一个面包
            System.out.println(Thread.currentThread().getName()+":"+"面包"+num+"出炉");
            notifyAll();//唤醒线程
        }
        public synchronized void saleBread(){
            while (num==0){
                System.out.println("面包箱没了,快做");
                try {
                    wait();//没面包了,就让该线程进入等待状态
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+":"+"面包"+num+"售出");
            num--;//卖出一个面包
            notifyAll();//唤醒线程
        }
    }
    

    测试类

    public class BreadTest {
        public static void main(String[] args) {
            BreadCabinet breadCabinet = new BreadCabinet(10);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        Bread bread = new Bread();
                        breadCabinet.doBread(bread);
                    }
                }
            },"生产者1").start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        breadCabinet.saleBread();
                    }
                }
            },"消费者1").start();
        }
    }
    

    结果:实现了产销平衡
    在这里插入图片描述

    七,线程的中断

    首先了解一下方法interruptinterruptedisInterruptedsleep三个方法
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    很明显上面的interrupt方法可以中断,但是需要特殊情况
    interruptedisInterrupted貌似只能测试这个线程是否被中断
    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    方法一:先看上面四张图介绍:经测试后,发现只要调用了上面的几个方法,就可以通过异常实现打断线程
    方法二futureTask.cancel()方法传入true(该方法底层调用了interrupted()方法)
    方法三: 通过接收信号的方式在子线程内主动结束线程(根据具体业务运用returnbreak等,)

    描述:现有主线程一个,子线程一个,如何让主线程中断子线程
    描述:让主线程在i=20时,结束子线程
    

    建立子线程

    public class CallableTask implements Callable<Integer> {
    
        @Override
        public Integer call() throws Exception {
            int sum=0;
            for (int i = 0; i < 100; i++) {
                sum+=i;
                System.out.println(Thread.currentThread().getName()+":"+sum);
            }
            return sum;
        }
    }
    

    开启子线程,主线程

    public class CallableTaskTest {
        public static void main(String[] args) {
            //开启子线程
            CallableTask callableTask = new CallableTask();
            FutureTask<Integer> futureTask = new FutureTask<>(callableTask);
            Thread thread = new Thread(futureTask, "线程一");
            thread.start();
            //主线程
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
                if (i==20){
                    thread.interrupt();
                    if (thread.isInterrupted()){
                        System.out.println("线程中断成功");
                    }
                }
            }
        }
    }
    

    在这里插入图片描述
    方法一详解:

    详解:
    当我们将sleep的异常throws出的时候,是能够实现中断的
    但当把该异常由throws改成try-catch再看看结果
    得到不仅没有打断成功,而且还报出了睡眠被吵醒的异常
    可见这是一种通过异常打断线程的方法,实现线程的打断的
    因此给子线程加上sleep等方法配合使用interrupt方法就可以实现线程打断
    
             //子线程的循环加sleep
            for (int i = 0; i < 100; i++) {
                sum+=i;
                System.out.println(Thread.currentThread().getName()+":"+sum);
                Thread.sleep(0);
            }
             
                for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
                if (i==20){
                    thread.interrupt();
                    if (thread.isInterrupted()){
                        System.out.println("线程中断成功");
                    }
                }
            }
    

    在这里插入图片描述
    在这里插入图片描述
    方法二详解:

     详解:内部调用了interrupt()方法,原理与法一类似
    
            //子线程的修改方式
            for (int i = 0; i < 100; i++) {
                sum += i;
                System.out.println(Thread.currentThread().getName() + ":" + sum);
                Thread.sleep(0);
            }
            
            //主线程的修改方式
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
                if (i==20){
                    futureTask.cancel(true);//发出线程中断信号
                    if (thread.isInterrupted()){
                        System.out.println("线程中断成功");
                    }
                }
            }
    

    方法三详解:

    详解:既然interrupted()可以发信号,isInterrupted()可以收信号,那就利用这一点来主动结束线程
    
            //子线程修改代码
            for (int i = 0; i < 100; i++) {
                sum += i;
                System.out.println(Thread.currentThread().getName() + ":" + sum);
                if (Thread.interrupted()){
                    System.out.println("线程中断成功");
                    break;//结束循环,相当于手动结束线程
                }
            }
            //主线程修改代码
                    for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
                if (i==20){
                    thread.interrupt();//发出终断信号
                    if (thread.isInterrupted()){
                        System.out.println("线程中断信号发出");
                    }
                }
            }
    
    展开全文
  • Mac os 多线程

    2021-01-20 20:25:09
    多线程的原理: 同一时间,CPU只能处理1条线程,只有1条线程在工作(执行) 多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换) 如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象 如果...
  • 原标题:多线程到底该设置多少个线程?我们在使用线程池的时候,会有两个疑问点:线程池的线程数量设置过多会导致线程竞争激烈如果线程数量设置过少的话,还会导致系统无法充分利用计算机资源那么如何设置才不会影响...
  • 1 多线程的优缺点

    2021-03-09 15:24:25
    多线程及其优缺点什么是线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。(wiki百科)创建线程的三种方式public class ThreadTest {public static void main(String[] args) ...
  • 并发操作系统会根据任务调度系统给线程分配线程的 CPU 执行时间,线程的执行会进行切换。 线程和进程的区别? 1、进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位)一个程序至少有一个...
  • 并发是并行运行个程序或程序的个部分的能力。如果可以异步或并行执行耗时的任务,则可以提高吞吐量和程序的交互性。现代计算机在一个CPU中具有个CPU或个内核。利用这些多核的能力可能是成功实现大批量应用...
  • 狂神说多线程笔记整理

    千次阅读 多人点赞 2021-02-16 13:29:26
    狂神说多线程笔记整理 笔记总结来自狂神说Java多线程详解 目录狂神说多线程笔记整理一、线程简介1.多任务2.多线程3.程序.进程.线程4.Process与Thread5.核心概念二、线程实现1.线程创建(三种方法)1.1继承Thread类...
  • 如何获取多线程执行结果-java

    千次阅读 2021-08-12 16:08:05
    在日常的项目开发中,我们会经常遇到通过多线程执行程序并需要返回执行结果的场景,下面我们就对获取多线程返回结果的几种方式进行一下归纳,并进行简要的分析与总结。 一、Thread.join 在一些简单的应用场景中...
  • Java多线程

    2021-07-13 15:52:16
    Java多线程前言进程与线程进程概念线程概念为什么要在代码中引入多线程?并发编程及优缺点并发编程的三要素并发、并行、串行的区别线程与进程的区别上下文切换守护线程和用户线程的区别用户态和内核态创建线程的几种...
  • c++多线程调用python

    2021-01-14 19:38:55
    此外,多线程支持促进了与多线程应用程序的集成,而且不影响性能。 你可以从本文的后面下载有关案例,该案例实现了一个内嵌Python解释器的多线程HTTP服务器。此外我推荐您去http://www.python.org/docs/api/阅读有关...
  • 在本教程中,我们将学习如何...具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,进而提升整体处理性能。并发指的是可以实现多个进程的并行执行,从而实现更快的运行时间。当执行基于I/O的任务(如...
  • 36.线程和进程的区别?37.守护线程是什么?38.创建线程有哪几种方式?39.说一下 runnable 和 callable 有什么区别?40.线程有哪些状态?41.sleep() 和 wait() 有什么区别?42.notify()和 notifyAll()有什么区别?43....
  • 多线程的学习方法 多线程作为面试的重灾区,如果我们能够进行深入的了解和使用,对我们而言是非常有有益的,尤其是在面试的时候,如果多线程回答的好,是非常能够加分的。这样才能够经受住面试官的夺命连环问! 不管...
  • 多线程是指从软件或者硬件上实现多个线程并发执行的技术,而计算机多线程就是指具有多线程能力的计算机,因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。进程是计算机中关于某数据集合的一次...
  • 《Linux多线程编程 多核编程》由会员分享,可在线阅读,更多相关《Linux多线程编程 多核编程(28页珍藏版)》请在人人文库网上搜索。1、Linux多线程编程,IEEE POSIX 标准 p1003.1c (Pthreads) 定义了处理线程的一系列C...
  • java多线程

    2021-01-10 14:28:05
    多线程的创创建: 方式一:继承Thread类 1.创建一个继承Thread类的子类。 2.重写Thread类的run()方法—>将此线程执行的操作声明在run()中。 3.创建Thread类的子类对象。(在主线程中做) 4.通过此对象调用start(). ...
  • 以下是本文所涵盖的所有专业的简要总结:什么是python中的多线程?什么时候在Python中使用多线程?如何在Python中实现多线程?如何在Python中创建线程?而不创建类通过扩展线程类不扩展线程类在Python中使用多线程的...
  • 线程技术已经不是什么新鲜事物了,但普通用户可能与双核心技术区分不开。例如开启了超线程技术的Pentium4530与PentiumD530在操作系统中都同样被识别为两颗处理器,它们究竟是不是一样的呢?这个问题确实具有迷惑性...
  • 报名 还有免费资料领取 (文末查看详细资料) 名额仅限当日前50名 提升你的Java高并发编程能力 05 1、Java教育领域专家倾心授课 此次三天的课程是由Java教学领域的专家李科霈老师进行讲解,具备多年的互联网工作经验...
  • JAVA NIO 结合多线程

    2021-02-12 18:48:53
    由于网络带宽等原因,在通道的读、写操作中是容易出现等待的, 所以在读、写操作中引入多线程,对性能提高明显,而且可以提高客户端的感知服务质量。所以本文的模型将主要通过使用读、写线程池 来提高与客户端的数据...
  • 这样的程序有单个控制线程,控制在这四个任务之间线性地移动。此外,因为所需的资源 ? 打印机、磁盘、数据库和显示屏 -- 由于硬件和软件的限制都有内在的潜伏时间,所以每项任务都包含明显的等待时间。因此,程序在...
  • 多线程是为了使得多个线程并行的工作以完成多项任务,以提高系统的效率。线程是在同一时间需要完成多项任务的时候被实现的。 打个比方: 多进程是立体交通系统(近似于立交桥),虽然造价高,上坡下坡多耗点油,...
  • 要说多线程 我们明确几个概念 1.1 名词 1.1.1 操作系统 操作系统是计算机系统中的一个系统软件,是一些程序模块的集合 它们能以尽量有效、合理的方式组合和管理计算机的软硬资源 系统效率,资源利用率 CPU利用率...
  • 【Linux】多进程与多线程的理解及优缺点

    千次阅读 多人点赞 2021-03-21 21:17:49
    首先我们的问题是,线程是什么? 线程是进程中的一条执行流,是CPU执行调度的基本单位,一个进程中可以有多个线程。在Linux下,线程执行流是通过PCB实现的,且一个进程中可能有多个PCB,这些PCB共享...多线程与多进程
  • 使用多线程肯定是首先想到的,但多线程之间的事务怎么保持一致呢?下面的代码就是在单个项目中使用多线程异步处理时的事务管理的方法。 具体思路打个比方来说明一下: 代码噼里啪啦的一顿敲,终于把需求实现了。 ...
  • Python解释执行原理我是一个Python线程,我的工作就是解释执行程序员编写的Python代码。之所以说是解释执行,是因为Python是高级语言,CPU那家伙不认识Python代码,需要运行的时候动态翻译成CPU指令。我把Python源...
  • JavaScript多线程详解

    2021-03-13 04:45:27
    虽然有越来越的网站在应用AJAX技术进行开发,但是构建一个复杂的AJAX应用仍然是一个难题。造成这些困难的主要原因是什么呢?是与服务器的异步通信问题?还是GUI程序设计问题呢?通常这两项工作都是由桌面程序来...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 260,970
精华内容 104,388
关键字:

多线程工作的能力

友情链接: puakement-windows.rar