1、并发与并行
当1个以上线程在操作的时候,若计算机只有一个cpu,根本不可能同时进行一个以上的处理,而是这样进行:
work1稍微操作一下暂停-->work2稍微操作一下暂停-->work1稍微操作一下暂停.....
当程序的处理像这样不断切换着操作的线程时候就被称为并发(concurrent)。
如果有一个以上cpu执行Java程序,线程操作可能就是并行的(parallel)而不是并发(concurrent)的。当一个以上线程并行操作的时候就可以同时进行一个以上的处理。
并发与并行的差异图如下:
二、线程的启动
线程的启动有两种方法:
1、利用java.lang.Thread类的子类实例,启动线程。
public class PrintThread extends Thread{
private String message;
public PrintThread(String message){
this.message = message;
}
public void run(){
for(int i = 0; i<100; i++){
System.out.print(message);
}
}
}
public class Main{
public static void main(String[] args){
new PrintThread("Good").start();//匿名实例启动线程
new PrintThread("Nice").start();
}
}
整个处理过程是这样:先创建PrintThread类的实例,然后调用实例的start()方法。调用start()方法的时候,会启动新的线程,然后调用实例的run()方法。上面的main()方法里只有一个语句去启动线程,不过创建线程类的实例和启动实例的线程是两个不同的处理。
当所有线程都结束时,程序才会结束。不过在判断是否结束时不包括守护线程,换句话说即使daemon thread还在,其他不是daemon thread的线程结束了,程序也就结束了。守护线程通过调用setDaemon()方法设置。
2、利用Runnable接口的实现类的实例,启动线程。
public class Printer implements Runnable{
private String message;
public Printer(Sting message){
this.message = message;
}
public void run(){
for(int i = 0; i<100;i++){
System.out.print(message);
}
}
}
public class Main{
public static void main(String[] args){
new Thread(new Printer("Good")).start();
new Thread(new Printer("Nice")).start();
}
}
在创建Thread类的实例的时候,输出Printer类的实例作为构造器的参数(Runnable target),然后利用start()方法启动线程。
无论是那种方法,启动线程的永远都是Thread类的start()方法。
三、线程的暂停
Thread.sleep(1000);
这行代码可以让当前的线程(执行这条语句的线程暂停约1000ms),这个方法要放在try-catch里面,因为sleep方法可能会抛出一个InterruptedException的异常,这个异常用在取消线程处理时的异常。
Thread.sleep(ms,ns);可以将时间控制得更精细,达到ns(10^(-9)s)。
四、线程的共享互斥
当多个线程访问同一个资源的时候,会出现一些和预想情况不符合的现象,这时候就需要“共享互斥”或者说“互斥控制”,Java在处理线程的共享互斥的时候,通常会使用关键字synchronized。
1、使用synchronized关键字修饰类的方法。
public class Test{
public synchronized void test(){
//to do
}
}
当一个线程执行Test实例的test方法时,其他线程就不能执行同一个实例test方法,对于同一个类里的非synchronized方法没有这个限制,对于同一个实例,多个线程可以同时访问。对于某个正在执行test方法的线程,当执行完test方法后,锁就会被释放(release)。当锁被释放之后,其他在等待的多个线程便开始抢锁,一定有且只有一个线程会获得锁,没有抢到就继续等待。
线程共享互斥的架构被称为监视(monitor),而获取锁有时也被称为持有(own)监视。
2、使用synchronized关键字修饰方法里的一部分(synchronized阻挡),格式如下:
synchronized(表达式){
//to do
}
对于synchronized实例方法和synchronized阻挡:
synchronized void method(){
//to do
}
和下面这个synchronized阻挡效果是一样的:
void method(){
synchronized(this){
//to do
}
}
换句话说,synchronized实例方法是使用this锁去做线程的互斥共享。
对于synchronized类静态方法和synchronized阻挡:
class Something{
static synchronized void method(){
//to do
}
}
和下面这个synchronized阻挡效果是是一样:
class Something{
static void method(){
synchronized(Something.class){
//to do
}
}
}
换句话说,synchronized类静态方法和使用该类的类对象的锁去做线程的共享互斥。
五、线程的协调
wait set----线程休息室
wait set是一个在执行该实例的wait方法时,操作停止时的线程集合,每个实例都有。
wait set实际上是一个虚拟的概念,不是实例的字段,也不是可以获取在实例上wait中线程的列表的方法。
一旦执行wait方法,线程便暂停操作,进入wait set,除非发生下面任何一种情况,否则线程永远留在这个wait set里:
1、有其他线程使用notify方法唤醒该线程;
2、有其他线程使用notifyAll方法唤醒该线程;
3、有其他线程使用interrupt方法唤醒该线程;
4、wait方法已到期。
wait方法----把线程放入wait set
使用wait方法后,线程进入wait set,如obj.wait();
当在实例内的时候直接执行wait()即可,等同于this.wait();
执行wait()方法的时候,线程需要获得锁,当线程进入wait set之后,已经释放了该实例的锁。
当线程A执行wait()方法时线程B仍然无法获得锁
当线程A执行了wait()方法之后,线程A进入了wait set,然后释放了锁。
线程B获得了锁,开始执行synchronized修饰的方法。
notify方法----从wait set中拿出线程
使用notify方法的时候,可以从wait set中抓取一个线程,唤醒这个线程,被唤醒的线程便退出wait set。
线程B执行notify方法唤醒线程A。
线程A退出wait set,打算执行自己wait()的下面的操作,但是刚才执行notify的线程B仍然握着锁不放手。
刚才执行过notify的线程B开始释放锁。
已经退出wait set的线程A获得锁,开始执行wait下面的操作。
线程需要有遇到用实例的锁,才能执行notify方法。
tips:
1、从上面的图也可以看出来,被notify唤醒的线程不是在notify的一瞬间重新开始执行,因为在notify的那一刻,执行notify的线程还握着锁不妨,所以其他线程无法获取该实例的锁。
2、假设执行notify方法的时候,wait set里正在等待的线程不止1个,规格里并没有注明应该选择哪个线程,依据Java处理系统而异,因此,最好不要将程序写成因所选线程而有所改动。
notifyAll方法----从wait set中拿出所有线程
使用notifyAll方法时,会将所有在wait set里的线程拿出来。
线程同样需要有欲调用实例的锁,才能调用notifyAll方法,否则会抛出IlleagalMonitorStateException。
tips:选择notify还是notifyAll?
选择notify的话,要唤醒的线程少,速度比notifyAll要好,但是选择notify若处理不慎有可能导致程序挂掉,因此notifyAll方法更可靠。当然能处理好的话,notify更好。
另外,wait,notify,notifyAll方法都是Object类下的方法,不是Thread下的。
六、线程的状态转移