几乎所有的操作系统都支持运行多个任务,一个任务就是一个程序,一个运行中的程序就是进程,当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程,进程是系统进行资源分配和调度的一个独立单位
进程有三个特征:
1.独立性,他有自己地盘,有自己的队伍,他不允许别人进来,别人就进不来
2.动态性:程序只是一个充气娃娃,虽然由各种指令集合组成,但是她是静态不能动的,进程是一个活人,是一个能活动的指令集合,有自己的寿命和喜怒哀乐
3.并发性:在一个处理器上几个进程可以并发执行,多个进程之间不会互相影响(同一时刻只能有一条指令执行,但是多个进程指令被快速轮换执行,使得宏观上看起来具有多个进程同时执行的效果)
大部分操作系统支持多进程并发进行,多线程则扩展了多进程的概念,就像进程在操作系统中的工作方式一样,所以进程被称为“轻量级进程”
多线程:同时可以执行多个顺序执行流,多个顺序流之间互不干扰。好比餐厅服务员,单线程是 只雇佣一个服务员,给一桌上完菜以后才可以给另一桌上,多线程是雇佣好几个服务员,同时给好几桌上上菜.
线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,他与父进程的其他线程共享该进程所拥有的全部资源
使用多线程编程优势:
1.进程之间不能共享内存,线程之间可以
2.系统创建进程时需要为该进程重新分配系统资源,创建线程代价小的多
3.java语言内置了多线程功能的支持,,而不是单纯的作为底层操作系统的调度方式,从而简化了java的多线程编程
线程的创建和启动
1.继承Thread类创建线程类
2.实现Runnable接口创建线程类
Tread子类即可代表线程对象,Runable子类只能作为线程对象的target。
1.为什么Runable子类需要通过Thread来创建线程:
在Java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口;Thread类是在java.lang包中定义的。一个类只要继承了Thread类同时覆写了本类中的run()方法就可以实现多线程操作了,但是一个类只能继承一个父类,这是此方法的局限,
下面看例子:
package org.thread.demo;
class MyThread extends Thread{
private String name;
public MyThread(String name) {
super();
this.name = name;
}
public void run(){
for(int i=0;i<10;i++){
System.out.println("线程开始:"+this.name+",i="+i);
}
}
}
package org.thread.demo;
public class ThreadDemo01 {
public static void main(String[] args) {
MyThread mt1=new MyThread("线程a");
MyThread mt2=new MyThread("线程b");
mt1.run();
mt2.run();
}
}
但是,此时结果很有规律,先第一个对象执行,然后第二个对象执行,并没有相互运行。在JDK的文档中可以发现,一旦调用start()方法,则会通过JVM找到run()方法。下面启动
start()方法启动线程:
package org.thread.demo;
public class ThreadDemo01 {
public static void main(String[] args) {
MyThread mt1=new MyThread("线程a");
MyThread mt2=new MyThread("线程b");
mt1.start();
mt2.start();
}
};这样程序可以正常完成交互式运行。那么为啥非要使用start();方法启动多线程呢?
在JDK的安装路径下,src.zip是全部的java源程序,通过此代码找到Thread中的start()方法的定义,可以发现此方法中使用了private native void start0();其中native关键字表示可以调用操作系统的底层函数,那么这样的技术成为JNI技术(Java Native Interface)
·Runnable接口
在实际开发中一个多线程的操作很少使用Thread类,而是通过Runnable接口完成。
public interface Runnable{
public void run();
}
例子:
package org.runnable.demo;
class MyThread implements Runnable{
private String name;
public MyThread(String name) {
this.name = name;
}
public void run(){
for(int i=0;i<100;i++){
System.out.println("线程开始:"+this.name+",i="+i);
}
}
};
但是在使用Runnable定义的子类中没有start()方法,只有Thread类中才有。此时观察Thread类,有一个构造方法:public Thread(Runnable targer)此构造方法接受Runnable的子类实例,也就是说可以通过Thread类来启动Runnable实现的多线程。(start()可以协调系统的资源):
package org.runnable.demo;
import org.runnable.demo.MyThread;
public class ThreadDemo01 {
public static void main(String[] args) {
MyThread mt1=new MyThread("线程a");
MyThread mt2=new MyThread("线程b");
new Thread(mt1).start();
new Thread(mt2).start();
}
}
· 两种实现方式的区别和联系:
在程序开发中只要是多线程肯定永远以实现Runnable接口为主,因为实现Runnable接口相比
继承Thread类有如下好处:
->避免点继承的局限,一个类可以继承多个接口。
->适合于资源的共享
2 网传的一种缪论:用Runnable就可以实现资源共享,而 Thread 不可以
有同学的例子是这样的,参考:http://developer.51cto.com/art/201203/321042.htm:
由此差别,有同学就得出了一个结论:用Runnable就可以实现资源共享,而 Thread 不可以,这是他们的主要差别之一。。。
其实仔细看看代码就知道,这只是两种写法的区别,根本就不是 implements Runnable 与 extends Thread 的区别:
如果像实现Runable接口的类创建线程一样创建实现Thread子类的线程,会发现,也能实现资源共享,但是你自己本来就有start()方法,再用别人的start方法,是不是有点别扭,你和另一个人在沙漠里,两个人都有一壶水,你对另一个人说,把你的水给我喝吧,他会不会打死你?
另外
其实,想要“资源共享”,Thread 也可以做到的:
通过 static 就可以实现拥有共同的ticket=10,但问题也来了,你会发现一二号窗口都卖了第 9 张票。这就引发了线程安全的问题
3.使用Callable和Funture创建线程
通过Runnable接口创建多线程时,Thread类的最用就是把run()方法包装城线程执行体。那么是否可以直接把任意方法都包装成线程执行体呢?
很抱歉,Java目前不行。
从java5开始,java提供了Callable接口,提供了一个call()方法可以作为线程执行体,call()方法比run()方法更为强大:1.可以有返回值 2.可以抛出异常
如果想把Callable对象当做Thread类的target,,发现他并没有实现Runnable接口,而且这个call()方法并不是直接调用,是作为线程执行体被调用的(通过Start()来调用线程执行体),那么如何获取call()方法的返回值呢?
答案就是Java5提供了一个future接口,并未future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口,在Future接口里定义了几个公共方法来控制它关联的Callable任务:
实现步骤:
1.创建Clallable接口的实现类,实现call()方法并有返回值,创建Callable实现类的实例
2.使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值
3.使用FutureTask对象作为Thread对象的target创建并启动新线程
4.调用FutureTask对象的get()方法获得子线程执行结束后的返回值
创建线程的三种方式对比:
继承Thread类和实现Runnable、Callable接口都可以实现多线程,不过实现Runnable和Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,因此可以把这两个接口归结为一种方式。
继承接口:
1.可以继承其他类或接口,避免了单一继承
2.可以多个线程共享一个target,非常适合多个线程共同处理同一个资源的时候
3.编程稍稍复杂,如果需要访问当前线程,必须使用Thread.currentThread方法
继承Thread类:
1.不能在继承其他父类
2.优势是,编程稍微简单一点,直接通过this就可以访问当前线程
线程的生命周期
1.新建和就绪状态
新建: 当使用new关键字创建了一个线程以后,该线程就处于新建状态,此时他和其他的java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体
就绪:当线程对象调用了start()方法以后,该线程就处于就绪状态,java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示这个程序可以运行了,至于何时能运行,就得看JVM里面线程调度器的调度
启动线程必须使用start()方法,系统会自动把run()方法当做线程执行体来处理。如果直接调用线程对象的run()方法,系统会把线程对象当成一个普通对象,也就是程序里只有一个主线程,并且run()方法里不能通过getName直接获得当前执行线程的名字了,必须通过Thread.currentThread()方法来获得当前线程,再通过getName()方法来获得
注意的是:只能对处于新建状态的线程调用start()方法。
2.运行和阻塞状态
运行:如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,则任何时候只有一个线程处于运行状态。
阻塞:一个线程开始运行后,不可能一直处于运行状态(除非运行时间特别特别短),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会。现代所有的桌面和服务器操作系统都采用抢占式调度策略,但一些小型设备如手机则可能采用协作式调度策略,这样的系统中,只有当一个线程调用了它的sleep()或yeild()方法才会放弃所占用的资源。当发生如下情况,线程会进入阻塞状态:
1.调用sleep()方法主动放弃所占用的处理器资源
2.线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
3.线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
4.线程在等某个通知
5.程序调用了线程的suspend()方法
被阻塞之后,会在合适的时候重新进入就绪状态,是就绪不是运行,也就是说阻塞接触后,要等待调度器重新调度他
针对上面的情况,发生下面的情况可以解除上面的阻塞,让该线程重新处于就绪状态。
1.调用sleep()方法的线程经过了指定时间
2.线程调用的阻塞式IO已经收回
3.线程成功的获得了试图取得的同步监视器
4.线程正在等待某个通知时,其他线程发出了一个通知
5.处于挂起状态的线程被调用了resume()恢复方法
线程死亡
线程以如下三种方式结束,结束后就处于死亡状态:
1run()或call()方法执行完成,线程正常结束
2.线程直接抛出一个未捕获的Exception或Error
3.直接调用该线程的stop()方法来结束该线程
当主线程结束时,其他线程不受任何影响,一旦子线程启动起来后,他就和主线程拥有同样的地位
isAlive()方法,可以测试线程是否已经死亡,当处于新建、死亡两种状态时,会放回false
不要试图用start()去救活一个已经死了的线程,死了就是死了
控制线程
1.join线程
让一个线程等待另个一线程完成的方法,join()方法通常由使用程序的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。所有的小问题都得到处理后,在调用主线程来进一步操作
2.后台线程
有一种线程是在后台运行的,他的任务是为其他县城提供服务,这种线程被称为后台线程、守护线程、精灵线程,JVM的垃圾回收线程就是典型的后台线程
如果前台线程都死亡,后台线程会自动死亡。
调用Thread对象的setDaemon(true)方法可以将指定线程设置成后台线程。
3.线程睡眠 sleep
让当前正在执行的线程暂停一段时间
可以看到输出两条字符串之间相隔三秒,这个时间受到系统计时器和线程调度器的影响
4.线程让步yield
让当前正在执行的线程暂停,但不会阻塞该线程,只是将该线程转入就绪状态
当某个线程使用yield()方法暂停以后,只有优先级与当前线程相同或者比当前线程高的才会获得机会执行
因为高级的优先级最高,所以暂停以后调度器调度的还是他,他继续执行
sleep()和yield()区别:
1.sleep()暂停后,给其他线程执行机会,不会理会优先级,yield()只会让和他一样或者比他厉害的执行
2.sleep()会让线程进入阻塞状态,阻塞时间过了才会转入就绪状态,yield()不会将线程进入阻塞状态,只是让当前线程进入就绪状态
3.sleep()方法抛出了InterruptedException异常,所以调用sleep()方法要么显示抛出该异常,要么捕捉,而yield()方法没有抛出任何异常
4.sleep()方法比yield()方法有更好的可移植性
5.改变线程优先级
每个线程都有一定的优先级,优先级高的获得较多的执行机会,优先级低的线程获得较少的执行机会
每个线程默认和创建他的父线程的优先级相同。在默认情况下,main线程具有普通优先级。
MAX_PRIORITY:其值是10
MIN_PRIORITY:其值是1
NORM_PRIORITY:其值是5
线程同步
1.线程安全问题
经典案例:银行取钱问题
当多个线程修改同一个数据时,可能会发生问题,即使发生几率是一亿分之一,那也是有问题
而发生这个问题的原因可能是:
多个线程可能出现同时访问account的情况。而任何一个线程在访问account的过程中都可以切换到其他的线程上。而其他线程一旦把account的数据改变了,再切换回来时,错误数据就有了。
2.解决线程安全问题
要解决上述的线程安全问题,错误数据问题。在一个线程进入到if中之后,当cpu切换到其他线程上时,不让其他的线程进入if语句,那么就算线程继续执行当前其他的线程,也无法进入到if中,这样就不会造成错误数据的出现。
1.同步代码块
为了解决上面的问题,java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法是同步代码块
语法是 synchronized(obj){
....
}
括号里的obj就是同步监视器,这段代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定
虽然java允许使用任何对象作为同步监视器,但是想一想同步监视器的目的:组织两个线程对同一个共享资源进行并发访问,因此通常使用可能被并发访问的共享资源充当同步监视器
上面例子中,应该使用账户作为同步监视器
对account加锁,在不释放锁的情况下, 即使切换到其他线程,也无法获得锁的钥匙,不能执行同步代码块的内容,知道获得锁的线程执行完释放以后其与线程才能执行,同一时刻最多有一个线程处于临界区内
2.同步方法
对于同步方法来说,无需显示指定同步监视器(非static方法),同步方法的同步监视器是this,也就是调用该方法的对象
使用同步方法可以很方便的实现线程安全的类,线程安全的类具有如下特征:
1、该类的对象可以被多个线程安全的访问
2、每个线程调用该对象的任意方法之后都将得到正确的方法
3.每个线程调用该对象的任意方法之后,该对象依然保持合理状态
不可变类总是线程安全的,可变类对象需要额外的方法来保证其线程安全
在account类的提供一个线程安全的draw()方法来完成取钱操作,这样更符合面向对象规则,每个类都应该是完备的领域对象,例如account是账户,应该提供用户账户的相关方法,通过draw()来完成取钱操作,而不是直接将setBalance()暴露出来任人操作,这样才可以更好的保护account对象的完整性和一致性
run()方法直接调用account的draw()方法,同步方法的同步监视器是tihis,this代表调用draw()方法的对象,也就是说,线程进入draw方法之前,必须先对account对象加锁
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全带来的负面影响,程序可以采用以下策略
1.只对那些会改变共享资源的方法进行同步
2.如果可变类有两种运行环境:单线程和多线程,应该为可变类提供两种版本,单线程使用线程不安全,多线程使用线程安全
3.释放同步监视器的锁定
程序无法显示释放同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定:
1.当前线程的同步方法,同步代码块执行结束,当前线程即释放同步监视器
2.当前线程在遇到同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程即释放同步监视器
3.当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法结束时,当前线程即释放同步监视器。
4.当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器
在下面所示的情况下,线程不会释放同步监视器
1.线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器
4.同步锁
从java5开始,可以通过显示定义同步锁来实现同步锁,同步锁由Lock对象充当。
死锁
当两个线程互相等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取任何措施来处理死锁情况,因此多线程编程时应采取措施避免死锁出现。
所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。
虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件。
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
在系统中已经出现死锁后,应该及时检测到死锁的发生,并采取适当的措施来解除死锁。目前处理死锁的方法可归结为以下四种:
1) 预防死锁。
这是一种较简单和直观的事先预防的方法。方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。预防死锁是一种较易实现的方法,已被广泛使用。但是由于所施加的限制条件往往太严格,可能会导致系统资源利用率和系统吞吐量降低。
2) 避免死锁。
该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。
3)检测死锁。
这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,此方法允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,然后采取适当措施,从系统中将已发生的死锁清除掉。
4)解除死锁。
这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。