一、进程与线程
进程:一个应用程序
线程:一个进程中的执行场景/执行单元
对于Java程序来说,当在terminal中输入 java HelloWorld
后,会先启动JVM,而JVM就是一个进程,JVM再启动一个主线程调用main方法;同时再启动一个垃圾回收线程负责看护、回收垃圾。也就是说,现在的Java程序至少有两个线程并发,一个是垃圾回收线程、一个是执行main方法的主线程。
在Java中,同一个Java程序中的两个不同的线程共享堆内存和方法区内存,但不共享栈内存——一个线程一个栈。
二、Thread类
使用:编写一个类,继承 Thread
类,并重写 run
方法。
重点: start()
方法的作用是启动一个分支栈,在JVM中开辟一个新的栈空间。start()
的任务完成之后,瞬间就结束了。启动成功之后,子线程会自动调用run方法(由JVM线程调度机制调度运行),并且run方法在分支栈底部 (压栈),main方法在主栈的栈底部,run和mian方法是平级的。
所以,若是仅调用run()方法,不调用start()方法,并不会创建子线程,这时 run()
方法与 start()
方法是同步的。
/**
* 实现线程的第一种方式:
* 编写一个类,直接继承 {@link Thread},重写run方法
*/
public class ThreadTest {
public static void main(String[] args) {
// 这里是main方法,这里的代码运行在主线程中
// 新建一个子线程
MyThread myThread = new MyThread();
// 启动线程
// start()方法的作用:启动一个分支线程,在JVM中开辟一个新的栈空间
// 这段代码认为完成之后,瞬间就结束了。
// 启动成功之后,线程会自动调用run方法,并且run方法在分支栈底部(压栈)
// main方法在主栈的栈底部,run和main方法是平级的。
myThread.start();
// 这里的代码还是运行在主线程中
for (int i = 0; i < 1000; i++) {
System.out.println("主线程---->" + i);
}
}
}
class MyThread extends Thread {
@Override
public void run() {
// 编写程序,这段程序运行在分支线程中
for (int i = 0; i < 1000; i++) {
System.out.println("分支线程---->" + i);
}
}
}
三、Runable接口
使用:编写一个类实现 java.lang.Runable
接口
建议使用该方式: 因为使用实现接口的方式,不耽误继承其他类。
public class RunnableTest {
public static void main(String[] args) {
// final MyRunnable myRunnable = new MyRunnable();
// final Thread t = new Thread(myRunnable);
// 采用匿名内部类的方式。
final Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("子线程--->" + i);
}
}
});
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程--->" + i);
}
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("子线程--->" + i);
}
}
}
3.1 sleep() 方法
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠 (暂停执行,进入阻塞状态,放弃占有CPU时间片,让给其他线程使用)
3.1.1 关于sleep()方法的面试题:
问题:这行代码会让线程t进入休眠状态码?
答:不会,因为sleep()是静态方法,和对象无关,所以这段代码等价于 => Thread.sleep(1000*5),也就是说会让当前线程也就是main线程休眠5s。
3.1.2 唤醒休眠中的线程
void interrupt() 中断线程。会让sleep()方法抛异常,导致捕获sleep抛出异常的try块结束,从而唤醒线程
public class ThreadTest {
public static void main(String[] args) {
final MyThread t = new MyThread();
t.start();
// 当使用interrupt方法,会使得sleep()方法抛异常,导致捕获sleep抛出异常的try块
// 结束,从而唤醒线程.
t.interrupt();
}
}
class MyThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(365L * 24 * 60 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
doOther();
}
public void doOther() {
// 编写程序,这段程序运行在分支线程中
for (int i = 0; i < 1000; i++) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "---->" + i);
}
}
}
3.1.3 优雅地终止一个线程
class MyThread extends Thread {
private boolean run = true;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
if (run) {
doOther(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
// 终止当前线程
// 在这里可以做一些善后工作
System.out.println(Thread.currentThread().getName() + "终止了");
return;
}
}
}
public void doOther(int i) {
// 编写程序,这段程序运行在分支线程中
System.out.println(Thread.currentThread().getName() + "do task " + i);
}
/**
* 优雅地关闭一个线程
*/
public void gracefulTerminate() {
run = false;
}
四、线程的生命周期
- 新建状态:刚new出来的线程对象
- 就绪状态:当处于新建状态的线程对象调用start()方法后,进入就绪状态,表示当前的线程具有抢夺cpu时间片的权利。当抢到cpu时间片之后,就开始执行run()方法。
运行状态:run()方法的开始执行,标志着这个线程进入了运行状态,当之前占有的CPU时间片用完之后,会重新回到就绪状态继续抢夺CPU时间片。
线程由运行状态到就绪状态之间的转换是靠JVM的调度来实现的。
阻塞状态:当处于运行状态中的线程,遇到阻塞事件时(例如接收用户键盘输入、sleep方法、IO事件等),会自动放弃当前占有的CPU时间片,进入阻塞状态。当阻塞状态解除后(需要的资源等到了),会再次回到就绪状态。
- 死亡状态:当run()方法执行结束之后,线程进入死亡状态。
五、线程的调度
5.1 线程调度模型
抢占式调度模型
Java采用抢占式线程调度模型,哪个线程的优先级较高,抢到的CPU时间片的概率高一些。
均分式调度模型
平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样。
平均分配,一切平等。
5.2 线程调度方法
5.2.1 线程优先级
Java中提供了一些方法与线程调度有关
void setPriority(int newPriority) 实例方法,设置线程的优先级 int getPriority() 实例方法,获取线程的优先级
Java提供的线程优先级常量
static int MAX_PRIORITY 线程可以具有的最高优先级,为10 static int MIN_PRIORITY 线程可以具有的最低优先级,为1 static int NORM_PRIORITY 分配给线程的默认优先级,为5
5.2.2 线程让位
static void yield() 让位方法,暂停当前正在执行的线程对象,并执行其他线程 yield()方法不是阻塞方法,让当前线程让出CPU执行权,让给其他线程使用。 yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
5.2.3 合并线程
void join() 实例方法。让当前线程进入阻塞状态,等待实例线程执行完毕后,当前线程才可以继续执行。
/**
* 合并线程
*/
public static void joinTest() throws InterruptedException {
final MyThread t = new MyThread();
t.start();
for (int i = 0; i < 100; i++) {
Thread.sleep(50);
System.out.println(Thread.currentThread().getName() + " do task " + i);
}
// 让main线程进入阻塞,t线程执行,直到t线程执行结束,main线程才可以继续执行
t.join();
System.out.println("所有线程执行结束!");
}
六、线程安全 ‼️
6.1 并发安全问题
问题:什么时候数据在多线程并发的环境下会存在安全问题呢?
满足以下的三个条件,就会存在线程安全问题:
- 条件一:多线程并发
- 条件二:有共享数据
- 条件三:共享数据有修改的行为
解决方法:线程同步(让线程排队执行!) 线程同步会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。
6.2 线程同步
- 异步编程模型(并发)
线程t1和线程t2,各自执行自己的任务,t1不管t2,t2不管t1,谁也不需要等谁。
这种编程模型称为异步编程模型。
- 同步编程模型(排队)
线程t1和线程t2,在线程t2执行的时候,必须等待t1执行完毕,两个线程之间发生了等待 关系,这就是同步编程模型。
6.2.1 synchronized (排他锁)
1、临界区
synchronized() {
// 线程同步代码块 (也叫临界区)
synchronized后面小括号中传的这个数据必须是多线程共享的数据
() 中写啥?
假设t1、t2、t3、t4、t5有5个线程,
你只希望t1、t2、t3排队,t4、t5不排队,
那么就一定要在()中写一个t1、t2、t3共享的对象
而这个对象对于t4、t5来说不是共享的
}
public void withDraw(double money) throws Exception {
// 以下代码必须排队执行,不能并发
// 当前账户对象是共享的,所以传入this
synchronized (this) {
// synchronized("abc") { // "abc" 在字符串在常量池中,会让所有线程排队
// synchronized(lock) // lock 若为实例变量,也可
double before = this.getBalance();
double after = before - money;
if (after < 0) {
throw new Exception("取款失败,余额不足");
}
this.setBalance(after);
}
}
在Java语言中,任何对象都有一把对象锁,以上代码的执行原理为:
- 假设 t1 和 t2 线程并发,开始执行以下代码的时候,肯定有一个先一个后;
- 假设 t1 先执行了,遇到了
synchronized
,这时,自动找后面共享对象的对象锁; - 找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中,t1一直都占有这把锁,直到同步代码块执行结束,这把锁才会被释放。
- 假设在 t1 占有锁的期间,t2 也遇到了
synchronized
关键字,也会尝试去占有后面共享对象的对象锁,但这把锁已经被t1占有,此时 t2 只能在同步代码块外等待 t1 的结束 (实际上 t2 此时会进入锁池<_lock poll_>并释放占有的CPU时间片)。 - 直到 t1 执行结束,释放了对象锁,此时t2终于等到了这把锁,然后 t2 占有这把锁,进入同步代码块执行程序。
2、synchronized 用在实例方法上,一定锁的是
缺点:这种方式不灵活,synchronized 出现在实例方法上,表示整个方法都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低,所以这种方式不常用。this
优点:代码简洁,如果共享的对象就是this
并且需要同步的代码块就是整个方法体,建议使用这种方式。public synchronized void withDraw(double money) throws Exception {
// 以下代码必须排队执行,不能并发
// 当前账户对象是共享的,所以传入this
synchronized (this) {
// synchronized("abc") { // "abc" 在字符串在常量池中,会让所有线程排队
// synchronized(lock) // lock 若为实例变量,也可
double before = this.getBalance();
double after = before - money;
if (after < 0) {
throw new Exception("取款失败,余额不足");
}
this.setBalance(after);
}
}
3、synchronized使用在静态方法上,找类锁
对象锁:1个对象1把锁。
类锁:该类的所有对象,共享1把类锁。6.2.2 哪些类型会有线程安全问题
Java中的三大变量:
- 实例变量:在堆中
- 静态变量:在方法区中
- 局部变量:在栈中
以上三大变量中:
局部变量永远都不会存在线程安全问题,因为局部变量不共享(在栈中)。
还有,常量也不会存在线程安全问题,因为常量不可修改。
类 | 是否线程安全 |
---|---|
StringBuilder |
否 |
StringBuffer |
是 |
ArrayList |
否 |
Vector |
是 |
HashMap 、 HashSet |
否 |
HashTable |
是 |
6.3 死锁
下面的代码要会写!!!
/**
* 死锁
* synchronized在开发中最好不要嵌套使用
*/
public class DeadLockTest {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
// 两个线程共享o1和o2
Thread t1 = new MyThread1(o1, o2);
Thread t2 = new MyThread1(o1, o2);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread {
Object o1;
Object o2;
public MyThread1(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
public void run() {
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
}
}
}
}
class MyThread2 extends Thread {
Object o1;
Object o2;
public MyThread2(Object o1, Object o2) {
this.o1 = o1;
this.o2 = o2;
}
public void run() {
synchronized (o2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
}
}
}
}
6.4 如何在开发中解决线程同步问题
是一上来就选择线程同步吗?synchronized
不是, synchronized
会让程序的执行效率降低,用户体验不好。
系统的用户吞吐量降低。用户体验差,在不得已的情况下再选择线程同步机制。
第一种方案:
尽量使用局部变量代替实例变量和静态变量。
第二种方案:
如果必须使用实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。
第三种方案:
如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized。
七、守护线程 (Daemon Thread)
Java语言中线程分为两大类:
- 用户线程
- 守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程就是一个典型的守护线程。
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
Thread t = new MyThread();
t.setDaemon(true); // 启动线程前,将线程设置为守护线程。
t.start();
八、定时器
九、wait和notify
十、Future模式
10.1 Future普通版本
JDK内部有一个 Future
接口,该接口除了提供 get()
方法来获得真实数据外,还提供了一组辅助方法,比如:
boolean cancel() 如果等太久,可以直接取消这个任务 boolean isCancelled() 任务是不是已经被取消了 boolean isDone() 任务是不是已经完成了 V get() 有2个get()方法,不带参数的表示无穷等待,或者你可以只等待给定时间
/**
* jdk中的Future
*/
public class FutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 定义一个线程池
final ExecutorService executor = Executors.newFixedThreadPool(1);
// 执行FutureTask, 相当于上例中的 client.request() 发送请求
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
final RealData realData = new RealData("hello future");
return realData.getResult();
}
});
System.out.println("request finished, preparing data...");
try {
System.out.println("do other work...");
Thread.sleep(4 * 1000);
System.out.println("other work done.");
} catch (InterruptedException ignored) {
}
// 如果此时call()方法没有执行完成,则依然会等待
System.out.println("data = " + future.get());
}
}
10.2 Future高阶版本
Future模式虽然好用,但也有一个问题,那就是将任务提交给线程后,调用线程并不知道这个任务什么时候执行,如果执行调用 get()
方法或者 isDone()
方法判断,可能会进行不必要的等待,那么系统的吞吐量很难提高。
为了解决这个问题,JDK对Future模式又进行了加强,创建了一个 CompletableFuture
, 它的最大作用就是提供了一个回调机制,可以在任务完成后,自动回调一些后续的处理,这样,整个程序可以把“结果等待”完全给移除了。
/**
* Future高阶版本 ----- {@link java.util.concurrent.CompletableFuture}
*/
public class CompletableFutureTest {
public static void main(String[] args) throws InterruptedException {
// 创建异步执行任务
System.out.println("start creating task...");
CompletableFuture.supplyAsync(CompletableFutureTest::getPrice)
.thenAccept(result -> {
System.out.println("price: " + result);
}) // 当出现异常时,会自动回调exceptionally()里面的方法
.exceptionally(e -> {
e.printStackTrace();
return null;
});
System.out.println("task creating done.");
System.out.println("do other work...");
Thread.sleep(2000);
System.out.println("other work done...");
}
static Double getPrice() {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
// 模拟一个异常
if (Math.random() < 0.3) {
throw new RuntimeException("Error occurred when get price");
}
return Math.random() * 20;
}
}