导论
并发和并行
我们知道,在单核机器上,“多进程”并不是真正的多个进程在同时执行,而是通过CPU时间分片,操作系统快速在进程间切换而模拟出来的多进程。我们通常把这种情况成为并发,也就是多个进程的运行行为是“一并发生”的,但不是同时执行的,因为CPU核数的限制(PC和通用寄存器只有一套,严格来说在同一时刻只能存在一个进程的上下文)。
1 基本概念
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。
同时”执行是人的感觉,在线程之间实际上轮换执行。
在Java中线程指两件不同的事情:
1)java.lang.Thread类的一个实例;
2)线程的执行
关于Java中线程的生命周期,下面给出的这幅图总结的非常详细生动,它包含了Java多线程中的所有重要知识点。
Java线程具有5种基本状态:
新建状态(new):当线程对象被创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法时进入就绪状态(t.start();),处于就绪状态只能说明该线程做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行。
运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行。即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1)等待阻塞,处于运行状态的线程,由于执行了wait()方法,主动进入等待阻塞状态。
2)同步阻塞,线程在获取synchronized同步锁失败(此时锁被其它线程占用),它会进入同步阻塞状态。
3)其它阻塞,通过调用线程的sleep()或join()或发出I/O请求时,线程会进入阻塞状态,当sleep()状态超时、join()等待线程终止或者I/O处理完毕时,线程重新转入就绪状态。
Thread类中的方法
sleep方法,这是一个静态方法,作用是让当前线程进入休眠状态(但线程不会释放已获取的锁),这个休眠状态其实就是我们上面提到过的Time Waiting状态,从休眠状态“苏醒”后,线程会进入到Runnable状态。
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
join方法实现是通过wait(小提示:Object 提供的方法)。当main线程调用t.join时候,main线程会获得线程对象t的锁(wait 意味着拿到该对象的锁),调用该对象的wait(等待时间),直到该对象唤醒main线程,比如退出后。这就意味着main 线程调用t.join时,必须能够拿到线程t对象的锁。
消亡状态(dead):线程执行完了,或者因异常退出了run()方法,该线程结束它的生命周期。
yield方法,这是一个静态方法,作用是让当前线程“让步”,目的是为了让优先级不低于当前线程的线程有机会运行,这个方法不会释放锁。
interrupt方法,这是一个实例方法。每个线程都有一个中断状态标识,这个方法的作用就是将相应线程的中断状态标记为true,这样相应的线程调用isInterrupted方法就会返回true。通过使用这个方法,能够终止那些通过调用可中断方法进入阻塞状态的线程。常见的可中断方法有sleep、wait、join,这些方法的内部实现会时不时的检查当前线程的中断状态,若为true会立刻抛出一个InterruptedException异常,从而终止当前线程。
2 Java多线程的创建与启动
Java中线程的创建有以下三种基本形式
1)继承Thread类,重写该类的run()方法。
class MyThread extends Thread {
@Override
public void run(){//重写run()方法,这是线程的执行体
System.out.println(Thread.currentThread().getName());
}
}
public class MyThreadTest{
public static void main(String []args){
Thread m1=new MyThread();//创建线程1,进入新建状态
Thread m2=new MyThread();//创建线程2,进入新建状态
m1.start();//线程1进入就绪状态
m2.start();//线程2进入就绪状态
}
}
2)实现Runnable接口,并重写该接口的run()方法,该方法同样是线程的执行体,先创建MyRunnable对象,并将该对象作为Thread类的target来创建Thread对象,该对象才是真正的线程的对象。
class MyRunnable implements Runnable{
@Override
public void run(){//重写run()方法,这是线程的执行体
System.out.println(Thread.currentThread().getName());
}
}
public class MyThreadTest{
public static void main(String []args){
Runnable myRunnable=new MyRunnable();//创建Runnable对象
Thread t1=new Thread(myRunnable);//将myRunnable作为Thread类的target创建线程1,该对象才是真正的线程对象
Thread t2=new Thread(myRunnable);//创建线程2
t1.start();//线程1进入就绪状态
t2.start();//线程2进入就绪状态
}
}
那么Thread类和Runnable接口二者有什么联系呢?其实,Thread类本身也实现了Runnable接口,run()方法最先是在Runnable接口中定义的。
public interface Runnable {
public abstract void run();
}
Thread类中对run()方法进行了重写(源于Thread源码)。
@Override
public void run() {
if(target!=null){
target.run();
}
}
也就是说当执行到Thread类中的run()方法时,会先判断target(构造函数中的Runnable对象)是否存在,如果存在则执行target中的run方法,也就是实现了Runnable接口并重写了run()方法的类中的run()方法。
上面两种方法它们有一个共同的缺点,那就是异步方法run没有返回值,也就是说我们无法直接获取它的执行结果,只能通过共享变量或者线程间通信等方式来获取。下面这种方法有返回值。
3)使用Callable和Future接口创建线程。具体方法是:首先创建Callable接口的实现类,并在该类中重写call()方法,然后使用FutureTask类来包装Callable实现类的对象,并以此FutureTask类的对象作为Thread类的target来创建线程。
class MyCallable implements Callable<Integer>{
@Override
public Integer call(){//区别于run(),call()方法具有返回值
System.out.println(Thread.currentThread().getName());
return 1;
}
}
public class MyThreadTest{
public static void main(String []args){
Callable<Integer> myCallable=new MyCallable();//创建Callable实现类对象
FutureTask<Integer> ft=new FutureTask<Integer>(myCallable);//使用FutureTask类来包装Callable实现类对象
Thread thread=new Thread(ft);//把ft作为Tread类的target来创建线程
thread.start();//线程进入就绪状态
try{
int sum=ft.get();//获取新创建线程的call()返回的结果
System.out.println(sum);
}catch (InterruptedException e) {//当一个线程处于等待,睡眠,或者占用,也就是说阻塞状态,而这时线程被中断就会抛出这类错误
thread.interrupt();//手动把自己的线程中断,这样可以保证线程一定能够被及时中断
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
3 Java多线程的就绪、运行和消亡状态
就绪->运行:当此线程得到cpu资源;
运行->就绪:此线程主动调用yield()方法或在运行过程中失去cpu资源
运行->消亡:此线程的执行体执行完毕或者发生异常。
注意:当线程调用yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。
由于实际的业务需要,常常会遇到需要在特定时机终止某一线程的运行,使其进入到死亡状态。目前最通用的做法是设置一boolean型的变量,当条件满足时,使线程执行体快速执行完毕。
class MyRunnable implements Runnable{
private Boolean stop;
@Override
public void run() {
if(!stop){
System.out.println(Thread.currentThread().getName());
}
}
public void stopThread() {
this.stop = true;
}
}
public class ThreadTest {
public static void main(String[] args) {
MyRunnable m=new MyRunnable();
Thread t=new Thread(m);
t.start();//进入就绪状态
m.stopThread();//使线程消亡
}
}