1. 进程
一个正在执行中的程序就是一个进程,系统会为这个进程发配独立的【内存资源】。进程是程序的一次执行过程,它有自己独立的生命周期,它会在启动程序时产生,运行程序时存在,关闭程序时消亡。
例如:正在运行的 QQ、IDE、浏览器就是进程。
2. 线程
线程是由进程创建的,是进程的一个实体,是具体干活的人,一个进程可能有多个线程。线程不独立分配内存,而是共享进程的内存资源,线程可以共享 CPU 的计算资源。
一个进程的线程就不能修改另一个线程的数据,隔离性更好,安全性更好。
3. 并发和并行
大部分操作系统的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。CPU 在不同的进程之间轮换,进程又在不同的线程之间轮换,因此线程是 CPU 执行和调度的最小单元。
任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态,等待下一个属于它的时间片的到来。这样每个任务都能得到执行,由于 CPU 的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发。
并发是两个队列交替使用一台咖啡机。
并行是两个队列同时使用两台咖啡机。
4. Java 中常见线程方式
4.1 继承 Thread 类
步骤:
- 定义类继承 Thread;
- 重写 Thread 类中的 run 方法;
- 调用线程的 start 方法:
public class Test01 {
public static void main(String[] args) {
System.out.println(1);
new MyThread01().start();
System.out.println(3);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(4);
}
}
class MyThread01 extends Thread{
@Override
public void run() {
System.out.println(2);
}
}
4.2 实现 Runnable 接口
步骤:
- 创建类实现 Runnable 接口
- 使用 Thread 为这个任务分配线程
- 调用线程的 start 方法
public class Test02 {
public static void main(String[] args) {
System.out.println(1);
//注意,这里 new 的是 Thread
new Thread(new MyRun()).start();
System.out.println(3);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(4);
}
}
class MyRun implements Runnable{
public void run() {
System.out.println(2);
}
}
4.3 实现 Callable 接口
步骤:
- 创建类实现 Callable 接口
- 通过 Callable 接口实现类创建 FutureTask
- 使用 Thread 为这个 FutureTask 分配线程
- 调用线程的 start 方法
public class Test03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println(2);
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
System.out.println(3);
new Thread(futureTask).start();
System.out.println(4);
int result = futureTask.get();
System.out.println(5);
System.out.println(result);
System.out.println(6);
}
}
class MyCallable implements Callable<Integer> {
public Integer call() throws Exception {
Thread.sleep(2000);
return 1;
}
}
futureTask.get(); 是一个阻塞的方法,意思就是,这个方法会一直等,主线程会一直等待,这个线程执行完成之后并有了返回值,才会继续执行。
5. 守护线程
Java 提供两种类型的线程: 用户线程和守护线程。
守护线程旨在为用户线程提供服务,并且仅在用户线程运行时才需要。
守护线程对于后台支持任务非常有用,例如垃圾收集,释放未使用对象的内存以及从缓存中删除不需要的数据。大多数 JVM 线程都是守护线程。
要将线程设置为守护线程,我们需要做的就是调用 Thread 的 setDaemon() 方法。
NewThread t = new NewThread();
t.setDaemon(true);
t.start();
6. 线程的生命周期
生命周期可以通俗地理解为“从出生到死亡”的整个过程。线程的生命周期包括从创建到销毁的整个过程。
线程的状态:
- NEW - 初始状态,一个新创建的线程,还没开始执行。
- RUNNABLE - 可执行的状态,要么是在执行,要么是一切就绪等待执行,例如等待分配 CPU 时间。
- WAITING - 等待状态,等待其他的线程去执行特定的动作,没有时间限制。
- TIMED_WAITING - 限时等待状态,等待其他的线程去执行特定的动作,这个是在一个指定的时间范围内。
- BLOCKED - 阻塞状态,等待锁,以便进入同步块儿。
- TERMINATED - 终止状态,线程执行结束。
7. 线程常用方法
currentThread() :该方法是 Thread 类中的类方法,可以用类名调用,方法返回当前正在使用 CPU 资源的线程。
sleep() :使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。
join() :让当前线程邀请调用方法的那个线程优先执行,在被邀请的线程执行结束之前当前线程一直处于阻塞状态,不再继续执行。
yield() :让当前线程直接放弃时间片返回就绪状态。
wait() :当前线程放弃监视器并进入睡眠状态,直到其他进入同一个监视器的线程调用 notify 为止。
notify() :唤醒同一监听器中调用 wait 的某一个线程。
notifyAll() :唤醒同一监听器中所有等待的线程。
8. 线程安全
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
9. 线程同步
线程同步:关键字 synchronized。
使用同步监视器来判断当前代码是否有线程在执行。线程开始执行同步代码块之前,必须先获得对同步监视器的锁。
任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然会释放对同步监视器对象的锁。
Java 程序运行可以使用任何对象来作为同步监视器对象。
synchronized 有三种方式来加锁,分别是:
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
10. synchronized 原理
Synchronized 底层是通过两个方法来完成同步:
- monitorenter
- monitorexit
10.1 monitorenter
每个对象有一个监视器锁(monitor)。当 monitor 被占用时就会处于锁定状态,线程执行 monitorenter 指令时尝试获取 monitor 的所有权,过程如下:
-
如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为 1,该线程即为 monitor 的所有者。
-
如果线程已经占有该 monitor,只是重新进入,则进入 monitor 的进入数加 1.
-
如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权。
10.2 monitorexit
执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。
指令执行时,monitor 的进入数减 1,如果减 1 后进入数为 0,那线程退出 monitor,不再是这个 monitor 的所有者。其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权。
11. 死锁
死锁问题:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,而该资源又被其他线程锁定,从而导致每一个线程都得等其它线程释放其锁定的资源,造成了所有线程都无法正常结束。结构如下图所示:
死锁示例代码:
public class MyThread01 implements Runnable{
static Object garlic = new Object();
static Object vinegar = new Object();
int flag = 0;
@Override
public void run() {
if (flag == 0) {
synchronized (vinegar) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {return;}
System.out.println("我张三正在吃醋,李四你给我点蒜瓣");
synchronized (garlic) {
System.out.println("我张三终于吃上蒜了,hiahia~");
}
}
}
if (flag == 1) {
synchronized (garlic) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {}
System.out.println("俺李四正在吃蒜,张三你给我点醋");
synchronized (vinegar) {
System.out.println("我李四终于吃上醋了,hiahia~");
}
}
}
}
public static void main(String[] args) {
MyThread01 ZhangSan = new MyThread01();
MyThread01 LiSi = new MyThread01();
ZhangSan.flag = 0;
LiSi.flag = 1;
Thread t1 = new Thread(ZhangSan);
Thread t2 = new Thread(LiSi);
t1.start();
t2.start();
}
}
程序运行结果: