前言:进程和线程的区别
- 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。
- 线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。
- 线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
- 多进程是指操作系统能同时运行多个任务(程序)。
- 多线程是指在同一程序中有多个顺序流在执行。
Java实现多线程的方法:继承Thread类,实现Runable接口
class Thread1 extends Thread {
public void run () {
//TODO
}
}
class Thread2 implements Runnable {
public void run () {
//TODO
}
}
public class Main {
....
new Thread1().start(); //继承Thread运行
new Thread(new Thread2()).start(); //实现Runnable运行
....
}
调用start
方法后,并不能立即启动线程,而是把线程变成可运行状态(Runnable),而且各个线程运行顺序不确定,重复调用start方法,会出现java.lang.IllegalThreadStateException异常。
实现Runnable接口比继承Thread类所具有的优势:
- 避免java单继承的限制。
- 容易实现资源共享。
线程状态转换
- 新建状态(new):新创建一个线程。
- 就绪状态(Runnable):调用线程对象的
start
方法后,状态就位于可运行线程池中,变得可运行,等待获取CPU使用权。 - 运行状态(Running):获取CPU后,执行代码。
- 阻塞状态(Blocked):因为某中国原因放弃CPU使用权,暂时停止运行,直到线程进入就绪状态,才有机会进入运行状态,阻塞情况分为三种:
- 等待同步:运行的线程执行
wait()
方法,JVM会把线程放入等待池中。- 同步阻塞:运行的线程若获取不到同步锁时,JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行
sleep()
或join()
或发出I/O请求,JVM会把线程置为阻塞状态,当sleep()
状态超时,join()
等待线程终止或者超时,或者I/O执行完毕,线程就重新转入就绪状态。
- 死亡状态(Dead):线程执行完了或者因异常退出了
run
方法,该线程结束生命周期。
线程调度
- 优先级:Java线程有优先级,优先级高的有机会获得更多的运行机会。
- 线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞,睡眠状态结束后,转为就绪状态。
- 线程等待:Object.wait(),会导致当前线程等待,直到其他线程调用该线程的notify()方法或notifyAll()唤醒方法,这两个唤醒方法也是Object类中方法。
- 线程让步:Thread.yield(),暂停当前正在执行的线程对象,把机会让给其他线程,线程状态直接变成可运行。
- 线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个线程运行结束,当前线程在由阻塞状态变为就绪状态。
- 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
常用函数说明
- Thread.sleep(long millis):在指定的毫秒数内让当期正在执行的线程休眠(暂停执行)。
- join():等待其他线程完毕,是Thread类的一个方法。
使用方式:
Thread t = new Thread(); t.start(); t.join();
什么时候使用join()?
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
- yield():暂停当前正在执行的线程对象,并执行其他线程。
Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。 - interrupt():中断某个线程,这种结束方式比较粗暴,如果t线程打开了某个资源还没来得及关闭也就是run方法还没有执行完就强制结束线程,会导致资源无法关闭 。(不建议使用)
- Object.wait()
Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。
wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。
线程同步
- 线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏。
- 线程同步方法是通过锁来实现,每个对象都有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法。
- 对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
- 对于同步,要时刻清醒在哪个对象上同步,这是关键。
- 编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
- 当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
- 死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。
线程数据传递
- 通过构造方法:在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。
- 通过变量和方法:在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。
- 实现Callable接口,根
run()
一样,不同的是call()
有返回值,返回类型应该跟implements Callable<T>的泛型类型一致。