我必须创建一个需要使用线程的应用程序,但它们不能正常工作

Posted

技术标签:

【中文标题】我必须创建一个需要使用线程的应用程序,但它们不能正常工作【英文标题】:I have to create an application where I need to use threads, but they don't work as they should 【发布时间】:2021-12-25 11:49:45 【问题描述】:

我有这段代码,其中每个人都由一个用随机票数据实例化的特殊线程表示,每 5 秒一个 FestivalStatisticsThread 唤醒并从门读取新数据并生成统计信息(有多少人进入并使用什么类型的票)。

public class Main 
    public static void main(String[] args) throws InterruptedException 

        FestivalGate gate = new FestivalGate();
        FestivalAttendeeThread festivalAttendee;
        FestivalStatisticsThread statsThread = new FestivalStatisticsThread(gate);
        TicketType ticketType;
        for (int i = 1; i < 85_000; i++) 
            ticketType = TicketType.randomTicket();
            festivalAttendee = new FestivalAttendeeThread(ticketType, gate);
            festivalAttendee.start();
            festivalAttendee.join();

        

        statsThread.start();

        System.out.println(gate.getValidatedTickets().size());

    

问题是我没有找到一种方法来每 5 秒唤醒一次该线程并生成统计信息,我能做到的唯一方法是在最后只生成一次统计信息 第二个问题是我应该在启动festivalAttendee 线程后使用join,否​​则我存储票证类型的列表不会存储所有票证类型,尽管是同步的。

我还将在这里留下执行我的代码内部工作的代码:

public class FestivalGate 
    private List<TicketType> validatedTickets = Collections.synchronizedList(new ArrayList<>());
    public List<TicketType> getValidatedTickets() 
        return validatedTickets;
    



public class FestivalAttendeeThread extends Thread 

    TicketType ticketType;
    FestivalGate festivalGate;

    public FestivalAttendeeThread(TicketType ticketType, FestivalGate festivalGate) 
        this.ticketType = ticketType;
        this.festivalGate = festivalGate;
    

    @Override
    public synchronized void run() 
        this.festivalGate.getValidatedTickets().add(ticketType);
    



public class FestivalStatisticsThread extends Thread 

    private int NR_OF_FULL;
    private int NR_OF_FULL_VIP;
    private int NR_OF_FREE_PASS;
    private int NR_OF_ONE_DAY;
    private int NR_OF_ONE_DAY_VIP;
    private int TOTAL_NUMBER_OF_PEOPLE;

    private FestivalGate gate;

    public FestivalStatisticsThread(FestivalGate gate) 
        this.gate = gate;
    

    @Override
    public void run() 
        calculateNumberOfEachTicketType();
        calculateNumberOfPeople();
        showStats();
        try 
            Thread.sleep(5000);
         catch (InterruptedException e) 
            e.printStackTrace();
        

    

    private void calculateNumberOfEachTicketType() 
        synchronized (this.gate.getValidatedTickets()) 
            for (TicketType ticketType : this.gate.getValidatedTickets()) 
                if (ticketType.equals(TicketType.FULL)) 
                    NR_OF_FULL += 1;
                 else if (ticketType.equals(TicketType.FULL_VIP)) 
                    NR_OF_FULL_VIP += 1;
                 else if (ticketType.equals(TicketType.FREE_PASS)) 
                    NR_OF_FREE_PASS += 1;
                 else if (ticketType.equals(TicketType.ONE_DAY)) 
                    NR_OF_ONE_DAY += 1;
                 else if (ticketType.equals(TicketType.ONE_DAY_VIP)) 
                    NR_OF_ONE_DAY_VIP += 1;
                
            
        
    


    private void calculateNumberOfPeople() 
        TOTAL_NUMBER_OF_PEOPLE +=
                NR_OF_FULL +
                        NR_OF_FULL_VIP +
                        NR_OF_FREE_PASS +
                        NR_OF_ONE_DAY +
                        NR_OF_ONE_DAY_VIP
        ;
    

    public void showStats() 
        System.out.println(
                TOTAL_NUMBER_OF_PEOPLE + " people have entered " + "\n" +
                        NR_OF_FULL + " have full tickets " + "\n" +
                        NR_OF_FREE_PASS + " have free passes " + "\n" +
                        NR_OF_FULL_VIP + " have full vip passes " + "\n" +
                        NR_OF_ONE_DAY + " have one day passes " + "\n" +
                        NR_OF_ONE_DAY_VIP + " have one day vip passes"
        );
        System.out.println("-----------------------------------------------------------");
        System.out.println("-----------------------------------------------------------");
    



【问题讨论】:

在启动后加入线程将停止执行for循环,直到该线程完成。 @Marvin 如果我不使用 join 不是所有类型的票都将存储在数组中 【参考方案1】:

引用Oracle tutorial:

t.join() 导致当前线程暂停执行,直到 t 的线程终止

因此,您实际上并没有真正使用线程。每次通过for 循环时,都会启动一个线程,然后等待它完成。这样做没有意义,您可以放弃线程并在当前线程中按顺序执行任务。


在现代 Java 中,我们很少需要直接寻址 Thread 类。相反,我们将Runnable/Callable 任务提交给ExecutorService

要安排每 5 秒重复一次计算,请使用 ScheduledExecutorService

ScheduledExecutorService ses = Executors. newSingleThreadScheduledExecutor() ;
…
ses.scheduleAtFixedRate( myStatsCalcRunnable , 1 , 5 , TimeUnit.SECONDS ) ;

注意:请务必最终关闭您的执行程序服务。否则它们的后备线程池可能会无限期地继续运行,就像僵尸一样?‍♂️。

改变这个:

public class FestivalStatisticsThread extends Thread  …  

...用run 方法进入:

public class FestivalStatistics  …  

... 使用 recalculate 方法。从Runnable 任务中调用recalculate 方法,该任务已提交到您计划的执行器服务。

或者也许该功能应该只是Gate 类的一部分。 (我不确定。我得再考虑一下。)


85,000 个并发线程可能对普通硬件造成太大负担。相反,使用有限数量的线程启动执行器服务。然后将您的 85,000 个任务提交为 Runnable/Callable 对象。执行器服务负责提供要在有限数量的线程上执行的任务。

将来,如果Project Loom 成功,您将能够一次使用 85,000 个(甚至数百万个)虚拟线程(也称为“纤程”)。要了解更多信息,请参阅 Ron Pressler 或 Loom 团队的其他成员最近的演讲。现在可以使用基于早期访问 Java 18 的实验性构建。 Loom 团队寻求反馈。


所有这些主题都已在 Stack Overflow 上得到解决。搜索以了解更多信息。我自己和其他人一样,用代码示例编写了多个答案。


顺便说一句,我会让FestivalGate 更加封装。其他类不应访问,甚至不知道其内部使用的同步List。添加收集和报告工单的方法。


这是我编写的一些快速代码,用于演示上述主题。

使用风险自负;我没有进行任何认真的测试,也没有进行过深思熟虑的代码审查。

也许我过分简化了您的域问题。您当然可以根据需要扩展或重新排列。但希望这清楚地表明了如何使用执行器服务。

我们有一个基本的Ticket 类来代表每张票。我们在这里使用record 来定义该类。我们嵌套了工单类型的枚举。

package work.basil.festival;

import java.util.UUID;

public record Ticket( UUID id , Type type )

    enum Type
     FULL_VIP, FREE_PASS, ONE_DAY, ONE_DAY_VIP 

我们有一个Gate 类来保存我们的域逻辑。我们嵌套了一个 Statistics 类来保存我们报告的到目前为止所拍摄票证的摘要。

package work.basil.festival;

import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

public class Gate

    final private Set < Ticket > ticketsTaken;

    public Gate ( )
    
        this.ticketsTaken = new HashSet <>();
    

    synchronized public boolean takeTicket ( final Ticket ticket )
    
        // Simulate hard work by sleeping some random amount of time.
        int millis = ThreadLocalRandom.current().nextInt( 2 );
        try  Thread.sleep( millis ); catch ( InterruptedException e )  e.printStackTrace(); 
        return this.ticketsTaken.add( ticket );
    

    synchronized public List < Ticket > ticketsTaken ( )
    
        return List.copyOf( this.ticketsTaken );  // Returns unmodifiable list of the `Ticket` objects contained in our private member set.
    

    record Statistics( Instant when , int countTicketsTaken , Map < Ticket.Type, Integer > countOfTicketsTakenByType )
    
    

    synchronized public Statistics statistics ( )
    
        int count = this.countTicketsTaken();
        Map < Ticket.Type, Integer > map = this.reportCountOfTicketsTakenByType();
        if ( count != map.values().stream().mapToInt( Integer :: intValue ).sum() )  throw new IllegalStateException( "Counts do not match in Gate.Statistics. Error # 898e905f-9432-4195-a3e0-118bede2872d." ); 
        return new Statistics( Instant.now() , count , map );
    

    private int countTicketsTaken ( )
    
        return this.ticketsTaken.size();
    

    private Map < Ticket.Type, Integer > reportCountOfTicketsTakenByType ( )
    
        // We use `AtomicInteger` here not for thread-safety, but for the convenience of its `incrementAndGet` method.
        Map < Ticket.Type, AtomicInteger > map = new EnumMap < Ticket.Type, AtomicInteger >( Ticket.Type.class );
        Arrays.stream( Ticket.Type.values() ).forEach( type -> map.put( type , new AtomicInteger( 0 ) ) ); // Initialize the map, so each ticket-type has an atomic integer set to zero.
        this.ticketsTaken.stream().forEach( ticket -> map.get( ticket.type() ).incrementAndGet() );

        // Convert the AtomicInteger values to simply `Integer` values.
        Map < Ticket.Type, Integer > result = map.entrySet().stream().collect( Collectors.toMap( Map.Entry :: getKey , atomicInteger -> atomicInteger.getValue().intValue() ) );
        return Map.copyOf( result ); // Return an unmodifiable map, as a good practice.
    

我们有一个App 课程来进行演示。

package work.basil.festival;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.stream.IntStream;

/**
 * Hello world!
 */
public class App

    public static void main ( String[] args )
    
        System.out.println( "Hello World!" );

        App app = new App();
        app.demo();
    

    private void demo ( )
    
        final List < Ticket > ticketsToTake = this.generateTicketsForFestival();

        Gate gate = new Gate();

        // Report every five seconds on the progress of our gate taking tickets.
        ScheduledExecutorService reportingExecutorService = Executors.newSingleThreadScheduledExecutor();
        reportingExecutorService.scheduleAtFixedRate( ( ) -> System.out.println( gate.statistics() ) , 0 , 5 , TimeUnit.SECONDS );

        ExecutorService ticketTakingExecutorService = Executors.newFixedThreadPool( 7 );
        for ( Ticket ticket : ticketsToTake )
        
            ticketTakingExecutorService.submit( ( ) -> gate.takeTicket( ticket ) );
        
        ticketTakingExecutorService.shutdown();
        try  ticketTakingExecutorService.awaitTermination( 10 , TimeUnit.MINUTES );  catch ( InterruptedException e )  e.printStackTrace(); 
        reportingExecutorService.shutdown();
        try  reportingExecutorService.awaitTermination( 10 , TimeUnit.MINUTES );  catch ( InterruptedException e )  e.printStackTrace(); 

        System.out.println( "« FIN » " + gate.statistics() );
    

    private List < Ticket > generateTicketsForFestival ( )
    
        List < Ticket > tickets = new ArrayList <>();
        for ( int i = 0 ; i < 85_000 ; i++ )
        
            tickets.add(
                    new Ticket(
                            UUID.randomUUID() ,  // Randomly generate a UUID, to identify uniquely each ticket.
                            Ticket.Type.values()[ ThreadLocalRandom.current().nextInt( Ticket.Type.values().length ) ]  // Randomly pick one of the ticket types.
                    )
            );
        
        return List.copyOf( tickets );
    

在 8 核 M1 MacBook Pro 上运行时。

Hello World!
Statistics[when=2021-11-14T02:28:52.746596Z, countTicketsTaken=0, countOfTicketsTakenByType=FREE_PASS=0, ONE_DAY=0, ONE_DAY_VIP=0, FULL_VIP=0]
Statistics[when=2021-11-14T02:28:57.800514Z, countTicketsTaken=7517, countOfTicketsTakenByType=FREE_PASS=1862, ONE_DAY=1953, ONE_DAY_VIP=1889, FULL_VIP=1813]
Statistics[when=2021-11-14T02:29:02.804886Z, countTicketsTaken=15128, countOfTicketsTakenByType=FREE_PASS=3791, ONE_DAY=3788, ONE_DAY_VIP=3775, FULL_VIP=3774]
Statistics[when=2021-11-14T02:29:07.746712Z, countTicketsTaken=22819, countOfTicketsTakenByType=FREE_PASS=5764, ONE_DAY=5653, ONE_DAY_VIP=5703, FULL_VIP=5699]
Statistics[when=2021-11-14T02:29:12.769943Z, countTicketsTaken=30577, countOfTicketsTakenByType=FREE_PASS=7687, ONE_DAY=7631, ONE_DAY_VIP=7641, FULL_VIP=7618]
Statistics[when=2021-11-14T02:29:17.803627Z, countTicketsTaken=38146, countOfTicketsTakenByType=FREE_PASS=9553, ONE_DAY=9552, ONE_DAY_VIP=9554, FULL_VIP=9487]
Statistics[when=2021-11-14T02:29:22.785355Z, countTicketsTaken=45896, countOfTicketsTakenByType=FREE_PASS=11455, ONE_DAY=11497, ONE_DAY_VIP=11499, FULL_VIP=11445]
Statistics[when=2021-11-14T02:29:27.768809Z, countTicketsTaken=53563, countOfTicketsTakenByType=FREE_PASS=13448, ONE_DAY=13393, ONE_DAY_VIP=13386, FULL_VIP=13336]
Statistics[when=2021-11-14T02:29:32.739398Z, countTicketsTaken=61189, countOfTicketsTakenByType=FREE_PASS=15358, ONE_DAY=15291, ONE_DAY_VIP=15310, FULL_VIP=15230]
Statistics[when=2021-11-14T02:29:37.751764Z, countTicketsTaken=68758, countOfTicketsTakenByType=FREE_PASS=17214, ONE_DAY=17136, ONE_DAY_VIP=17226, FULL_VIP=17182]
Statistics[when=2021-11-14T02:29:42.759303Z, countTicketsTaken=76446, countOfTicketsTakenByType=FREE_PASS=19136, ONE_DAY=19057, ONE_DAY_VIP=19171, FULL_VIP=19082]
Statistics[when=2021-11-14T02:29:47.768858Z, countTicketsTaken=84030, countOfTicketsTakenByType=FREE_PASS=21086, ONE_DAY=20930, ONE_DAY_VIP=21062, FULL_VIP=20952]
« FIN » Statistics[when=2021-11-14T02:29:48.406351Z, countTicketsTaken=85000, countOfTicketsTakenByType=FREE_PASS=21321, ONE_DAY=21174, ONE_DAY_VIP=21305, FULL_VIP=21200]

【讨论】:

以上是关于我必须创建一个需要使用线程的应用程序,但它们不能正常工作的主要内容,如果未能解决你的问题,请参考以下文章

C#程序窗口假死

我怎么知道何时创建界面?

Java多线程概述及创建

如果线程必须写在同一个向量上,是不是可以使用线程

Java程序员必须掌握的线程知识-Callable和Future

在繁忙的 UI 线程上运行 ProgressBar