进程和线程
进程:执行中的程序,一个进程至少包含一个线程。
线程:进程中负责程序执行的执行单元, 线程本身依靠程序进行运行,线程是程序中的顺序控制流,只能使用分配给程序的资源和环境。
单线程和多线程
单线程:程序中只存在一个线程,实际上主方法就是一个主线程。
多线程:在一个程序中运行多个任务目的是更好地使用CPU资源。
线程的实现
继承Thread类
在java.lang
包中定义, 继承Thread类必须重写run()
方法
1
2
3
4
5
6
7
8
9
10
11
12
|
class
MyThread
extends
Thread{
private
static
int
num =
0
;
public
MyThread(){
num++;
}
@Override
public
void
run() {
System.out.println(
"主动创建的第"
+num+
"个线程"
);
}
}
|
创建好了自己的线程类之后,就可以创建线程对象了,然后通过start()方法去启动线程。注意,不是调用run()方法启动线程,run方法中只是定义需要执行的任务,如果调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
class
Test {
public
static
void
main(String[] args) {
MyThread thread =
new
MyThread();
thread.start();
}
}
class
MyThread
extends
Thread{
private
static
int
num =
0
;
public
MyThread(){
num++;
}
@Override
public
void
run() {
System.out.println(
"主动创建的第"
+num+
"个线程"
);
}
}
|
在上面代码中,通过调用start()方法,就会创建一个新的线程了。为了分清start()方法调用和run()方法调用的区别,请看下面一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public
class
Test {
public
static
void
main(String[] args) {
System.out.println(
"主线程ID:"
+Thread.currentThread().getId());
MyThread thread1 =
new
MyThread(
"thread1"
);
thread1.start();
MyThread thread2 =
new
MyThread(
"thread2"
);
thread2.run();
}
}
class
MyThread
extends
Thread{
private
String name;
public
MyThread(String name){
this
.name = name;
}
@Override
public
void
run() {
System.out.println(
"name:"
+name+
" 子线程ID:"
+Thread.currentThread().getId());
}
}
|
运行结果:
从输出结果可以得出以下结论:
1)thread1和thread2的线程ID不同,thread2和主线程ID相同,说明通过run方法调用并不会创建新的线程,而是在主线程中直接运行run方法,跟普通的方法调用没有任何区别;
2)虽然thread1的start方法调用在thread2的run方法前面调用,但是先输出的是thread2的run方法调用的相关信息,说明新线程创建的过程不会阻塞主线程的后续执行。
实现Runnable接口
在Java中创建线程除了继承Thread类之外,还可以通过实现Runnable接口来实现类似的功能。实现Runnable接口必须重写其run方法。
下面是一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
class
Test {
public
static
void
main(String[] args) {
System.out.println(
"主线程ID:"
+Thread.currentThread().getId());
MyRunnable runnable =
new
MyRunnable();
Thread thread =
new
Thread(runnable);
thread.start();
}
}
class
MyRunnable
implements
Runnable{
public
MyRunnable() {
}
@Override
public
void
run() {
System.out.println(
"子线程ID:"
+Thread.currentThread().getId());
}
}
|
Runnable的中文意思是“任务”,顾名思义,通过实现Runnable接口,我们定义了一个子任务,然后将子任务交由Thread去执行。注意,这种方式必须将Runnable作为Thread类的参数,然后通过Thread的start方法来创建一个新线程来执行该子任务。如果调用Runnable的run方法的话,是不会创建新线程的,这根普通的方法调用没有任何区别。
事实上,查看Thread类的实现源代码会发现Thread类是实现了Runnable接口的。
在Java中,这2种方式都可以用来创建线程去执行子任务,具体选择哪一种方式要看自己的需求。直接继承Thread类的话,可能比实现Runnable接口看起来更加简洁,但是由于Java只允许单继承,所以如果自定义类需要继承其他类,则只能选择实现Runnable接口。
通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
实例代码:
- package com.thread;
- import java.util.concurrent.Callable;
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.FutureTask;
- public class CallableThreadTest implements Callable<Integer>
- {
- public static void main(String[] args)
- {
- CallableThreadTest ctt = new CallableThreadTest();
- FutureTask<Integer> ft = new FutureTask<>(ctt);
- for(int i = 0;i < 100;i++)
- {
- System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
- if(i==20)
- {
- new Thread(ft,"有返回值的线程").start();
- }
- }
- try
- {
- System.out.println("子线程的返回值:"+ft.get());
- } catch (InterruptedException e)
- {
- e.printStackTrace();
- } catch (ExecutionException e)
- {
- e.printStackTrace();
- }
- }
- @Override
- public Integer call() throws Exception
- {
- int i = 0;
- for(;i<100;i++)
- {
- System.out.println(Thread.currentThread().getName()+" "+i);
- }
- return i;
- }
- }
(1)Callable规定的方法是call(),Runnable规定的方法是run().
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
(3)call方法可以抛出异常,run方法不可以
(4)运行Callable任务可以拿到一个Future对象,Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如果线程没有执行完,Future.get()方法可能会阻塞当前线程的执行;如果线程出现异常,Future.get()会throws InterruptedException或者ExecutionException;如果线程已经取消,会跑出CancellationException。取消由cancel 方法来执行。isDone确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明Future<?> 形式类型、并返回 null 作为底层任务的结果。
线程的状态
- 创建(new)状态: 准备好了一个多线程的对象
- 就绪(runnable)状态: 调用了
start()
方法, 等待CPU进行调度 - 运行(running)状态: 执行
run()
方法 - 阻塞(blocked)状态: 暂时停止执行, 可能将资源交给其它线程使用
- 终止(dead)状态: 线程销毁
线程从创建到消亡之间的状态:
注:sleep和wait的区别:
sleep
是Thread
类的方法,wait
是Object
类中定义的方法.Thread.sleep
不会导致锁行为的改变, 如果当前线程是拥有锁的, 那么Thread.sleep
不会让线程释放锁.Thread.sleep
和Object.wait
都会暂停当前的线程. OS会将执行时间分配给其它线程. 区别是, 调用wait
后, 需要别的线程执行notify/notifyAll
才能够重新获得CPU执行时间.
上下文切换
对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。
由于可能当前线程的任务并没有执行完毕,所以在切换时需要保存线程的运行状态,以便下次重新切换回来时能够继续切换之前的状态运行。举个简单的例子:比如一个线程A正在读取一个文件的内容,正读到文件的一半,此时需要暂停线程A,转去执行线程B,当再次切换回来执行线程A的时候,我们不希望线程A又从文件的开头来读取。
因此需要记录线程A的运行状态,那么会记录哪些数据呢?因为下次恢复时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
说简单点的:对于线程的上下文切换实际上就是 存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。
线程的常用方法
编号 | 方法 | 说明 |
---|---|---|
1 | public void start() | 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
2 | public void run() | 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
3 | public final void setName(String name) | 改变线程名称,使之与参数 name 相同。 |
4 | public final void setPriority(int priority) | 更改线程的优先级。 |
5 | public final void setDaemon(boolean on) | 将该线程标记为守护线程或用户线程。 |
6 | public final void join(long millisec) | 等待该线程终止的时间最长为 millis 毫秒。 |
7 | public void interrupt() | 中断线程。 |
8 | public final boolean isAlive() | 测试线程是否处于活动状态。 |
9 | public static void yield() | 暂停当前正在执行的线程对象,并执行其他线程。 |
10 | public static void sleep(long millisec) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
11 | public static Thread currentThread() | 返回对当前正在执行的线程对象的引用。 |
静态方法
currentThread()方法
currentThread()方法可以返回代码段正在被哪个线程调用的信息。
1
2
3
4
5
|
public
class
Run1{
public
static
void
main(String[] args){
System.out.println(Thread.currentThread().getName());
}
}
|
sleep()方法
方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。
sleep方法有两个重载版本:
1
2
|
sleep(
long
millis)
//参数为毫秒
sleep(
long
millis,
int
nanoseconds)
//第一参数为毫秒,第二个参数为纳秒
|
sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。看下面这个例子就清楚了:
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
|
public
class
Test {
private
int
i =
10
;
private
Object object =
new
Object();
public
static
void
main(String[] args)
throws
IOException {
Test test =
new
Test();
MyThread thread1 = test.
new
MyThread();
MyThread thread2 = test.
new
MyThread();
thread1.start();
thread2.start();
}
class
MyThread
extends
Thread{
@Override
public
void
run() {
synchronized
(object) {
i++;
System.out.println(
"i:"
+i);
try
{
System.out.println(
"线程"
+Thread.currentThread().getName()+
"进入睡眠状态"
);
Thread.currentThread().sleep(
10000
);
}
catch
(InterruptedException e) {
// TODO: handle exception
}
System.out.println(
"线程"
+Thread.currentThread().getName()+
"睡眠结束"
);
i++;
System.out.println(
"i:"
+i);
}
}
}
}
|
输出结果:
从上面输出结果可以看出,当Thread-0进入睡眠状态之后,Thread-1并没有去执行具体的任务。只有当Thread-0执行完之后,此时Thread-0释放了对象锁,Thread-1才开始执行。
注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。
yield()方法
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public
class
MyThread
extends
Thread{
@Override
public
void
run() {
long
beginTime=System.currentTimeMillis();
int
count=
0
;
for
(
int
i=
0
;i<
50000000
;i++){
count=count+(i+
1
);
//Thread.yield();
}
long
endTime=System.currentTimeMillis();
System.out.println(
"用时:"
+(endTime-beginTime)+
" 毫秒!"
);
}
}
public
class
Run {
public
static
void
main(String[] args) {
MyThread t=
new
MyThread();
t.start();
}
}
|
执行结果:
1
|
用时:
3
毫秒!
|
如果将 //Thread.yield();
的注释去掉,执行结果如下:
1
|
用时:
16080
毫秒!
|
sleep和yield的区别
sleep是让当前线程处于阻塞状态。
yidld是让当前线程处于就绪状态。
对象方法
start()方法
start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
run()方法
run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
getId()
getId()的作用是取得线程的唯一标识
代码:
1
2
3
4
5
6
|
public
class
Test {
public
static
void
main(String[] args) {
Thread t= Thread.currentThread();
System.out.println(t.getName()+
" "
+t.getId());
}
}
|
输出:
1
|
main
1
|
isAlive()方法
方法isAlive()的功能是判断当前线程是否处于活动状态
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
MyThread
extends
Thread{
@Override
public
void
run() {
System.out.println(
"run="
+
this
.isAlive());
}
}
public
class
RunTest {
public
static
void
main(String[] args)
throws
InterruptedException {
MyThread myThread=
new
MyThread();
System.out.println(
"begin =="
+myThread.isAlive());
myThread.start();
System.out.println(
"end =="
+myThread.isAlive());
}
}
|
程序运行结果:
1
2
3
|
begin ==
false
run=
true
end ==
false
|
方法isAlive()的作用是测试线程是否偶处于活动状态。什么是活动状态呢?活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。
有个需要注意的地方
1
|
System.out.println(
"end =="
+myThread.isAlive());
|
虽然上面的实例中打印的值是true,但此值是不确定的。打印true值是因为myThread线程还未执行完毕,所以输出true。如果代码改成下面这样,加了个sleep休眠:
1
2
3
4
5
6
7
|
public
static
void
main(String[] args)
throws
InterruptedException {
MyThread myThread=
new
MyThread();
System.out.println(
"begin =="
+myThread.isAlive());
myThread.start();
Thread.sleep(
1000
);
System.out.println(
"end =="
+myThread.isAlive());
}
|
则上述代码运行的结果输出为false,因为mythread对象已经在1秒之内执行完毕。
join()方法
t.start();
t.join();
1 public class TestJoin { 2 3 public static void main(String[] args) { 4 5 MyThread2 t1 = new MyThread2("TestJoin"); 6 t1.start(); 7 try { 8 t1.join(); //join()合并线程,子线程运行完之后,主线程才开始执行 9 }catch (InterruptedException e) { } 10 11 for(int i=0 ; i <10; i++) 12 System.out.println("I am Main Thread"); 13 } 14 } 15 16 class MyThread2 extends Thread { 17 18 MyThread2(String s) { 19 super(s); 20 } 21 22 public void run() { 23 for(int i = 1; i <= 10; i++) { 24 System.out.println("I am "+getName()); 25 try { 26 sleep(1000); //暂停,每一秒输出一次 27 }catch (InterruptedException e) { 28 return; 29 } 30 } 31 } 32 }
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am TestJoin
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
I am Main Thread
总结:也就是说那个线程调用join()方法 那么 调用join()方法的线程会先于其他线程执行,直到该线程(调用join方法的线程)执行完毕后,其他线程才会执行。
getName和setName
用来得到或者设置线程名称。
getPriority和setPriority
用来获取和设置线程优先级。
setDaemon和isDaemon
用来设置线程是否成为守护线程和判断线程是否是守护线程。
守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。
Thread类中的方法调用到底会引起线程状态发生怎样的变化呢?下面一幅图就是在上面的图上进行改进而来的:
停止线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
- 使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期的方法,使用他们可能产生不可预料的结果。
- 使用interrupt方法中断线程,但这个不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。
暂停线程
interrupt()方法
线程的优先级
在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。
设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
设置线程的优先级使用setPriority()方法,此方法在JDK的源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public
final
void
setPriority(
int
newPriority) {
ThreadGroup g;
checkAccess();
if
(newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw
new
IllegalArgumentException();
}
if
((g = getThreadGroup()) !=
null
) {
if
(newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
|
在Java中,线程的优先级分为1~10这10个等级,如果小于1或大于10,则JDK抛出异常throw new IllegalArgumentException()。
JDK中使用3个常量来预置定义优先级的值,代码如下:
1
2
3
|
public
final
static
int
MIN_PRIORITY =
1
;
public
final
static
int
NORM_PRIORITY =
5
;
public
final
static
int
MAX_PRIORITY =
10
;
|
线程优先级特性:
- 继承性
比如A线程启动B线程,则B线程的优先级与A是一样的。 - 规则性
高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。 - 随机性
优先级较高的线程不一定每一次都先执行完。
守护线程
在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。
Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。
守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) ;但是有几点需要注意:
- thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)
- 在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)
- 不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了。
本文参考自:http://www.importnew.com/21136.html,
https://www.cnblogs.com/jpwz/p/6248000.html