进程是处于运行过程中的程序,具有一定独立功能,它是系统进行资源分配和调度的一个独立单位。
继承Thread类创建线程类:
1定义Thread类的子类,重写该类的run方法,该run方法的方法体就代表了线程需要完成的任务。
2创建Thread类的子类的实例,也就是创建了线程对象。
3用线程对象的start方法来启动该线程。
例子:
public class FirstThread extends Thread { private int i; //重写run方法,该run方法的方法体就代表了线程需要完成的任务。 public void run() { for(;i<100;i++) { //用getName方法返回当前线程名 System.out.println(getName()+" "+i); } } public static void main(String[] args) { for(int i=0;i<100;i++) { //调用Thread类的静态currentThread方法获取当前线程 System.out.println(Thread.currentThread().getName()+" "+i); if(i==20) { //创建并启动两条线程 new FirstThread().start(); new FirstThread().start(); } } } }
在此方法中,新创建的两条线程他们之间无法共享线程类的实例变量也就是i。
实现Runnable接口创建线程类:
1定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体就代表了线程需要完成的任务。
2创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,也就是真正的线程对象。(需要说明的一点是,在java中,Thread必须使用Runnable对象的run方法作为线程执行体)
3调用线程对象的start方法来启动线程。
例子:
public class SecondThread implements Runnable { private int i; public void run() { for(;i<100;i++) { //这里不能直接使用getName方法 System.out.println(Thread.currentThread().getName()+" "+i); } } public static void main(String[] args) { for(int i=0;i<100;i++) { System.out.println(Thread.currentThread().getName()+" "+i); if(i==20) { SecondThread st = new SecondThread(); new Thread(st).start(); new Thread(st).start(); } } } }
我们将上述两种创建线程的方式做一个对比:
采用实现Runnable接口的创建方式由于它只是实现了runnable接口,所以还可以继承其他类,在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况。但是这种方法稍稍复杂,如果我们想访问当前线程不得不使用thread.currentThread()方法。
通过继承Thread类来创建线程的方式,由于已经继承了Thread类,所以不能再继承其他父类,但这种方式编写简单,如果我们想要访问当前线程,只需直接使用this即可获得当前线程。但是实际上所有的多线程应用都采用实现Runnable接口的方法。
接下来我们谈谈线程的生命周期:
在线程的生命周期中,包括五种状态,分别是:新建(New),就绪(Runnable),运行(Running),阻塞(Blocking),和死亡(Dead)。下面就分别介绍这五种状态。
当我们用new关键字创建了一个线程后,该线程就处于新建状态,此时仅仅由java虚拟机为其分配了内存,并初始化了其成员变量的值。
当线程对象调用了start方法(注意是start方法而不是run方法,如果你直接调用线程对象的run方法,则系统就会把线程对象当做一个普通的对象,run方法也就成了一个普通的方法而不是线程执行体。如果一个线程已经处于启动状态,则不能再次调用start方法,否则会引发IllegalThreadStateException异常。)后,该线程就处于了就绪状态,java虚拟机会为其创建方法调用栈和程序计数器,但此时线程并没有开始运行,它只是表示其可以运行了。
此时,如果处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,那么这个线程就处于了运行状态,但线程不可能一直处于运行状态,它需要被中断以使得其他线程得到执行的机会。对于采用抢占式策略的系统而言,系统会给每个线程一个小时间段来处理任务,当该时间段用完,系统就会剥夺该线程占据的资源,让其他线程获得执行的机会。为了使其他线程获得执行的机会,我们需要阻塞当前正在执行的线程,而这个被阻塞的线程会在合适的时候重新进入就绪状态(注意不是运行状态),也就是说当它的阻塞解除后,必须重新等待线程调度器再次调度它。
发生如下情况线程会进入阻塞状态:
线程调用sleep方法主动放弃所占用的处理器资源。
线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
线程在等待某个通知(notify)。
程序调用了线程的suspend方法将该线程挂起。(此法易导致死锁,应避免使用)
针对上面的几种情况,当发生如下的几种情况可解除阻塞,让线程重新进入就绪状态:
调用sleep方法的线程经过了指定时间。
线程调用的阻塞式IO方法已经返回。
线程成功获得了试图取得的同步监视器。
线程正在等待某个通知时,其他线程发出了一个通知。
处于关起状态的线程被调用了resume恢复方法。
线程死亡:1run()方法执行完毕,线程正常结束。2线程抛出一个未捕获的Exception或Error。3直接调用该线程的stop方法结束该线程。(易导致死锁,不推荐使用)我们可以使用线程对象的isAlive方法测试某条线程是否死亡,只有当线程处于新建和死亡两种状态时,isAlive方法会返回false。如果一个线程已死,就不要再对他调用start方法,否则会引发IllegalThreadStateException异常,start方法只适用于新建状态的线程,且只能使用一次start方法。
Join线程:当在某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join方法加入的join线程完成为止。
例子:
public class JoinThread extends Thread { private int i; public JoinThread(String name) { super(name); } public void run() { for(;i<100;i++) { System.out.println(getName()+" "+i); } } public static void main(String[] args)throws Exception { new JoinThread("新线程").start(); for(int i=0;i<100;i++) { if(i==20) { JoinThread jt = new JoinThread("被join的线程"); jt.start(); //main线程调用了jt线程的join方法,main线程必须等jt执行结束后才会继续向下执行 //还有一个带参的join方法:join(long millis),单位是毫秒 jt.join(); } System.out.println(Thread.currentThread().getName()+" "+i); } } }
后台线程:它在后台运行,为其他线程提供服务,又称守护线程或精灵线程。如果所有前台线程死亡,后台线程会自动死亡。
例子:
public class DaemonThread extends Thread { public void run() { for(int i=0;i<1000;i++) { System.out.println(getName()+" "+i); } } public static void main(String[] args) { DaemonThread dt = new DaemonThread(); //将此线程设置为后台线程,必须要先设为后台线程才能启动否则会引发IllegalThreadStateException异常 dt.setDaemon(true); dt.start();//启动后台线程 for(int i=0;i<10;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } } }
线程睡眠sleep:这是一个静态方法,它有两种重载的形式,static void sleep(long millis)和static void sleep(long millis,int nanos),此方法可以让当前正在执行的线程暂停一段时间,并进入阻塞状态。当一线程调用sleep方法进入阻塞状态后,在其sleep时间段内,该线程不会获得执行的机会,即使系统中没有其他可以执行的线程。我们可以写一个小程序测试下:
public class DaemonThread { public static void main(String[] args)throws Exception { for(int i=0;i<10;i++) { System.out.println("当前时间:"+new Date()); Thread.sleep (1000); } } }
我们会看到上面的程序会没隔一秒输出一串字符串。
线程让步yield:它也是Thread类的一个静态方法,它会让当前正在执行的线程暂停,但不会阻塞该线程,只是将该线程转入就绪状态。只有优先级与当前线程相同或比当前线程高的已经进入就绪状态的线程才会获得执行的机会,而sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级。Sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态,而yield会强制将当前线程进入就绪状态。Sleep方法会抛出IllegalThreadStateException异常,而yield不会抛出异常。我们通常不要依靠yield来控制并发线程的执行,sleep方法比yield有更好的可移植性。
每个线程在执行时都有一定的优先级,优先级高的线程获得较多的执行机会,相反则少。每个线程默认的优先级都与创建它的父线程具有相同的优先级,默认情况下,main线程具有普通优先级。我们可以通过setPriority(int newPriority)和getPriority()两个方法来设置和获取指定线程的优先级,newPriority是一个取值在1-10之间的整数,也可以取Thread类的三个静态常量:MAX_PRIORITY(10)MIN_PRIORITY(1)和NORM_PRIORITY(5)。
例子:
public class PriorityTest extends Thread { public PriorityTest(){} public PriorityTest(String name) { super(name); } public void run() { for(int i=0;i<50;i++) { System.out.println(getName()+",的优先级是:"+getPriority()+",循环变量的值是:"+i); } } public static void main(String[] args) { //改变主线程优先级为6 Thread.currentThread().setPriority(6); for(int i=0;i<30;i++) { if(i==10) { PriorityTest low = new PriorityTest("低优先级"); low.start(); System.out.println("创建之初的优先级为:"+low.getPriority()); low.setPriority(MIN_PRIORITY); } if(i==20) { PriorityTest high = new PriorityTest("高优先级"); high.start(); System.out.println("创建之初的优先级为:"+high.getPriority()); high.setPriority(MAX_PRIORITY); } } } }
线程的同步*
线程同步的方法有三种,分别是同步代码块,同步方法和同步锁(Lock)。
为了解决并发可能造成的异常,java的多线程支持引入了同步监视器,使用同步监视器的通用方法就是同步代码块。其语法格式如下:
Synchronized(obj)
{
…此处的代码就是同步代码块
}
obj既是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。任何时刻只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监视器的锁定。Java允许使用任何对象来作为同步监视器,但我们建议使用可能被并发访问的共享资源作为同步监视器。
同步方法就是用synchronized关键字来修饰某个方法,此方法无需显示指定同步监视器,同步方法的同步监视器是this也就是对象本身。对于一个可变类,他的对象就需要额外的方法来保证其线程安全。例如一个账户类,它的余额属性是可以改变的,当两个线程同时修改这个余额属性时就可能引发异常,我们此时就需要将账户类中可以修改余额的那个方法修改成同步方法,也就是:
public synchronized void change(double yu^e){}
由于同步方法的同步监视器是this,因此对于同一个账户而言,任何时刻只能有一条线程获得对账户对象的锁定,然后进入这个change方法进行取钱等操作,这也就保证了多条线程并发取钱的线程安全。
同步锁通过显示定义同步锁对象来实现同步,在实现线程安全的控制中,通常喜欢使用可重入锁就是ReentrantLock。代码格式如下:
class x { //定义锁对象 Private final ReentrantLock lock = new ReentrantLock(); //… //定义需要保证线程安全的方法 Public void m() { //加锁 lock.lock(); try { //需要保证线程安全的代码 //。。。method body } //使用finaly块来保证释放锁 finaly { lock.unlock(); } } }
死锁:当两个线程相互等待对方释放同步监视器时,就会发生死锁。看如下代码:
class A { public synchronized void foo(B b) { System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入了A实例的foo方法"); try { Thread.sleep(200); } catch(Exception e) { e.printStackTrace(); } System.out.println("当前线程名:"+Thread.currentThread().getName()+"企图调用B实例的last方法"); b.last(); } public synchronized void last() { System.out.println("进入了A类的last方法内部"); } } class B { public synchronized void bar(A a) { System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入了B实例的bar方法"); try { Thread.sleep(200); } catch(Exception e) { e.printStackTrace(); } System.out.println("当前线程名:"+Thread.currentThread().getName()+"企图调用A实例的last方法"); a.last(); } public synchronized void last() { System.out.println("进入了B类的last方法内部"); } } public class DeadLock implements Runnable { A a = new A(); B b = new B(); public void init() { Thread.currentThread().setName("主线程"); a.foo(b); System.out.println("进入主线程后"); } public void run() { Thread.currentThread().setName("副线程"); b.bar(a); System.out.println("进入副线程后"); } public static void main(String[] args) { DeadLock dl = new DeadLock(); new Thread(dl).start(); dl.init(); } }
线程的协调运行:
Object类提供了wait(),notify()和notifyAll()三个方法,但这三个方法并不属于Thread类,我们可以由同步监视器对象调用这三个方法来实现线程的通信。由上面的内容可知,对于使用synchronized修饰的同步方法,同步监视器就是this,因此在方法体中直接调用这三个方法,对于使用synchronized修饰的方法块,我们必须使用synchronized后括号里的对象来调用这三个方法。
wait():导致当前线程等待,直到其他线程调用该同步监视器的notify或nitifyAll方法来唤醒该线程。
notify():唤醒在此同步监视器上等待的单个线程。
notifyAll:唤醒在此同步监视器上等待的所有线程。
例如,如果我们想实现一种存钱取钱交替运行的功能,那么我们可以先通过一个旗标flag来表示账户中是否有存款,有为true没有为false,再定义一个存钱的deposit方法和一个取钱的draw方法,我们的draw方法就可以写为:
public synchronized void draw(double drawAmount) { Try { //先看是否有人存钱进去 If(!flag) { wait();//没有人存钱进去阻塞取钱方法 } else { …//执行取钱操作,最后将flag设为false并唤醒其他线程 flag = false; notifyAll(); } } catch(Exception e) { e.printStackTrace(); } }
同样deposit方法与上面类似。
如果我们的程序是使用Lock对象来保证同步的,则我们不能再使用上面三个方法来协调线程的运行。此时我们要用到一个Condition类,此类提供了三个方法await(),signal()和signalAll(),在这之前我们还要调用Lock对象的newCondition方法来获得特定Lock实例的Condition实例,然后使用这个Condition实例来控制线程的协调运行。还是上面那个例子:
public class Account { //显示定义Lock对象 private final Lock lock = new ReentrantLock(); //获得指定Lock对象对应的条件变量 private final Condition cond = lock.newCondition(); …… puclic void draw(double drawAmount) { lock.lock();//加锁 Try { //先看是否有人存钱进去 If(!flag) { cond.await();//没有人取钱进去阻塞取钱方法 } else { …//执行取钱操作,最后将flag设为false并唤醒其他线程 flag = false; cond.signalAll(); } } catch(Exception e) { e.printStackTrace(); } finally { lock.unlock();//使用finally块来确保释放锁} } }
第三种实现线程通信的方式为管道流,但是通常没有必要使用管道流来控制两个线程之间的通信,因为两个线程属于同一个进程,他们可以非常方便的共享数据,这种方式才是线程之间进行信息交换的最好方式,而不是管道流。
线程组的概念:java中使用ThreadGroup来表示线程组,他可以对一批线程进行分类管理。在Thread类中提供了如下几个构造方器来设置创建的线程属于哪个线程组:Thread(ThreadGroup group,Runnable target)就是以target的run方法作为线程执行体创建新线程,属于group线程组,Thread(ThreadGroup group,Runnable target,String name), Thread(ThreadGroup group,String name)。我们在中途是不可以改变线程所属的组的,但是我们可以调用getThreadGroup方法来返回该线程所属的线程组,此方法返回的是一个ThreadGroup的对象。ThreadGroup类有两个构造器分别是ThreadGroup(String name)和ThreadGroup(ThreadGroup parent,String name)parent为父线程组。线程组总是有一个字符串名字,我们可以调用getName方法获得,没有setName方法因为不允许改变线程组名字。常用方法有:
int activeCount():放回线程组中活动线程的数目
interrupt():中断此线程组中所有线程
isDaemon():判断是否是后台线程组
setDaemon(boolean daemon):设置为后台线程组
setMaxPriority(int pri):设置线程组的最高优先级
Callable和Future:
Java提供了Callable接口,类似于Runnable接口的增强版,Callable接口提供了call方法可以作为线程执行体,它与run方法不同之处就是call方法可以有返回值,可以抛出异常。为了获得call方法的返回值,我们有一个FutureTask类,这个类是Future接口的一个实现类,而Future接口就代表了call方法的返回值,因此我们可以用FutureTask类的一个实例来作为Thread类的target。
步骤如下;
1 创建Callable接口的实现类,并实现call方法,该call方法将作为线程执行体,且有返回值。
2创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了call方法的返回值。
3使用FutureTask对象作为Thread对象的target来创建并启动线程。
4调用FutureTask对象的方法来获得call方法的返回值。
例子:
class A implements Callable<Integer> { Public Integer call() { …… return i; } } public class CallableTest { Public static void main(String[] args) { A a = new A();//创建Callable对象 FutureTask<Integer> task = new FutureTask<Integer>(a);//使用FutureTask来包装Callable对象 new Thread(task,”name”).start(); System.out.println(task.get());//使用get方法来获取线程返回值也就是call方法的返回值 } }