多线程内容总结
一、线程的生命周期及五种基本状态:
关于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调度时机。
- 优点:可以直接使用 Thread 类中的方法,代码简单;
- 缺点:继承 Thread 类之后就不能继承其他的类。
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() 方法。
- 优点:即时自定义类已经有父类了也不受影响,因为可以实现多个接口;
- 缺点:在run() 方法内部需要获取到当前线程的Thread对象后才能使用Thread中的方法。
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()方法。
三. Java多线程的就绪、运行和死亡状态
就绪状态转换为运行状态:当此线程得到处理器资源;
运行状态转换为就绪状态:当此线程主动调用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
1、synchronized 关键字的作用域有二种:
① 是某个对象实例内,synchronized aMethod(){} 可以防止多个线程同时访问这个对象的 synchronized 方法(如果一个对象有多个 synchronized 方法,只要一个线程访问了其中的一个synchronized 方法,其它线程不能同时访问这个对象中任何一个 synchronized 方法)。这时,不同的对象实例的 synchronized 方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的 synchronized 方法;
② 是某个类的范围,synchronized static aStaticMethod{} 防止多个线程同时访问这个类中的 synchronized static 方法。它可以对类的所有对象实例起作用。
2、除了方法前用 synchronized 关键字,synchronized 关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的作用域是当前对象;
3、synchronized 关键字是不能继承的,也就是说,基类的方法 synchronized f(){} 在继承类中并不自动是 synchronized f(){},而是变成了 f(){}。继承类需要你显式的指定它的某个方法为 synchronized 方法;
总的说来,synchronized 关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized 可作用于 instance 变量、object reference(对象引用)、static 函数和 class literals (类名称字面常量)身上。
在进一步阐述之前,我们需要明确几点:
A.无论 synchronized 关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
B.每个对象只有一个锁(lock)与之相关联。
C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
讨论synchronized用到不同地方对代码产生的影响:
假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都可以调用它们。
1、把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 变量
ThreadLocal 是Java里一种特殊的变量。每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。
简单说 ThreadLocal 就是一种以空间换时间的做法,在每个 Thread 里面维护了一个以开地址法实现的 ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。
五、线程池的类型及工作场景:
线程池的优点
1、避免线程的创建和销毁带来的性能开销。
2、避免大量的线程间因互相抢占系统资源导致的阻塞现象。
3、能够对线程进行简单的管理并提供定时执行、间隔执行等功能。
4、使用线程池还可以根据项目灵活地控制并发的数目。
线程池的类型
① newFixedThreadPool:创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
特点:是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
② newCachedThreadPool:创建一个可缓存的线程池。这种类型的线程池特点是:
1).工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
2).如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
特点:在线程池空闲时,即线程池中没有可运行任务时,它会释放工作线程,从而释放工作线程所占用的资源。但是,但当出现新任务时,又要创建一新的工作线程,又要一定的系统开销。并且在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
③ newSingleThreadExecutor:创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行。
单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的 。 该线程池存在的意义是:在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。
④ newScheduleThreadPool:创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。
线程池的关闭
ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是 shutdown() 和 shutdownNow()。
shutdown():不会立即的终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
线程池的注意事项:
在使用线程池时需注意线程池大小与性能的关系,注意并发风险、死锁、资源不足和线程泄漏等问题。
(1) 线程池大小:多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及应用本身的特点决定线程池的大小。一般来说,如果代码结构合理的话,线程数目与CPU 数量相适合即可。如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可采用自适应算法来动态调整线程池的大小,以提高CPU 的有效利用率和系统的整体性能。
(2) 并发错误:多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意避免死锁现象的发生。
(3) 线程泄漏:这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。
六、多线程相关概念区别:
1、start() 方法和 run() 方法的区别?
只有调用了 start() 方法,才会表现出多线程的特性,不同线程的 run() 方法里面的代码交替执行。如果只是调用 run() 方法,那么代码还是同步执行的,必须等待一个线程的 run() 方法里面的代码全部执行完毕之后,另外一个线程才可以执行其 run() 方法里面的代码。
2、Runnable 接口和 Callable 接口的区别?
Callable 是在 JDK1.5 增加的,接口中的 call() 方法是有返回值的,是一个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果。也即 Callable 可以返回装载有计算结果的 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future 对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
Runnable() 的 run() 方法不可以抛出异常,Callable 的 call() 方法也可以抛出异常。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而 Callable + Future/FutureTask 却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。
3、CyclicBarrier 和 CountDownLatch 的区别?
两个看上去有点像的类,都在java.util.concurrent(JUC)下,都可以用来表示代码运行到某个点上,二者的区别在于:
① CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。
② CyclicBarrier 只能唤起一个任务,CountDownLatch 可以唤起多个任务。
③ CyclicBarrier 可重用,CountDownLatch 不可重用,计数值为0该 CountDownLatch 就不可再用了。
4、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 接口。
5、sleep() 方法和 wait() 方法有什么区别?
sleep() 方法和 wait()方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep() 方法不会放弃这个对象的监视器,wait() 方法会放弃这个对象的监视器。
6、wait() 方法和notify() / notifyAll() 方法在放弃对象监视器时有什么区别?
wait() 方法和 notify() / notifyAll() 方法在放弃对象监视器时的区别在于:wait() 方法立即释放对象监视器,notify() / notifyAll() 方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
7、synchronized 和 ReentrantLock 的区别?
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。
既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock 比 synchronized 的扩展性体现在几点上:
① ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁;
② ReentrantLock 可以获取各种锁的信息;
③ ReentrantLock 可以灵活地实现多路通知;
另外,二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操作的应该是对象头中 mark word(这点不确定)。
8、锁和同步的对比?
锁的使用场景有很多,如共享实例变量、共享连接资源时以及包括并发包中BlockingQueue、ConcurrentHashMap等并发集合中都大量使用了锁。基体上使用同步的地方都可以改成锁来用,但是使用锁的地方不一定能改成同步来用。
① 同步synchronized算是一个关键词,是来来修饰方法的,但是锁lock是一个实例变量,通过调用lock()方法来取得锁
② 只能同步方法,而不能同步变量和类,锁也是一样
③ 同步无法保证线程取得方法执行的先后顺序。锁可以设置公平锁来确保。
④ 不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
⑤ 如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。锁也是一样。
⑥ 线程睡眠时,它所持的任何锁都不会释放。
⑦ 线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
⑧ 同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
⑨ 在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。例如:
最后,还需要说的一点是。如果使用锁,那么一定的注意编写代码,但不很容易出现死锁!避免方法后文后讲。
8、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 对象。
9、什么是线程安全?
一句话解释:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
这个问题有值得一提的地方,就是线程安全也是有几个级别的:
① 不可变
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
② 绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的。不过绝对线程安全的类,Java中也有,比方说 CopyOnWriteArrayList、CopyOnWriteArraySet。
③ 相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像 Vector 这种,add、remove 方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个 Vector、有个线程同时在 add 这个 Vector,99%的情况下都会出现 ConcurrentModificationException,也就是fail-fast机制。
④ 线程非安全
ArrayList、LinkedList、HashMap等都是线程非安全的类。
10、Java中如何获取到线程 dump 文件?
死循环、死锁、阻塞、页面打开慢等问题,打线程 dump 是最好的解决问题的途径。所谓线程 dump 也就是线程堆栈,获取到线程堆栈有两步:
① 获取到线程的 pid,可以通过使用 jps 命令,在Linux环境下还可以使用 ps -ef | grep java;
② 打印线程堆栈,可以通过使用 jstack pid 命令,在Linux环境下还可以使用 kill -3 pid;
另外,Thread 类提供了一个 getStackTrace() 方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈。
11、一个线程如果出现了运行时异常会怎么样?
如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个对象的监视器,那么这个对象监视器会被立即释放。
12、如何在两个线程之间共享数据?
通过在线程之间共享对象就可以了,然后通过 wait/notify/notifyAll、await/signal/signalAll 进行唤起和等待,比方说阻塞队列 BlockingQueue 就是为线程之间共享数据而设计的。
13、生产者消费者模型的作用是什么?
这个问题很重要:
① 通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用;
② 解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约。
14、为什么 wait() 方法和 notify()/notifyAll() 方法要在同步块中被调用?
这是JDK强制的,wait() 方法和 notify()/notifyAll() 方法在调用前都必须先获得对象的锁。
15、wait() 方法和 notify()/notifyAll() 方法在放弃对象监视器时有什么区别?
区别在于:wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
16、怎么检测一个线程是否持有对象监视器?
Thread 类提供了一个 holdsLock(Object obj) 方法,当且仅当对象 obj 的监视器被某条线程持有的时候才会返回 true,注意这是一个 static 方法,这意味着“某条线程”指的是当前线程。
17、ConcurrentHashMap 的并发度是什么?
ConcurrentHashMap 的并发度就是 segment 的大小,默认为16,这意味着最多同时可以有16条线程操作 ConcurrentHashMap,这也是 ConcurrentHashMap 对 Hashtable 的最大优势,任何情况下,Hashtable 都不能同时有两条线程获取Hashtable中的数据。
18、ReadWriteLock是什么?
首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
因为这个,才诞生了读写锁 ReadWriteLock。ReadWriteLock 是一个读写锁接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
19、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方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后变回返回一个失败信息。
21、怎么唤醒一个阻塞的线程?
如果线程是因为调用了 wait()、sleep()或者join() 方法而导致的阻塞,可以中断线程,并且通过抛出 InterruptedException 来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
22、不可变对象对多线程有什么帮助?
前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
23、什么是多线程的上下文切换?
是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。
24、如果你提交任务时,线程池队列已满,这时会发生什么?
如果你使用的 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务;如果你使用的是有界队列比方说 ArrayBlockingQueue 的话,任务首先会被添加到 ArrayBlockingQueue 中,如果 ArrayBlockingQueue 满了,则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy。
25、Java中用到的线程调度算法是什么?
抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
26、Thread.sleep(0)的作用是什么?
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
27、什么是自旋?
很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
28、什么是Java内存模型?
Java内存模型定义了一种多线程访问Java内存的规范。简单总结一下Java内存模型的几部分内容:
① Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去;
② 定义了几个原子操作,用于操作主内存和工作内存中的变量;
③ 定义了 volatile 变量的使用规则;
④ happens-before,即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定 lock 的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的 happens-before 规则,则这段代码一定是线程非安全的。
29、什么是CAS?
CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要 volatile 变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。
30、什么是乐观锁和悲观锁?
① 乐观锁:对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
② 悲观锁:对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized,不管三七二十一,直接上了锁就操作资源了。
31、什么是AQS?
AQS全称为 AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore 等等都用到了它。AQS实际上以双向队列的形式连接所有的 Entry,比方说 ReentrantLock,所有等待的线程都被放在一个 Entry 中并连成双向队列,前面一个线程使用 ReentrantLock 好了,则双向队列实际上的第一个Entry开始运行。
AQS定义了对双向队列所有的操作,而只开放了 tryLock() 和 tryRelease() 方法给开发者使用,开发者可以根据自己的实现重写 tryLock() 和 tryRelease() 方法,以实现自己的并发功能。
32、单例模式的线程安全性?
首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:
① 饿汉式单例模式的写法:线程安全;
② 懒汉式单例模式的写法:非线程安全;
③ 双检锁单例模式的写法:线程安全。
33、Semaphore 有什么作用?
Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore 有一个构造函数,可以传入一个 int 型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数n=1,相当于变成了一个 synchronized 了。
34、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”假设被翻译成了三句汇编语句执行,完全可能执行完第一句,线程就切换了。
35、线程类的构造方法、静态块是被哪个线程调用的?
这是一个非常***钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的,而 run() 方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了 Thread1,main 函数中 new 了 Thread2,那么:
① Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run() 方法是 Thread2 自己调用的;
② Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run() 方法是 Thread1 自己调用的。
35、同步方法和同步块,哪个是更好的选择?
同步块,意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。
借着这一条,额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说 StringBuffer,它是一个线程安全的类,自然最常用的 append() 方法是一个同步方法,我们写代码的时候会反复 append 字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次 append() 方法调用的代码进行一个锁粗化的操作,将多次的 append 的操作扩展到 append() 方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。
36、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
并发编程网上看到的一个问题,这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是:
① 高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换;
② 并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务;
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和①一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换。
③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考②。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
七、线程调度策略
1、抢占式调度策略
Java运行时系统的线程调度算法是抢占式的。Java运行时系统支持一种简单的固定优先级的调度算法。如果一个优先级比其他任何处于可运行状态的线程都高的线程进入就绪状态,那么运行时系统就会选择该线程运行。新的优先级较高的线程抢占了其他线程。但是Java运行时系统并不抢占同优先级的线程。换句话说,Java运行时系统不是分时的。然而,基于Java Thread类的实现系统可能是支持分时的,因此编写代码时不要依赖分时。当系统中的处于就绪状态的线程都具有相同优先级时,线程调度程序采用一种简单的、非抢占式的轮转的调度顺序。
2、时间片轮转调度策略
有些系统的线程调度采用时间片轮转调度策略。这种调度策略是从所有处于就绪状态的线程中选择优先级最高的线程分配一定的CPU时间运行。该时间过后再选择其他线程运行。只有当线程运行结束、放弃(yield)CPU或由于某种原因进入阻塞状态,低优先级的线程才有机会执行。如果有两个优先级相同的线程都在等待CPU,则调度程序以轮转的方式选择运行的线程。