关于线程与多线程的较详细的理解可以参考:线程的解释 和多线程的解释。
而我们要做的是,对其进行“精炼"。我们每天都在和电脑、手机打交道,每天都在使用各种各样的利用软件。
打开上电脑的任务管理器,就能够看到有1项名为"进程"的栏目,点击到里面可能就会发现1系列熟习的名称:QQ,360等等。
所以首先知道了,QQ、360之类的利用软件在计算机上被称为1个进程。
而1个利用程序都会有自己的功能,用以履行这些进程当中的个别功能的程序履行流就是所谓的线程。
所以,线程有时候也被称为轻量级进程,是程序履行流当中的最小单元。
线程的划分尺度小于进程,其不能够独立履行,必须依存在利用程序中,由利用程序提供多个线程履行控制。
进程在履行进程中具有独立的内存单元,而多个线程同享内存,所以能极大地提高了程序的运行效力。
所以简而言之的概括的话,就是:1个程序最少有1个进程,1个进程最少有1个线程。
以360杀毒来讲,里面的1项功能任务“电脑体检”就是该利用程序进程中的1个线程任务。
而除此任务以外,我们还可以同时进行多项操作。例如:“木马查杀”、“电脑清算”等。
那末,以上同时进行的多项任务就是所谓的存活在360利用程序进程中的多线程并发。
多线程的有益的地方,不言而喻。在传统的程序设计语言中,同1时刻只能履行单任务操作,效力非常低。
假定在某个任务履行的进程中产生梗塞,那末下1个任务就只能1直等待,直至该任务履行完成后,才能接着履行。
而得益于多线程能够实现的并发操作,即便履行进程中某个线程因某种缘由产生阻塞,也不会影响到其它线程的履行。
也就是说,多线程并发技术带来的最大好处就是:很大程度上提高了程序的运行效力。
似乎百里而无1害的多线程并发技术,还有弊端吗?从某种程度上来讲,也是存在的:会致使任务履行效力的下降。
之所以这样讲,是由于所谓的“并发”其实不是真正意义上的并发,而是CPU在多个线程之间做着快速切换的操作。
但CPU的运算速度肯定是远远高于人类的思惟速度,所以就带来了1种“并发”的错觉。
那就不难想象了:假定某1进程中,线程A与线程B并发履行,CPU要做的工作就是:
不断快速且随机的在两个线程之间做着切换,分别处理对应线程上的线程任务,直到两个线程上的任务都被处理完成。
那末,也就能够斟酌这样的情况:CPU履行完本来线程A的线程任务只需要5秒;但如今由于另外一个线程B的并发加入。
CPU则不能不分出1部份时间切换到线程B上进行运算处理。因而可能CPU完成该线程任务A的时间反而延长到了7秒。
所以所谓的效力下降,就是指针对某单个任务的履行效力而言的。
也就是说,如果在多线程并发操作时,如果有某个线程的任务你认为优先级很高。那末则可以:
通过设置线程优先级或通过代码控制等手段,来保证该线程享有足够的“特权”。
注:Java中设置线程优先级,实际上也只是设置的优先级越大,该线程被CPU随机访问到的几率会相对高1些。
这个进程可以替换成1些实际生活中的情形来进行思考。快过年了,以家庭团圆为例。
假定你除准备炒1桌子美味的菜肴以外,过年自然还要有1顿热腾腾的饺子。那末:
传统单任务的操作进程可以被理解为:先把准备的菜肴都做好;菜都端上桌后便开始煮饺子。
这样做的坏处就是:如果在炒菜的中途产生1些意外情况,那末随着炒菜动作的暂停。煮饺子的动作也将被无穷期延后。
而对应于多线程并发的操作就是:1边炒菜,1边煮饺子。这时候你就是CPU,你要做的动作多是这样的:
炒菜的中途你能会抽空去看看锅里的饺子煮好没有;发现没有煮好,又回来继续炒菜。炒好1道菜后,再去看看饺子能出锅了没。
由此你发现,你做的工作与CPU处理多线程并发的工作是1样的:不断的在“煮饺子”与“炒菜”两个任务之间做着切换。
Java中线程的全部生命周期基本可以划分为以下4种状态:
以下是Java中1些用于改变线程状态的方法列表:
开发中通常选择通过实现Runnbale接口的方式创建线程,好处在于:
1.Java中不支持多继承,所以使用Runnable接口可以免此问题。
2.实现Runnable接口的创建方式,等因而将线程要履行的任务单独分离了出来,更符合OO要求的封装性。
春运将至了,还是先通过1个老话题来看1个多线程并发的例子,来看看多线程可能存在的安全隐患。
而根据程序的输出结果,我们发现的安全隐患是:有坐位号为0号的车票被售出了,买到这张车票的顾客该找谁说理去呢?
我们来分析1下为何会出现这样的毛病情况,其构成的缘由多是这样的:
当线程1履行完“1号售票窗口售出了2号票”以后,根据while循环的规则,再1次开始售票工作。
首先判断while为true,进入到while循环体;接着判断if语句,此时余票数为1张(也就是只剩下坐位号为1的车票了)。
1大于0,满足判断条件,进入到if语句块当中。此时履行到"Thread.sleep(10)"语句。
OK,当前线程进入到梗塞状态,暂时失去了Cpu的履行资格。因而Cpu重新切换,开始履行线程2。
因而线程2开始履行线程任务,又是老模样:while判断 - if判断,由于上次线程1判断后还没履行售票工作,就被阻塞了。
所以这次if判断依然为"1>0",满足判断条件,继续履行,又履行到线程休眠语句,因而线程2也进入阻塞状态。
此时两个线程暂时都不具有履行资格,但我们指定线程休眠的时间为10毫秒,因而10毫秒后,可能两个线程都苏醒了,恢复了Cpu的履行资格。
面对两个都处于可履行状态的线程,Cpu又只好随机选择1个先履行了。因而Cpu选择了线程2,线程2恢复履行。
线程2开始做自己上次没做完的事,因而履行表达式和输出语句,因而得到输出信息"2号售票窗口售出了1号票"。
线程2继续履行while判断,没问题。再履行if判断"0>0",不满足判断条件,因而履行到了break语句。
线程2到此退出循环,完成了所有线程任务,因而自然灭亡进入done状态。
因而现在Cpu的履行权自然就属于线程1了,线程1也犹如线程21样,从美梦中醒来,开始上次没做完的事。
问题就在这里出现了,虽然这个时候,堆内存中寄存的对象成员变量“ticket_num”的值实际上已是0了。
但是!由于上1次线程1已经过了if判断进入到了if语句块以内。所以它将直接开始履行表达式,并输出。
就构成了我们看到的毛病信息:“1号售票窗口售出了0号票”。并且这个时候实际上余票数的值已是“⑴”了。
所以,实际上之所以我们在处理卖票的代码之前加上让线程休眠10毫秒的代码,目的也就是为了摹拟线程安全隐患的问题。
而根据这个例子我们能够得到的信息就是:之所以多线程并发存在着安全隐患,正是CPU的实际处理方式是在不同线程之间做着随机的快速切换。
这意味着它其实不会保证当处理1个线程的任务时,1定会履行完该次线程的所有代码才做切换。而是可能做到1半就切换了。
所以,我们可以归纳线程安全隐患之所以会出现的缘由就是由于:
既然已了解了线程安全隐患之所以产生,就是由于线程在操作同享数据的途中,其它线程被参与了进来。
那末我们想要解决这1类的安全隐患,自然就是保证在某个线程在履行线程任务的时候,不能让其余线程来捣乱。
在样的做法,在Java当中被称为同步锁,也就是说给封装在同步当中的代码加上1把锁。
每次只能由1个线程能够获得到这把锁,只有当前持有锁的线程才能履行同步当中的代码,其它线程将被拒之门外。
Java中对同步的使用方式通常分为两种,即:同步代码块和同步函数。关键字synchronized用以声明同步。其格式分别为:
这正是由于我们通过同步代码块,将希望每次只有有1个线程履行的代码封装了起来,为它们加上了1把同步锁(对象)。
同步最需要注意的地方,就是要保证锁的1致性。这是由于我们说过了:
同步的原理就是锁,每次当有线程想要访问同步当中的代码的时候,只有获得到该锁才能履行。
所以如果锁不能保证是同1把的话,自然也就实现不了所谓的同步了。
可以试着将定义在TicketOffice的成员变量objLock移动定义到run方法当中,就会发现线程安全问题又出现了。
这正是由于,将对象类型变量objLock定义为成员变量,它会随着该类的对象存储在堆内存当中,该变量在内存中独此1份。
而移动到run方法内,则会存储在栈内存当中,而每个线程都会在栈内存中,单独开辟1条方法栈。
这样就等于每一个线程都有1把独自的锁,自然也就不是所谓的同步了。
而同步函数的原理实际上与同步代码块是相同的,不同的只是将本来包括在同步代码块当中的代码单独封装到1个函数中:
同步代码块:可使用任1对象锁。
同步函数:使用this作为锁。
静态同步函数:使用该函数所在类的字节码文件对象作为锁。
提到同步,就不能不提到与之相干的1个概念:死锁。
死锁是指两个或两个以上的进程在履行进程中,因争取资源而酿成的1种相互等待的现象,若无外力作用,它们都将没法推动下去。
此时称系统处于死锁状态或系统产生了死锁,这些永久在相互等待的进程称为死锁进程。同理,线程也会出现死锁现象。
线程1开启履行后,判断标记为true,因而先获得了锁A,并输出信息。
此时CPU做切换,线程2开启履行,判断标记为false,首先获得锁B,并输出相干信息。
但这时候候不管CPU再怎样样切换,程序都已没法继续推动了。
由于线程1想要继续推动必须获得的资源锁B现在被线程2持有,反之线程2需要的锁A被线程1持有。
这正是由于两个线程由于相互争取资源而酿成的死锁现象。
死锁还是很蛋疼的,1旦出现,程序的调试和查错修改工作都会变得很麻烦
关于多线程编程,类似于车站卖票的例子是1种常见的使用处径。
这类利用途径通常为:多个线程操作同享数据,并且履行的是同1个动作(线程任务)。
车站售票:多个线程都是操作同1组车票,并且都是履行同1个动作:出售车票。
那末在多线程当中的另外一个经典例子:生产者与消费者,就描写的是另外一种常见的利用途径。
不同线程间的通讯应当怎样样来完成,其手段是通过Object类当中提供的几个相干方法:
notify()
方法或notifyAll()方法前,致使当前线程等待。首先,我们可能会思考的1点就是:既然是针对线程之间相互通讯的方法,为何没有被定义在线程类,反而被定义在了Object类当中。
由于这些方法事实上我们可以视作是线程监视器的方法,监视器其实就是锁。
我们知道同步中的锁,可以是任意的对象,那末既然是任1对象调用的方法,自然1定被定义在Object类中。
可以将所有使用同1个同步的线程视作被存储在同1个线程池当中,而该同步的锁就是该线程池的监视器。
由该监视器来调度对应线程池内的各个线程,从而到达线程通讯的目的。
接下来就来看生产者与消费者的例子:
1.生产者生产商品;
2.消费者购买商品。
3.可能会同时存在多个生产者与多个消费者。
4.多个生产者中某个生产者生产1件商品,就暂停生产,并在多个消费者中通知1个消费者进行消费;
消费者消费掉商品后,停止消费,再通知任逐一个生产者进行新的生产工作。
这就是对线程通讯1个简单的利用。而需要记住的是:关于线程的停止与唤醒都必须定义在同步中。
由于我们说过了,关于所谓的线程通讯工作。实际上是通过监视器对象(也就是锁),来完成对线程的停止或唤醒的操作的。
既然使用的是锁,那末自然必须被定义在同步中。并且,必须确保相互通讯的线程使用的是同1个锁。
这是10分重要的,试想1下,如果试图用线程池A的监视器锁A去唤醒另外一个线程池B内的某1个线程,这自然是办不到的。
简单解释下,你可能已注意到在上面的例子中,我是直接采取"wait()"和"notifyAll()"的方式来唤醒和阻塞线程的。
那末你应当明白这其实对应于隐式的"this.wait()"与"this.notifyAll()",而同时我们已说过了:
在同步方法中,使用的锁正是this。也就是说,在线程通讯中,你可以将同步锁this看作是1个线程池的对象监视器。
当某个线程履行到this.wait(),就代表它在该线程池内阻塞了。而通过this.notify()则可以唤醒阻塞在这个线程池上的线程。
而到了这里,另外一值得1提的1点就是:
Thread类的sleep()方法和Object类的wait()方法都可使当前线程挂起,而它们的不同的地方在于:
1:sleep方法必须线程挂起的时间,超过指定时间,线程将自动从挂起中恢复。而wait方法可以指定时间,也能够不指定。
2:线程调用sleep方法会释放Cpu的履行资格(也就是进入到non Runnable状态),但不会释放锁;
而通过调用wait方法,线程即会释放cpu的履行资格,同时也会释放掉锁。
与之前说过的卖票用例1样,对线程通讯的通讯也应当谨慎谨慎,否则也可能会引发相干的毛病。常见的问题例如:
1、使用notify而不是notifyAll唤醒线程可能会出现的问题
我在最初接触多线程的时候,容易这样斟酌,既然想要到达的目的是:生产者线程生产1件商品,则唤醒1个消费者线程。消费者进行消费,则唤醒1个生产者线程。
既然notify()方法用于唤醒单个线程,而notifyAll()用于唤醒所有线程,那使用notifyAll不是浪费效力吗?
后来明白,很惋惜的是,我们要做的是唤醒单个对方线程。而notify没有这么强大。
它只是随机的唤醒1个处于阻塞状态下的线程,所以如果使用notify(),可能会看到以下的毛病情况:
没错,操蛋,又出现了坑爹的死锁。为何出现这样的情况呢?我们来分析1下:
履行到此,当前处于可履行状态的线程为:生产者2、消费者1、消费者2
因而,当前处于可履行状态的线程变成了:消费者1、消费者2
到此,当前处于可履行状态的线程变成了:生产者2、消费者2
好了,处于可履行状态的线程只剩下:生产者2。
至此,唯1处于可履行状态的线程变成了:生产者1
这下好了,4个线程都进入了阻塞状态,而不是灭亡状态。自然的,死锁了。
2、使用if而不是使用while判断isEmpty可能出现的问题
如果使用if而不是while对isEmpty进行判断,可能会出现的毛病为:
1、不同的生产者连续生产了多件商品,但消费者只消费掉其中1件。
2、1个生产者生产了1件商品以后,有多个消费者进行连续消费。
出现这样的安全问题是由于if的判断机制酿成的:通过if来判断标记,只会履行1次判断。
所以可能会致使不该运行的线程运行了,从而出现数据毛病的情况。
这类问题的出现也就是与我们上面说的“售票处售出0号票”的毛病类似。
我们前面已说到了,关于生产者与消费者的问题中。
我们的目的是,每当1个线程履行终了1次任务后,只唤醒单1的对方线程。
而在JDK1.5之前,为了不死锁的产生,我们不能不使用notifyAll()来唤醒线程。
而这样做有1个缺点就在于:每次都要唤醒所有处于阻塞的线程,自然就会致使效力下降。
在JDK1.5以后,,Java提供了新的工具用于解决此类问题,就是:Lock和Condition接口。
简答的说,就是对将本来的同步锁synchronized与对象监视器进行了封装,分别对应于于Lock及Condition。
并且,重要的是相对1.5之前,新的工具具有更灵活及更广泛的操作。
1、Lock的使用及注意事项
1、通过Lock lock = new ReentrantLock();获得1个Lock对象。
2、通过成员方法lock(),用于对代码进行同步管理。
3、通过成员方法unlock(),用于同步代码履行终了后,释放锁对象。
4、由于不管在同步代码的履行进程中是不是出现异常,最后都必须释放该锁,否则可能会致使死锁现象的产生。所以通常在使用lock时,都会遵守以下格式:
lock.lock();
try{
{
// 同步代码....
}finally{
lock.unlock();
}
}
2、对象监视器Condition的使用及注意事项
1、可以通过Lock对象使用成员方法newCondition()来获得1个新的监视器对象。
2、Condition分别使用await();signal();signalAll()来替换本来Object类当中的wait();notify();及notifyAll()方法。
3、同1个Lock对象可以具有多个不同的Condition对象。
请注意1个很关键的特性:同1个Lock对象可以具有多个不同的Condition对象!
也就是说:通过此特性,我们可以获得多个Condition对象,将操作不同线程任务的线程分别寄存在不同的Condition对象当中。
例如在前面所说的生产者消费者例子当中,我们就能够生成两组监视器,1组监视生产者线程,1组监视消费者线程。
从而到达我们想要的每次只唤醒对方线程而不唤醒本方线程的目的,修改后的例子代码以下:
最后,看1下1些关于线程的经常使用方法。
1、线程的中断工作
1、通常使用自然中断的做法,也就是当某个线程的线程任务履行结束以后,该线程就会自然终结。
2、通过标记控制。如果线程任务中存在循环(通常都有),那末,可以在循环中使用标记,通过标记来控制线程的中断。
2、interrupt()方法:中断线程
我们知道sleep及wait等方法都可使线程进入阻塞状态。所以可能你在程序通过使用标记的方式来控制线程的中断,但由于进程中线程堕入了
冻结(挂起/阻塞)状态,这时候通过标记将没法正常的控制线程中断。这时候,就能够通过interrupt方法来中断线程的冻结状态,强迫恢复到运行状态中来,让线程具有cpu的履行资格。但是由于此方法具有强迫性,所以会引发InterruptedException,所以要记得处理异常。
3、setDaemon()方法:将该线程标记为守护线程或用户线程。
所谓守护线程,可以理解为后台线程。对应的,我们在程序中开辟的线程都可以视为前台线程,在Java中,当所有的前台线程都履行结束以后,后台线程也将随之结束。
例如:你在某个程序中开辟两个线程,1个用于接收输入,1个用于控制输出。由于只有当有输入存在时,才会存在输出。这时候就能够通过setDaemon将输出线程设置为守护线程。这样当输入线程中断结束时,输出线程就会随之自动中断,而没必要再人为控制中断。
4、控制线程优先级
所谓控制线程优先级,是指我们可以通过设置线程的优先级来控制线程被CPU运行到的概率,线程的优先级越高,被CPU运行的几率越大。
通过setPriority()与getPriority()方法可以分别设置和获得某个线程的优先级。Java中线程的优先级取值范围为:1⑴0
Thread类中使用MAX_PRIORITY(10),NORM_PRIORITY(5),MIN_PRIORITY(1)3个常量代表最经常使用的线程优先级值。
5、join()方法
线程使用join方法,意味着该线程申请加入履行,所以通常如果要临时加入1个线程,可使用join()方法。并且,当履行到join方法以后,其余线程将等待使用该方法的线程履行完线程任务以后,再继续履行。
6、yiled()方法
暂停正在履行的线程对象,并履行其他线程。
上一篇 灰度图像--频域滤波 概论