什么叫多线程程序?
一个进程至少有一个线程在运行,当一个进程中出现多个线程时,就称这个应用程序是多线程应用程序。
java编写的程序都是多线程的,因为最少有俩线程,main主线程和gc线程。
每个线程在栈区中都有自己的执行空间,自己的方法区、自己的变量。
线程为cpu增加了一条执行路径。
线程的创建方式?
Java可以用四种方式来创建线程,如下所示:
1. 继承于Thread类,重写run()方法
Thread thread = new MyThread();
//线程启动
thread.start();
SonThread 类
//继承Thread
class SonThread extends Thread{
//重写run方法
@Override
public void run() {
//任务内容....
System.out.println("当前线程是:"+Thread.currentThread().getName());
}
}
运行结果:
当前线程是:Thread-0
其实在线程类使用的很少的情况下,我们可以选择使用匿名内部类,看下面:
Thread thread = new Thread(){
@Override
public void run() {
//任务内容....
System.out.println("当前线程是:"+Thread.currentThread().getName());
}
};
2. 实现Runable接口,实现里面的run()方法
像第一种一般情况下是不建议用的,因为java是单继承结构,一旦继承了Thread类,就无法继承其他类了。所以建议使用 实现Runable接口 的方法。
Thread thread = new Thread(new MyTask());
//线程启动
thread.start();
MyTask 类:`
//实现Runnable接口
class MyTask implements Runnable{
//重写run方法
public void run() {
//任务内容....
System.out.println("当前线程是:"+Thread.currentThread().getName());
}
}
3. 使用 FutureTask 实现有返回结果的线程
FutureTask 是一个可取消的异步计算任务,是一个独立的类,实现了 Future、Runnable接口。FutureTask 的出现是为了弥补 Thread 的不足而设计的,可以让程序员跟踪、获取任务的执行情况、计算结果 。
因为 FutureTask实现了 Runnable,所以 FutureTask 可以作为参数来创建一个新的线程来执行,也可以提交给 Executor 执行。FutureTask 一旦计算完成,就不能再重新开始或取消计算。
//创建一个 FutureTask,一旦运行就执行给定的 Callable。
public FutureTask(Callable<V> callable);
//创建一个 FutureTask,一旦运行就执行给定的 Runnable,并安排成功完成时 get 返回给定的结果 。
public FutureTask(Runnable runnable, V result)
下面是 FutureTask 的简单例子
public class Test {
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<Double> task = new FutureTask(new MyCallable());
//创建一个线程,异步计算结果
Thread thread = new Thread(task);
thread.start();
//主线程继续工作
Thread.sleep(1000);
System.out.println("主线程等待计算结果...");
//当需要用到异步计算的结果时,阻塞获取这个结果
Double d = task.get();
System.out.println("计算结果是:"+d);
//用同一个 FutureTask 再起一个线程
Thread thread2 = new Thread(task);
thread2.start();
}
}
class MyCallable implements Callable<Double>{
@Override
public Double call() {
double d = 0;
try {
System.out.println("异步计算开始.......");
d = Math.random()*10;
d += 1000;
Thread.sleep(2000);
System.out.println("异步计算结束.......");
} catch (InterruptedException e) {
e.printStackTrace();
}
return d;
}
}
运行结果:
异步计算开始.......
主线程等待计算结果...
异步计算结束.......
计算结果是:1002.7806590582911
4. 使用线程池ExecutorSerice、Executors
前面三种方法,都是显式地创建一个线程,可以直接控制线程,如线程的优先级、线程是否是守护线程,线程何时启动等等。
而第四种方法,则是创建一个线程池,池中可以有1个或多个线程,这些线程都是线程池去维护,控制程序员不需要关心这些细节,只需要将任务提交给线程池去处理便可,非常方便。
创建线程池的前提最好是你的任务量大,因为创建线程池的开销比创建一个线程大得多。
那么接下来看看创建线程池的方式
ExecutorService 是一个比较重要的接口,实现这个接口的子类有两个 ThreadPoolExecutor (普通线程池)、ScheduleThreadPoolExecutor (定时任务的线程池)。你可以通过这两个类来创建一个线程池,但要传入各种参数,不太方便。
Executors 创建单线程的线程池
@ Example2 Executors 创建单线程的线程池
public class MyTest {
public static void main(String[] args) {
//创建一个只有一个线程的线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
//创建任务,并提交任务到线程池中
executorService.execute(new MyRunable("任务1"));
executorService.execute(new MyRunable("任务2"));
executorService.execute(new MyRunable("任务3"));
}
}
class MyRunable implements Runnable{
private String taskName;
public MyRunable(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println("线程池完成任务:"+taskName);
}
}
线程的五种状态
先看看五种状态的关系图:
(1)新建状态:即单纯地创建一个线程,通常是通过new关键字。
(2)就绪状态:在创建了线程之后,调用Thread类的start()方法来启动一个线程,即表示线程进入就绪状态。
(3)运行状态:当线程获得CPU时间,线程才从就绪状态进入到运行状态。
(4)阻塞状态:线程进入运行状态后,可能由于多种原因让线程进入阻塞状态。
在这里阻塞又分为:
- 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
- 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
- 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
(5)死亡状态:run()方法的正常退出就让线程进入到死亡状态,还有当一个异常未被捕获而终止了run()方法的执行也将进入到死亡状态。
线程的常用方法
1. sleep()方法
线程休眠:指的是让线程暂缓执行一下,等到了预计时间之后在恢复执行。
线程休眠会交出CPU,让CPU去执行其他的任务。但是有一点要注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象,使线程进入阻塞态。
方法:public static native void sleep(long millis) throws InterruptedException(单位毫秒)
class MyThread implements Runnable{
public void run() {
for(int i=0;i<1000;i++) {
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程:"+Thread.currentThread().getName()+",i="+i);
}
}
}
public class Line{
public static void main(String[] args) {
MyThread mt=new MyThread();
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
运行结果:
当前线程:Thread-2,i=0
当前线程:Thread-0,i=0
当前线程:Thread-1,i=0
当前线程:Thread-2,i=1
当前线程:Thread-0,i=1
当前线程:Thread-1,i=1
当前线程:Thread-2,i=2
当前线程:Thread-1,i=2
当前线程:Thread-0,i=2
当前线程:Thread-2,i=3
当前线程:Thread-0,i=3
当前线程:Thread-1,i=3
当前线程:Thread-2,i=4
当前线程:Thread-1,i=4
当前线程:Thread-0,i=4
2. 线程让步 yied()方法
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会解放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获得CPU执行时间的机会,如果没有同等优先级的线程,那么yield()方法什么也不做。
注意:调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只是需要等待重新获取CPU执行时间。
class MyThread implements Runnable{
public void run() {
for(int i=0;i<5;i++) {
Thread.yield();
System.out.println("当前线程:"+Thread.currentThread().getName()+",i="+i);
}
}
}
public class Line{
public static void main(String[] args) {
MyThread mt=new MyThread();
new Thread(mt).start();
new Thread(mt).start();
new Thread(mt).start();
}
}
运行结果:
当前线程:Thread-0,i=0
当前线程:Thread-2,i=0
当前线程:Thread-1,i=0
当前线程:Thread-2,i=1
当前线程:Thread-0,i=1
当前线程:Thread-2,i=2
当前线程:Thread-1,i=1
当前线程:Thread-0,i=2
当前线程:Thread-2,i=3
当前线程:Thread-1,i=2
当前线程:Thread-0,i=3
当前线程:Thread-2,i=4
当前线程:Thread-1,i=3
当前线程:Thread-0,i=4
当前线程:Thread-1,i=4
3. join()方法
等待该线程终止,意思就是如果在主线程中调用该方法时就会让主线程休眠,让调用该方法的线程run()方法先执行完毕之后再开始执行主线程。
class MyThread implements Runnable{
public void run() {
try {
System.out.println("\r\n主线程睡眠前的时间");
Line.printTime();
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName());
System.out.println("睡眠结束的时间");
Line.printTime();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Line{
public static void main(String[] args) throws InterruptedException {
MyThread mt=new MyThread();
Thread thread=new Thread(mt,"子线程A");
thread.start();
System.out.print(Thread.currentThread().getName());
thread.join();
System.out.println("代码结束");
}
public static void printTime() {
Date date=new Date(12);
DateFormat format=new SimpleDateFormat("yyy-MM-dd HH:mm:ss");
String time=format.format(date);
System.out.println(time);
}
}
运行结果:
main
主线程睡眠前的时间
1970-01-01 08:00:00
子线程A
睡眠结束的时间
1970-01-01 08:00:00
代码结束
4. wait()和 notify() 方法
当线程调用wait()方法后会进入等待队列(进入这个状态会释放所占有的所有资源,与阻塞状态不同),进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒(由于notify()只是唤醒一个线程,但我们由不能确定具体唤醒的是哪一个线程,也许我们需要唤醒的线程不能够被唤醒,因此在实际使用时,一般都用notifyAll()方法,唤醒有所线程)。
线程被唤醒后会进入锁池,等待获取锁标记。wait() 使得线程进入阻塞状态,
它也有两种形式:一种允许指定以毫秒为单位的一段时间作为参数;另一种没有参数。
前者当对应的 notify()被调用或者超出指定时间时线程重新进入可执行状态即就绪状态,后者则必须对应的 notify()被调用。当调用wait()后,线程会释放掉它所占有的“锁标志”,从而使线程所在对象中的其它synchronized数据可被别的线程使用。wait()和notify()因为会对对象的“锁标志”进行操作,所以它们必须在synchronized函数或synchronizedblock中进行调用。如果在non-synchronized函数或non-synchronizedblock中进行调用,在运行时会发生IllegalMonitorStateException的异常。
注意区别:初看起来wait() 和 notify() 方法与suspend()和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的suspend()及其它所有方法在线程阻塞时都不会释放占用的锁(如果占用了的话),而wait() 和 notify() 这一对方法则相反。
5. join()方法
join方法的主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。
看下面这个例子:
public class JoinTest {
public static void main(String [] args) throws InterruptedException {
ThreadJoinTest t1 = new ThreadJoinTest("科比");
ThreadJoinTest t2 = new ThreadJoinTest("詹姆斯");
t1.start();
/**join的意思是使得放弃当前线程的执行,并返回对应的线程,例如下面代码的意思就是:
程序在main线程中调用t1线程的join方法,则main线程放弃cpu控制权,并返回t1线程继续执行直到线程t1执行完毕
所以结果是t1线程执行完后,才到主线程执行,相当于在main线程中同步t1线程,t1执行完了,main线程才有执行的机会
*/
t1.join();
t2.start();
}
}
class ThreadJoinTest extends Thread{
public ThreadJoinTest(String name){
super(name);
}
@Override
public void run(){
for(int i=0;i<1000;i++){
System.out.println(this.getName() + ":" + i);
}
}
}
上面程序结果是先打印完科比线程,然后打印詹姆斯线程;
上面注释也大概说明了join方法的作用:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。注意,这里调用的join方法是没有传参的,join方法其实也可以传递一个参数给它的,具体再看下面的例子:
public class JoinTest {
public static void main(String [] args) throws InterruptedException {
ThreadJoinTest t1 = new ThreadJoinTest("白娘子");
ThreadJoinTest t2 = new ThreadJoinTest("许仙");
t1.start();
/**join方法可以传递参数,join(10)表示main线程会等待t1线程10毫秒,10毫秒过去后,
* main线程和t1线程之间执行顺序由串行执行变为普通的并行执行
*/
t1.join(10);
t2.start();
}
}
class ThreadJoinTest extends Thread{
public ThreadJoinTest(String name){
super(name);
}
@Override
public void run(){
for(int i=0;i<1000;i++){
System.out.println(this.getName() + ":" + i);
}
}
}
上面代码结果是:程序执行前面10毫秒内打印的都是白娘子线程,10毫秒后,白娘子和许仙程序交替打印。
所以,join方法中如果传入参数,则表示这样的意思:如果A线程中掉用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。
如果有兴趣的同学通过查看源码会发现:join方法的原理就是调用相应线程的wait方法进行等待操作的,例如A线程中调用了B线程的join方法,则相当于在A线程中调用了B线程的wait方法,当B线程执行完(或者到达等待时间),B线程会自动调用自身的notifyAll方法唤醒A线程,从而达到同步的目的。