java多线程用法整理_java 线程池 stepping-程序员宅基地

技术标签: java  

本文主要整理Java的多线程机制、Java多线程的原理以及使用方法。

线程的创建(基础)

在Java中创建线程有两种方法:使用Thread类和使用Runnable接口。
但在使用Runnable接口时需要建立一个Thread实例。所以无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它的子类的实例。Thread构造函数如下:

public Thread();
public Thread(Runnable target);
public Thread(String name);
public Thread(Runnable target, String name);
public Thread(ThreadGroup group, Runnable target);
public Thread(ThreadGroup group, String name);
public Thread(ThreadGroup group, Runnable target, String name);
public Thread(ThreadGroup group, Runnable target, String name, long stackSize);

1、继承Thread类并覆盖run方法demo:

public class Test {
    
    public static void main(String[] args){
    
        ThreadDemo d = new ThreadDemo();
        d.start();
        for(int i=0;i<60;i++){
    
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
	private static class ThreadDemo extends Thread{
    
     	 public void run(){
    
         	for(int i=0;i<60;i++){
    
            	System.out.println(Thread.currentThread().getName()+i);
         	}
     	}
 	}
}

2、实现Runnable接口demo:

public class Test {
    
    public static void main(String[] args){
    
        ThreadDemo d =new ThreadDemo();
        Thread t = new Thread(d);
        t.start();
        for(int x=0;x<60;x++){
    
            System.out.println(Thread.currentThread().getName()+x);
        }
    }
	private static class ThreadDemo implements Runnable{
    
    	public void run(){
    
        	for(int x=0;x<60;x++){
    
            	System.out.println(Thread.currentThread().getName()+x);
        	}
    	}
    }
}
线程的生命周期及线程池(步进)

1.线程的生命周期
线程的生命周期分为:开始(等待)、运行、挂起和停止四种不同的状态。这四种状态都可以通过Thread类中的方法进行控制,下面给出了Thread类中和这四种状态相关的方法:

// 开始线程
publicvoid start( );
publicvoid run( );
// 挂起和唤醒线程
publicvoid resume( ); // 不建议使用
publicvoid suspend( ); // 不建议使用
publicstaticvoid sleep(long millis);
publicstaticvoid sleep(long millis, int nanos);
// 终止线程
publicvoid stop( ); // 不建议使用
publicvoid interrupt( );
// 得到线程状态
publicboolean isAlive( );
publicboolean isInterrupted( );
publicstaticboolean interrupted( );
// join方法
publicvoid join( ) throws InterruptedException;

1)线程的创建
线程在建立(new)后并不马上执行run方法中的代码,而是处于等待状态。线程处于等待状态时,可以通过Thread类的方法来设置线程不各种属性,如线程的优先级(setPriority)、线程名(setName)和线程的类型(setDaemon)等。

2)线程的运行
当调用start方法后,线程开始执行run方法中的代码,线程进入运行状态。此时,可以通过Thread类的isAlive方法来判断线程是否处于运行状态。当线程处于运行状态时,isAlive返回true;当isAlive返回false时,可能线程处于等待状态,也可能处于停止状态。

3)线程的挂起
一但线程开始执行run方法,就会一直到这个run方法执行完成这个线程才退出。但在线程执行的过程中,可以通过两个方法使线程暂时停止执行。这两个方法是suspend和sleep。在使用suspend挂起线程后,可以通过resume方法唤醒线程。而使用sleep使线程休眠后,只能在设定的时间后使线程处于就绪状态(在线程休眠结束后,线程不一定会马上执行,只是进入了就绪状态,等待着系统进行调度)。
在使用sleep方法时有两点需要注意:
3.1)sleep方法有两个重载形式,其中一个重载形式不仅可以设毫秒,而且还可以设纳秒(1,000,000纳秒等于1毫秒)。但大多数操作系统平台上的Java虚拟机都无法精确到纳秒,因此,如果对sleep设置了纳秒,Java虚拟机将取最接近这个值的毫秒。
3.2)在使用sleep方法时必须使用throws或try{…}catch{…}。因为run方法无法使用throws,所以只能使用try{…}catch{…}。当在线程休眠的过程中,使用interrupt方法中断线程时sleep会抛出一个InterruptedException异常。sleep方法的定义如下:

publicstaticvoid sleep(long millis) throws InterruptedException
publicstaticvoid sleep(long millis, int nanos) throws InterruptedException

4)线程的终止:
线程的终止方式有三种,分别如下:
4.1)使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
4.2)使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。
4.3) 使用interrupt方法中断线程。
4.4)使用退出标志终止线程
当run方法执行完后,线程就会退出。但有时run方法是永远不会结束的。列如:在服务端程序中使用线程进行监听客户端请求,或是其他的需要循环处理的任务。在这种情况下,一般是将这些任务放在一个循环中,如while循环。如果想让循环永远运行下去,可以使用while(true){…}来处理。但要想使while循环在某一特定条件下退出,最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。

2.线程池的使用
线程池的概念:
线程池,本质上是一种对象池,用于管理线程资源。在任务执行前,需要从线程池中拿出线程来执行。在任务执行完成之后,需要把线程放回线程池。通过线程的这种反复利用机制,可以有效地避免直接创建线程所带来的坏处。

线程池的好处:
降低资源的消耗。线程本身是一种资源,创建和销毁线程会有CPU开销;创建的线程也会占用一定的内存。提高任务执行的响应速度。任务执行时,可以不必等到线程创建完之后再执行。提高线程的可管理性。线程不能无限制地创建,需要进行统一的分配、调优和监控。

线程池处理逻辑:
判断核心线程池是否已满,如果不是,则创建线程执行任务;如果核心线程池满了,判断队列是否满了,如果队列没满,将任务放在队列中;如果队列满了,则判断线程池是否已满,如果没满,创建线程执行任务;如果线程池也满了,则按照拒绝策略对任务进行处理。

2.1)创建线程池:
理论上,我们可以通过Executors来创建线程池,这种方式非常简单。但正是因为简单,所以限制了线程池的功能。比如:无长度限制的队列,可能因为任务堆积导致OOM,这是非常严重的bug,应尽可能地避免,所以还是建议通过更底层的方式来创建线程池。
ThreadPoolExecutor的构造方法及参数说明:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler);

corePoolSize,线程池中的核心线程数
maximumPoolSize,线程池中的最大线程数
keepAliveTime,空闲时间,当线程池数量超过核心线程数时,多余的空闲线程存活的时间,即:这些线程多久被销毁。
unit,空闲时间的单位,可以是毫秒、秒、分钟、小时和天,等等
workQueue,等待队列,线程池中的线程数超过核心线程数时,任务将放在等待队列,它是一个BlockingQueue类型的实现对象
threadFactory,线程工厂,我们可以使用它来创建一个线程
handler,拒绝策略,当线程池和等待队列都满了之后,需要通过该对象的回调函数进行回调处理

重要参数:workQueue、threadFactory、handler,详情如下:

workQueue:等待队列是BlockingQueue类型的,理论上只要是它的子或实现类,我们都可以用来作为等待队列。jdk内部自带的一些阻塞队列如下:
ArrayBlockingQueue,队列是有界的,基于数组实现的阻塞队列;
LinkedBlockingQueue,队列可以有界,也可以无界,基于链表实现的阻塞队列;
SynchronousQueue,不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作将一直处于阻塞状态,该队列也是Executors.newCachedThreadPool()的默认队列;
PriorityBlockingQueue,带优先级的无界阻塞队列;
通常情况下,我们需要指定阻塞队列的上界(比如1024)。

threadFactory:ThreadFactory为线程工厂接口,只有一个方法,可以用来生产一个线程对象,接口的定义如下:

public interface ThreadFactory {
    
    /**
     * Constructs a new {@code Thread}.  Implementations may also initialize
     * priority, name, daemon status, {@code ThreadGroup}, etc.
     *
     * @param r a runnable to be executed by new thread instance
     * @return constructed thread, or {@code null} if the request to
     *         create a thread is rejected
     */
    Thread newThread(Runnable r);
}

Executors的实现使用了默认的线程工厂-DefaultThreadFactory。它的实现主要用于创建一个线程,线程的名字为pool-{poolNum}-thread-{threadNum},代码如下:

static class DefaultThreadFactory implements ThreadFactory {
    
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
    
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
    
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

若需要自定义线程名字,只需要自己实现ThreadFactory,用于创建特定场景的线程即可。

handler:拒绝策略,当线程池满了、队列也满了的时候,对任务采取的措施。或者丢弃、或者执行、或者其他…;jdk自带4种拒绝策略:
CallerRunsPolicy // 在调用者线程执行;
AbortPolicy // 直接抛出RejectedExecutionException异常;
DiscardPolicy // 任务直接丢弃,不做任何处理;
DiscardOldestPolicy // 丢弃队列里最旧的那个任务,再尝试执行当前任务。
这四种策略各有优劣,比较常用的是DiscardPolicy,但是这种策略有一个弊端就是任务执行的轨迹不会被记录下来。所以往往需要实现自定义的拒绝策略, 通过实现RejectedExecutionHandler接口的方式。

2.2)提交任务的方式:
线程池中提交任务,主要有两种方法:execute()和submit()。

execute()用于提交不需要返回结果的任务。demo:

public static void main(String[] args) {
    
    ExecutorService executor = Executors.newFixedThreadPool(2);
    executor.execute(() -> System.out.println("hello"));
}

submit()用于提交一个需要返回果的任务。该方法返回一个Future对象,通过调用这个对象的get()方法,我们就能获得返回结果。get()方法会一直阻塞,直到返回结果返回。可以通过它的重载方法get(long timeout, TimeUnit unit)设置超时,这个方法也会阻塞,但是在超时时间内仍然没有返回结果时,将抛出异常TimeoutException。demo:

public static void main(String[] args) throws Exception {
    
    ExecutorService executor = Executors.newFixedThreadPool(2);
    Future<Long> future = executor.submit(() -> {
    
        System.out.println("task is executed");
        return System.currentTimeMillis();
    });
    System.out.println("task execute time is: " + future.get());
}

2.3)关闭线程池
在线程池的使命完成之后,需要对线程池中的资源进行释放操作,可以调用线程池对象的shutdown()和shutdownNow()方法来关闭线程池。主要有shutdown()及shutdownNow()两种方法:
shutdown()会将线程池状态置为SHUTDOWN,不再接受新的任务,同时会等待线程池中已有的任务执行完成再结束。
shutdownNow()会将线程池状态置为SHUTDOWN,对所有线程执行interrupt()操作,清空队列,并将队列中的任务返回回来。

3)配置线程池的参数
一般根据任务的特性及系统硬件来分析以配置线程池的参数。参考系如下:
任务的性质:CPU密集型、IO密集型和混杂型
任务的优先级:高中低
任务执行的时间:长中短
任务的依赖性:是否依赖数据库或者其他系统资源
不同的性质的任务,我们采取的配置将有所不同。
通常来说,如果任务属于CPU密集型,那么我们可以将线程池数量设置成CPU的个数,以减少线程切换带来的开销。如果任务属于IO密集型,我们可以将线程池数量设置得更多一些,比如CPU个数*2。(可以通过Runtime.getRuntime().availableProcessors()来获取CPU的个数)

4)线程池监控
利用监控,可以在问题出现前提前感知到,也可以根据监控信息来定位可能出现的问题。

通过ThreadPoolExecutor自带的一些方法,可以查看线程池状态:
long getTaskCount(); //获取已经执行或正在执行的任务数;
long getCompletedTaskCount(); //获取已经执行的任务数;
int getLargestPoolSize(); //获取线程池曾经创建过的最大线程数,根据这个参数,可以知道线程池是否满载过;
int getPoolSize(); //获取线程池线程数;
int getActiveCount(); //获取活跃线程数(正在执行任务的线程数);

ThreadPoolExecutor留给开发者自行处理的回调方法有3个,它在ThreadPoolExecutor中为空实现(也就是什么都不做)。
protected void beforeExecute(Thread t, Runnable r) // 任务执行前被调用
protected void afterExecute(Runnable r, Throwable t) // 任务执行后被调用
protected void terminated() // 线程池结束后被调用
3个方法的demo:

public class ThreadPoolTest {
    
    public static void main(String[] args) {
    
        ExecutorService executor = new ThreadPoolExecutor(1, 1, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1)) {
    
            @Override protected void beforeExecute(Thread t, Runnable r) {
    
                System.out.println("beforeExecute is called");
            }
            @Override protected void afterExecute(Runnable r, Throwable t) {
    
                System.out.println("afterExecute is called");
            }
            @Override protected void terminated() {
    
                System.out.println("terminated is called");
            }
        };

        executor.submit(() -> System.out.println("this is a task"));
        executor.shutdown();
    }
}

以上demo输出如下:
beforeExecute is called
this is a task
afterExecute is called
terminated is called

5)特殊问题
任何代码在使用的时候都可能遇到问题,线程池也不例外。我们来看一个例子:

public class ThreadPoolTest {
    
    public static void main(String[] args) {
    
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
    
            executor.submit(new DivTask(100, i));
        }
    }

    static class DivTask implements Runnable {
    
        int a, b;

        public DivTask(int a, int b) {
    
            this.a = a;
            this.b = b;
        }

        @Override public void run() {
    
            double result = a / b;
            System.out.println(result);
        }
    }
}

该代码执行的结果如下:
](https://img-blog.csdnimg.cn/20200314230621430.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTQ1MjQwMQ==,size_16,color_FFFFFF,t_70)
循环了5次,理论上应该有5个结果被输出。可是最终的执行结果却很让人很意外–只有4次输出。通过进一步分析发现,当第一次循环,除数为0时,理论上应该抛出异常才对,但是这儿却没有,异常被莫名其妙地吞掉了!
这又是为什么呢?进一步看看submit()方法,这个方法是一个非阻塞方法,有一个返回对象,返回的是Future对象。那么就猜测,会不会是因为没有对Future对象做处理导致的?将代码微调一下,重新运行,异常信息终于可以打印出来了。(在使用submit()的时候一定要注意它的返回对象Future,为了避免任务执行异常被吞掉的问题,需要调用Future.get()方法。若使用execute()将不会出现这种问题。在调线程池submit()方法的时候,一定要尽量避免任务执行异常被吞掉的问题)

for (int i = 0; i < 5; i++) {
    
    Future future= executor.submit(new DivTask(100, i));
    try {
    
        future.get();
    } catch (Exception e) {
    
        e.printStackTrace();
    }
}
多线程问题(提升)

1.join方法
join方法的功能就是使异步执行的线程变成同步执行。也就是说,当调用线程实例的start方法后,这个方法会立即返回,如果在调用start方法后后需要使用一个由这个线程计算得到的值,就必须使用join方法。如果不使用join方法,就不能保证当执行到start方法后面的某条语句时,这个线程一定会执行完。而使用join方法后,直到这个线程退出,程序才会往下执行。例如:

pubic class Test{
    
	private static int i = 0;
	public static void main(String[] args){
    
        Thread t = new Thread(new Runnable(){
    
        	public void run() {
    
        		Thread.sleep(500L);
        		i += 1;
        	}
        });
        t.start();
        t.join();//如果不加join下面会打印0,加了就打印1
        System.out.println(i);
    }
}

主线程在加了join函数以后start后会一直阻塞直到 t 线程执行完毕后才继续执行。

2.多线程共享数据安全

问题原因:当多个线程语句同时在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与进来执行,导致共享数据的错误。
解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不执行。

1)同步函数锁与静态同步函数锁:顾名思义,静态同步函数锁相对于同步函数锁不同的地方在于静态函数同步锁是静态的而同步函数所是非静态的~,一个存在于栈中,另外一个存在于堆中。但其实对象是任意的,只要保证是唯一性:多个线程用的是同一个锁就行。

pubic class Test{
    
	private static int i = 0;
	private static int ii = 0;
	public static void main(String[] args){
    
		Runnable run = new Runnable(){
    
        	public void run() {
    
        		while(true){
    
        			ii += 1;
        			System.out.println(ii);
        			synchronized(Test.class){
    
        				i += 1;
        				System.out.println(i);
        			}
        			Thread.sleep(500L);
        		}
        	}
        }
        Thread t = new Thread(run);
        t.start();
        Thread tt = new Thread(run);
        tt.start();
    }
}

以上代码,i的结果必定是递增的,而ii的结果有可能不是递增的,ii就是多线程操作共享数据引发的数据不安全的示例。Test.class为静态同步函数锁,我们可以直接把run替换Test.class作为同步函数锁。同步函数就是利用synchronized修饰整个函数,这里不列出。

2)线程等待唤醒机制

public class Test{
    
	private static boolean flags = false;

	private static class Person {
    
		private String name;
		private String gender;

		public void set(String name, String gender) {
    
			this.name = name;
			this.gender = gender;
		}

		public void get() {
    
			System.out.println(this.name + "...." + this.gender);
		}
	}

	private static Person p = new Person();

	public static void main(String[] args) {
    
		new Thread(new Runnable() {
    
			public void run() {
    
				int x = 0;
				while (true) {
    
					synchronized (p) {
    
						if (flags) {
    
							try {
    
								p.wait();
							} catch (InterruptedException e) {
    
								e.printStackTrace();
							}
						}

						if (x == 0) {
    
							p.set("张三", "男");
						} else {
    
							p.set("lili", "nv");
						}
						x = (x + 1) % 2;
						flags = true;
						p.notifyAll();
					}
				}
			}
		}).start();
		new Thread(new Runnable() {
    
			public void run() {
    
				while (true) {
    
					synchronized (p) {
    
						if (!flags) {
    
							try {
    
								p.wait();
							} catch (InterruptedException e) {
    
								e.printStackTrace();
							}
						}

						p.get();
						flags = false;
						p.notifyAll();
					}
				}
			}
		}).start();
	}
}

上面例子中,等待和唤醒必须是同一把锁

3)生产消费机制
3.1)单生产者,单个消费者

public class Test {
    

	private static class Goods {
    
		private String name;
		private int num;

		public synchronized void produce(String name) {
    
			if (flags) {
    
				try {
    
					wait();
				} catch (InterruptedException e) {
    
					e.printStackTrace();
				}
			}
			this.name = name + "编号:" + num++;
			System.out.println("生产了...." + this.name);
			flags = true;
			notifyAll();
		}

		public synchronized void consume() {
    
			if (!flags) {
    
				try {
    
					wait();
				} catch (InterruptedException e) {
    
					e.printStackTrace();
				}
			}
			System.out.println("消费了******" + name);
			flags = false;
			notifyAll();
		}

	}

	private static boolean flags = false;

	private static Goods g = new Goods();

	public static void main(String[] args) {
    
		new Thread(new Runnable() {
    
			public void run() {
    
				while (true) {
    
					g.produce("商品");
				}
			}
		}).start();
		new Thread(new Runnable() {
    
			public void run() {
    
				while (true) {
    
					g.consume();
				}
			}
		}).start();
	}
}

上面例子中商品的生产和消费会按顺序进行。

3.2)多生产者,多消费者

public class Test {
    

	private static class Goods {
    
		private String name;
		private int num;

		public synchronized void produce(String name) {
    
			while (flags) {
    
				try {
    
					wait();
				} catch (InterruptedException e) {
    
					e.printStackTrace();
				}
			}
			this.name = name + "编号:" + num++;
			System.out.println(Thread.currentThread().getName() + "生产了...." + this.name);
			flags = true;
			notifyAll();
		}

		public synchronized void consume() {
    
			while (!flags) {
    
				try {
    
					wait();
				} catch (InterruptedException e) {
    
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + "消费了******" + name);
			flags = false;
			notifyAll();
		}

	}

	private static boolean flags = false;

	private static Goods g = new Goods();

	public static void main(String[] args) {
    
		new Thread(new Runnable() {
    
			public void run() {
    
				while (true) {
    
					g.produce("商品");
				}
			}
		}, "生产者一号").start();
		new Thread(new Runnable() {
    
			public void run() {
    
				while (true) {
    
					g.produce("商品");
				}
			}
		}, "生产者二号").start();
		new Thread(new Runnable() {
    
			public void run() {
    
				while (true) {
    
					g.consume();
				}
			}
		}, "消费者一号").start();
		new Thread(new Runnable() {
    
			public void run() {
    
				while (true) {
    
					g.consume();
				}
			}
		}, "消费者二号").start();
	}
}

在没有仓库的情况下,多个生产者会竞争product资源,多个消费着会竞争consume资源,但是商品依然会按照生产->消费的顺序进行,只不过生产者一号生产的产品不一定会被消费者一号消费。所以在有仓库的模式下,利用阻塞队列并且同步修饰offer及poll可以做到一个合理的生成消费模型。

4)死锁
死锁发生的场景大多为同步的嵌套,demo如下:

public class Test {
    

	private static class MyRunnable implements Runnable {
    

		private boolean flag;

		MyRunnable(boolean flag) {
    
			this.flag = flag;
		}

		public void run() {
    
			if (flag) {
    
				synchronized (MyLock.locka) {
    
					System.out.println(Thread.currentThread().getName() + "  if..locka");
					synchronized (MyLock.lockb) {
    
						System.out.println(Thread.currentThread().getName() + "if..lockb");
					}
				}
			} else {
    
				synchronized (MyLock.lockb) {
    
					System.out.println(Thread.currentThread().getName() + "  else..lockb");
					synchronized (MyLock.locka) {
    
						System.out.println(Thread.currentThread().getName() + "else..locka");
					}
				}
			}
		}
	}

	private static class MyLock {
    
		public static final Object locka = new Object();
		public static final Object lockb = new Object();
	}

	public static void main(String[] args) {
    
		MyRunnable a = new MyRunnable(true);
		MyRunnable b = new MyRunnable(false);
		Thread t1 = new Thread(a);
		Thread t2 = new Thread(b);
		t1.start();
		t2.start();
	}
}

以上代码结果:
Thread-0 if…locka
Thread-1 else…lockb
如上所示,在第一次执行时就阻塞了,两条线程均无法继续执行。
解析:
t1.start();开启线程0,走if(flag),拿到锁(MyLock.locka),执行打印if…locka,下一步想拿MyLock.lockb。
t2.start();开启线程1,走else,拿到锁(MyLock.lockb),执行打印if…lockb,下一步想拿MyLock.locka。
此时双方想拿的都被对方拿着,且各自的程序还未走完,都不能放,所以都拿不到,从而发生死锁。

本文多线程的描述章参考于:https://zhuanlan.zhihu.com/p/38614452?utm_source=wechat_session&utm_medium=social&utm_oi=945759358957740032的基础上进行少部分修改整理,静态同步函数锁与同步函数锁概念参考于:https://www.cnblogs.com/wsw-bk/p/8041981.html,死锁部分参考于:http://blog.sina.com.cn/s/blog_7502ae530102wui4.html,线程池部分参考于:https://www.jianshu.com/p/7ab4ae9443b9

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_45452401/article/details/104866335

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf