java多线程
参考博客:
一. 概述
- 首先,我们需要分清楚一些概念,什么是进程(进城),什么是线程(献城).
在任务管理器中我们可以看到进程在执行.还有一些进程的信息,例如CPU的占有率,占用内存大小.简单来说一个运行的程序就是进程. - 这里给出一个定义:
一个具有独立功能的程序在一个数据集合上的一次动态执行
也就是说把一个可执行文件(程序)加载到内存中,CPU执行的这个动态过程称为进程. - 我们可以用多个进程来完成一件任务,但是进程间的通讯,共享数据实现比较的麻烦,同时系统维护进程的开销也比较的大(例如 分配资源,建立PCB,撤销进程,回收资源,撤销PCB,进程的切换),为了解决这些问题,提出了线程. 将进程的责任进行进一步的划分.进程来执行管理功能.将
进程看做是一个资源的平台(环境),提供各种的资源(地址空间,打开的文件,数据段,代码段).而进程的执行功能,交给线程.``线程的定义:进程中的一条执行流程
线程可以访问进程的资源,那么多个线程完成一个任务,比多个进程来完成实现简单. - 线程=进程- 共享资源
线程的优点 :
- 一个进程多以同时存在多个线程,多个执行流
- 线程可以并发的执行
- 每个线程直接共享地址空间和文件资源
线程的缺点 : - 一个线程崩溃,会导致所属的进程的所有线程崩溃.
并发和并行:
-
并发:以前的计算机只有一个CPU中只有一个执行单元,任何的时刻只能执行一条代码,但是速度足够的快,我们可以不断的切换进程(线程),(虽然只有一个进程(线程)在运行,但是速度足够的快,我们在使用的过程中感觉不到进程(线程)的切换,仿佛同时执行多个任务.也就是说在一段时间内,它们是同时执行的.
-
并行 :现在的CPU中有多个执行单元,相当于有多个流水线,可以同一时刻执行多条指令.同一时刻,可以同时处理对个进程(线程).
-
多线程(进程)究竟是并发还是并行是操作系统的调度来确定的,程序编写者无法确定.但是,并发还是并行它们的最终的结果还是不变的.
-
虽然说Java的多线程与具体和平台无关,但是要明白Java多线程,还是需要了解一下操作系统中线程和进程相关的知识.
二.Java多线程的入门
Java实现多线程有四种机制:
- 继承
Thread
类 - 实现
Runable
接口 - 通过
Callable
和Future
创建 - 使用线程池创建
继承Thread
类
步骤:
- 定义一个继承Thread类的子类.并重写该类的run()方法
- 创建Thread子类的实例,即创建了线程对象
- 调用该线程对象的start()方法,启动线程
public class MyThread extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i= 0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+" "+i); //currentThread()是一个静态方法,获得当前执行的线程.
}
}
public static void main(String[] args) {
Mythread myThread = new ThreadLocalDemo();
myThread.start(); //启动线程
for(int i= 0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
main 0
main 1
main 2
main 3
Thread-0 0
main 4
Thread-0 1
Thread-0 2
main 5
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
main 6
Thread-0 8
Thread-0 9
main 7
main 8
main 9
实现Runable
接口
步骤:
- 定义Runnable接口的实现类,并重写该接口的run()方法
- 创建Runnable实现类的实例
- 并以此实例作为Thread的target对象传入.
- 启动该线程的thread的实现类
public class MyThread implements Runnable {
@Override
public void run() {
for(int i= 0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
MyThread myThread= new MyThread();
Thread thread1= new Thread(myThread);
thread1.start();
for(int i= 0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
通过Callable
和Future
创建
这种方法可以使得线程执行有返回值
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值其中.
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// TODO Auto-generated method stub
for(int i =0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
return "执行完成";
}
public static void main(String[] args) {
FutureTask task = new FutureTask<String>( new MyCallable());
Thread thread1 = new Thread(task,"myThread");
thread1.start();
for(int i =0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
y {
System.out.println(task.get()); //get方法值阻塞的.除非等待task中的线程执行完成
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
使用线程池创建
日后在补
三. 线程的生命周期
- 新建状态
用new创建一个Tread实例后,在线程便处于新生状态,此时线程已经拥有自己的栈,应该还拥有类似一份TCB的数据结构,维护自己线程信息的结构. - 就绪态
此时线程已经具备可执行的条件,但是没有获得CPU,出入线程就绪队列中,等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态,继续执行代码 - 运行状态
程序获得CPU的执行权,执行指令. 此时线程可能会进入阻塞状态或者线程死亡
进入阻塞状态的分类:
- 主动的放弃CPU(调用Sleep())
- 线程调用一个阻塞式I/O,在I/O方法返回前,线程都是出于阻塞态
- 线程试图获得一个互斥资源,而该资源被其他线程占用
- 线程在等待某个通知
- 线程调用suspend()方法.当时该方法容易导致死锁.
当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、destory()方法等等,就会从运行状态转变为死亡状态。
四 线程的管理
- sleep() 线程睡眠
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法。sleep是一个静态方法,调用该方法会使当前的线程阻塞.java线程是有JVM的调度算法决定的,我们不可能精确的控制,sleep()阻塞1s后,进入就绪态,当时不保证立马被调用. - yield()线程让步
也是Thread中的一个静态方法,作用是让当前的线程进入到就绪状态.yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。
sleep和yield的区别:
- sleep方法会使暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
- sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。
- sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。
- join() 线程合并
线程的合并就将几个并发的线程合并成一个"单线程执行".应用场景是当一个线程必须等待另一个线程执行完毕才能执行,join()方法不是静态的
join的三个重载方法 :
void join()
//当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
void join(long mills)
// 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
void join(long mills, int nanos)
- 设置线程的优先级
在算法调度中,并不是简单的先进先出队列来实现调度机制.通常每个线程都有一个优先级属性,优先级属性越高,获得执行的机会就越多,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
Thread类提供了setPriority(int newPriority)和getPriority()来 设置优先级,虽然Java设置是1-10,当时对应操作系统划分的等级却不一定,所以JAVA用三个静态常量来实现.
MAX_PRIORITY =10;
MIN_PRIORITY =1;
NORM_PRIORITY =5;
- 后台守护线程
守护线程使用的情况比较的少. 例如JVM的垃圾,内存管理等线程都是守护线程,守护线程的用途通常用来执行一些后台作业,例如运行程序的时候播放音乐,文字编辑器里的自动语法检查.守护线程的好处,不需要关心它的结束问题.只有JVM虚拟机退出的时候才退出.
public static void setDeamom(boolean on) //将该线程标记为守护线程或者用户线程,当正在运行的线程都是守护线程时,JVM退出
// on -true,则将该线程标记为守护线程
// 抛出:
// IlleagalThreadStateException 如果该线程处于活动态
// SecurityException 如果当前线程无法修改
注:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台线程时候一定要注意这个问题。
6. 正确的执行线程
Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用下面的方法:
• 正常执行完run方法,然后结束掉;
• 控制循环条件和判断条件的标识符来结束掉线程。
class MyThread extends Thread {
int i=0;
boolean next=true;
@Override
public void run() {
while (next) {
if(i==10)
next=false;
i++;
System.out.println(i);
}
}
}