Java中多线程技术
Java中的多线程技术剖析
1、进程和线程
进程:就是当一个应用程序被加载到内存当中并准备运行时,我们就说创建了一个进程
线程:一个应用程序被分成多个更小的执行单元,例如:有的单元负责UI交互、有的单元进行后台的数据处理、有的进行文档的存取等,这些小的执行单元就被称为线程
当这些线程能够各自独立的运行自己的代码,而不受其他的进程的干扰时,应用程序的效率就会提高,并且能够给用户流畅的体验
2、线程优先级
在操作系统这一级来说,所有的线程都是有优先级的,每种优先级的线程都有各自的一个队列,如:

①对于同一个优先级的队列中的线程而言,CPU会为每个线程分配一个时间片段用来执行这些线程,当一个线程(设为进程1)的时间片段用完,但是该线程还没有结束时,CPU就会为下一个线程(设为进程2)分配时间片段来执行线程2,而线程1只能被插到队尾,等待再次轮到它时,才能够继续执,重复这个过程,直到所有的线程全被执行完毕;
②上面的执行方式,CPU要从优先级最高的队列开始,当优先级最高的队列中所有的线程被执行完毕时,才能够轮到下一个优先级队列
③当然,上面的执行过程是最基本的过程;实际当中,操作系统会根据每个线程从上一次被执行到现在所搁置的时间的长短,来实时的提升这些线程的优先级,这样一来就不会造成某些优先级低的线程被饿死
④实际问题是,现在的计算机都是多核的,也就说,多个线程是可能在不同的CPU核上同时执行,这就会因为并发而引发同步处理问题,下面会介绍
⑤从操作系统调用线程的方式上看,分为两种:
一种是:抢占式调度
CPU会为每个线程分配时间片段,当一个线程正处在被执行的时间片段中时,如果这时程序恰好创建了优先级比正在执行的这个线程的优先级更高的线程或者某个线程的优先级被操作系统临时提升到更高的优先级上,那么这时刚刚出现的这个有限级更高的线程就会马上取代正在运行的线程,这中方式能够成分的利用CPU资源
另一种是:非抢占式调度
这是一种较早的调度方式,现在几乎不用了
这种调度方式是,当一个线程被CPU的时间片段执行时,即使当前出现了更高级别的线程,当前被执行的线程也不会被马上抢占,必须等到他的这个时间片段被用完时,那个更高级别的线程才能够被CPU的时间片段执行
3、JVM是如何处理线程的调度问题的:
在早期的java中,使用的是一种称之为“绿色线程”的方式,所谓的绿色线程是:java程序中所有的线程的调度全部由java虚拟机JVM来实线,也就是说,线程调度的所有操作,全部在操作系统的层面的上部(——用户级别),而没有通过操作系统(——系统级别)来实现线程的调度;
现在的java中采用的方式是:将java中设定的线程的各种优先级和操作系统中预设的优先级对应起来,形成一种映射关系,这样一来java的线程调度就服从于底层的操作系统的线程调度了
————————————————————————《》现在开始正式介绍java的多线程处理技术:
1、在java中,线程也是用类实现的,要想创建一个线程对象,就必须先创建一个线程类,构造线程类的方法有两种:
①通过构造java.lang.Thread的子类
之后覆盖Thread中的run()方法,创建的线程的对象执行的就是run()中的内容,当其中的内容执行完毕后,该线程也就结束了;
如 class MyThread extends Thread{
public void run(){
………………………
………………………}
}
MyThread mth = new MyThread() ;
mth.start() ;
②通过实现java.lang.Runnable接口
实际上java.lang.Thread也实现了这个接口;
这个结口中只有一个抽象方法:public void run();
只要实现这个方法就行了,
如:class MyThread implements Runnable{
public void run(){
……………………….
}
}
MyThread mth = new MyThread() ;
Thread th = new Thread(mth) ;
th.start();
注意:《》虽然说,线程的执行实际上就是对run()方法的执行,但是绝对不能够直接调用run()方法,因为直接调用的话实际上就会马上这个线程,这就失去了多线程的功能了,调用start方法的原理是,当调用start方法后,创建的线程就被启动,这时该线程就会被加入到线程队列中,有系统决定何时调度这个进程,当调度这个进程时,run()方法就会自动被执行;
《》还有一点是,在实际问题中到底应该使用哪种方式创建一个线程呢?实际上包含着这样一层意思:直接继承Thread创建的对象,意思是创建的这个对象是run()任务的执行者;而实现Runnable()接口创建的对象,意思只是说这个对象只是定义了一项任务,这个任务的执行者还要通过Thread对象来完成,所以才有上面的代码:
MyThread mth = new MyThread() ;
Thread th = new Thread(mth) ;
但是当某些特殊情况下就必须使用第二种方法:
因为java中的类是不能够实现多继承的,这就说,如果一个类已经继承了某个类,他就不能够在继承Thread类了,这时只能够通过第二种方法来将之变成一个线程类
2、java中的线程分成两类:守护线程和非守护线程
守护线程又称为后台线程;非守护线程又称为用户线程
①守护线程满足这样的规则:当一个守护线程的父线程结束时,那么这个守护线程就会自动的随之结束;
②非守护线程不同,当它的父线程结束时,这个非守护线程并不会随之结束,而是继续执行,直到它本身被执行完毕
③程序中创建的线程默认情况下都是非守护线程;
可以通过java.lang.Thread类中的成员方法:
public final boolean inDaemon();
来判定一个线程是否为守护线程
java.lang.Thread中的方法:
public void setDaemon(boolean b) ;
能够设定当前线程的属性,true—设为守护线程
要注意的是,只能够在调用start()方法之前改变当前线程的属性,一旦线程被启动,就不够在修改他的属性,否则会抛出异常;
3、注意这样一个问题:
在Thread的构造方法中,有的构造方法中含有参数
String name ;这个参数是用来设定创建的线程的名字的,
你可能会问,比如:
上面的第一种构造方法中
MyThread mth = new MyThread() ;
mth.start();
这个mth不就是创建的线程的名字吗?实际上不是,因为mth只是线程的对象的引用,并不是线程对象的名字,我们可以使用任何MyThread类型的引用变量来指向一个线程;
我们想这样一个问题,如果在创建线程时,并不把线程对象的引用赋给mth,即:
new Thread() ;
那么创建这个线程对象也一定会被加入到系统的线程队列中,但是我们并没有给这个线程对象名字,如果我们在该线程的执行过程中,不需要操作它还好,但是如果我们需要在执行这个线程的过程中,操作这个线程(如中断它),我们就必须得到这个线程,但是这个线程没有名字,我们如何得到他呢,这就是一个问题;
实际上,在调用构造方法时如果没有指定创建的线程的名字的话,程序会自动的为之赋予一个名字,如果该线程是第n个被创建的话,那么就命名为 “Thread-n”;
所以最好为线程设置名字;
Thread的构造方法有很多,如:
public Thread()
public Thread(String name)
public Thread(Runnable target)
public Thread(Runnable target, String name)
4、在java之前的版本中有一个叫做线程组的类java.lang.ThreadGroup,但是就像《thinking in java》中说的那样:这个类是一次失败的尝试
5、Thread类中有一个静态的方法:
publicstaticnative Thread currentThread();
这个方法返回当前所处位置处运行的线程对象的引用,这个方法非常的有用
6、Thread类中有一个静态方法:
public static void sleep(long millis)
throws InterruptedException
当调用这个方法时,那么该语句所在的线程就会暂时停止运行,进入休眠状态,再过了指定的休眠时间之后,该线程就会继续运行,即开始执行在导致进入睡眠态的sleep语句之后的语句;需要注意的是:
①这个方法是一个静态方法,要通过类名直接调用;
②这个方法虽然与使用循环语句会产生相同的效果,但是,这两种方法有本质的区别,因为使用sleep后线程实际上是不会占用CPU的,即当前的线程是一种freeze状态;但是使用循环的话,CPU还在被当前的线程占用
③还有一点非常的重要:使用这个方法,虽然会使线程处于一种冻结状态,但是它并不会释放对象锁,也就是说对象锁不会因为一个线程点用了这个方法而打开;这一点在线程的并发处理上面尤其要注意
7、Thread类中的设定优先级的成员方法:
我们可以设定一个线程的优先级,为的是让那些处于高优先级的线程能够尽快的被执行完毕,但是由于不同的操作系统上对于优先级的处理方式是不同的,有时候甚至被忽略,所以优先级的设定最好只是作为一种参考因素,不能够让自己的业务逻辑依赖于优先级的高低;
public final void setPriority( int newPriority)
public final int getPriority() ;
在Thread的成员域中定义了三种优先级:
|
static int |
MAX_PRIORITY(10) |
|
static int |
MIN_PRIORITY(1) |
|
static int |
|
在java中优先级一共从1到10;除了可以用上面的静态域之外,还可直接使用1—10之间的整型数据
8、线程的中断技术,详见《Java卷 ΙΙ 》
《》线程的同步处理
1、多线程引发的问题:
由于可能多个线程同时会共享相同的资源,这就造成麻烦;比如说,如果现在有一个文件和两个线程,一个线程需要将一个数据写入这个文件,另一个线程需要从这个文件中读出数据,这就产生了一个问题,那就是到底应该先执行谁,如果按照之前将的线程调度机制,而不加以任何限定的话,程序就会在逻辑上乱套;为此产生了java的多线程的同步处理机制;所以要记住一点那就是
只有当多线程共享资源的情况下才需要进行多线程的同步处理
2、下面举几个因为多线程同步进行,而出现的问题:
实例一:



结果是:“实验数据出现了不一致的情况“
实例二:


我们预期的结果是输出:m_data= 0 ;
但是结果是不为0 的,这是因为两个线程的并发造成的;由于两个线程可能同时执行(多核CPU),这样一来如果同时要求m_data +1和-1就会造成无法正确完成命令,从而引发了问题
3、在介绍线程的同步处理之前,我们有必要将线程所处的状态介绍一下,这样有利于准确把握线程同步处理机制中的时机问题

①当创建一个线程对象时,这个线程状态就处于新生态
②当一个线程对象调用start方法时,这个线程对象就处于就绪态
③当Java虚拟机调度了一个线程对象的整个运行过程中,这线程对象就处于运行态
④当一个线程对象运行结束时,这个线程对象就处于死亡态
⑤如果某个线程调用了wait方法的话,这个线程对象就处于等待态
⑥一旦一个处于等待态的线程对象的等待时间到了,或者调用了关联对象的notifyAll、notify方法的话,这个线程对象就会进入就绪态,注意不是直接进入运行态
⑦当一个线程对象调用了sleep方法的话,那么这个线程对象就会进入睡眠态,一旦睡眠时间到了,这个线程马上就会进入运行态,继续上次的执行
⑧线程的同步处理机制是,当多个线程同时共享一个资源时,在每次运行时,只能够允许一个线程使用这个资源,这样其他的线程对象就都会被对象锁,锁在外面,在这个过程当中这些其余的线程对象都会处于对象阻塞态;
⑨需要注意的一点是:在同步处理当中,java虚拟机会为每个对象创建一个对象锁和等候集;等候集当中的所有的线程对象都是处于等待态,而不包括阻塞态;所以notify和notifyAll方法只针对于一个对象的所有的处于等待态的线程对象,而并不包括处于阻塞态的线程对象
4、在java.lang.Thread类中为我们提供了方法用来判定一个线程对象当前处于何种状态:
①public boolean isAlive() ;
//判定一个线程对象当前是否已经启动,当一个线程对象调用了start方法一直到这个线程执行完毕(包括由于产生了异常),这个过程当中,线程一直处于isAlive状态
②其他的状态的判定是通过下面的这个方法判定的:
public Thread.State getState() ;
//这个方法返回的是Thread.State类型的数据,Thread.State是一个枚举类型,其中的枚举常量分别对应的不同的线程的状态:
- NEW
A thread that has not yet started is in this state. - RUNNABLE
A thread executing in the Java virtual machine is in this state.
《就像上面写的那样,当一个线程对象调用了start方法之后,就会由JVM虚拟机接管,当然这个线程可能由于对象锁、wait、sleep等情况进入一种没有被执行的状态,除此之外,线程就会被执行,但是在线程被正常执行的过程中,还可能处于两种状态:正在运行、由于没有分配到CPU的时间片段而处于只用非运行状态,这两种状态都是在线程执行中的自然状态,这两种状态就是RUNNABLE状态,这就是不称为RUNNING的原因,但是由于CPU的高效性,使得这两个状态之间几乎没有时间上的间隔,所以可以认为当一个状态处于RUNNABLE状态时,该线程就是正在被CPU执行》
- BLOCKED
A thread that is blocked waiting for a monitor lock is in this state.
《线程在下列情况会进入阻塞状态:
①等待某个操作的返回,如I/O操作——该操作返回之前,线程是不会执行下面的代码的
②等待某个对象锁的解开,在对象锁解开之前,线程是不会继续执行的
③线程调用了sleep方法》
- WAITING
A thread that is waiting indefinitely for another thread to perform a particular action is in this state. - TIMED_WAITING
A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state. - TERMINATED
A thread that has exited is in this state.
5、多线程处理的基本原理:
java虚拟机为每个对象配备一把锁和一个等候集

①java虚拟机通过对象的锁,确保在任何同一个时刻内最多只有一个线程能够运行与该对象相关联的同步方法和同步语句块
②一旦有线程进去运行这些与对象相关联的同步方法和听不语句块时,对象锁就会自动的锁上,从而使得其他的需要进入的线程处于阻塞状态;
③如果线程执行执行完同步方法和同步语句块后并从中退出来,则对象锁就会自动的打开
④这里要注意的一点是:只有那些调用了wait方法的线程才能够进入等候集,那些从执行完毕的线程并不会进入等候集,notify和notifyAll方法只能够针对于等候集中的线程;
其次,如果某个线程在进入对象锁中的同步方法或者同步语句块中后,调用了sleep方法,这时该线程进入了睡眠状态,但是,这时的对象锁并不会被打开
6、在线程同步处理中的各种方法:
①java虚拟机唯美个对象配备一把对象锁,这里的对象包括普通的实例对象,还包括类对象,类对象是针对于类中的静态方法和静态成员域而言的,之前应经介绍过这个名词;也就是说每个类多会有一个类对象,这个类对象的应用可以通过类java.lang.Class中的静态成员方法:
public static Class forName(String className)
throws ClassNotFoundException
来获得;参数className指定对应的类或者接口的名称,返回类或者接口的引用
②一个类的静态成员域和静态成员方法隶属于这个类对象;
而非静态的域则隶属于实例对象
③wait方法:
public final void wait() throws InterruptedException
public final void wait(long times)
throws InterruptedException
public final void wait(long time , int nanos)
throws InterruptedException
其中 参数 time 单位是毫秒 ; nanos单位是微毫秒
这是用来指定当前线程的等候时间,当等候时间到了之后,线程就会进入就绪状态;但是通过使用不带参数的wait方法或者将参数全部置为0的话,当前线程只能够通过notify或者notifyAll方法来唤醒;
④基础类java.lang.Object的成员方法中:
public final void notify()
public final void notifyAll()
两个成员方法都只能够在同步方法或者同步语句块中调用,而且激活的线程只能够是在这些同步方法或者同步语句块中所关联的对象的等候集中的线程。notify()只能够随机的激活等候集中的一个线程,而notifyAll()将等候集中的所有的线程全部激活;
⑤之后将会看到,wait、notify、notifyAll方法的配合使用可以用来协调线程之间的执行顺序
《》下面通过一些实例来理解一下对象锁的工作原理:
1、在多线程处理当中的静态方法和非静态方法
如果需要将一个成员方法设为同步方法,只要给方法加上一个修饰词synchronized,如果是静态方法,则受类对象锁的控制,如果是非静态方法,则受实例对象锁的控制



分析:①从运行结果中也可以看到,实际上静态方法由类对象锁控制,非静态方法有实例对象锁控制;这两个锁之间是没有任何关联的,即两个锁互不影响
因此你会看到如下两行输出

2、在多线程同步处理中同一个实例对象拥有多个同步方法
这时对象锁会同时控制所有的非静态同步方法




从上面的图可以看出:当线程t[0]进入m_method1时对象锁就会锁上,但是一旦t[0]从m_method1中退出时,对象锁就会自动打开,而并不是等到t[0]将m_method2也执行完毕后对象锁才打开;如果我们把上面程序的
34—37行之间语的句块变成和实例对象相关联的同步语句块的话,那么上面的图中的①②两个箭头就应该去掉,也就是说,只有一个线程将m_method1和m_method2两个方法全部执行完毕后,对象锁才能够被打开;
3、同步语句块的定义与原理
①定义格式:
synchronized(引用类型的表达式)
{ 语句块 }
②()中的表达式必须是引用类型的表达式,也就是说应该是一个对象的引用才行,这就指定了,这个同步语句块由指定的对象的对象锁类控制,将语句块和该对象关联起来
③在同步语句块中,如果想让引用类型的表达式所指向的对象为类对象,可以通过类java.lang.Object的成员方法:
public static Class forName(String className)
throws ClassNotFoundException
来返回指定的类或接口的引用;
④使用类对象作为关联对象的实例:



分析:
这里Class.forName(“MyGUI”)不能够直接使用类名MyGUI代替,因为MyGUI是一个类名,而不是引用数据类型的表达式。另外,也不能够用字符串“MyGUI”代替,否则与同步语句相关的对象锁,是由字符串对象“MyGUI”控制的对象锁,虽然能够达到一样的效果,但是这样使得程序的可读性下降,失去了原来的意义;
⑤使用实例对象作为关联对象的失败实例:
在此之前,我们首先解释一下这样一个问题:
关键字this本身就是一个当前实例对象的引用,但是,在实际情况中,我们要慎用this作为关联对象,因为在程序执行的过程当中,this指向的对象可能是动态改变的,这样,就不能够起到限制多个线程同时进入同步语句块中的情形了;
比如将上面的那个程序中的引用参数表达式更改为this,同时去掉try-catch异常处理,这样一来输出的结果就不再是m_data= 0 了;
原因非常的简单,由于程序当中创建了两个线程对象t1和t2;这样一来当线程t1执行同步语句块时,this指向实例对象t1,对象锁由t1控制;但是当线程t2执行同步语句块时,this指向实例对象t2,对象锁由t2控制;所以实际上,程序并没有阻止两个线程同时执行同步语句块,所以程序的结果没有达到预期。
⑥使用实例对象作为关联对象的成功实例:




由此可见本文章的第24页上部文字的正确性
还有一点值得注意的是,这个程序非常的规范,在没个方法结束时都有注释,使得层次结构非常的明确,非常容易读懂代码,值得提倡
《》通过使用方法——wait/notify/notifyAll
来控制线程之间执行的先后顺序
①我们知道有时候线程之间的先后顺序是非常的重要的,只有一个线程完成某项工作后,另一个线程才能够开始某项工作,比如数据的处理就是这样,只有当数据更新完毕后,我们才能够对数据进行分析,这两项工作是不够同时并行的,但是仅仅保证两个线程不产生共享资源的并行冲突是不行的,必须要保证两个线程的先后顺序:
我们将文章开头介绍“因为多线程同步进行,而出现的问题”举得实例一,改造一下,使得两个线程有先后顺序






这样一来,就使得数据的更新和数据的分析交替进行,即先进行数据的更新,再进行数据的分析,并且分析的数据恰好是刚刚更新之后的数据
《》死锁问题
①java中并没有处理死锁问题的机制,要想避免死锁问题,就必须要求程序员再设计程序时,要充分考虑是否会发生死锁;
②出现死锁的情况有两种:
(1)线程之间互占资源的同时在等待若干资源
比如:线程A正在执行同步语句块1
线程B正在执行同步语句块2
这时,线程A想要进入同步语句块2
同时,线程B却想进入同步语句块1
这就形成了死锁
(2)程序当中的所有的线程在某一时刻,都处于阻塞状态或者等待状态,而没有任何唤醒它们的语句
③下面我们举一个例子:







浙公网安备 33010602011771号