1、进程与线程
1.1、进程
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程。
1.2、线程
一个进程之内可以分为一到多个线程。 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器。
1.3、二者对比
-
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
-
进程拥有共享的资源,如内存空间等,供其内部的线程共享
-
进程间通信较为复杂
-
同一台计算机的进程通信称为 IPC(Inter-process communication)
-
不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
-
-
线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
-
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
1.4、并行与并发
单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是 同时运行 的 。总结为一句话就是: 微观串行,宏观并行 。一般会将这种 线程轮流使用 CPU 的做法称为并发concurrent,多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。
-
并发(concurrent)是同一时间应对(dealing with)多件事情的能力
-
并行(parallel)是同一时间动手做(doing)多件事情的能力
ok说到这里其实已经完成了,但是突然想到一个概念:同步和异步,同步与异步是指访问数据的机制,同步一般指主动请求并等待IO操作完成的方式。异步则指主动请求数据后便可以继续处理其它任务,随后等待IO操作完毕的通知。同步和异步最大的区别就在于:同步需要等待,异步不需要等待。
说到这里来不得不说一下阻塞和非阻塞,阻塞与非阻塞是描述线程在访问某个资源时,数据是否准备就绪的一种处理方式。当数据没有准备就绪时:阻塞:线程持续等待资源中数据准备完成,直到返回响应结果。非阻塞:线程直接返回结果,不会持续等待资源准备数据结束后才响应结果。
总结:
-
阻塞与非阻塞是针对线程来说的,阻塞可能发生在IO期间也可能发生在IO之前。
-
同步与异步是针对IO操作来说的,同步是用户线程一直盯着IO直到完成,异步是用户线程在IO完成时会收到通知。
2、java线程
2.1、创建线程对象
方式1:直接使用 Thread
@Slf4j(topic = "c.Ltest1")//日志 topic是日志的标签
public class Ltest1 {
public static void main(String[] args) {
/*new Thread(() -> {
log.info("创建的 runing");
}).start();*/
Thread t = new Thread() {
public void run() {
log.debug("runing");
}
};
t.setName("小李");
t.start();
log.debug("main running");
}
}
方式2:使用Runable配合Thread
@Slf4j(topic = "c.Ltest2")
public class Ltest2 {
public static void main(String[] args) {
/*Runnable runnable = new Runnable() {
@Override
public void run() {
log.debug("运行代码");
}
};*/
Runnable runnable = () -> log.debug("运行代码");
Thread t=new Thread(runnable);
t.start();
}
}
方式3:使用FutureTask配合Thread
@Slf4j(topic = "c.Ltest3")
public class Ltest3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.info("任务。。。");
Thread.sleep(2000);
return 111;
}
});
Thread xiali = new Thread(task, "xiali");
xiali.start();
log.info("{}",task.get());
}
}
2.2、栈与栈帧
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
-
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
-
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
2.3、线程上下文切换
以下一些原因会导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
-
线程的 cpu 时间片用完
-
垃圾回收
-
有更高优先级的线程需要运行
-
线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的。
-
状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
-
Context Switch 频繁发生会影响性能
3、线程中的方法
方法名 | static | 说明 | 注意事项 |
---|---|---|---|
start | 启动一个新线 程,在新的线程 运行 run 方法 中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException | |
run | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则 线程启动后会调用 Runnable 中的 run 方法,否则默 认不执行任何操作。但可以创建 Thread 的子类对象, 来覆盖默认行为 | |
join | 等待线程运行结束 | ||
join(long n) | 等待线程运行结 束,最多等待 n 毫秒 | ||
getId | 获取线程长整型 的 id | 唯一 | |
getName | 获取线程名 | ||
setName | 修改线程名 | ||
getPriority | 获取线程优先级 | ||
setPriority | 修改线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级 能提高该线程被 CPU 调度的机率 | |
getState | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED | |
isInteerupted | 判断是否被打断 | 不会清除 打断标记 | |
isAlive | 线程是否存活 (还没有运行完 毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断 的线程抛出 InterruptedException,并清除 打断标 记 ;如果打断的正在运行的线程,则会设置 打断标 记 ;park 的线程被打断,也会设置 打断标记 | |
interrupted() | static | 判断当前线程是 否被打断 | 会清除 打断标记 |
currentThread() | static | 获取当前正在执 行的线程 | |
sleep(long n) | static | 让当前执行的线 程休眠n毫秒, 休眠时让出 cpu 的时间片给其它 线程 | |
yield() | static | 提示线程调度器 让出当前线程对 CPU的使用 | 主要是为了测试和调试 |
3.1、start 与 run
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug(Thread.currentThread().getName());
FileReader.read(Constants.MP4_FULL_PATH);
}
};
t1.run();
log.debug("do other things ...");
}
执行结果:
19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...
执行方法的还是我们的主线程而不是我们创建的t1线程!
#将 t1.run()更换为t1.start()
程序在 t1 线程运行, FileReader.read() 方法调用是异步的。
总结:
直接调用 run 是在主线程中执行了 run,没有启动新的线程 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码。注意start只能调用一次!
3.2、sleep和yield
sleep
-
调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
-
其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
-
睡眠结束后的线程未必会立刻得到执行
-
建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
@Slf4j(topic = "c.Test7")
public class Test7 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("enter sleep...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.debug("wake up...");
e.printStackTrace();
}
}
};
t1.start();
Thread.sleep(1000);
log.debug("interrupt...");
t1.interrupt();
}
}
yield:
-
调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
-
具体的实现依赖于操作系统的任务调度器
总结:
使用Sleep的线程时从Runing状态到Timed Waiting(阻塞状态)任务调度器不会将时间分片分给它,但是对于使用yield来说,不一样!它时从Runing状态到Runable(就绪状态),在这个期间是有可能会被分配时间片的。
3.3、线程优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它,如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用。
@Slf4j(topic = "c.Test9")
public class Test9 {
public static void main(String[] args) {
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
Thread.yield();
System.out.println(" ---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
3.4、join方法
@Slf4j(topic = "c.Test10")
public class Test10 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
},"t1");
t1.start();
//t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
}
现在想知道r的结果,啧啧啧,按照我们的想法就是想要打印的是10,但是我们由于是两个线程,异步操作,在执行t1中的run方法的时候,主线程已经执行了,此时r的是0,那么如何让打印的结果是10,让主线程睡一会嘛!虽然没毛病但是不太好,有更好的方式使用join()
上面是一个线程,如果时两个、三个。。。想要等待线程执行结果,那么只用想要等待的线程执行join方法。
同时,可以设置最大的等待时间join(毫秒),你小子等了那么就还没好!不等了拜拜了您嘞!
3.5、interrupt 方法
打断 sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态打断 sleep 的线程, 会清空打断状态,以 sleep 为例。
打断阻塞状态
@Slf4j(topic = "c.Test11")
public class Test11 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000); // wait, join
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
log.debug("打断标记:{}", t1.isInterrupted());
}
}
执行结果:
16:53:38.253 c.Test11 [t1] - sleep...
16:53:40.264 c.Test11 [main] - interrupt
16:53:40.264 c.Test11 [main] - 打断标记:false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at cn.itcast.test.Test11.lambda$main$0(Test11.java:12)
at java.lang.Thread.run(Thread.java:750)
打断正常运行
@Slf4j(topic = "c.Test12")
public class Test12 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("被打断了, 退出循环{}",interrupted);
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
}
}
两阶段终止模式
在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。
4、线程状态
4.1、守护线程和主线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
@Slf4j(topic = "c.Test15")
public class Test15 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
log.debug("结束");
}, "t1");
//设置为守护线程
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.debug("结束");
}
}
-
垃圾回收器线程就是一种守护线程
-
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求
4.2、线程的状态
4.2.1、操作系统
-
【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
-
【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
-
【运行状态】指获取了 CPU 时间片运行中的状态 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
-
【阻塞状态】
-
如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】
-
等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
-
与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们
-
-
【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
4.2.2、API
-
NEW 线程刚被创建,但是还没有调用 start() 方法
-
RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
-
BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
-
TERMINATED 当线程代码运行结束
5、共享模型
是这样的,在高并发的情况下,不可避免的会出现多个线程同时对公共资源进行数据操作业务,这种情况下会出现指令交错的情况,这就导致可能出现和“超卖”问题。
临界区
一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源,多个线程读共享资源其实也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题,一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
5.1、synchronized
5.1.1、使用synchronized
synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
注意 :虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
-
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
-
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点。
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
注意:使用synchronized想要保护的资源,多个线程要使用同一把锁,也就是保证锁的唯一性。
5.1.2、synchronized
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
5.3、Monitor
Monitor 被翻译为监视器或管程每个Java对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。
-
刚开始 Monitor 中 Owner 为 null
-
当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner
-
在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
-
Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
-
图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析
5.4、 轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。 轻量级锁对使用者是透明的,即语法仍然是 synchronized 假设有两个方法同步块,利用同一个对象加锁。
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
当然不是每一次都错开,存在出现同时加锁的时刻,那么毕然会导致一个加锁成功一个加锁失败,这时候就出现锁膨胀的,然后为锁对象申请Monitor锁,此时锁升级为重量级锁。
但是这种情况下,如果获取锁一次不成功,直接让当前线程到EntryList中等待,这也不太好叭,万一下一次就成功呐,所以在重量级锁竞争的时候,可以用自旋进行优化,自旋也太高大上了,其实就是让其多重复几次去获取锁(多核cpu下有意义!)。当然重试了几次后依然获取锁失败,那没有办法只能进入EntryList中等待叭!
-
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
-
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
-
Java 7 之后不能控制是否开启自旋功能
5.5、偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。 Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
5.6、死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
诱发死锁的原因:
-
互斥条件
-
占用且等待
-
不可抢夺(或不可抢占)
-
循环等待
以上4个条件,同时出现就会触发死锁。
解决死锁:
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
-
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
-
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题,对共享的资源要么全申请完毕要么全不申请。
-
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
-
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。