为什么需要多线程?
- 任何事物对于现代CPU来说都实在是太慢了,3GHz的CPU一个时钟周期为3纳秒,而内存寻址过程约为10微秒,而IO操作(包括网络IO,文件IO)对于CPU来说更是慢得一塌糊涂。
- 我们无法一直缩短CPU的时钟周期,因为这会增加CPU的发热量。所以聪明的人类,找到了让CPU更快的方法:增加核心数。于是现代CPU都是多核的。
- Java的执⾏模型是同步/阻塞(block)的。一条指令会等待上一条指令执行完成(无论多么耗时)才会执行。如果一条指令“卡”住了,那么下面的指令都将无法完成。
- 默认情况下只有⼀个线程也符合人类的思想,处理问题⾮常⾃然,但是具有严重的性能问题。
- 多线程是这样一种机制,它允许在程序中并发执行多个指令流,每个指令流都称为一个线程,彼此间互相独立。线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度。
- 一句话总结多线程:相同的一份代码,你雇佣了很多工人去执行他。
什么是Thread
- 在 Java 中,
Thread
类的每一个实例代表一个JVM中的线程,需要注意的是Runnable
/Callable
都不是线程(它们只是一小段代码,描述线程完成的任务,它可以被线程执行)。相较于Callable
来说,Runnable
有局限性,表现在:Runnable
不能返回值,Runnable
不能抛出checked exception
. -
Thread.start()
之后,JVM中就会增加一个执行流(工人)和一套方法栈。 - 不同执行流的同步执行是一切线程问题的来源。
Java线程模型 & 开启一个新线程
- Java中使用
Thread
开启一个线程,使用start
方法并发执行。(run
方法在我看来就是顺序执行)
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// do some thing
}
}).start();
}
-
每多开⼀个线程,就多⼀个执⾏流(工人)。每一线程栈的栈底都是
Thread.run
方法。
-
⽅法栈(局部变量)是线程私有的,静态变量/类变量是被所有线程共享的。如下图所示:
- 当一个线程去修改堆里的变量时,那么它是否立刻生效呢?答案:不是的。在现代的多核CPU上,CPU自己有个缓存,调度器可以轻松地将多个线程分配给多个CPU去执行,这样效率会特别高。但是这样会有一个问题,CPU和主内存的交换是很慢的(一次内存寻址大概需要1微秒实现,差不多相当于1000-3000个时钟周期),所以 Java 线程模型允许每一个线程自己有一份私有的共享变量的副本,CPU定期会把副本同步回主内存。
但是这样会带来一些问题。举一个例子,如下面代码所示:开启一个线程去做一些初始化操作,并将initSignal
设置为true。但是main中的initSignal和thread中的initSignal
是同一个吗?理论上存在一种情况,就是thread中的initSignal
是它自己缓存中的,还没有同步到主内存中去,这样有可能就会出现错误。还有一个问题,编译器和处理器都可能对指令进⾏重排,导致问题。简单来说init()
和initSignal=true
这两行代码是没有相互依赖的,可能会颠倒执行。这就出现大问题了,明明还没有初始化,却将会去做接下来的任务。这两个问题都可以用volatile
关键词去解决。
public class Main {
static boolean initSignal = false;
private static void init() {
// 初始化操作
}
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
init();
initSignal=true;
}).start();
while (true){
if(initSignal){
//如果初始化完成,则做接下来的任务
}else {
Thread.sleep(500);
}
}
}
}
如何解决线程自身存在的副本导致可能发生的错误呢?java引入了一个关键词volatile
,当你把一个变量声明成一个volatile
时,所有线程对共享变量的修改会立刻写回主内存,从变量读取会直接读取主内存 。换句话说,读写都是最新的。这叫做可见性,值得注意的时,可见性并非原子性。volatile
也会禁⽌指令重排。有同步操作(synchronized
/Lock
/AtomicInteger
,详情见Java 多线程——线程安全、线程池初步)的时候⽆需volatile
,同步操作自带原子性和可见性。
volatile static boolean init = false;
多线程问题的来源
- 当多线程试图去修改一个静态变量时,噩梦也就开始了,很容易造成数据错误。如下代码所示,我们期望它打印的最后一个值为30000,而最终结果是29997。可怕的是,每次运行的结果都是无法确定的。
class Main {
private static int num = 0;
public static void main(String[] args) {
new Thread(Main::addOne).start();
new Thread(Main::addOne).start();
new Thread(Main::addOne).start();
}
public static void addOne(){
for (int i = 0; i < 10000; i++) {
num++;
System.out.println(num);
}
}
}
-
CPU会给线程分配时间,如果时间到了,不会等待某一线程的指令执行完,而是直接终止,等待下一次轮到再执行。
- 上文中,
num++
不是一个原子操作。这可以看成三个步骤。1.得到num
的值。2.num
自增。3.将自增结果写回num
。如果在2、3两个步骤中间,该线程的时间正好到了,没有成功将结果写回,下一个线程开始工作。此时num
的值并没有成功被更新。 - 多线程问题的本质来源是,同⼀份代码(非原子操作),被不同的工人⼈在疯狂地乱序执⾏。
线程的生命周期
- 建议直接看Java自带的文档
Thread.state
,最权威。
* <ul>
* <li>{@link #NEW}<br>
* 一个线程还没有开始执行,刚new出来。
* A thread that has not yet started is in this state.
* </li>
* <li>{@link #RUNNABLE}<br>
* 一个线程正在Java虚拟机中被执行
* A thread executing in the Java virtual machine is in this state.
* </li>
* <li>{@link #BLOCKED}<br>
* 一个线程正在等待一个monitor lock。
* 我们使用synchronized块锁住一个对象,有多个线程没有拿到锁,进入阻塞状态。
* A thread that is blocked waiting for a monitor lock
* is in this state.
* </li>
* <li>{@link #WAITING}<br>
* 处于wait状态的线程(调用了wait方法)
* A thread that is waiting indefinitely for another thread to
* perform a particular action is in this state.
* </li>
* <li>{@link #TIMED_WAITING}<br>
* 处于wait状态的线程(调用了wait方法),有时间限制。
* A thread that is waiting for another thread to perform an action
* for up to a specified waiting time is in this state.
* </li>
* <li>{@link #TERMINATED}<br>
* 一个线程退出了的状态。
* A thread that has exited is in this state.
* </li>
* </ul>
适合使用多线程的场合
- 对于IO密集型应⽤极其有⽤:
- ⽹络IO(通常包括数据库)。
- ⽂件IO。
- 对于CPU密集型(疯狂地进行运算)应⽤有折扣。
- 性能提升的上限在哪⾥?
- 单核CPU上限为 100%
- 多核CPU上限为 N * 100%
本文完。
网友评论