1 什么是多线程
多线程,就是多个任务同时进行,提高程序的运行效率和CPU的利用率,也叫并发。
- 一个程序可能包含多个可以同时运行的任务。线程是指一个任务从头到尾的执行流程。
2 线程的状态
- 创建状态: 对象已经生成(new出来了),但没有调用start方法,处于创建状态。
- 就绪状态: 调用start方法后,向CPU和内存申请到资源了,处于就绪状态。(从sleep与wait等恢复也为就绪状态)。
- 运行状态:线程调度将当前线程设置为当前线程,这时就是运行状态,开始运行run方法的代码。
- 阻塞状态:线程在运行的过程中暂停/等待,即阻塞。
- 死亡状态: run方法调用结束或者被调用interrupt方法,线程死亡,无法调用start方法。
3 线程创建
- 继承 Thread类, 重写run方法。
public class Thread_Test extends Thread{
@Override
public void run() {
System.out.println("线程已启动");
}
}
- 实现Runnable接口,重写run方法。
public class Thread_Test implements Runnable{
@Override
public void run() {
System.out.println("这是线程");
}
}
- 实现Callable<返回值> 接口, 重写call方法。
4 线程启动与中断
- 线程启动:直接调用start方法,不要调用run方法!!!!。
- 线程中断:
强制暂停(suspend)和停止(stop),已经弃用。
需要停止一条线程时,应使用interrupt()方法请求终止线程,调用该方法时,该线程进入中断状态(Boolean标志,用于检查线程状态),注意此时并不会中断线程的运行,我们需要的线程中断是以下状态:
- 线程被阻塞:调用Object.wait()、Thread.sleep()或Thread.join();
- 调用interrupt方法,此时线程被中断,抛出InterruptedExpectation错误。
示例:
public static void main(String[] args) {
int cnt = 0;
Runnable t = () -> {
try{
while (!Thread.currentThread().isInterrupted()) {
System.out.println("running");
Thread.currentThread().interrupt();
};
if (Thread.currentThread().isInterrupted()) {
Thread.sleep(1000);
}
}catch (InterruptedException e){
System.out.println("线程已结束");
}
};
Thread thread = new Thread(t);
thread.start();
// thread.wait();
}
5 线程锁
1 乐观锁和悲观锁
- 悲观锁:用一切悲观的形态来看待数据,在多个线程对同一数据进行操作的时候,就会进入竞态,竞争到的线程给数据加锁,直到完成数据的修改才释放,这时剩余等待的线程继续进入竞态。
- 乐观锁:用乐观的态度去应对数据,认为数据不会受到影响,在对数据完成修改之后才会对数据进行校验,如果不匹配则会发生冲突,不会修改数据。
2 原子操作
-
什么是原子操作:一个或者多个操作在CPU执行过程中不会被中断的特性,分为CPU指令级别和高级语言级别。
-
原子操作实现方式:锁和自旋CAS。
- 悲观锁的实现方式:方法锁和类名锁。
- 关键字:synchronized
- 类名锁:将类名作为一把锁,当有线程调用的时候将类名上锁,别处调用同样的类名锁时会进入等待状态,直到调用结束释放锁。
例子:
static int cnt = 0;
public static void main(String[] args) throws InterruptedException {
LinkedList <Thread> threads = new LinkedList<>();
for (int i = 0; i < 100; i++) {
threads.add(new Thread(() -> {
for (int j = 0; j < 10; j++)
synchronized (Main.class) {
cnt++;
}
}));
}
for (var t : threads)
t.start();
for (var t : threads)
t.join();
System.out.println(cnt);
}
- 方法锁:将方法作为一把锁,调用该方法时,只允许一条线程调用,等到方法调用结束后才可以进行下一步的调用。
例子:
static int cnt = 0;
public static void main(String[] args) throws InterruptedException {
LinkedList<Thread> threads = new LinkedList<>();
for (int i = 0; i < 100; i++) {
threads.add(new Thread(() -> {
for (int j = 0; j < 10; j++)
add();
}));
}
for (var t : threads)
t.start();
for (var t : threads)
t.join();
System.out.println(cnt);
}
private static synchronized void add(){
cnt++;
}
3 死锁
- 什么是死锁:两个锁被调用,相互阻塞等待,进入僵持状态。
- 例子:
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (o1) {
System.out.println("拿到锁1了,在等待锁2");
synchronized (o2) {
System.out.println("拿到锁2了!");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (o2) {
System.out.println("拿到锁2了,在等待锁1");
synchronized (o1) {
System.out.println("拿到锁1了!");
}
}
}
});
t1.start();
t2.start();
}
通过上例不难发现,线程t1会在拿到锁1后继续拿锁2,线程t2会在拿到锁2后继续拿锁1,但都被对方占用了,两个线程一直处于等待状态,形成死锁。
-
怎么发现程序中的死锁:
通过jconsole查看
-Djava.rmi.server.hostname=127.0.0.1
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8888
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
6 线程等待和唤醒
6.1 Object.wait()方法(线程等待)
- 使当前线进入等待状态并且释放线程锁
- 仅在Object作为类锁的时候可用
- 当线程进入等待状态后,如果该线程调用了interrupt方法,则会抛出InterruptExpectation错误。
6.2 Object.notify()方法(线程唤醒)
- 随机恢复一条处于正在等待的线程
- 必须在持有当前锁的代码块下使用
- 恢复后的线程不会立即继续执行,会等待当前线程执行完成重新把方法锁释放后再继续执行
- notifyAll()恢复所有线程
7 守护线程
-
守护线程指在其他线程结束后,它也会跟着结束,即不会因为自己而影响到整个程序的结束
-
setDaemon(boolean);
-
在守护线程下开出来的线程也是守护线程