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

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法