Java并发(1)线程基础

Java并发(1)线程基础

重新学习 Java 并发,本文包括线程的创建,线程的状态切换方法,守护线程。

As We All Know
进程是系统分配资源和调度的基本单位
线程是在 CPU 上运行的基本单位

在 Java 中,启动 main 函数即启动一个 JVM 进程,main 函数的线程是这个进程的一个线程,也称主线程。线程有自己的程序计数器(执行位置)和(局部变量);进程有方法区,前者存放 new 的对象实例,后者存放 JVM 加载的类、常量、静态变量,他们都是线程共享的。

线程创建

Java有三种方式创建线程:

  1. 继承 Thread 类, 重写 run() 方法

  2. 实现 Runnable 接口

  3. 使用 FutureTask

1.继承 Thread 类,重写 run() 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyThread extends Thread{  
private int ticket;
MyThread(int t) {
ticket = t;
}

public void run(){
for(int i=0;i<ticket;i++){
if(ticket>0){
System.out.println(this.getName()+" sold ticket"+ i);
}
}
}
};

main 函数中:

1
2
3
4
MyThread thread1 = new MyThread(); //创建线程
thread1.start(); //启动线程
MyThread thread2 = new MyThread(); //创建线程
thread2.start(); //启动线程

这种方法直接调用 this 就可以获得当前进程,不必使用 Thread.currentThread() 方法。缺点是 Java 不支持多继承,继承 Thread 类就不能继承其他类了。

2.实现 Runnable 接口:
1
2
3
4
5
6
7
8
9
10
public static class RunnableTask implements Runnable{
@Override
public void run(){
System.out.println("I am a child thread");
}
}
//main函数中:
RunnableTask task = new RunnableTask();
new Thread(task).start();
new Thread(task).start();

新建线程共用一个代码逻辑,并且线程可以继承其他类。

3. FutureTask 方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static class CallerTask implements Callable<String>{
@Override
public String call() throws Exception {
return "hello";
}
}

public static void main(String[] args) throws InterruptedException{
//创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
//启动线程
new Thread(futureTask).start();
try{//任务执行完成,返回结果
String result = futureTask.get();
System.out.println(result);
}
catch (ExecutionException e){
e.printStackTrace();
}
}

创建了一个 FutureTask 的对象,构造函数为 CallerTask ,使用 FutureTask 对象作为任务创建线程,这个对象可以调用 get() 方法,执行的是重写的 call()。

三种方法的后两者都是先创建任务 task,再启动。

线程状态切换

wait() 函数:

wait(), wait(long timeout), wait(long timeout,int nanos)
timeout: 超时则返回,参数 =0 时为 wait()
nanos: >0时timeout++

wait() 是 Object 方法,使用前需要获得对象的监视器锁。

某线程调用共享对象的 wait() 方法时,该线程会挂起,释放对象的监视器锁。解除 wait() 状态需要其他线程调用共享对象的 notify() 或者 notifyAll() 方法,或者调用 wait() 线程的 interrupt() 方法抛出 IllegalMonitorStateException 异常。
当调用 wait() 方法时,只有该对象的监视器锁被线程释放。

虚假唤醒: 莫名其妙被唤醒,在 wait() 外使用 while 循环防范,不满足则继续挂起。PS :这内部类看着真舒服

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class WaitNotifyInterupt {
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException{
//创建线程
Thread threadA = new Thread(new Runnable() {
public void run() {
try {
System.out.println("begin");
synchronized (obj) {
obj.wait();
}
System.out.println("end");

} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
Thread.sleep(1000);
System.out.println("begin interrupt threadA");
threadA.interrupt();
System.out.println("end interrupt");
}
}

//执行结果:begin begin interrupt threadA end interrupt 中断报错

notify(),notifyAll():
一个线程调用共享对象的 notify() 方法,会唤醒一个在该变量上调用 wait() 挂起的线程,若有多个则随机唤醒一个(notifyAll() 全部唤醒)。调用 notify() 方法会释放对象监视器锁,唤醒的线程需要竞争获得监视器锁。
wait 与 notify 都是 Object 的方法.

join(), 无参且返回值为 void,线程的方法,等待该线程执行完毕。

sleep(), 线程的方法,阻塞自己一段时间,调用睡眠时不释放监视器锁

yield(), 线程的方法,让出 CPU 使用权,线程不被阻塞挂起,故下一次可能还是调度它执行

interrupt(), 中断,将线程的中断标志设置为 true 并返回,实际上还可以往下运行。但如果被 wait(),sleep(),join() 阻塞,则会在调用这些方法的地方抛出 InterruptedException 异常

boolean isInterrupted() : 检测线程是否中断,返回布尔值
boolean interrupted():检测当前线程是否中断,返回布尔值,如果中断则清除中断标志。static 可以通过 Thread 类直接调用,在主线程中调用 threadOne.interrupted 实际上返回的是主线程的中断标志
可以在线程内调 Thread.currentThread().interrupted() 实现子线程的 interrupted 方法。

中断异常处理实例:

线程A执行过程中,sleep 阻塞20s,但可能中间某一时刻就可以继续运行了,这时候用中断处理,将它中断,强制 sleep()方法发生中断异常而返回。关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
Thread threadOne = new Thread(new Runnable(){
public void run(){
try{
System.out.println("threadOne begin sleep for 20s");
Thread.sleep(200000);
System.out.println("threadOne awaking");
}
catch (InterruptedException e){
System.out.println("threadOne is interrupted while sleeping");
return;
}
System.out.println("threadOne-leaving normally");
});

上下文切换:在线程调度、上下文切换时,要保存现场,再次执行时恢复现场

线程死锁:操作系统都学过的四个条件:
1. 互斥条件,资源排他性
2. 请求和保持,不主动释放
3. 不剥夺条件,不能剥夺
4. 环路等待,死锁环
预防死锁的主要方法是使用资源申请的有序性原则,假如线程 A 和线程 B 都需要资源 1, 2, 3, …, n 时,对资源进行排序,线程 A 和线程 B 都只有在获取了资源 n-1 时才能去获取资源 n. 这样做是从2,4条件入手,避免死锁。

守护线程:
Java 中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程),在 JVM 启动时会调用 main 函数,main 函数所在的线程就是一个用户线程,在 JVM内部同时还启动了许多守护线程,比如垃圾回收线程。
在 Java 中,父线程结束,子线程不一定结束。而只要有一个用户线程还没结束,正常情况下 JVM 就不会退出。如果我们想让一个线程不影响JVM结束工作,可以把它设置为守护线程。thread.setDaemo(true) ;