java 线程与池

线程与池这两个东西在很多语言中都存在,这是一种编程模式。
池的思维是根本。举个例子,如果某个数据库有一个远程交互的API允许你写代码来远程操作,那么你如果不用池,每条命令都重新发起一个连接,然后要数据库认证身份,然后执行命令。这样的效率不高,你会很当然的想到我要把连接保持住,让多个命令的执行只进行一次身份认证。这个时候就引入池的概念。每次从池中取出对应的连接,执行完命令再放回到池里。
同样的思维在ACM比赛中我们常用内存池的方法来避免频繁申请内存。

线程作为一种资源,也需要申请,申请也要开销,所以能将申请次数降低就可以提高效率,所以我们搞个池。
线程是一个进程的执行单元。一个进程中可以有若干线程,每个线程都是独立的执行单元。线程的调度比进程调度要快很多,因为线程调度的资源涉及更小。

那么为什么要多线程?

先来讲一下并行和并发。
并行:“真正的两个机器同时工作”;
并发:“看上去的同时执行的,实际上在同一个cpu上轮转执行的”;
并发的场景非常多,最基本的就是同时过来两个请求需要做,如果你是单线程的程序,就必须等待前一个完全做完再处理后面一个。如果你是多线程程序,就可以从线程池里取一个线程来处理新的请求,可以保证工作效率。这个地方其实有两个东西,一个是用户体验,一个是异步操作。前一个好理解,后一个讲一下我的理解。
现在很多时候我们都要将处理步骤拆成异步的来做,异步处理的实现方式其中就有多线程。同步就是我前一句代码没有做完,后面的代码后面的步骤就阻塞在那里了。如果我们用异步的方式,开一个新的线程来搞某个步骤。例如写日志和返回执行结果,我们就可以起一个线程去写日志,然后主线程直接返回处理结果,这个处理结果对日志是弱依赖的,日志写的如何并不影响我返回结果的操作,不能因为我日志写失败了我就不把结果返回去,有可能磁盘页被其他程序上锁了,一时半会儿都搞不完,这个时候多线程的优势就显现出来了。

感觉到这里讲了很多东西,都可以讲的很细的,但是讲的太细看的人都没兴趣了,我自己也不需要写太多。

如何实现多线程

在Java中封装的很好,用起来也很简单。实现多线程有两种方法,一种是继承Thread类,一种是实现Runable 接口。

Thread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Thread1 extends Thread{  
private String name;
public Thread1(String name) {
this.name=name;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "运行 : " + i);
try {
sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {

public static void main(String[] args) {
Thread1 mTh1=new Thread1("A");
Thread1 mTh2=new Thread1("B");
mTh1.start();
mTh2.start();

}
}

这种方法就是继承Thread类并重写run() 方法,在执行这个run的时候并不是直接调用,而是直接调用start() 方法。这里是个跟正常思维不一样的地方,因为涉及到线程的状态问题。因为线程是在进程下面的,所以你新起一个线程需要被进程管控的,新加入一个线程并不能立即被执行,而是要将其状态置成“就绪”状态,进程会将其挂进队列里,进行调度。

Runable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Thread2 implements Runnable{  
private String name;

public Thread2(String name) {
this.name=name;
}

@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(name + "运行 : " + i);
try {
Thread.sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

}
public class Main {

public static void main(String[] args) {
new Thread(new Thread2("C")).start();
new Thread(new Thread2("D")).start();
}

}

感觉差不多的。调用的时候也是用start

Thread 与 Runable的区别

Runable实现的多线程能够在同一个实例上运行,而Thread的不可以。

1
2
3
4
5
6
7
8
9
10
public class Main {  

public static void main(String[] args) {

Thread2 my = new Thread2();
new Thread(my, "C").start();
new Thread(my, "D").start();
new Thread(my, "E").start();
}
}

这样的调用,会让三个线程在同一个实例上运行,比如售票系统中,不同的线程是不同的用户,但是减的票数在同一个实例上。

关于线程还有很多东西,才疏学浅,暂时不深究,因为没用到,在造轮子的时候就会用到了。

线程池

上面只是简单的写写多线程,并没有涉及到池,也就是一种暴力的多线程。
线程池在Java中也是封装好的一个类,也很方便。

创建线程池

1
2
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
keepAliveTime, milliseconds,runnableTaskQueue, threadFactory,handler);

参数说明

- corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;

- maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;

- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

创建完线程池,要向线程池提交程序,跟多线程一样,你的类实现Runable接口,重写run() 函数就可以了。

1
2
3
executor.execute(myTask);

// myTask 是你要多线程执行的类的实例

简述了一些这方面的东西。一般我们在开发项目使用的时候都会初始化很多个线程池,不同的业务代码用不同的线程池,用线程的时候最重要的就是线程安全的问题。这个下次遇到再写,没有碰到过就理解不深刻。

Talk is not cheap.