在本文中,主要介绍Java多线程的知识,涉及的内容有线程基础知识、线程和进程的区别、并发性和并行性的区别、两种创建线程的方法、线程的生命周期、线程的控制等知识。
线程的基础知识
1、单线程:一条顺序执行流,从main()方法开始,依次向下执行每行代码,如果程序执行某行代码时遇到了阻塞,程序将会停滞在该处。
2、多线程:
(1) 多线程扩展了多进程的概念。
(2) 线程也被称为轻量级的进程。每个进程启动都会启动一个主线程,也可以创建多个辅助的线程。 线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。
(3) 线程可以拥有自己的堆栈,自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程的所有资源。线程是独立运行的,也是抢占式,一个线程可以创建和撤销另一个线程。(参考:http://www.jianshu.com/p/729c61448fbd)
(4) 多线程的优点:
a. 进程之间不能共享内存,线程之间共享内存很方便
b. 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率要高
线程和进程的区别
1、运行中的任务对应一个进程(Process)
进程是系统进行资源分配和调度的一个独立单位
2、操作系统支持同时运行多个任务,一个任务就是一个程序,每个运行中的程序就是一个进程,当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。
并发性和并行性的区别
1、并发:同一时刻只能执行一条指令,多个进程指令被快速轮换执行。对于一个CPU而言,它在某一个时刻只能执行一个程序,也就是说,只能运行一个进程,CPU不断的在这些进程之间进行轮换执行。
2、并行:同一时刻,有多条指令在多个处理器上同时执行。
两种创建线程的方法
1、继承Thread类创建线程类
//当前类继承自Thread类
public class TestThread extends Thread{
//重写run()方法,即执行体(run()方法的方法体代表了线程需要完成的任务)
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//当前线程继承Thread类,直接使用this即可获取当前线程
//Thread的静态方法getName()获取当前线程的名字
System.out.println("run()方法里面,当前线程:" + this + " ->" + getName() + " " + i);
//输出的结果如:run()方法里面,当前线程:Thread[Thread-0,5,main] ->Thread-0 3 (5代表优先级,这是默认值)
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
//调用Thread的currentThread()方法获取当前线程(主线程)
String threadName = Thread.currentThread().getName();
System.out.println("当前线程" + threadName + " " + i);
if (i == 99) {
//创建Thread子类的实例,即创建线程对象
TestThread thread1 = new TestThread();
TestThread thread2 = new TestThread();
//调用start()方法来启动线程
thread1.start();
thread2.start();
}
}
}
}
2、实现Runnable接口创建线程类
//当前类实现Runnable接口
public class TestThread implements Runnable{
private int i = 0;
//重写run()方法
@Override
public void run() {
for (; i < 100; i++) {
//需要通过Thread.currentThread().getName()获取当前线程
System.out.println("R " + Thread.currentThread().getName() + " 第" + i + "个顺序");
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println("在main方法里面," + Thread.currentThread().getName() + " " + i);
if (i == 20) {
//创建Runnable实现类的实例,以此作为Thread的target来创建线程对象
//声明一个target
TestThread tr = new TestThread();
//创建2个线程,公用一个target
new Thread(tr, "新线程1").start();
new Thread(tr, "新线程2").start();
}
}
}
}
(1) 继承Thread类创建线程类:
a. 优点:
代码简单,获取当前线程只需要通过this即可
b. 缺点:
已经继承了Thread类,无法再继承其他父类
(2) 实现Runnable接口创建线程类:
a. 优点:
线程类只是实现了Runable接口,还可以继承其他类
多个线程共享一个target对象,比较适合多个相同线程处理同一份资源的情况,将CPU、代码与数据分开,较好的体现了面向对象的思想(实现与规范分离)
b. 缺点:
编程稍微复杂,访问当前线程需要Thread.currentThread().getName()
线程的生命周期
1、新建(New)和就绪(Runable):
(1) new一个Thread之后处于新建状态
(2) 调用start()方法之后处于就绪状态,此时只是表示该线程可以运行了,至于何时开始执行,取决于JVM里线程调度器的调度情况
(3) 永远不要直接调用run()方法,调用它相当于单线程程序了:
a. 启动线程使用start方法,而不是run方法!
b. 调用start方法来启动线程,系统会把该run方法当成线程执行体来处理;
c. 如果直接调用线程对象的run方法,则run方法立即就会被执行,而且在run方法返回之前其他线程无法并发执行。也就是说,如果直接调用线程对象的run方法,系统把线程对象当成一个普通对象,而run方法也是一个普通方法,而不是线程执行体。(参考:http://m.blog.csdn.net/article/details?id=51541161)
(4) start() 方法不要调用多次,否则会出现IllegalThreadStateException
3、运行(Running)和阻塞(Blocked)
(1) 线程的调度取决于操作系统,抢占式调度或者协作式调度
抢占式多任务处理是计算机操作系统中,一种实现多任务处理的方式,相对于协作式多任务处理而言。协作式环境下,下一个进程被调度的前提是当前进程主动放弃时间片;抢占式环境下,操作系统完全决定进程调度方案,操作系统可以剥夺耗时长的进程的时间片,提供给其它进程。
(2) 线程不会永远一直运行下去,会中断或者阻塞
(3) 调用sleep()方法主动放弃占用的资源,当前线程阻塞,处于等待等待状态
5、死亡(Dead)
(1) run()方法执行完成,线程正常结束
(2) 线程抛出一个未捕获的Exception或者Error
(3) 直接调用该线程的stop()方法结束该线程(容易造成死锁,不推荐用)(死锁的概念可以参考:http://wiki.jikexueyuan.com/project/java-concurrent/deadlock.html)
(4) 主线程结束,其他线程不会受到任何影响
注意:当主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,不会受到主线程结束的影响。
(5) 不要对已经处于死亡状态的线程调用start()方法,否则会引发IllegalThreadStateException异常
线程的控制
1、join线程
(1) 等待另外一个线程执行完成
(2) 当某个程序执行流中调用其他线程的join()方法时,调用线程将会被阻塞,直到被join的线程执行完成为止
2、后台线程
(1) 运行在后台,它的任务是为其他的线程提供服务
(2) 称为:Daemon Thread,守护线程
(3) JVM的垃圾回收线程就是典型的后台线程
(4) 如果所有的前台线程死亡,后台线程会自动死亡
(5) 调用setDaemon(true)可以将指定线程设置成后台线程,使用isDaemon()方法可以查看当前线程是否后台线程
3、线程睡眠
(1) 如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,这可以通过Thread.sleep() 来实现
(2) static void sleep(long mills) 比较常用
(3) static void sleep(long mills,int nanos) 让当前正在执行的线程暂停mills毫秒,并加上nanos 微秒
4、线程让步
(1) 线程让步使用yield() 静态方法:
a. 跟sleep()类似,区别在于不会yield()阻塞该线程,而是进入就绪状态
b. 当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会得到执行的机会
(2)改变线程优先级: setPriority(int newPriority)
a. Thread.MAX_PRIORITY: 10
c. Thread.MIN_PRIORITY: 0
d. Thread.NORM_PRIORITY: 5
如何让线程按顺序执行,比如让三个线程按照一定的次数循环输出ABCABC...
package thread_15th;
public class LoopPrint extends Thread{
public LoopPrint(String name) {
super(name);
}
public static void main(String[] args) throws InterruptedException {
int times = 10,i = 0;
while (i < times) {
//分别命名三个线程:A、B、C
LoopPrint l1 = new LoopPrint("A");
LoopPrint l2 = new LoopPrint("B");
LoopPrint l3 = new LoopPrint("C");
//l1调用start()方法后处于就绪状态,接着开始运行
l1.start();
//l1调用join()方法使l1执行完毕
l1.join();
l2.start();
l2.join();
l3.start();
i++;
}
}
@Override
public void run() {
System.out.print(Thread.currentThread().getName());
}
}
运行结果:
ABCABCABCABCABCABCABCABCABCABC
线程同步——同步代码块和同步方法
1、为什么需要同步线程?synchronized(obj){
//todo
}
相当于把里面的代码块进行锁定,目的是阻止多个线程对同一个共享资源进行并发访问