Java的多线程
用多线程的只有一个目的,那就是更好的利用CPU资源.
Java给多线程编程提供了内置的支持.一条线程值的是进程中的一个单一顺序的控制流,一个进程可以并发多个线程,每条线程执行不同的 任务.
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销
- 多线程:指的是这个程序(一个进程)运行时产生不止一个线程
- 并行:多个CPU实例或者多套机器同时执行一段处理逻辑,是真正的同时
- 并发:通过CPU调度算法.让用户看上去同时执行,实际上从cpu操作层面不是真正的同时.并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
- 线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:
- 同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。
线程的生命周期
线程是一个动态的执行过程,从一个产生到死亡的过程
新建状态:
使用new关键字和Thread类或其子类建成一个线程对象后,该对象处于新建状态.它保持这个状态知道程序start()这个线程
就绪状态:
当线程对象调用start()方法之后,该线程就进入就绪状态.就绪状态的线程处于就绪队列和中,要等待JVM里线程调度气的调度
运行状态:
如果就绪状态的线程获取CPU资源,就可以执行run(),此时线程便处于运行状态,此时状态最为复杂,可以为阻塞,就绪和死亡状态
阻塞状态:
如果一个线程执行了sleep(睡眠),suspend(挂起)等方法,失去锁占用资源之后,该线程就从运行状态进入阻塞状态.在睡眠时间已到或获得设备资源后可以重新进入就绪状态.可以分为三种
1.等待阻塞:运行状态中线程执行wait()方法,是线程进入到等待阻塞状态.
2.同步阻塞:线程在获取synchronized 同步锁失败(因为同步锁被替他线程占用)
3.其他阻塞:通过调用线程的sleep()或join()发出了I/O请求是,线程就会进入到阻塞状态.当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态
一个运行状态的线程完成任务或者其他终止条件发射时,改线程就切换到终止状态
线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
创建一个线程
Java提供三种创建方式
- 通过实现Runnable接口
- 通过继承Thread类本身
- 通过Callable和Future创建线程
一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享
通过继承Thread来创建线程
Thread类是一个具体的类,即不是抽象类,该类封装了线程的行为.要创建一个线程,要创建一个线程,必须创建一个从Thread类导出的新类.必须覆盖Thread的run函数完成有用的工作.用户并不直接调用此函数;而是必须调用Thread的start()函数,改函数在调用run();
import java.util.*;
public clas TimePrinter extends Thread{
public int pauseTime;
public String name;
public TimePrinter(int x, String n){
pauseTime = x;
name = n;
}
public void run(){
while(true){
try{
System.out.println(name + ":" + new Date(System.currentTimeMillis()));
Thread.sleep(pauseTime);
}catch(Exception e){
System.out.println(e);
}
}
}
public static void main(String args[]){
TimePrinter tp1 = new TimePrinter (1000, "Fast Guy");
tp1.start();
TimePrinter tp2 = new TimePrinter(3000, "Slow Guy");
tp2.start();
}
}
在本例中,我们可以看到一个简单的程序,它按两个不同的时间间隔(1 秒和 3 秒)在屏幕上显示当前时间。这是通过创建两个新线程来完成的,包括 main() 共三个线程。但是,因为有时要作为线程运行的类可能已经是某个类层次的一部分,所以就不能再按这种机制创建线程。虽然在同一个类中可以实现任意数量的接口,但 Java 编程语言只允许一个类有一个父类。同时,某些程序员避免从 Thread 类导出,因为它强加了类层次。对于这种情况,就要 runnable 接口。
Runable接口
此接口只有一个函数,此函数必须由实现了此接口的类实现。但是,就运行这个类而论,其语义与前一个实例稍有不同
import java.util.*;
class TimePrinter
implements Runnable {
int pauseTime;
String name;
public TimePrinter(int x, String n) {
pauseTime = x;
name = n;
}
public void run() {
while(true) {
try {
System.out.println(name + ":" + new
Date(System.currentTimeMillis()));
Thread.sleep(pauseTime);
} catch(Exception e) {
System.out.println(e);
}
}
}
static public void main(String args[]) {
Thread t1 = new Thread (new TimePrinter(1000, "Fast Guy"));
t1.start();
Thread t2 = new Thread (new TimePrinter(3000, "Slow Guy"));
t2.start();
}
}
请注意,当使用 runnable 接口时,您不能直接创建所需类的对象并运行它;必须从 Thread 类的一个实例内部运行它。许多程序员更喜欢 runnable 接口,因为从 Thread 类继承会强加类层次。
synchronized关键字
在大多数有用的程序中,线程之间通常有信息流,试考虑一个金融应用程序,它有一个 Account 对象
一个银行中多项活动
public class Account{
String holderName;
float amount;
public Account(String name,float amt){
holderName = name;
amount = amt;
}
public void deposit(float amt){
amount += amt;
}
public void withdraw(float amt){
amount -=amt
}
public falot checkBalnace(){
return amount;
}
}
在此代码样例中潜伏着一个错误。如果此类用于单线程应用程序,不会有任何问题。但是,在多线程应用程序的情况中,不同的线程就有可能同时访问同一个 Account 对象,比如说一个联合帐户的所有者在不同的 ATM 上同时进行访问。在这种情况下,存入和支出就可能以这样的方式发生:一个事务被另一个事务覆盖。这种情况将是灾难性的。但是,Java 编程语言提供了一种简单的机制来防止发生这种覆盖。每个对象在运行时都有一个关联的锁。这个锁可通过为方法添加关键字 synchronized 来获得。这样,修订过的 Account 对象(如下所示)将不会遭受像数据损坏这样的错误:
对一个银行中的多心啊活动进行同步处理
public class Accout{
String holderName;
float amount;
public Account(String name, float amt){
holderName = name;
amount = amt;
}
public synchronized void deposit(float amt){
amount += amt;
}
public synchronized void withdraw(float amt){
return amount;
}
}
deposit() 和 withdraw() 函数都需要这个锁来进行操作,所以当一个函数运行时,另一个函数就被阻塞。请注意, checkBalance() 未作更改,它严格是一个读函数。因为 checkBalance() 未作同步处理,所以任何其他方法都不会阻塞它,它也不会阻塞任何其他方法,不管那些方法是否进行了同步处理。
Java编程语言中的高级多线程支持
线程组:
线程是被个别创建的,但可以将它们归类到线程组中,以便于调试和监视。只能在创建线程的同时将他与一个线程组相关联.在使用大量线程的程序中,使用线程组组织线程可能很有帮助.可以将他们看作是计算机上的目录和文件结构.
线程间的发信
当线程在继续执行前需要等待一个条件时,仅有synchronized关键字是不够的.虽然synchronized关键字阻止并发更新一个对象,但它没有实现,线程间发信.
Object类为此提供了三个函数:wait() ,notify()和notifyAll();以全球气候预测程序为例。这些程序通过将地球分为许多单元,在每个循环中,每个单元的计算都是隔离进行的,直到这些值趋于稳定,然后相邻单元之间就会交换一些数据。所以,从本质上讲,在每个循环中各个线程都必须等待所有线程完成各自的任务以后才能进入下一个循环。这个模型称为 屏蔽同步,下例说明了这个模型:
public class BSync{
int totalThreads;
int currentThreads;
public BSync(int x){
totalThread = x;
currentThread= 0;
}
public synchronized void waitForAll(){
currentThreads++;
if(currentThreads < totalThreads){
try{
wait();
}catch(Exception e){}
}else {
currentThreads = 0;
notifyAll();
}
}
}
当对一个线程调用 wait() 时,该线程就被有效阻塞,只到另一个线程对同一个对象调用 notify() 或 notifyAll() 为止。因此,在前一个示例中,不同的线程在完成它们的工作以后将调用 waitForAll() 函数,最后一个线程将触发 notifyAll() 函数,该函数将释放所有的线程。第三个函数 notify() 只通知一个正在等待的线程,当对每次只能由一个线程使用的资源进行访问限制时,这个函数很有用。但是,不可能预知哪个线程会获得这个通知,因为这取决于 Java 虚拟机 (JVM) 调度算法。
将 CPU 让给另一个线程
当线程放弃某个稀有的资源(如数据库连接或网络端口)时,它可能调用 yield() 函数临时降低自己的优先级,以便某个其他线程能够运行
守护线程
有两类线程:用户线程和守护线程。 用户线程是那些完成有用工作的线程。 守护线程 是那些仅提供辅助功能的线程。Thread 类提供了 setDaemon() 函数。Java 程序将运行到所有用户线程终止,然后它将破坏所有的守护线程。在 Java 虚拟机 (JVM) 中,即使在 main 结束以后,如果另一个用户线程仍在运行,则程序仍然可以继续运行。
调试线程化的程序
在线程化的程序中,可能发生的某些常见而讨厌的情况是死锁,活锁,内存损耗和资源耗尽
死锁:( 所谓死锁: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进
程称为死锁进程。)
死锁可能是多线程程序最常见的问题.当一个线程需要一个资源而另一个线程持有该资源,就会发生死锁.这种情况通常很难检测;但是解决方法相当好:在所有线程中按相同的次序获取所有资源锁.例如,如果四个资源-A,B,C,和D,并且一个线程可能要获取四个资源中任何一个资源的锁,则请确保在获取对 B 的锁之前首先获取对 A 的锁,依此类推。如果“线程 1”希望获取对 B 和 C 的锁,而“线程 2”获取了 A、C 和 D 的锁,则这一技术可能导致阻塞,但它永远不会在这四个锁上造成死锁。
活锁:
当一个线程忙于接受新任务以至于他永远没有机会完成任何任务时,就会发生活锁.这个线程最终超出缓冲区并导致程序崩溃.
内存损耗:
如果明智地使用 synchronized 关键字,则完全可以避免内存错误这种气死人的问题;
资源耗尽:
某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线程数远远超过了可用的资源数,则最好使用 资源池。一个最好的示例是数据库连接池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为 资源库。
调试大量的线程
有时一个程序因为有大量的线程在运行而极难调试。在这种情况下,下面的这个类可能会派上用场:
public class Probe extends Thread{
public Probe(){}
public void run(){
while(true){
Thread[] x = new Thread[100];
Thread.enumerate(x);
for(int i = 0; i<100; i++){
Thread t = x[i];
if(t==null)
break;
else
System.out.println(t.getName() + "\t" + t.getPriority()
+ "\t" + t.isAlive() + "\t" + t.is
}
}
}
}
PS:
- 使用线程不会增加CPU的能力。如果使用JVM的本地线程实现,则不同的线程可以在不同的处理器上同时运行(在CPU)的机器中。从而使多CPU机器得到充分利用
- 如果应用程序是计算密集型,并接受CPU功能的制约,则只有多 CPU 机器能够从更多的线程中受益。
- 当应用程序必须等待缓慢的资源(如网络连接或数据库连接)是,或者当应用程序是非交互式的时,多线程通常是有利的
- 上下文的切换开销也很重要,如果你创建了太多的线程,CPU 花费在上下文的切换的时间将多于执行程序的时间
基于Intern阿特的软件有必要是多线程的;否则,用户将感觉应用程序反映迟钝。例如,当开发要支持大量客户胡的服务器是,多线程可以使编程较为容易。在这种情况下,每个吸纳从可以为不同的客户或客户组服务。从而缩短响应的时间。