什么叫多线程程序?

一个进程至少有一个线程在运行,当一个进程中出现多个线程时,就称这个应用程序是多线程应用程序。
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);
    }
}

线程的五种状态

先看看五种状态的关系图:

java多线程编程等待执行结果 java多线程状态_线程池


(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线程,从而达到同步的目的。