300 行代码带你秒懂 Java 多线程!

java 版连连看(300行代码) java 版文字连连看算法查找路线是最快的,代码简单易懂,很合适初学者提高面向对象思想,可以添加一些附加功能 立即下载

线程

线程的概念,百度是这样解释的:

线程(英语:Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(Lightweight Processes),但轻量进程更多指内核线程(Kernel Thread),而把用户线程(User Thread)称为线程。

1.1 线程与进程的区别

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。

线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。

也就是,进程可以包含多个线程,而线程是程序执行的最小单位。

1.2 线程的状态

  • NEW:线程刚创建

  • RUNNABLE: 在JVM中正在运行的线程,其中运行状态可以有运行中RUNNING和READY两种状态,由系统调度进行状态改变。

  • BLOCKED:线程处于阻塞状态,等待监视锁,可以重新进行同步代码块中执行

  • WAITING : 等待状态

  • TIMED_WAITING: 调用sleep() join() wait()方法可能导致线程处于等待状态

  • TERMINATED: 线程执行完毕,已经退出

1.3 Notify和Wait :

Notify和Wait 的作用

首先看源码给出的解释,这里翻译了一下:

Notify:唤醒一个正在等待这个对象的线程监控。如果有任何线程正在等待这个对象,那么它们中的一个被选择被唤醒。选择是任意的,发生在执行的酌情权。一个线程等待一个对象通过调用一个{@code wait}方法进行监视。

Notify()需要在同步方法或同步块中调用,即在调用前,线程也必须获得该对象的对象级别锁

Wait:导致当前线程等待,直到另一个线程调用{@link java.lang.Object#notify()}方法或{@link java.lang.Object#notifyAll()}方法。

换句话说,这个方法的行为就像它简单一样执行调用{@code wait(0)}。当前线程必须拥有该对象的监视器。

线程释放此监视器的所有权,并等待另一个线程通知等待该对象的监视器的线程,唤醒通过调用{@code notify}方法或{@code notifyAll}方法。然后线程等待,直到它可以重新取得监视器的所有权,然后继续执行。

Wait()的作用是使当前执行代码的线程进行等待,它是Object类的方法,该方法用来将当前线程置入预执行队列中,并且在Wait所在的代码行处停止执行,直到接到通知或被中断为止。

在调用Wait方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用Wait方法。

Wait和Sleep的区别:

  • 它们最大本质的区别是,Sleep()不释放同步锁,Wait()释放同步锁。

  • 还有用法的上的不同是:Sleep(milliseconds)可以用时间指定来使他自动醒过来,如果时间不到你只能调用Interreput()来强行打断;Wait()可以用Notify()直接唤起。

  • 这两个方法来自不同的类分别是Thread和Object

  • 最主要是Sleep方法没有释放锁,而Wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

1.4 Thread.sleep() 和Thread.yield()的异同

  • 相同 :Sleep()和yield()都会释放CPU。

  • 不同:Sleep()使当前线程进入停滞状态,所以执行Sleep()的线程在指定的时间内肯定不会执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。Sleep()可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会;yield()只能使同优先级的线程有执行的机会。

1.5 补充:死锁的概念

死锁:指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁产生的四个必要条件(缺一不可):

  • 互斥条件:顾名思义,线程对资源的访问是排他性,当该线程释放资源后下一线程才可进行占用。

  • 请求和保持:简单来说就是自己拿的不放手又等待新的资源到手。线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。

  • 不可剥夺:在没有使用完资源时,其他线性不能进行剥夺。

  • 循环等待:一直等待对方线程释放资源。

我们可以根据死锁的四个必要条件破坏死锁的形成。

1.6 补充:并发和并行的区别

并发:是指在某个时间段内,多任务交替的执行任务。当有多个线程在操作时,把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行。在一个时间段的线程代码运行时,其它线程处于挂起状。

并行:是指同一时刻同时处理多任务的能力。当有多个线程在操作时,CPU同时处理这些线程请求的能力。

区别就在于CPU是否能同时处理所有任务,并发不能,并行能。

1.7 补充:线程安全三要素

  • 原子性:Atomic包、CAS算法、Synchronized、Lock。

  • 可见性:Synchronized、Volatile(不能保证原子性)。

  • 有序性:Happens-before规则。

1.8 补充:如何实现线程安全

  • 互斥同步:Synchronized、Lock。

  • 非阻塞同步:CAS。

  • 无需同步的方案:如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性。

1.9 补充:保证线程安全的机制:

  • Synchronized关键字

  • Lock

  • CAS、原子变量

  • ThreadLocl:简单来说就是让每个线程,对同一个变量,都有自己的独有副本,每个线程实际访问的对象都是自己的,自然也就不存在线程安全问题了。

  • Volatile

  • CopyOnWrite写时复制

随着CPU核心的增多以及互联网迅速发展,单线程的程序处理速度越来越跟不上发展速度和大数据量的增长速度,多线程应运而生,充分利用CPU资源的同时,极大提高了程序处理速度。

创建线程的方法

继承Thread类:

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

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
    }
}

实现Runable接口:

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

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
    }
}

通过Callable和Future创建线程:

public class CallableCreateTest {
    public static void main(String[] args) throws Exception {
         // 将Callable包装成FutureTask,FutureTask也是一种Runnable
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();

        // get方法会阻塞调用的线程
        Integer sum = futureTask.get();
        System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
    }
}


class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tstarting...");

        int sum = 0;
        for (int i = 0; i <= 100000; i++) {
            sum += i;
        }
        Thread.sleep(5000);

        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tover...");
        return sum;
    }
}

线程池方式创建:

实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承,但可以多实现啊),只能实现接口。同时,线程池也是非常高效的,很容易实现和使用。

实际开发中,阿里巴巴开发插件一直提倡使用线程池创建线程,原因在下方会解释,所以上面的代码我就只简写了一些Demo。

2.1 线程池创建线程

线程池,顾名思义,线程存放的地方。和数据库连接池一样,存在的目的就是为了较少系统开销,主要由以下几个特点:

降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗(主要)。

提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。

Java提供四种线程池创建方式:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  • newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  • newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。

  • newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

通过源码我们得知ThreadPoolExecutor继承自AbstractExecutorService,而AbstractExecutorService实现了ExecutorService。

public class ThreadPoolExecutor extends AbstractExecutorService

public abstract class AbstractExecutorService implements ExecutorService

2.2 ThreadPoolExecutor介绍

实际项目中,用的最多的就是ThreadPoolExecutor这个类,而《阿里巴巴Java开发手册》中强制线程池不允许使用Executors去创建,而是通过New ThreadPoolExecutor实例的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

我们从ThreadPoolExecutor入手多线程创建方式,先看一下线程池创建的最全参数。

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

参数说明如下:

  • corePoolSize:线程池的核心线程数,即便线程池里没有任何任务,也会有corePoolSize个线程在候着等任务。

  • maximumPoolSize:最大线程数,不管提交多少任务,线程池里最多工作线程数就是maximumPoolSize。

  • keepAliveTime:线程的存活时间。当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。

  • Unit:这个用来指定keepAliveTime的单位,比如秒:TimeUnit.SECONDS。

  • BlockingQueue:一个阻塞队列,提交的任务将会被放到这个队列里。

  • threadFactory:线程工厂,用来创建线程,主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。

  • handler:拒绝策略,当线程池里线程被耗尽,且队列也满了的时候会调用。

2.2.1BlockingQueue

对于BlockingQueue个人感觉还需要单独拿出来说一下。

BlockingQueue:阻塞队列,有先进先出(注重公平性)和先进后出(注重时效性)两种,常见的有两种阻塞队列:ArrayBlockingQueue和LinkedBlockingQueue

队列的数据结构大致如图:

队列一端进入,一端输出。而当队列满时,阻塞。BlockingQueue核心方法:1. 放入数据put2. 获取数据take。常见的两种Queue:

2.2.2 ArrayBlockingQueue

基于数组实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。

一段代码来验证一下:

    package map;

    import java.util.concurrent.*;

    public class MyTestMap {
        // 定义阻塞队列大小
        private static final int maxSize = 5;
        public static void main(String[] args){
            ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(maxSize);
            new Thread(new Productor(queue)).start();
            new Thread(new Customer(queue)).start();
        }
    }

    class Customer implements Runnable {
        private BlockingQueue<Integer> queue;
        Customer(BlockingQueue<Integer> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            this.cusume();
        }

        private void cusume() {
            while (true) {
                try {
                    int count = (int) queue.take();
                    System.out.println("customer正在消费第" + count + "个商品===");
                    // 只是为了方便观察输出结果
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    class Productor implements Runnable {
        private BlockingQueue<Integer> queue;
        private int count = 1;
        Productor(BlockingQueue<Integer> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            this.product();
        }
        private void product() {
            while (true) {
                try {
                    queue.put(count);
                    System.out.println("生产者正在生产第" + count + "个商品");
                    count++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

//输出如下
/**
生产者正在生产第1个商品
生产者正在生产第2个商品
生产者正在生产第3个商品
生产者正在生产第4个商品
生产者正在生产第5个商品
customer正在消费第1个商品===
*/

2.2.3 LinkedBlockingQueue

基于链表的阻塞队列,内部也维护了一个数据缓冲队列。需要我们注意的是如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小。

LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

2.2.4 LinkedBlockingQueue和ArrayBlockingQueue的主要区别

  • ArrayBlockingQueue的初始化必须传入队列大小,LinkedBlockingQueue则可以不传入。

  • ArrayBlockingQueue用一把锁控制并发,LinkedBlockingQueue俩把锁控制并发,锁的细粒度更细。即前者生产者消费者进出都是一把锁,后者生产者生产进入是一把锁,消费者消费是另一把锁。

  • ArrayBlockingQueue采用数组的方式存取,LinkedBlockingQueue用Node链表方式存取。

2.2.5handler拒绝策略

Java提供了4种丢弃处理的方法,当然你也可以自己实现,主要是要实现接口:RejectedExecutionHandler中的方法。

  • AbortPolicy:不处理,直接抛出异常。

  • CallerRunsPolicy:只用调用者所在线程来运行任务,即提交任务的线程。

  • DiscardOldestPolicy:LRU策略,丢弃队列里最近最久不使用的一个任务,并执行当前任务。

  • DiscardPolicy:不处理,丢弃掉,不抛出异常。

2.2.6线程池五种状态

    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

RUNNING:在这个状态的线程池能判断接受新提交的任务,并且也能处理阻塞队列中的任务。

SHUTDOWN:处于关闭的状态,该线程池不能接受新提交的任务,但是可以处理阻塞队列中已经保存的任务,在线程处于RUNNING状态,调用shutdown()方法能切换为该状态。

STOP:线程池处于该状态时既不能接受新的任务也不能处理阻塞队列中的任务,并且能中断现在线程中的任务。当线程处于RUNNING和SHUTDOWN状态,调用shutdownNow()方法就可以使线程变为该状态。

TIDYING:在SHUTDOWN状态下阻塞队列为空,且线程中的工作线程数量为0就会进入该状态,当在STOP状态下时,只要线程中的工作线程数量为0就会进入该状态。

TERMINATED:在TIDYING状态下调用terminated()方法就会进入该状态。可以认为该状态是最终的终止状态。

回到线程池创建ThreadPoolExecutor,我们了解了这些参数,再来看看ThreadPoolExecutor的内部工作原理:

  • 判断核心线程是否已满,是进入队列,否:创建线程

  • 判断等待队列是否已满,是:查看线程池是否已满,否:进入等待队列

  • 查看线程池是否已满,是:拒绝,否创建线程

2.3深入理解ThreadPoolExecutor

进入Execute方法可以看到:

  public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
      //判断当前活跃线程数是否小于corePoolSize,如果小于,则调用addWorker创建线程执行任务
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
      //如果不小于corePoolSize,则将任务添加到workQueue队列。
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
      //如果放入workQueue失败,则创建线程执行任务,如果这时创建线程失败(当前线程数不小于maximumPoolSize时),就会调用reject(内部调用handler)拒绝接受任务。
        else if (!addWorker(command, false))
            reject(command);
    }

AddWorker方法:

  • 创建Worker对象,同时也会实例化一个Thread对象。在创建Worker时会调用threadFactory来创建一个线程。

  • 然后启动这个线程。

2.3.1线程池中CTL属性的作用是什么?

看源码第一反应就是这个CTL到底是个什么东东?有啥用?一番研究得出如下结论:

CTL属性包含两个概念:

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

    private static int ctlOf(int rs, int wc) { return rs | wc; }

  • runState:即rs 表明当前线程池的状态,是否处于Running,Shutdown,Stop,Tidying。

  • workerCount:即wc表明当前有效的线程数。

我们点击workerCount即工作状态记录值,以RUNNING为例,RUNNING = -1 << COUNT_BITS;,即-1无符号左移COUNT_BITS位,进一步我们得知COUNT_BITS位29,因为Integer位数为31位(2的五次方减一)

    private static final int COUNT_BITS = Integer.SIZE - 3;

既然是29位那么就是Running的值为:

1110 0000 0000 0000 0000 0000 0000 0000 
|||
31~29位

那低28位呢,就是记录当前线程的总线数啦:

    // Packing and unpacking ctl
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    private static int ctlOf(int rs, int wc) { return rs | wc; }

从上述代码可以看到workerCountOf这个函数传入ctl之后,是通过CTL&CAPACITY操作来获取当前运行线程总数的。

也就是RunningState|WorkCount&CAPACITY,算出来的就是低28位的值。因为CAPACITY得到的就是高3位(29-31位)位0,低28位(0-28位)都是1,所以得到的就是ctl中低28位的值。

而runStateOf这个方法的话,算的就是RunningState|WorkCount&CAPACITY,高3位的值,因为CAPACITY是CAPACITY的取反,所以得到的就是高3位(29-31位)为1,低28位(0-28位)为0,所以通过&运算后,所得到的值就是高3为的值。

简单来说就是ctl中是高3位作为状态值,低28位作为线程总数值来进行存储。

2.3.2 shutdownNow和shutdown的区别

看源码发现有两种近乎一样的方法,shutdownNow和shutdown,设计者这么设计自然是有它的道理,那么这两个方法的区别在哪呢?

  • shutdown会把线程池的状态改为SHUTDOWN,而shutdownNow把当前线程池状态改为STOP。

  • shutdown只会中断所有空闲的线程,而shutdownNow会中断所有的线程。

  • shutdown返回方法为空,会将当前任务队列中的所有任务执行完毕;而shutdownNow把任务队列中的所有任务都取出来返回。

2.3.3 线程复用原理

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

就是任务在并不只执行创建时指定的firstTask第一任务,还会从任务队列的中自己主动取任务执行,而且是有或者无时间限定的阻塞等待,以保证线程的存活。

默认的是不允许。

2.4 CountDownLatch和CyclicBarrier区别

countDownLatch是一个计数器,线程完成一个记录一个,计数器递减,只能只用一次。

CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供Reset功能,可以多次使用。

3. 多线程间通信的几种方式

提及多线程又不得不提及多线程通信的机制。首先,要短信线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析:

题目:有两个线程A、B,A线程向一个集合里面依次添加元素"abc"字符串,一共添加十次,当添加到第五次的时候,希望B线程能够收到A线程的通知,然后B线程执行相关的业务操作。

3.1使用volatile关键字

package thread;

/**
 * 
 * @author hxz
 * @description 多线程测试类
 * @version 1.0
 * @data 2020年2月15日 上午9:10:09
 */
public class MyThreadTest {

    public static void main(String[] args) throws Exception {

        notifyThreadWithVolatile();

    }

    /**
     * 定义一个测试
     */
    private static volatile boolean flag = false;
    /**
     * 计算I++,当I==5时,通知线程B
     * @throws Exception
     */
    private static void notifyThreadWithVolatile() throws Exception {
        Thread thc = new Thread("线程A"){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    if (i == 5) {
                        flag = true;
                        try {
                            Thread.sleep(500L);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        break;
                    }
                    System.out.println(Thread.currentThread().getName() + "====" + i);
                }
            }
        };

        Thread thd = new Thread("线程B") {
            @Override
            public void run() {
                while (true) {
                    // 防止伪唤醒 所以使用了while
                    while (flag) {
                        System.out.println(Thread.currentThread().getName() + "收到通知");
                        System.out.println("do something");
                        try {
                            Thread.sleep(500L);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        return ;
                    }

                }
            }
        };

        thd.start();
        Thread.sleep(1000L);
        thc.start();

    }
}

个人认为这是基本上最好的通信方式,因为A发出通知B能够立马接受并Do Something。

原文链接:

https://blog.youkuaiyun.com/weixin_44104367/article/details/104481510

(完)

推荐阅读

经验:一个秒杀系统的设计思考

如何在SpringBoot中异步请求和异步调用

SpringBoot+JWT+Shiro+MybatisPlus实现Restful快速开发后端脚手架


好文!必须点赞
java版扫雷,300行,带注释 写java一年了,无聊写个小扫雷游戏,写了注释,提供给新手参考 立即下载

相关推荐

用Java猜数字游戏(300行左右)

用Java写的猜数字游戏,很简单,没有JFrame等窗口,纯粹cmd 小黑窗运行。

java 300行代码 冒险闯关小游戏(代码+讲解)

作为一个男孩子,从小就喜欢晚一些游戏。今天就用java写一个类似马里奥的冒险闯关小游戏,但这也不是一两天能完成的事情,我将会持续更新这个游戏(有什么好的介意也非常欢迎大家提出来,也能在我的基础上自己接着写)。目前完成的功能有:人物的流畅运动、跳跃以及自由下落,障碍物(地面)的添加,怪物的添加、移动以及死亡。(代码基本上是完整的,直接粘贴复制就能用)

逗呵呵的博客 1万+

Java经典编程300例源代码

汇集了300个Java日常开发中广泛使用的实例代码,配套使用《Java经典编程300例》(清华大学出版社)

日撸 Java 三百行day1-10

java基础知识学习与巩固

fulishafulisha的博客 1303

基于STM32的馆藏文物保存环境监测系统设计与实现

针对馆藏文物保存环境中温湿度、光照、有害气体等参数波动易导致文物损坏的问题,本文设计了一种基于STM32微控制器的馆藏文物保存环境监测系统。该系统以STM32L431RCT6为核心,集成高精度传感器实现多参数实时采集,通过有线与无线结合的通信方式构建监测网络,具备数据存储、异常预警、远程监控等功能。测试结果表明,系统温湿度测量精度分别达±0.2℃、±2%RH,光照与有害气体检测误差≤5%,可满足文物保存环境的高精度监测需求,为文物保护提供科学的数据支撑。

m0_74026776的博客 1543

PyTorch实战:从零构建CNN图像分类模型

卷积神经网络(CNN)作为计算机视觉的核心架构,通过局部连接和权值共享机制高效处理图像数据。其核心组件包括卷积层、池化层和全连接层,其中PyTorch的动态计算图特性为CNN实现提供了灵活的开发环境。在工程实践中,使用CIFAR-10这类标准数据集可以快速验证模型性能,而数据增强和批归一化等技术能有效提升模型泛化能力。通过PyTorch实现CNN不仅能够掌握图像分类的基本原理,还能为后续迁移学习和复杂视觉任务打下基础,特别是在结合Adam优化器和学习率调度策略后,模型收敛速度和准确率都能得到显著提升。

weixin_42405592的博客 318

日撸 Java 三百行(31-40天,图)

图是数据结构最困难的部分, 掌握好就无敌了.

minfanphd的博客 3481

WINDOWS下SIP拨号的实现

在WIN10上拨打SIP电话  由于我的交换原理实验有一个是关于SIP的抓包的。而之前从未接触过SIP,所以在如何完成一次SIP拨号上遇到了不少问题,查了好多资料都没有完整的或者写的很清楚的,这里就对那些资料做一个总结,给出一个比较完善的解决方案,一来是记录此次实验的收获,而来也可能为其他有同样困惑的人解惑。  废话不多说直接开始。首先是下载一个可以拨打SIP电话的客户终端,这里我选择的是MicroSIP, 下载红圈内的文件,下载好之后直接一路安装即可,没有什么特别注意的地方。  安装好客户端之后,就需要注

weixin_43918519的博客 3808

300行代码手写mini版本Spring

注解会接受一个value参数,如果有的话优先使用value作为key,然后就是判断类是否有实现接口如果有的话还要插入一条记录,该记录的值还是类的实例化但是此时key变成接口的全称,这里主要是为了用接口定义属性的时候的注入问题。将类实例化然后存到ioc容器中,因为我们这里实现的是mini版本的spring,所以这里我们就不管类是否设置懒加载等属性而是直接初始化的时候就实例化完成,而且都是容器式单例。这里也需要注意的是不是所有的属性我们都要注入,同样的我们也要判断属性是否带有。至此我们的配置阶段也就算完成了。

老男孩的架构路 323

超详细!Linux 服务器 6 大核心性能测试(附自动脚本 + 结果分析秘籍)---Linux 服务器性能测试文档

本文档旨在提供 Linux 服务器的 CPU、内存和磁盘性能测试方案,包括详细的测试大纲、操作步骤以及一键化测试脚本,帮助管理员快速评估服务器性能状况。通过以上测试方案,可以全面评估 Linux 服务器的 CPU、内存和磁盘性能,为系统优化、容量规划和问题排查提供重要参考依据。使用 sysbench 进行 CPU 基准测试。使用 sysbench 测试内存读写性能。使用 fio 工具进行更全面的磁盘测试。使用 dd 命令测试磁盘顺序读写性能。使用 dd 命令测试内存读写速度。记录磁盘 IO 性能。

jundao1997的专栏 381

基于FMU的Star CCM+与Amesim复杂控制联合分析

基于FMU的Star CCM+与Amesim复杂控制联合分析

2201_75375391的博客 2020

300 行代码带你秒懂 Java 多线程!| 原力计划

作者 |永远在路上【】责编 | 胡巍巍出品 | 优快云博客线程线程的概念,百度是这样解释的:线程(英语:Thread)是操作系统能够进行运算调度的最...

程序人生的博客 1759

10万张数据集持续迭代:YOLO模型的场景专属自进化体系如何搭建?

做工业质检的朋友应该都有过这样的经历——拿着在COCO上刷榜的YOLO模型跑到产线上一测,精度直接腰斩。这不是模型不行,是场景不对口。过去一年,我深度参与了一个工业视觉项目:产线每天产生超过10万张图像,需要持续迭代检测模型。我们踩过无数坑,最终摸索出一套YOLO模型的场景专属自进化体系。今天不聊虚的,直接上干货。让模型在真实场景数据中持续学习、自动进化,而不是每次依赖人工标注从头训练。下面我从主动学习筛选、增量持续学习、架构选型、部署优化、安全防护五个维度,拆解整个搭建过程。声明。

2601_95807009的博客 106

Java多线程300毫秒,300 行代码带你秒懂 Java 多线程!

原标题:300行代码带你秒懂Java多线程!线程线程的概念,百度是这样解释的:线程(英语:Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在UnixSystemV及SunOS中也被称为轻量进程(LightweightProcesses),但轻量进程更多指...

weixin_39950083的博客 272

数字图像处理 基于matlab、opencv计算图像的梯度方向和梯度幅值

图像可以被视为标量场(即二维函数)。通过微分将标量场转换为矢量场。梯度是一个向量,描述了在x或y方向上移动时,图像变化的速度。我们使用导数来回答这样的问题,图像梯度的大小告诉图像变化的速度,而梯度的方向告诉图像变化最快的方向。因为梯度有方向和大小,所以将这些信息编码为向量是很自然的。该向量的长度提供了梯度的大小,而其方向提供了梯度方向。因为梯度在每个位置可能不同,所以我们在每个图像位置用不同的向量来表示它。

学以致用 知行合一 3203

数据分析案例-电脑笔记本价格数据可视化分析3

本文基于Kaggle笔记本数据集(1303条记录,13个变量),使用Python进行数据可视化分析。通过自定义函数预处理了CPU频率、内存类型等关键特征,利用Seaborn和Matplotlib绘制了品牌分布、内存类型占比、价格影响因素等可视化图表。研究发现:1)Dell和Lenovo市场份额最大;2)SSD是主流存储类型;3)高RAM配置普遍采用SSD;4)Intel处理器占据绝对优势。分析揭示了不同品牌、配置与价格的关系,为消费者购买决策和市场策略制定提供了数据支持。

m0_64336780的博客 1万+

线程释放_300 行代码带你秒懂 Java 多线程!

线程的概念,百度是这样解释的:线程(英语:Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(Lightweight Processes),但轻量进程更多指内核线程(Kernel Thre...

weixin_31560425的博客 293

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

上一篇: JVM 史上最最最完整深入解析(12000 字噢)
下一篇: 最新Java学习资料与思维导图分享,免费领取!
Java思维导图
博客等级 码龄10年 306粉丝 71原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值