关于Java中线程的生命周期,首先看一下下面这张较为经典的图:java
新建状态(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 处理完毕时,线程从新转入就绪状态。多线程
死亡状态(Dead):线程执行完了或者因异常退出了 run() 方法,该线程结束生命周期。架构
Thread类中经常使用的方法:并发
start():启动线程,该方法调用后,线程不会当即执行,当JVM调用run方法时才会真正执行。举例:start是开车前的打火操做,此时汽车不会走的。run方法至关于挂挡抬离合。
setName(String name):设置线程的名字。
getName():获取线程的名字。
currentThread():获取当前线程的对象,在使用实现 Runnable 接口去建立线程的时候,就能够使用该方法获取线程的对象了。
setPriority(int newPriority):设置线程的优先级,1~10之间的整数,数字越大优先级越高。具备较高线程优先级的线程对象仅表示此线程具备较多的执行机会,而非优先执行。每一个线程默认的优先级都与建立它的线程的优先级相同。main线程默认具备普通优先级。
sleep(long millis):让当前的正在执行的线程暂停指定的时间,并进入阻塞状态。待睡眠时间事后会自动苏醒,从而继续执行任务。在其睡眠的时间段内,该线程因为不是处于就绪状态,所以不会获得执行的机会。即便此时系统中没有任何其余可执行的线程,处于 sleep() 中的线程也不会执行。所以 sleep() 方法经常使用来暂停线程执行。前面有讲到,当调用了新建的线程的 start() 方法后,线程进入到就绪状态,可能会在接下来的某个时间获取CPU时间片得以执行,若是但愿这个新线程必然性的当即执行,直接调用原来线程的 sleep(1) 便可。
interrupt():
若是线程处于正常活动状态,那么会将该线程的中断标志设置为 true。被设置中断标志的线程可能不会当即终端,而是继续正常运行,不受影响。小时后家人叫你回家吃饭,你能够选择在外面继续玩耍一会以后再回去吃饭。
唤醒正在睡眠的线程,调用interrupt方法会抛出一个 InterruptedException 的异常。
Thread 类中的 stop 方法已经不建议使用了,该方法过于暴力。而上面中的 interrupt 方法并不能中止线程,那么该如何正确的中止线程呢?Thread 类中有一个 isInterrupted() 方法,它会返回一个 boolean 类型的值,当调用 interrupt() 方法以后,isInterrupted() 方法会返回true。咱们能够在多线程的代码中添加判断,当 isInterrupted() 方法会返回 true 时,手动的抛出一个异常,经过这种方式去中止线程。
yield():当前线程在执行该方法以后会进行礼让。即原本CPU会执行A线程,可是在A线程中调用了 yield() 方法,此时CPU会放弃A线程的执行权,可是放弃的时间不肯定,有可能刚放弃,A线程立刻又得到了CPU的执行权。yield() 方法还与线程优先级有关,当某个线程调用 yiled() 方法从运行状态转换到就绪状态后,CPU从就绪状态线程队列中只会选择与该线程优先级相同或优先级更高的线程去执行。举例:坐公交车或地铁时,看到老人上车后,你会起身让座,从你起身到老人坐下,这段时间是不肯定的,而且也有可能你刚起身让座,老人表示一站就到目的地不想作了,此时你会继续坐回座位上。
join():线程加入,能够理解为两个线程的合并,有两个线程A和B,A线程须要等B线程执行完成以后再执行,此时就能够使用join方法。当B线程调用join方法后,A线程内部至关于调用了 wait() 方法进入到等待状态,直到B线程执行结束后,A线程才会被唤醒。这时A和B两个线程能够当作合并为一个线程而进行同步执行。
setDaemon(true):设置后台线程(守护线程)。后台线程主要是为其余线程(相对能够称之为前台线程)提供服务,或“守护线程”。如JVM中的垃圾回收线程。生命周期:后台线程的生命周期与前台线程生命周期有必定关联。主要体如今:当全部的前台线程都进入死亡状态时,后台线程会自动死亡。设置后台线程:调用 Thread 对象的 setDaemon(true) 方法能够将指定的线程设置为后台线程。
判断线程是不是后台线程:调用thread对象的 isDeamon() 方法。
注:main 线程默认是前台线程,前台线程中建立的子线程默认是前台线程,后台线程中建立的线程默认是后台线程。调用 setDeamon(true) 方法将前台线程设置为后台线程时,须要在 start() 方法调用以前。前台线程都死亡后,JVM通知后台线程死亡,但从接收指令到做出响应,须要必定的时间。
Java中线程的建立常见有如三种基本形式
1.继承 Thread 类,重写该类的 run() 方法。
class MyThread extends Thread { private int i = 0; @Override public void run() { for (i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
public class ThreadTest { public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Thread myThread1 = new MyThread(); // 建立一个新的线程myThread1 此线程进入新建状态 Thread myThread2 = new MyThread(); // 建立一个新的线程myThread2 此线程进入新建状态 myThread1.start(); // 调用start()方法使得线程进入就绪状态 myThread2.start(); // 调用start()方法使得线程进入就绪状态 } } } }
如上所示,继承 Thread 类,经过重写 run() 方法定义了一个新的线程类 MyThread ,其中 run() 方法的方法体表明了线程须要完成的任务,称之为线程执行体。当建立此线程类对象时,一个新的线程得以建立,并进入到线程新建状态。经过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不必定会立刻得以执行,这取决于CPU调度时机。
2.实现 Runnable 接口,并重写该接口的 run() 方法,该 run() 方法一样是线程执行体,建立 Runnable 实现类的实例,并以此实例做为 Thread 类的 target 来建立 Thread 对象,该 Thread 对象才是真正的线程对象。
class MyRunnable implements Runnable { private int i = 0; @Override public void run() { for (i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
public class ThreadTest { public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Runnable myRunnable = new MyRunnable(); // 建立一个Runnable实现类的对象 Thread thread1 = new Thread(myRunnable); // 将myRunnable做为Thread target建立新的线程 Thread thread2 = new Thread(myRunnable); thread1.start(); // 调用start()方法使得线程进入就绪状态 thread2.start(); } } } }
那么 Thread 和 Runnable 之间究竟是什么关系呢?咱们首先来看下面这个例子。
public class ThreadTest { public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Runnable myRunnable = new MyRunnable(); Thread thread = new MyThread(myRunnable); thread.start(); } } } } class MyRunnable implements Runnable { private int i = 0; @Override public void run() { System.out.println("in MyRunnable run"); for (i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } } class MyThread extends Thread { private int i = 0; public MyThread(Runnable runnable){ super(runnable); } @Override public void run() { System.out.println("in MyThread run"); for (i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
一样的,与实现 Runnable 接口建立线程方式类似,不一样的地方在于
Thread thread = new MyThread(myRunnable);
那么这种方式能够顺利建立出一个新的线程么?答案是确定的。至于此时的线程执行体究竟是 MyRunnable 接口中的 run() 方法仍是 MyThread 类中的 run() 方法呢?经过输出咱们知道线程执行体是 MyThread 类中的 run() 方法。其实缘由很简单,由于 Thread 类自己也是实现了 Runnable 接口,而 run() 方法最早是在 Runnable 接口中定义的方法。
public interface Runnable { public abstract void run(); }
咱们看一下 Thread 类中对 Runnable 接口中 run() 方法的实现:
@Override public void run() { if (target != null) { target.run(); } }
也即,当执行到 Thread 类中的 run() 方法时,会首先判断 target 是否存在,存在则执行 target 中的 run() 方法,也就是实现了 Runnable 接口并重写了 run() 方法的类中的 run() 方法。可是上述给到的列子中,因为多态的存在,根本就没有执行到 Thread 类中的 run() 方法,而是直接先执行了运行时类型即 MyThread 类中的 run() 方法。
3.使用 Callable 和 Future 接口建立线程。具体是建立 Callable 接口的实现类,并实现 call() 方法。并使用 FutureTask 类来包装 Callable 实现类的对象,且以此 FutureTask 对象做为 Thread 对象的 target 来建立线程。
public class ThreadTest { public static void main(String[] args) { Callable<Integer> myCallable = new MyCallable(); // 建立MyCallable对象 FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象 for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Thread thread = new Thread(ft); //FutureTask对象做为Thread对象的target建立新的线程 thread.start(); //线程进入到就绪状态 } } System.out.println("主线程for循环执行完毕.."); try { int sum = ft.get(); //取得新建立的新线程中的call()方法返回的结果 System.out.println("sum = " + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } class MyCallable implements Callable<Integer> { private int i = 0; // 与run()方法不一样的是,call()方法具备返回值 @Override public Integer call() { int sum = 0; for (; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); sum += i; } return sum; } }
首先,咱们发现,在实现 Callable 接口中,此时再也不是 run() 方法了,而是 call() 方法,此 call() 方法做为线程执行体,同时还具备返回值!在建立新的线程时,是经过 FutureTask 来包装 MyCallable 对象,同时做为了 Thread 对象的 target 。那么看下 FutureTask 类的定义:
public class FutureTask<V> implements RunnableFuture<V> { //.... }
public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }
咱们发现 FutureTask 类其实是同时实现了 Runnable 和 Future 接口,由此才使得其具备 Future 和 Runnable 双重特性。经过 Runnable 特性,能够做为 Thread 对象的 target ,而 Future 特性,使得其能够取得新建立线程中的call()方法的返回值。
执行此程序,咱们发现 sum = 4950 永远都是最后输出的。而“主线程for循环执行完毕..”则极可能是在子线程循环中间输出。由CPU的线程调度机制,咱们知道,“主线程for循环执行完毕..”的输出时机是没有任何问题的,那么为何 sum =4950 会永远最后输出呢?
缘由在于经过 ft.get() 方法获取子线程 call() 方法的返回值时,当子线程此方法还未执行完毕,ft.get() 方法会一直阻塞,直到 call() 方法执行完毕才能取到返回值。
上述主要讲解了三种常见的线程建立方式,对于线程的启动而言,都是调用线程对象的start()方法,须要特别注意的是:不能对同一线程对象两次调用start()方法。
就绪状态转换为运行状态:当此线程获得处理器资源;
运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程当中失去处理器资源。
运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。
此处须要特别注意的是:当调用线程的 yield() 方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪一个线程具备必定的随机性,所以,可能会出现A线程调用了 yield() 方法后,接下来CPU仍然调度了A线程的状况。
因为实际的业务须要,经常会遇到须要在特定时机终止某一线程的运行,使其进入到死亡状态。目前最通用的作法是设置 boolean 型的变量,当条件知足时,使线程执行体快速执行完毕。如:
public class ThreadTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { thread.start(); } if(i == 40){ myRunnable.stopThread(); } } } } class MyRunnable implements Runnable { private boolean stop; @Override public void run() { for (int i = 0; i < 100 && !stop; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } public void stopThread() { this.stop = true; } }
synchronized
一、synchronized 关键字的做用域有二种:
① 是某个对象实例内,synchronized aMethod(){} 能够防止多个线程同时访问这个对象的 synchronized 方法(若是一个对象有多个 synchronized 方法,只要一个线程访问了其中的一个synchronized 方法,其它线程不能同时访问这个对象中任何一个 synchronized 方法)。这时,不一样的对象实例的 synchronized 方法是不相干扰的。也就是说,其它线程照样能够同时访问相同类的另外一个对象实例中的 synchronized 方法;
② 是某个类的范围,synchronized static aStaticMethod{} 防止多个线程同时访问这个类中的 synchronized static 方法。它能够对类的全部对象实例起做用。
二、除了方法前用 synchronized 关键字,synchronized 关键字还能够用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的做用域是当前对象;
三、synchronized 关键字是不能继承的,也就是说,基类的方法 synchronized f(){} 在继承类中并不自动是 synchronized f(){},而是变成了 f(){}。继承类须要你显式的指定它的某个方法为 synchronized 方法;
总的说来,synchronized 关键字能够做为函数的修饰符,也可做为函数内的语句,也就是平时说的同步方法和同步语句块。若是再细的分类,synchronized 可做用于 instance 变量、object reference(对象引用)、static 函数和 class literals (类名称字面常量)身上。
在进一步阐述以前,咱们须要明确几点:
A.不管 synchronized 关键字加在方法上仍是对象上,它取得的锁都是对象,而不是把一段代码或函数看成锁――并且同步方法极可能还会被其余线程的对象访问。
B.每一个对象只有一个锁(lock)与之相关联。
C.实现同步是要很大的系统开销做为代价的,甚至可能形成死锁,因此尽可能避免无谓的同步控制。
讨论synchronized用到不一样地方对代码产生的影响:
假设P一、P2是同一个类的不一样对象,这个类中定义了如下几种状况的同步块或同步方法,P一、P2就均可以调用它们。
一、把synchronized看成函数修饰符时,示例代码以下:
Public synchronized void methodAAA(){ //…. }
这也就是同步方法,那这时 synchronized 锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不一样的线程中执行这个同步方法时,它们之间会造成互斥,达到同步的效果。可是这个对象所属的 Class 所产生的另外一对象P2却能够任意调用这个被加了 synchronized 关键字的方法。
上边的示例代码等同于以下代码:
public void methodAAA(){ synchronized (this){ //(1) //….. } }
(1)处的 this 指的就是调用这个方法的对象,如P1。可见同步方法实质是将 synchronized 做用于 object reference。――那个拿到了P1对象锁的线程,才能够调用P1的同步方法,而对P2而言,P1这个锁与它绝不相干,程序也可能在这种情形下摆脱同步机制的控制,形成数据混乱:(
2.同步块,示例代码以下:
public void method3(SomeObject so){ synchronized(so){ //….. }
这时,锁就是 so 这个对象,谁拿到这个锁谁就能够运行它所控制的那段代码。当有一个明确的对象做为锁时,就能够这样写程序,但当没有明确的对象做为锁,只是想让一段代码同步时,能够建立一个特殊的 instance 变量(它得是一个对象)来充当锁:
class Foo implements Runnable{ private byte[] lock = new byte[0]; // 特殊的instance变量 Public void methodA(){ synchronized(lock) { //… } } //….. }
注:零长度的byte数组对象建立起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操做码,而Object lock = new Object()则须要7行操做码。
3.将synchronized做用于static 函数,示例代码以下:
Class Foo{ public synchronized static void methodAAA(){ // 同步的static 函数 //…. } public void methodBBB(){ synchronized(Foo.class) // class literal(类名称字面常量) } }
代码中的 methodBBB() 方法是把 class literal 做为锁的状况,它和同步的 static 函数产生的效果是同样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而再也不是由这个Class产生的某个具体对象了)。
记得在《Effective Java》一书中看到过将 Foo.class 和 P1.getClass() 用于做同步锁还不同,不能用 P1.getClass() 来达到锁这个Class的目的。P1指的是由Foo类产生的对象。
能够推断:若是一个类中定义了一个 synchronized 的 static 函数A,也定义了一个 synchronized 的 instance 函数B,那么这个类的同一对象 Obj 在多线程中分别访问A和B两个方法时,不会构成同步,由于它们的锁都不同。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。
volatile关键字:
理解volatile关键字的做用的前提是要理解Java内存模型,volatile关键字的做用主要有两个:
① 多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,必定是最新的数据;
② 代码底层执行不像咱们看到的高级语言—-Java程序这么简单,它的执行是Java代码–>字节码–>根据字节码执行对应的C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用volatile则会对禁止语义重排序,固然这也必定程度上下降了代码执行效率
从实践角度而言,volatile的一个重要做用就是和CAS结合,保证了原子性,详细的能够参见java.util.concurrent.atomic包下的类,好比AtomicInteger。
ThreadLocal 是Java里一种特殊的变量。每一个线程都有一个 ThreadLocal 就是每一个线程都拥有了本身独立的一个变量,竞争条件被完全消除了。若是为每一个线程提供一个本身独有的变量拷贝,将大大提升效率。首先,经过复用减小了代价高昂的对象的建立个数。其次,你在没有使用高代价的同步或者不变性的状况下得到了线程安全。
简单说 ThreadLocal 就是一种以空间换时间的作法,在每一个 Thread 里面维护了一个以开地址法实现的 ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,天然就没有线程安全方面的问题了。
一、避免线程的建立和销毁带来的性能开销。
二、避免大量的线程间因互相抢占系统资源致使的阻塞现象。
三、可以对线程进行简单的管理并提供定时执行、间隔执行等功能。
四、使用线程池还能够根据项目灵活地控制并发的数目。
线程池的类型
① newFixedThreadPool:建立一个指定工做线程数量的线程池。每当提交一个任务就建立一个工做线程,若是工做线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
特色:是一个典型且优秀的线程池,它具备线程池提升程序效率和节省建立线程时所耗的开销的优势。可是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工做线程,还会占用必定的系统资源。
② newCachedThreadPool:建立一个可缓存的线程池。这种类型的线程池特色是:
1).工做线程的建立数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
2).若是长时间没有往线程池中提交任务,即若是工做线程空闲了指定的时间(默认为1分钟),则该工做线程将自动终止。终止后,若是你又提交了新的任务,则线程池从新建立一个工做线程。
特色:在线程池空闲时,即线程池中没有可运行任务时,它会释放工做线程,从而释放工做线程所占用的资源。可是,但当出现新任务时,又要建立一新的工做线程,又要必定的系统开销。而且在使用CachedThreadPool时,必定要注意控制任务的数量,不然,因为大量线程同时运行,颇有会形成系统瘫痪。
③ newSingleThreadExecutor:建立一个单线程化的Executor,即只建立惟一的工做者线程来执行任务,若是这个线程异常结束,会有另外一个取代它,保证顺序执行。
单工做线程最大的特色是可保证顺序地执行各个任务,而且在任意给定的时间不会有多个线程是活动的 。 该线程池存在的意义是:在线程死后(或发生异常时)从新启动一个线程来替代原来的线程继续执行下去。
④ newScheduleThreadPool:建立一个定长的线程池,并且支持定时的以及周期性的任务执行,相似于Timer。
线程池的关闭
ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是 shutdown() 和 shutdownNow()。
shutdown():不会当即的终止线程池,而是要等全部任务缓存队列中的任务都执行完后才终止,但不再会接受新的任务。
shutdownNow():当即终止线程池,并尝试打断正在执行的任务,而且清空任务缓存队列,返回还没有执行的任务。
线程池的注意事项:
在使用线程池时需注意线程池大小与性能的关系,注意并发风险、死锁、资源不足和线程泄漏等问题。
(1) 线程池大小:多线程应用并不是线程越多越好,须要根据系统运行的软硬件环境以及应用自己的特色决定线程池的大小。通常来讲,若是代码结构合理的话,线程数目与CPU 数量相适合便可。若是线程运行时可能出现阻塞现象,可相应增长池的大小;若有必要可采用自适应算法来动态调整线程池的大小,以提升CPU 的有效利用率和系统的总体性能。
(2) 并发错误:多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意避免死锁现象的发生。
(3) 线程泄漏:这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。
一、start() 方法和 run() 方法的区别?
只有调用了 start() 方法,才会表现出多线程的特性,不一样线程的 run() 方法里面的代码交替执行。若是只是调用 run() 方法,那么代码仍是同步执行的,必须等待一个线程的 run() 方法里面的代码所有执行完毕以后,另一个线程才能够执行其 run() 方法里面的代码。
二、Runnable 接口和 Callable 接口的区别?
Callable 是在 JDK1.5 增长的,接口中的 call() 方法是有返回值的,是一个泛型,和 Future、FutureTask 配合能够用来获取异步执行的结果。也即 Callable 能够返回装载有计算结果的 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。经过 Future 对象能够了解任务执行状况,可取消任务的执行,还可获取执行结果。
Runnable() 的 run() 方法不能够抛出异常,Callable 的 call() 方法也能够抛出异常。
这实际上是颇有用的一个特性,由于多线程相比单线程更难、更复杂的一个重要缘由就是由于多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候咱们指望的数据是否已经赋值完毕?没法得知,咱们能作的只是等待这条多线程的任务执行完毕而已。而 Callable + Future/FutureTask 却能够获取多线程运行的结果,能够在等待时间太长没获取到须要的数据的状况下取消该线程的任务,真的是很是有用。
三、CyclicBarrier 和 CountDownLatch 的区别?
两个看上去有点像的类,都在java.util.concurrent(JUC)下,均可以用来表示代码运行到某个点上,两者的区别在于:
① CyclicBarrier 的某个线程运行到某个点上以后,该线程即中止运行,直到全部的线程都到达了这个点,全部线程才从新运行;CountDownLatch 则不是,某线程运行到某个点上以后,只是给某个数值-1而已,该线程继续运行。
② CyclicBarrier 只能唤起一个任务,CountDownLatch 能够唤起多个任务。
③ CyclicBarrier 可重用,CountDownLatch 不可重用,计数值为0该 CountDownLatch 就不可再用了。
四、submit() 和 execute() 的区别?
线程池中的execute方法即开启线程执行池中的任务。还有一个方法submit也能够作到,它的功能是提交指定的任务去执行而且返回Future对象,即执行的结果。
二者的三个区别:
① 接收的参数不同;
② submit有返回值,而execute没有;
用到返回值的例子,好比说我有不少个作validation的task,我但愿全部的task执行完,而后每一个task告诉我它的执行结果,是成功仍是失败,若是是失败,缘由是什么。而后我就能够把全部失败的缘由综合起来发给调用者。
③ submit方便Exception处理意思就是若是你在你的task里会抛出checked或者unchecked exception,而你又但愿外面的调用者可以感知这些exception并作出及时的处理,那么就须要用到submit,经过捕获Future.get抛出的异常。
JDK5日后,任务分两类:一类是实现了 Runnable 接口的类,一类是实现了 Callable 接口的类。二者均可以被 ExecutorService 执行,它们的区别是:
execute(Runnable r) 没有返回值。能够执行任务,但没法判断任务是否成功完成。——实现 Runnable 接口;
submit(Runnable r) 返回一个 future。能够用这个 future 来判断任务是否成功完成。——实现 Callable 接口。
五、sleep() 方法和 wait() 方法有什么区别?
sleep() 方法和 wait()方法均可以用来放弃CPU必定的时间,不一样点在于若是线程持有某个对象的监视器,sleep() 方法不会放弃这个对象的监视器,wait() 方法会放弃这个对象的监视器。
六、wait() 方法和notify() / notifyAll() 方法在放弃对象监视器时有什么区别?
wait() 方法和 notify() / notifyAll() 方法在放弃对象监视器时的区别在于:wait() 方法当即释放对象监视器,notify() / notifyAll() 方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
七、synchronized 和 ReentrantLock 的区别?
synchronized 是和 if、else、for、while 同样的关键字,ReentrantLock 是类,这是两者的本质区别。
既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,能够被继承、能够有方法、能够有各类各样的类变量,ReentrantLock 比 synchronized 的扩展性体如今几点上:
① ReentrantLock 能够对获取锁的等待时间进行设置,这样就避免了死锁;
② ReentrantLock 能够获取各类锁的信息;
③ ReentrantLock 能够灵活地实现多路通知;
另外,两者的锁机制其实也是不同的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操做的应该是对象头中 mark word(这点不肯定)。
八、锁和同步的对比?
锁的使用场景有不少,如共享实例变量、共享链接资源时以及包括并发包中BlockingQueue、ConcurrentHashMap等并发集合中都大量使用了锁。基体上使用同步的地方均可以改为锁来用,可是使用锁的地方不必定能改为同步来用。
① 同步synchronized算是一个关键词,是来来修饰方法的,可是锁lock是一个实例变量,经过调用lock()方法来取得锁
② 只能同步方法,而不能同步变量和类,锁也是同样
③ 同步没法保证线程取得方法执行的前后顺序。锁能够设置公平锁来确保。
④ 没必要同步类中全部的方法,类能够同时拥有同步和非同步方法。
⑤ 若是线程拥有同步和非同步方法,则非同步方法能够被多个线程自由访问而不受锁的限制。锁也是同样。
⑥ 线程睡眠时,它所持的任何锁都不会释放。
⑦ 线程能够得到多个锁。好比,在一个对象的同步方法里面调用另一个对象的同步方法,则获取了两个对象的同步锁。
⑧ 同步损害并发性,应该尽量缩小同步范围。同步不但能够同步整个方法,还能够同步方法中一部分代码块。
⑨ 在使用同步代码块时候,应该指定在哪一个对象上同步,也就是说要获取哪一个对象的锁。例如:
最后,还须要说的一点是。若是使用锁,那么必定的注意编写代码,但不很容易出现死锁!避免方法后文后讲。
八、FutureTask与Future的关系?
FutureTask 表示一个异步运算的任务,FutureTask 类是 Future 的一个实现,并实现了 Runnable,因此可经过 Excutor (线程池) 来执行,也可传递给 Thread 对象执行。FutureTask里面能够传入一个Callable的具体实现类,能够对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操做。固然,因为FutureTask也是Runnable接口的实现类,因此FutureTask也能够放入线程池中。
若是在主线程中须要执行比较耗时的操做时,但又不想阻塞主线程时,能够把这些做业交给 Future 对象在后台完成,当主线程未来须要时,就能够经过 Future 对象得到后台做业的计算结果或者执行状态。 Executor 框架利用 FutureTask 来完成异步任务,并能够用来进行任何潜在的耗时的计算。通常 FutureTask 多用于耗时的计算,主线程能够在完成本身的任务后,再去获取结果。FutureTask 类既能够使用 new Thread(Runnable r) 放到一个新线程中跑,也能够使用 ExecutorService.submit(Runnable r) 放到线程池中跑,并且两种方式均可以获取返回结果,但实质是同样的,即若是要有返回结果那么构造函数必定要注入一个 Callable 对象。
九、什么是线程安全?
一句话解释:若是你的代码在多线程下执行和在单线程下执行永远都能得到同样的结果,那么你的代码就是线程安全的。
这个问题有值得一提的地方,就是线程安全也是有几个级别的:
① 不可变
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新建立一个,所以这些不可变对象不须要任何同步手段就能够直接在多线程环境下使用
② 绝对线程安全
无论运行时环境如何,调用者都不须要额外的同步措施。要作到这一点一般须要付出许多额外的代价,Java中标注本身是线程安全的类,实际上绝大多数都不是线程安全的。不过绝对线程安全的类,Java中也有,比方说 CopyOnWriteArrayList、CopyOnWriteArraySet。
③ 相对线程安全
相对线程安全也就是咱们一般意义上所说的线程安全,像 Vector 这种,add、remove 方法都是原子操做,不会被打断,但也仅限于此,若是有个线程在遍历某个 Vector、有个线程同时在 add 这个 Vector,99%的状况下都会出现 ConcurrentModificationException,也就是fail-fast机制。
④ 线程非安全
ArrayList、LinkedList、HashMap等都是线程非安全的类。
十、Java中如何获取到线程 dump 文件?
死循环、死锁、阻塞、页面打开慢等问题,打线程 dump 是最好的解决问题的途径。所谓线程 dump 也就是线程堆栈,获取到线程堆栈有两步:
① 获取到线程的 pid,能够经过使用 jps 命令,在Linux环境下还能够使用 ps -ef | grep java;
② 打印线程堆栈,能够经过使用 jstack pid 命令,在Linux环境下还能够使用 kill -3 pid;
另外,Thread 类提供了一个 getStackTrace() 方法也能够用于获取线程堆栈。这是一个实例方法,所以此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈。
十一、一个线程若是出现了运行时异常会怎么样?
若是这个异常没有被捕获的话,这个线程就中止执行了。另外重要的一点是:若是这个线程持有某个对象的监视器,那么这个对象监视器会被当即释放。
十二、如何在两个线程之间共享数据?
经过在线程之间共享对象就能够了,而后经过 wait/notify/notifyAll、await/signal/signalAll 进行唤起和等待,比方说阻塞队列 BlockingQueue 就是为线程之间共享数据而设计的。
1三、生产者消费者模型的做用是什么?
这个问题很重要:
① 经过平衡生产者的生产能力和消费者的消费能力来提高整个系统的运行效率,这是生产者消费者模型最重要的做用;
② 解耦,这是生产者消费者模型附带的做用,解耦意味着生产者和消费者之间的联系少,联系越少越能够独自发展而不须要收到相互的制约。
1四、为何 wait() 方法和 notify()/notifyAll() 方法要在同步块中被调用?
这是JDK强制的,wait() 方法和 notify()/notifyAll() 方法在调用前都必须先得到对象的锁。
1五、wait() 方法和 notify()/notifyAll() 方法在放弃对象监视器时有什么区别?
区别在于:wait()方法当即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
1六、怎么检测一个线程是否持有对象监视器?
Thread 类提供了一个 holdsLock(Object obj) 方法,当且仅当对象 obj 的监视器被某条线程持有的时候才会返回 true,注意这是一个 static 方法,这意味着“某条线程”指的是当前线程。
1七、ConcurrentHashMap 的并发度是什么?
ConcurrentHashMap 的并发度就是 segment 的大小,默认为16,这意味着最多同时能够有16条线程操做 ConcurrentHashMap,这也是 ConcurrentHashMap 对 Hashtable 的最大优点,任何状况下,Hashtable 都不能同时有两条线程获取Hashtable中的数据。
1八、ReadWriteLock是什么?
首先明确一下,不是说ReentrantLock很差,只是ReentrantLock某些时候有局限。若是使用ReentrantLock,可能自己是为了防止线程A在写数据、线程B在读数据形成的数据不一致,但这样,若是线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,可是仍是加锁了,下降了程序的性能。
由于这个,才诞生了读写锁 ReadWriteLock。ReadWriteLock 是一个读写锁接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提高了读写的性能。
1九、Linux环境下如何查找哪一个线程使用CPU最长?
这是一个比较偏实践的问题,这种问题我以为挺有意义的。能够这么作:
① 获取项目的 pid,jps 或者 ps -ef | grep java,这个前面有讲过;
② top -H -p pid,顺序不能改变。
这样就能够打印出当前的项目,每条线程占用CPU时间的百分比。注意这里打出的是 LWP,也就是操做系统原生线程的线程号。
使用”top -H -p pid”+”jps pid”能够很容易地找到某条占用CPU高的线程的线程堆栈,从而定位占用CPU高的缘由,通常是由于不当的代码操做致使了死循环。注意,”top -H -p pid”打出来的LWP是十进制的,”jps pid”打出来的本地线程号是十六进制的,转换一下,就能定位到占用CPU高的线程的当前线程堆栈了。
20、Java编程写一个会致使死锁的程序?
死锁:线程A和线程B相互等待对方持有的锁致使程序无限死循环下去。几个步骤:
① 两个线程里面分别持有两个Object对象:lock1 和 lock2。这两个 lock 做为同步代码块的锁;
② 线程1的 run() 方法中同步代码块先获取 lock1 的对象锁,Thread.sleep(xxx),时间50毫秒差很少,而后接着获取 lock2 的对象锁。这么作主要是为了防止线程1启动一会儿就连续得到了 lock1 和 lock2 两个对象的对象锁
③ 线程2的 run() 方法中同步代码块先获取 lock2 的对象锁,接着获取 lock1 的对象锁,固然这时 lock1 的对象锁已经被线程1锁持有,线程2确定是要等待线程1释放 lock1 的对象锁。这样,线程1″睡觉”结束时,线程2已经获取了 lock2 的对象锁了,线程1再尝试获取 lock2 的对象锁,便被阻塞,此时一个死锁就造成了。
产生死锁的简单代码:
public class DeadLock{ private final Object left = new Object(); private final Object right = new Object(); public void leftRight() throws Exception{ synchronized (left){ Thread.sleep(2000); synchronized (right){ System.out.println("leftRight end!"); } } } public void rightLeft() throws Exception{ synchronized (right){ Thread.sleep(2000); synchronized (left){ System.out.println("rightLeft end!"); } } } }
写两个线程分别调用它们:
public class Thread0 extends Thread{ private DeadLock dl; public Thread0(DeadLock dl){ this.dl = dl; } public void run(){ try{ dl.leftRight(); } catch (Exception e){ e.printStackTrace(); } } } public class Thread1 extends Thread{ private DeadLock dl; public Thread1(DeadLock dl){ this.dl = dl; } public void run(){ try{ dl.rightLeft(); } catch (Exception e){ e.printStackTrace(); } } }
写个main函数调用一下:
public static void main(String[] args){ DeadLock dl = new DeadLock(); Thread0 t0 = new Thread0(dl); Thread1 t1 = new Thread1(dl); t0.start(); t1.start(); while(true); }
结果,什么语句都不会打印,由于死锁了。
如何定位死锁问题:
① jps得到当前Java虚拟机进程的pid:
② jstack打印堆栈。jstack打印内容的最后其实已经报告发现了一个死锁,咱们分析死锁产生的缘由,看前面的部分:
避免死锁的方式
① 让程序每次至多只能得到一个锁。固然,在多线程环境下,这种状况一般并不现实;
② 设计时考虑清楚锁的顺序,尽可能减小嵌在的加锁交互数量;
③ 既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就行了。固然synchronized不具有这个功能,可是咱们能够使用Lock类中的tryLock方法去尝试获取锁,这个方法能够指定一个超时时限,在等待超过该时限以后变回返回一个失败信息。
2一、怎么唤醒一个阻塞的线程?
若是线程是由于调用了 wait()、sleep()或者join() 方法而致使的阻塞,能够中断线程,而且经过抛出 InterruptedException 来唤醒它;若是线程遇到了IO阻塞,无能为力,由于IO是操做系统实现的,Java代码并无办法直接接触到操做系统。
2二、不可变对象对多线程有什么帮助?
前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不须要进行额外的同步手段,提高了代码执行效率。
2三、什么是多线程的上下文切换?
是指CPU控制权由一个已经正在运行的线程切换到另一个就绪并等待获取CPU执行权的线程的过程。
2四、若是你提交任务时,线程池队列已满,这时会发生什么?
若是你使用的 LinkedBlockingQueue,也就是无界队列的话,不要紧,继续添加任务到阻塞队列中等待执行,由于 LinkedBlockingQueue 能够近乎认为是一个无穷大的队列,能够无限存听任务;若是你使用的是有界队列比方说 ArrayBlockingQueue 的话,任务首先会被添加到 ArrayBlockingQueue 中,若是 ArrayBlockingQueue 满了,则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy。
2五、Java中用到的线程调度算法是什么?
抢占式。一个线程用完CPU以后,操做系统会根据线程优先级、线程饥饿状况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
2六、Thread.sleep(0)的做用是什么?
因为Java采用抢占式的线程调度算法,所以可能会出现某条线程经常获取到CPU控制权的状况,为了让某些优先级比较低的线程也能获取到CPU控制权,能够使用Thread.sleep(0)手动触发一次操做系统分配时间片的操做,这也是平衡CPU控制权的一种操做。
2七、什么是自旋?
不少 synchronized 里面的代码只是一些很简单的代码,执行时间很是快,此时等待的线程都加锁多是一种不太值得的操做,由于线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得很是快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界作忙循环,这就是自旋。若是作了屡次忙循环发现尚未得到锁,再阻塞,这样多是一种更好的策略。
2八、什么是Java内存模型?
Java内存模型定义了一种多线程访问Java内存的规范。简单总结一下Java内存模型的几部份内容:
① Java内存模型将内存分为了主内存和工做内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在本身的工做内存中有一份拷贝,运行本身线程代码的时候,用到这些变量,操做的都是本身工做内存中的那一份。在线程代码执行完毕以后,会将最新的值更新到主内存中去;
② 定义了几个原子操做,用于操做主内存和工做内存中的变量;
③ 定义了 volatile 变量的使用规则;
④ happens-before,即先行发生原则,定义了操做A必然先行发生于操做B的一些规则,好比在同一个线程内控制流前面的代码必定先行发生于控制流后面的代码、一个释放锁unlock的动做必定先行发生于后面对于同一个锁进行锁定 lock 的动做等等,只要符合这些规则,则不须要额外作同步措施,若是某段代码不符合全部的 happens-before 规则,则这段代码必定是线程非安全的。
2九、什么是CAS?
CAS,全称为Compare and Swap,即比较-替换。假设有三个操做数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改成B并返回true,不然什么都不作并返回false。固然CAS必定要 volatile 变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,不然旧的预期值A对某条线程来讲,永远是一个不会变的值A,只要某次CAS操做失败,永远都不可能成功。
30、什么是乐观锁和悲观锁?
① 乐观锁:对于并发间操做产生的线程安全问题持乐观状态,乐观锁认为竞争不老是会发生,所以它不须要持有锁,将比较-替换这两个动做做为一个原子操做尝试去修改内存中的变量,若是失败则表示发生冲突,那么就应该有相应的重试逻辑。
② 悲观锁:对于并发间操做产生的线程安全问题持悲观状态,悲观锁认为竞争老是会发生,所以每次对某资源进行操做时,都会持有一个独占的锁,就像 synchronized,无论三七二十一,直接上了锁就操做资源了。
3一、什么是AQS?
AQS全称为 AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
若是说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore 等等都用到了它。AQS实际上以双向队列的形式链接全部的 Entry,比方说 ReentrantLock,全部等待的线程都被放在一个 Entry 中并连成双向队列,前面一个线程使用 ReentrantLock 好了,则双向队列实际上的第一个Entry开始运行。
AQS定义了对双向队列全部的操做,而只开放了 tryLock() 和 tryRelease() 方法给开发者使用,开发者能够根据本身的实现重写 tryLock() 和 tryRelease() 方法,以实现本身的并发功能。
3二、单例模式的线程安全性?
首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被建立一次出来。单例模式有不少种的写法,我总结一下:
① 饿汉式单例模式的写法:线程安全;
② 懒汉式单例模式的写法:非线程安全;
③ 双检锁单例模式的写法:线程安全。
3三、Semaphore 有什么做用?
Semaphore 就是一个信号量,它的做用是限制某段代码块的并发数。Semaphore 有一个构造函数,能够传入一个 int 型整数n,表示某段代码最多只有n个线程能够访问,若是超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此能够看出若是 Semaphore 构造函数中传入的 int 型整数n=1,至关于变成了一个 synchronized 了。
3四、Hashtable的size()方法中明明只有一条语句”return count”,为何还要作同步?
某个方法中若是有多条语句,而且都在操做同一个类变量,那么在多线程环境下不加锁,势必会引起线程安全问题,这很好理解,可是size()方法明明只有一条语句,为何还要加锁?
本身的理解主要有两点:
① 同一时间只能有一条线程执行固定类的同步方法,可是对于类的非同步方法,能够多条线程同时访问。因此,这样就有问题了,可能线程A在执行 Hashtable 的 put() 方法添加数据,线程B则能够正常调用size()方法读取 Hashtable 中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,可是没有对size++,线程B就已经读取size了,那么对于线程B来讲读取到的 size 必定是不许确的。而给 size() 方法加了同步以后,意味着线程B调用 size() 方法只有在线程A调用put方法完毕以后才能够调用,这样就保证了线程安全性;
② CPU执行代码,执行的不是Java代码,这点很关键,必定得记住。Java代码最终是被翻译成汇编代码执行的,汇编代码才是真正能够和硬件电路交互的代码。即便你看到Java代码只有一行,甚至你看到Java代码编译以后生成的字节码也只有一行,也不意味着对于底层来讲这句语句的操做只有一个。一句”return count”假设被翻译成了三句汇编语句执行,彻底可能执行完第一句,线程就切换了。
3五、线程类的构造方法、静态块是被哪一个线程调用的?
这是一个很是刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的,而 run() 方法里面的代码才是被线程自身所调用的。
若是说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了 Thread1,main 函数中 new 了 Thread2,那么:
① Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run() 方法是 Thread2 本身调用的;
② Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run() 方法是 Thread1 本身调用的。
3五、同步方法和同步块,哪一个是更好的选择?
同步块,意味着同步块以外的代码是异步执行的,这比同步整个方法更提高代码的效率。请知道一条原则:同步的范围越小越好。
借着这一条,额外提一点,虽然说同步的范围越少越好,可是在Java虚拟机中仍是存在着一种叫作锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说 StringBuffer,它是一个线程安全的类,天然最经常使用的 append() 方法是一个同步方法,咱们写代码的时候会反复 append 字符串,这意味着要进行反复的加锁->解锁,这对性能不利,由于这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,所以Java虚拟机会将屡次 append() 方法调用的代码进行一个锁粗化的操做,将屡次的 append 的操做扩展到 append() 方法的头尾,变成一个大的同步块,这样就减小了加锁–>解锁的次数,有效地提高了代码执行的效率。
3六、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
并发编程网上看到的一个问题,这个问题很是好、很是实际、很是专业。关于这个问题,我的见解是:
① 高并发、任务执行时间短的业务,线程池线程数能够设置为CPU核数+1,减小线程上下文的切换;
② 并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操做上,也就是IO密集型的任务,由于IO操做并不占用CPU,因此不要让全部的CPU闲下来,能够加大线程池中的线程数目,让CPU处理更多的业务;
b)假如是业务时间长集中在计算操做上,也就是计算密集型任务,这个就没办法了,和①同样吧,线程池中的线程数设置得少一些,减小线程上下文的切换。
③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于总体架构的设计,看看这些业务里面某些数据是否能作缓存是第一步,增长服务器是第二步,至于线程池的设置,设置参考②。最后,业务执行时间长的问题,也可能须要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
一、抢占式调度策略
Java运行时系统的线程调度算法是抢占式的。Java运行时系统支持一种简单的固定优先级的调度算法。若是一个优先级比其余任何处于可运行状态的线程都高的线程进入就绪状态,那么运行时系统就会选择该线程运行。新的优先级较高的线程抢占了其余线程。可是Java运行时系统并不抢占同优先级的线程。换句话说,Java运行时系统不是分时的。然而,基于Java Thread类的实现系统多是支持分时的,所以编写代码时不要依赖分时。当系统中的处于就绪状态的线程都具备相同优先级时,线程调度程序采用一种简单的、非抢占式的轮转的调度顺序。
二、时间片轮转调度策略
有些系统的线程调度采用时间片轮转调度策略。这种调度策略是从全部处于就绪状态的线程中选择优先级最高的线程分配必定的CPU时间运行。该时间事后再选择其余线程运行。只有当线程运行结束、放弃(yield)CPU或因为某种缘由进入阻塞状态,低优先级的线程才有机会执行。若是有两个优先级相同的线程都在等待CPU,则调度程序以轮转的方式选择运行的线程。