(一)Java并发-多线程基础

TOC
  1. 1. 线程的几种状态
    1. 1.1. 新建状态(New)
    2. 1.2. 就绪状态(Runnable)
    3. 1.3. 运行状态(Running)
    4. 1.4. 阻塞状态(Blocked)
      1. 1.4.1. 1.等待阻塞
      2. 1.4.2. 2.同步阻塞
      3. 1.4.3. 3.其他阻塞
    5. 1.5. 死亡状态(Dead)
  2. 2. 多线程实现
    1. 2.1. java.lang.Thread 类扩展
    2. 2.2. java.lang.Runnable 接口实现
    3. 2.3. Callable 接口实现
  3. 3. 几个常用的函数
    1. 3.1. stop()停止线程
    2. 3.2. interrupt()
    3. 3.3. yield() 礼让线程
    4. 3.4. join() 用法
    5. 3.5. setPriority()线程优先级
    6. 3.6. 守护线程 Deamon
    7. 3.7. 其他方法
  4. 4. 线程间通信
    1. 4.1. 定时任务(TimerTask 和 Timer)

线程的几种状态

图片来源网络

新建状态(New)

当线程对象对创建后,即进入了新建状态。

1
Thread t = new MyThread();

就绪状态(Runnable)

当调用线程对象的 start()方法

1
2
Thread t = new MyThread();
t.start();

线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待 CPU 调度执行,并不是说执行了 t.start()此线程立即就会执行;

  • 调用 start()方法
  • 阻塞事件解除,重新进入就绪状态
  • 调用 yield(),让出 CPU 调度,从运行状态进入就绪状态
  • JVM 切换线程

运行状态(Running)

当 CPU 开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

  • 处于就绪状态的线程获得 CPU

阻塞状态(Blocked)

处于运行状态中的线程由于某种原因,暂时放弃对 CPU 的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被 CPU 调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

  • sleep() 占用 CPU 资源
  • wait() 释放 CPU 资源
  • join() 等其他线程结束后才执行,会释放 CPU 资源
  • I/O 操作,read(),write()操作

1.等待阻塞

运行状态中的线程执行 wait()方法,使本线程进入到等待阻塞状态;

2.同步阻塞

线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

3.其他阻塞

通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

死亡状态(Dead)

线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。

多线程实现

在 Java 中实现多线程,有三种方法。

java.lang.Thread 类扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package Thread;
//重写该类的run方法
class testThread extends Thread{
private String name;
public testThread(String name){
this.name = name;
}
public void run(){
for(int i = 0; i < 5;i++){
System.out.println(name+"运行: "+i);
}
}
}
public class learnThread {
public static void main(String[] args) {
// TODO Auto-generated method stub
testThread testTh1 = new testThread("A");
testThread testTh2 = new testThread("B");
testTh1.start();
testTh2.start();
}
}

输出的结果每次都不一样。

1
2
3
4
5
6
7
8
9
10
A运行:  0
B运行: 0
B运行: 1
B运行: 2
A运行: 1
A运行: 2
A运行: 3
A运行: 4
B运行: 3
B运行: 4

.start()调用后,线程并不会直接进入运行态,而是进入就绪态,获得除 CPU 之外的所有资源,等待获得 CPU 资源,一旦获得 CPU 资源即可进入运行态。每次执行多线层程序,得到的结果都不相同。适合线程调度顺序不影响结果的程序。

1
为什么要通过start()方法调用run方法,而不是直接调用run方法?

查看源代码:

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
28
29
30
31
32
33
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();

/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);

boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}

private native void start0();

发现在源代码中,调用了一次 start0()方法,并且 start0()只声明未定义。可以看到 start0()是由 native 声明,native 一般在本地声明,异地用 C 和 C++ 来实现,也就是说 native 是调用的本机的原生系统函数。start0()是由 JVM 虚拟机实现

java.lang.Runnable 接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package Thread;

class testThread1 implements Runnable{
private String name;
public testThread1(String name){
this.name = name;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 5;i++){
System.out.println(name+"运行: "+i);
}
}

}
public class learningRunnable {
public static void main(String[] args) {
// TODO Auto-generated method stub
new Thread(new testThread1("A")).start();
new Thread(new testThread1("B")).start();
}
}

main 方法其实也是一个线程。在 Java 中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到 CPU 的资源。在 Java 中,每次程序运行至少启动 2 个线程。一个是 main 线程,一个是垃圾收集线程。因为每当使用 Java 命令执行一个类的时候,实际上都会启动一个 JVM,每一个 JVM 实习在就是在操作系统中启动了一个进程。
实际开发中多线程的操作很少使用 Thread 类,而是通过 Runnable 接口完成。因为实现 Runnable 接口相比继承 Thread 类有如下好处:

1
2
避免Java单继承的限制,一个类可以继承多个接口。
适合多个相同的程序代码的线程去处理同一个资源(适合于资源的共享)

Callable 接口实现

Callable 和 Runnable 区别:

1
2
两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
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
package Thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyThread implements Callable<String>{

@Override
public String call() throws Exception {
for (int i = 0; i < 20; i++) {
System.out.println("买票 , x = " + i);
}
return "票没了";
}
}
public class learningCallable {

public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<String> task = new FutureTask<String>(new MyThread());
new Thread(task).start();
System.out.println(task.get());

}

}

几个常用的函数

stop()停止线程

不推荐使用

interrupt()

中断某个线程,这种结束方式比较粗暴,如果 t 线程打开了某个资源还没来得及关闭也就是 run 方法还没有执行完就强制结束线程,会导致资源无法关闭

yield() 礼让线程

yield()暂停线程,直接进入就绪状态,执行其他线程

1
2
3
4
5
6
new Thread(()->{
for (int i = 0; i < 1000; i++){
System.out.println("01");
Thread.yield();
}
},"01").start();

join() 用法

Thread.join()把指定线程 Thread 加到当前线程,将两个并发执行的线程合并为顺序执行的线程。例如下面的代码,加 Father 线程中调用了 Son 线程的 join 方法,直到 Son 线程执行完毕后,才会继续执行 Father 线程。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class JavaTest {
/**
*
* @param args
*/
public static void main(String[] args) {
System.out.println("爸爸和儿子买酱油的故事。。。");
new Thread(new Father()).start();
}

}
class Father extends Thread{
public void run(){
System.out.println("酱油没了");
System.out.println("让儿子打酱油。。。");
Thread t = new Thread(new Son());
t.start();
try {
t.join();
System.out.println("酱油终于来了");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}
class Son extends Thread{
public void run(){
System.out.println("接过钱、、、、、");
System.out.println("发现了个游戏厅,玩了一会。。");
for (int i = 0; i < 10; i++){
System.out.println(i + "秒过去了。。。。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("赶紧打酱油去。。。。");
}
}

setPriority()线程优先级

线程优先级是 1 到 10,并是不设置优先级高了,就一定优先执行。设置优先级在线程启动之前。

  • NORM_PRIORITY 5 默认
  • MIN_PRIORITY 1
  • MAX_PRIORITY 10
1
2
3
Thread t = new Thread(mp);
t.setPriority(Thread.MAX_PRIORITY);
t.start();

守护线程 Deamon

守护线程是为用户线程服务的,JVM 停止不用等待守护线程执行完毕。
默认线程是用户线程,JVM 等待用户线程执行完毕才会停止。

1
2
3
Thread t = new Thread(mp);
t.setDaemon(true); //默认是false;
t.start();

其他方法

方法 功能
isAlive() 判断线程是否还活着,即线程是否还未终止
setName() 给线程起名字
getName() 获取线程的名字
currentThread() 取得当前正在运行的线程对象,也就是获取自己本身

线程间通信

Java 提供了 3 个方法解决线程之间的通信问题

方法名 作用
final void wait() 表示线程一直等待,直到其他线程通知,与 sleep 不同,会释放锁
final void wait() 指定等待的毫秒数
final void notifiy() 唤醒一个处于等待状态的线程
final void notifyAll() 唤醒同一个对象上所有调用 wait()方法的线程,优先级别高的线程先调度

定时任务(TimerTask 和 Timer)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//任务类
class MyTask extends TimerTask{

@Override
public void run() {
for(int i=0;i<10;i++) {
System.out.println("放空大脑休息一会");
}
System.out.println("------end-------");
}

}
public static void main(String[] args) {
Timer timer = new Timer();
//执行安排
//timer.schedule(new MyTask(), 1000); //执行任务一次
//timer.schedule(new MyTask(), 1000,200); //执行多次
Calendar cal = new GregorianCalendar(2019,12,31,21,53,54);
timer.schedule(new MyTask(), cal.getTime(),200); //指定时间
}