Java 线程控制
一、线程控制
和线程相关的操作都定义在Thread类中,但在运行时可以获得线程执行环境的信息。比如查看可用的处理器数目(这也行?):
public class RunTimeTest { public static void main(String[] args) { Runtime rt=Runtime.getRuntime(); System.out.println(rt.availableProcessors()); System.out.println(rt.totalMemory()); System.out.println(rt.freeMemory()); System.out.println(rt.maxMemory()); } }
以上程序的输出为:
- JVM可用的处理器数量:8
- JVM的内存总量:128974848
- JVM的可用(free)内存量:125561424
- JVM将尝试使用的最大内存量:1890582528
线程还提供了一些方法用于对线程进行便捷的控制。
1、线程睡眠
静态方法Thread.sleep(long millis)强制正在执行的线程暂停进入睡眠状态,进入阻塞状态。睡眠结束后,线程转为就绪状态。
/*自定义的睡眠时间单位为毫秒*/
/*必须要进行异常处理*/
try {
Thread.sleep(lengthOfPause);
} catch(InterruptedException e) {
e.printStacktrace();
}
值得强调的是:
- 线程睡眠是帮助其他所有线程获得运行机会的最好方法;
- 线程睡眠到期自动苏醒,并返回到就绪状态,不是运行状态。然后,sleep()中参数指定的时间是停止运行的最短时间,而无法保证从睡眠苏醒后就开始执行;
- sleep()是静态方法,只能控制当前正在运行的线程;
2、线程让步
yield()方法使当前线程让出CPU占有权,与sleep()类似,但让出时间无法设定。而且yield()使线程进入就绪状态,更利于有同优先级的其它线程获得运行机会。只是实际中,无法保证yield()达到让步目的,比如让步的线程又被JVM选中。
yield()方法不会释放锁标志。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于就绪状态。若有,则把CPU占有权交给此线程;若没有,继续运行原来的线程。所以yield()尝试做的,是把运行机会给同等级的其它线程,而无法让低级别的线程获得。
3、线程加入
在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态。直到另一个线程运行结束,阻塞的线程重回就绪状态。
join()是Thread类的非静态方法,还有带有超时限制的重载版本,如t.join(5000)是让线程等待5000ms,超时后,阻塞的进程进入就绪状态。
4、线程优先级
当应用启动时,主线程是创建的第一个用户线程,程序可以创建多个用户线程和守护线程。
当所有的用户线程执行完毕,JVM终止进程。
设置线程的优先级:
/*优先级为1~10*/
Thread t=new MyThread();
/*设置线程优先级*/
t.setPriority(8);
/*获取线程优先级*/
t.getPriority();
t.start();
//只是说优先级更高的线程有更多的机会获得运行,并不保证先于优先级低的线程运行。
守护线程的优先级别是最低的,用于为系统中的其他对象和线程提供服务,例如JVM中的资源自动回收线程。
将一个用户线程设置为守护线程的方法是在线程对象创建之前调用线程对线的setDaemon()方法。
5、线程分组管理
ThreadGroup类
一旦线程加入某个线程组,该线程就一直存在于该线程组中直至死亡,不能中途改变线程所属的组。
二、线程同步
线程调度的意义在于,JVM对运行的多个线程进行系统级别的协调,以避免多个线程争夺有限的资源而导致应用崩溃。
同步是一种防止对共享资源访问导致数据不一致的机制。即当多个线程访问同一个共享资源时,需要确保该资源在一段时间內仅被一个线程访问。
1、锁机制
锁机制限制在同一时间只允许一个线程访问产生竞争的临界区。
关键字synchronized为代码块or方法加锁,实现同步。线程在使用临界资源时加锁,拒绝其他线程的访问,直至该线程解锁。
实际上,任何对象都有一个监视器用于加锁和解锁,当synchronized声明的代码块or方法被执行时,说明当前线程已经成功获取了对象监视器上的锁。当正常执行完毕或异常退出时,当前线程所获取的锁会被自动释放
线程可以在一个对象上加多次锁,JVM保证获取锁之前和释放锁之后的变量的值是与主存中的内容同步的。
(1)同步方法(参考)
使用synchronized声明方法。
当访问某个对象的同步方法,这个对象会加锁,而不是仅仅为该方法加锁!因此当对象的同步方法被某个线程执行时,其他线程无法访问该对象的任何同步方法,但是可以调用其它非同步方法。(即对象加锁,意味着所有的同步方法只能被持锁的线程访问)
当调用一个对象的静态同步方法时,它锁定的不是同步方法所在对象,而是它对应的Class对象。因此,其他线程不能调用该类的其他静态同步方法,但是可以调用非静态同步方法。
(2)同步代码块
使用synchronized声明同步代码块,锁定其所在的对象or特定的对象,还对象作为可执行的标志从而达到同步效果。
/*同步代码块*/ public void method(){ synchronized(表达式){ } }
如果想要使用synchronized同步代码块达到和使用synchronized()方法同样的效果,可以锁定this引用:
synchronized(this){ ... }
同步方法是一种粗粒度的并发控制,某一时刻只能有一个线程执行该同步方法。同步代码块是细粒度的并发控制,只会将块中的代码同步,代码块之外的代码可被其他线程同时访问。
使用synchronized要注意:
- synchronized关键字不能继承;
- 定义接口方法时不能使用synchronized;
- 构造方法不能使用synchronized关键字,但可以使用synchronized代码块进行同步;
三、线程协作
线程之间会有协作关系,一起来完成某项任务。如生产者—消费者模式。
多线程可以通过访问和修改同一份资源(对象)来进行交互和通信,只是需要注意线程访问的安全性。当线程所要求的资源不足时,就进入等待状态;而另外的线程则负责在合适的时机发出通知来唤醒等待中的线程。
等待—通知机制是完成线程间同步的基本机制。
1、wait与notify原语
在对象上调用wait()方法时,首先要检查当前线程是否获取到了该对象上的锁。若没有,会直接抛出IllegalMonitorStateException异常。如果有锁,就会把当前线程添加到对象的等待集合中,并释放其所拥有的锁,这样另一个线程就可以获得当前对象的锁,从而进入synchronized()方法中。
sleep()和yeild()方法并不释放锁,调用wait()方法后,当前线程被阻塞,无法继续执行,知道被等待集合移除。
引起某个线程从对象的等待集合中被移除的原因有:
- 对象上的notify()方法被调用;
- 对象上的notifyAll()方法被调用;
- 线程被中断;
- 对于有超时限制的wait操作,当超过时间限制时;
从上面的说明中,总结三条结论:
(1) wait/notify/notifyAll操作需要放在synchronized代码块或者方法中,这保证执行它们时,当前线程已经获得所需要的锁;
(2) 当对象的等待集合中的线程数目无法确定时,最好使用notifyAll()方法而不是notify()方法;
(3) notifyAll()方法会导致线程在没有必要的情况下被唤醒而产生性能影响,但是使用上更简单。
由于线程可能在非正常情况下被意外唤醒,一般应把wait操作放在循环中检查所要求的逻辑条件是否满足。典型的使用形式:
/*使用一个私有的lock作为加锁的对象*/ /*好处是可以避免其它代码错误地使用这个对象*/ private Object lock=new Object(); synchronized(lock){ while(/*逻辑条件不满足时*/){ try{ lock.wait(); }catch(InterruptedException e){} } }
2、生产者-消费者问题
生产者-消费者问题是线程协作的典型方式,即生产者生产产品的速度和消费者消费的速度可能不匹配,产品超出库存能力,此时生产者必须等待,直到消费者消耗了产品。
对应到线程协作中,就是线程间协调的“生产者-消费者-仓储”模型,生产者和消费者通过仓储通信,根据仓储容量和产品数目协同生产消费过程。
wait()方法的使用,必须存在2个以上线程,而且必须在不同的条件下唤醒等待中的线程。
四、线程池
五、线程同步控制的新特征