多线程
一、概述
说起多线程,我们就需要首先来谈谈什么叫做进程。所谓进程,就是在计算机上正在进行执行中的一个程序,注意时态表述是说“当前正在执行的”,不是所有在计算机中的程序都是进程。每一个进程都有一个或者多个执行的顺序,该顺序就叫做执行路径,叫控制单元。
所谓线程,可以说一个列子来描述何为线程:一个打开的word文档程序必然是一个进程,在这个文档中可以再打字的同时移动鼠标,在键盘上输出的字符传入word文档中是一个进程的一段执行路径(控制单元),移动鼠标又是另一端进程的执行路径(控制单元),这两段执行路径在同时执行。所以说,所谓线程,就是进程中一个独立的控制单元,线程控制着进程的执行,当所有线程结束之后进程才能结束。
对于java虚拟机来说,我们启动它的时候,它就会有一个叫做Java.exe的进程。该进程中至少有一个线程负责java程序的执行,而这个线程的运行代码存在于主函数当中,我们称这个线程叫做主线程。其实,在更细节上来说,java虚拟机不止有这一个线程,它还有清楚无用的内存空间等功能。
而线程与线程之间最大的特点就在于:在时间上来看,这两段代码是同时间在执行。
二、如何自定义一个线程?
上面说那么多,我们知道一个程序是由一个或者多个执行路径(线程)组成的,那么对一个我们自己编写的程序,怎么来实现线程的技术呢?
根据万物皆对象的理念,我们通过对javaapi 的查找,了解到java已经提供这样一个系列的类来对线程这一事物进行描述,就叫做Thread类。
创建一个线程有两种方式,恰巧与比较器类似,就是:
继承法:所谓继承法,就是自定义一个类继承Thread类,复写Thread类中的run()方法,然后在主函数中实现该类对象,调用对象start()方法(该方法有两个作用:1、启动线程,2、调用run()方法)。
对于继承法,它有它的局限性,因为它需要继承Thread类,所以根据Java对象和类的规则:多实现,单继承的思想,该类就无法继承其它父类了,这是一个巨大的局限,所以我们需要另一种方法:实现法。
实现法:我们通过对javaapi 的查阅,发现在Thread系列类中有一个runnable接口,我们可以自定义一个类实现runnable接口,复写其ran()方法,然后在主函数中实例化一个Thread对象,将该该自定义类对象传入该对象构造函数中,在调用Thread对象的start()方法。
列子:
package com.itheima;
class MyThread extends Thread//多线程不需要调用包,这个事继承式的使用方法
{
private int count = 300;
public int getCount(){
return this.count;
}
public void SetCount(int count){
this.count = count;
}
public void run(){
while(count>=1){
System.out.println((Thread.currentThread().getName())+"----已经消费了"+this.getCount());//注意currentThread()和getName()方法。
count--;
}
}
}
class MyThread1 implements Runnable//多线程不需要调用包,这个事实现式的使用方法
{
private int count = 300;
public int getCount(){
return this.count;
}
public void SetCount(int count){
this.count = count;
}
public void run(){
while(count>=1){
System.out.println((Thread.currentThread().getName())+"----已经消费了"+this.getCount());//注意currentThread()和getName()方法。
count--;
}
}
}
public class ThreadDemo {
public static void main(String[] args){
/*MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();//继承式
*/
MyThread1 a = new MyThread1();
Thread t1 = new Thread(a);
Thread t2 = new Thread(a);
t1.start();
t2.start();//实现式
}
}
就这样,这上面的程序中我们获得了三个线程,t1,t2已经主线程,他们可以同时执行!
三、线程状态。
当执行以上代码,在用到继承式的我们会奇怪的发现打印的结果每一次都不会相同这是为什么呢?
这是因为,所谓的两个线程同时运行,在实际上是多个线程都同时抢劫执行权,cup执行到谁,谁就运行,而没有执行到的则会被阻塞(不会丧失执行权,但没有执行)。明确的说,在具体的某一个时刻,只能有一个线程在运行,cup总是坐着线程间的快速切换,以达到看起来过个线程同时运行的状态。这是多线程的一个特性:随机性,线程谁抢到cup谁执行,至于执行多长时间,cup说了算。
所以,这些线程在运行过程中有多个状态,如图
我们来具体的了解一下这幅图。从线程对象被创建,调用该对象的Start()方法,则线程开始执行,若cup此时正在执行别的程序,则该线程处于等待执行状态-阻塞状态,这是一个临时的状态,线程在该状态下不在运行,但是有执行权力,去其他阻塞的线程一同争抢cpu执行权。
不论是处于运行状态的线程还是处于阻塞状态的线程都可以通过调用继承Object类对象的wait()方法将当前线程变成冻结状态。当线程处于冻结状态时,该线程不具有执行权,也不能执行,直到有对象调用notify()方法来使得线程重新进入阻塞状态或者直接执行该线程。
在一个进程中所有线程都结束之前,该进程是不会结束的。当该进程的一个线程一直在阻塞状态而无法执行,而它的其它线程早就执行完毕,那么我们需要将该线程立马结束掉,不管其是否执行完毕,这时候我们就是讲线程进入了消亡状态,通过Thread对象的interrupt()方法来终止线程。
以上状态将要在进程间的通信以及生产者和消费者问题上得到充分的应用。
四、线程之间的同步。
当我们使用实现式的线程实现方法的时候,我们会发现结构中会出现0等多种不正确的答案,这是为什么呢?
这是因为多线程之间执行的随机性:多线程共同抢夺cpu的执行权,而且这些线程共享了同一个资源(从列子中可知t1线程和t2线程是共享了MyThread1类中的count资源,它们都可以对count资源进行改变)所导致的。
具体的来说,当count=1时,t1线程执行到while(count>=1),线程执行权被t2所抢得,t2执行到程序结束,这时count已经为了0,但是t1还是会继续执行完成,这时候就会导致打印出了一个根本不合乎逻辑的0,然后t1线程就会结束,这时就会出现问题。
我们将这种因为不同线程之间共享同一个资源而导致的错误叫做线程的不同步,如何解决这个问题?
对线程间同步问题,我们可以将多个线程所共享的资源加上一个锁,当某一个线程操作共享资源时,只能让其执行完毕,才能让下一个线程来操作该资源。
java提供了多线程的同步问题提供了专业的解决方案:同步代码块。
synchronized(对象)
{
需要被同步的代码块
}
其中的对象就如同锁,一个线程拿到了该锁就会一直执行完毕该代码块中的代码,然后释放锁,其他线程才能拿到锁,从而进入同步代码块执行代码。
就如同火车上的卫生间。多个人要上同一个卫生间的时候,一个人进去就会将卫生间的门锁住,知道该人使用完毕卫生间,其他人才能进入卫生间。
同步必须要有前提:1、在进程中必须要有两个或者两个以上的线程;2、两个线程必须要用同一个锁。
同步的好处是:保证了多线程的安全问题。但是弊端:多个线程都要判断锁,及其的消耗资源。
synchronized不仅可以定义在代码块上,还可以定义在方法上。当它定义在静态方法上是,每一个线程需要的锁都是该类在方法的字节码类文件,方式为XXX.class;当它定义在非静态方法上时,线程锁需要的锁就是该类对象:this.
列子:
package com.itheima;
class MyThread1 implements Runnable//多线程不需要调用包,这个事实现式的使用方法
{
private int count = 300;
public int getCount(){
return this.count;
}
public void SetCount(int count){
this.count = count;
}
/*方法一 :定义在代码块上
public void run(){
synchronized(new Object()){
while(count>=0){
count--;
System.out.println((Thread.currentThread().getName())+"----已经消费了"+this.getCount());//注意currentThread()和getName()方法。
}
}
}*/
//方法二:定义在方法上,该方法调用的是this锁
public synchronized void run(){
while(count>=0){
count--;
System.out.println((Thread.currentThread().getName())+"----已经消费了"+this.getCount());//注意currentThread()和getName()方法。
}
}
}
public class TongBuDemo {
public static void main(String[] args){
MyThread1 a = new MyThread1();
Thread t1 = new Thread(a);
Thread t2 = new Thread(a);
t1.start();
t2.start();//实现式
}
}
五、同步练习
北京一家麦当劳餐厅做活动每天七点开始有特价1元早餐汉堡出售。但是一天该种汉堡只卖300个,卖完为止。该加餐厅同时有三个柜台出售特价汉堡。
package com.itheima;
class MacDonald {//定义共同使用的资源
private int hamburger = 300;
public synchronized void buyer(){
System.out.println(Thread.currentThread().getName()+"。。。还剩下"+hamburger--+"个特价汉堡,快买啦!");
}
}
class ABuyer implements Runnable{//定义线程
private MacDonald m;
ABuyer(MacDonald m){
this.m=m;
}
public void run(){
m.buyer();
}
}
public class HamburgerDemo {
public static void main(String[] args){//线程开始!
MacDonald m = new MacDonald();
Thread t1=new Thread(new ABuyer(m));
Thread t2=new Thread(new ABuyer(m));
t1.start();
t2.start();
}
}
六、线程间的通信以及生产者和消费者问题
在多个线程操作同一个资源时,每一个线程的操作方式都不同,当只有两个线程对同一资源进行不同操作的时候,我们称该问题为线程间通信问题。当有多个线程对同一资源进行操作,则为生产者和消费者的问题。
对于线程间通信问题。我们主要解决的方法为:等待/唤醒机制。
什么叫做等待唤醒机制?这里就要用到wait()和notify()方法!我们可以将共享资源和对该资源的不同操作封装成一个类,将该类对象分别传入不同线程当中,在run()方法中设立一个标记,当前执行线程执行到判断标记为true时,调用wait()方法(抛出异常,且一定会释放锁,产生异常需要处理)将该线程暂时冻结,若标记为false则继续执行该线程之末尾释放锁并调用notify()方法唤醒另一个线程前来执行。
那么我们来提供一个列子:
package com.itheima;
class Resource {
private String name;
private int age;
boolean flag=false;//定义标记
public void setResource(String name,int age){
if(this.flag=true)
try{this.wait();}catch(Exception e){}//当标记为true时,该线程被冻结
this.age=age;
this.name=name;
flag=true;
this.notify();//将标记改为true,而且唤醒另外一个线程
}
public void getResource(){
if(flag=false)
try{this.wait();}catch(Exception e){}//当标记为false时,该线程被冻结
System.out.println(this.name+"。。。。"+this.age);
flag=false;
this.notify();//将标记改为false,而且唤醒另外一个线程
}
}
class Inputer implements Runnable{
private Resource r;
private int x=0;
Inputer(Resource r){
this.r=r;
}
public void run(){
while(true){
synchronized(r){//同步代码块
if(this.x==0)
r.setResource("zhangsan", 18);
else
r.setResource("lisi", 19);
x=(x+1)%2;//当x=0时,存入"zhangsan", 18;当x=1时,存入"lisi", 19
}
}
}
}
class Outputer implements Runnable{
private Resource r;
Outputer(Resource r){
this.r=r;
}
public void run(){
while(true){
synchronized(r){//同步代码块
r.getResource();//将存入的数据打印在控制台上
}
}
}
}
public class InputOutputStreamDemo {
public static void main(String[] args){
Resource r=new Resource();
Thread t1=new Thread(new Inputer(r));
Thread t2=new Thread(new Outputer(r));
t1.start();
t2.start();
}
}
对于生产者和消费者问题,实际上就是进程间通信的多个线程模式。假设有两个生产者和两个消费者共同操作同一个共享的资源,如上列。那么该如何思考?
首先,当多个生产者生产或者多个消费者消费时,在判断标记时候不能只判断一次。假设当前正在执行的是生产者线程,那么当它执行到notify()语句时,则很可能唤醒的是另一个已经在冻结状态的生产者,此时flag标记本已经转为true了,该生产者执行却执行了,不和规则,此时需要多次循环判断该标记是否为true。
其次,当多个生产者生产或者多个消费者消费时,也不能再当前执行线程最后使用notify()方法,因为该方法会可能唤醒同一类线程,若被唤醒的线程也在阻塞就会导致所有的线程都在阻塞,这样程序会无法执行下去。所以我们要将notify()方法换成notifyAll()方法,这样就会唤醒所有冻结的线程,总有一个线程会执行下去的。
列子:
package com.itheima;
class Res {
private String name;
private int age;
boolean flag=false;//定义标记
public void setResource(String name,int age){
while(this.flag=true)//循环标记,防止唤醒生产者时执行下面代码
try{this.wait();}catch(Exception e){}//当标记为true时,该线程被冻结
this.age=age;
this.name=name;
flag=true;
this.notifyAll();//将标记改为true,而且唤醒所有在冻结的线程
}
public void getResource(){
while(flag=false)//循环标记,防止唤醒消费者时执行下面代码
try{this.wait();}catch(Exception e){}//当标记为false时,该线程被冻结
System.out.println(this.name+"。。。。"+this.age);
flag=false;
this.notifyAll();//将标记改为true,而且唤醒所有在冻结的线程
}
}
class Creater implements Runnable{
private Res r;
private int x=0;
Creater(Res r){
this.r=r;
}
public void run(){
while(true){
synchronized(r){//同步代码块
if(this.x==0)
r.setResource("zhangsan", 18);
else
r.setResource("lisi", 19);
x=(x+1)%2;//当x=0时,存入"zhangsan", 18;当x=1时,存入"lisi", 19
}
}
}
}
class Comsumer implements Runnable{
private Res r;
Comsumer(Res r){
this.r=r;
}
public void run(){
while(true){
synchronized(r){//同步代码块
r.getResource();//将存入的数据打印在控制台上
}
}
}
}
public class CreatComsumerDemo {
public static void main(String[] args){
Res r=new Res();
Thread t1=new Thread(new Creater(r));
Thread t2=new Thread(new Creater(r));
Thread t3=new Thread(new Comsumer(r));
Thread t4=new Thread(new Comsumer(r));
t1.start();
t2.start();
t3.start();
t4.start();
}
}
同时,对于生产者和消费者问题我们还有另外一种解决办法。思考:当一个线程执行到notifyAll()方法时,会唤醒所有的冻结线程,这样会很多的线程共同抢占cup较为消耗资源,我们是否可以让生产者只能notify()消费者,让消费者只能notify()生产者呢?
在java中为了解决这个问题,特别构造一个Lock系列类,Lock类是这一系列类的顶层接口,它就是一个锁,当它调用其lock()方法时,就会给当前代码块上锁,调用unLock()方法就释放锁。它的最大的好处就是它可以调用newCondition()方法获取Condition类对象,该类封装了可以唤醒和冻结同一类线程的signal()和wait()方法。
注意,使用该Lock对象需要导入import java.util.concurrent.locks.*包。
列子:
package com.itheima;
import java.util.concurrent.locks.*;//注意,使用Lock锁需要导入该包
class Res1 {
private String name;
private int age;
boolean flag=false;//定义标记
Lock l = new ReentrantLock();//创建一个Lock锁
Condition cre = l.newCondition();//构造一个生产者阻塞/唤醒机制
Condition com = l.newCondition();//构造一个消费者阻塞/唤醒机制
public void setResource(String name,int age){
while(this.flag=true)//循环标记,防止唤醒生产者时执行下面代码
try{cre.wait();}catch(Exception e){}//当标记为true时,该生产者线程被冻结
this.age=age;
this.name=name;
flag=true;
com.notify();//将标记改为true,而且唤醒消费者冻结的线程
}
public void getResource(){
while(flag=false)//循环标记,防止唤醒消费者时执行下面代码
try{com.wait();}catch(Exception e){}//当标记为false时,该消费者线程被冻结
System.out.println(this.name+"。。。。"+this.age);
flag=false;
cre.notify();//将标记改为true,而且唤醒生产者冻结的线程
}
}
class Creater1 implements Runnable{
private Res1 r;
private int x=0;
Creater1(Res1 r){
this.r=r;
}
public void run(){
while(true){
r.l.lock();//获取锁
if(this.x==0)
r.setResource("zhangsan", 18);
else
r.setResource("lisi", 19);
x=(x+1)%2;//当x=0时,存入"zhangsan", 18;当x=1时,存入"lisi", 19
r.l.unlock();//释放锁
}
}
}
class Comsumer1 implements Runnable{
private Res1 r;
Comsumer1(Res1 r){
this.r=r;
}
public void run(){
while(true){
r.l.lock();//获取锁
r.getResource();//将存入的数据打印在控制台上
r.l.unlock();//释放锁
}
}
}
public class LockDemo {
public static void main(String[] args){
Res1 r=new Res1();
Thread t1=new Thread(new Creater1(r));
Thread t2=new Thread(new Creater1(r));
Thread t3=new Thread(new Comsumer1(r));
Thread t4=new Thread(new Comsumer1(r));
t1.start();
t2.start();
t3.start();
t4.start();
}
}
七、死锁
何谓死锁?可以用一个比喻来说明:有两个人面前有一碗饭,刚好这两人都饿了,但是我们每人手里都只拿了一只筷子,他们互相都要拿走对方的筷子才能吃饭,这会导致谁也不能拿到一双筷子来吃饭,这就是死锁。用术语来说就是:同步之中嵌套了同步,两个线程都各自拿着一段锁,但是需要两个锁同时拿着才能操作共享资源,这样的情况会导致任何一个线程都无法拿到两个锁,造成死锁现象。
列子:
package com.itheima;
class Resourcer{
private int count=300;
Object obj=new Object();
public int getCount(){
return this.count;
}
public synchronized void getResource(){
while(true){
synchronized(this.obj){
System.out.println(Thread.currentThread().getName()+"-----"+count--);
}
}
}
}
class OurThread implements Runnable{
private int x = 0;
private Resourcer r;
OurThread(Resourcer r){
this.r=r;
}
public void run(){
if(this.x==0){
synchronized(r.obj){//t1线程首先拿的是obj锁,然后去寻求r锁
r.getResource();
}
x=(1+x)%2;
}
else
r.getResource();//t2线程拿的是r锁,然后去需求obj锁
}
}
public class DeadLock {
public static void main(String[] args){
Resourcer r=new Resourcer();
OurThread o = new OurThread(r);
Thread t1=new Thread(o);
Thread t2=new Thread(o);
t1.start();
t2.start();
}
}
八、对线程的操作。
对线程的操作我们从上面的几章中已经陆陆续续的讲到了很多,现在总结并加入几个新的操作。
start()方法:该方法是线程开启操作,调用该方法意味着线程的开启。
wait()方法:该方法是每一个类的对象都具有的方法,它的作用就是将该对象所处的线程转移到冻结状态,而且调用该方法一定会产生异常并且释放锁。
notify()方法:该方法是每一个类的对象都具有的方法,它的作用就是将该对象所处的线程从冻结状态中转移到执行状态或者阻塞状态,该方法不会产生异常但是会释放锁。
sleep()方法:该方法是Thread类中的静态方法,用于使正在执行的线程暂停执行,但是不会释放锁。
currentThread()方法:该方法是Thread类中的静态方法,用于获取正在执行的线程对象。
getName()方法:该方法是Thread类中的非静态方法,用于获取该线程对象的名称。
setDaemon(boolean)方法:将线程标记为后台线程,后台线程和前台线程一样,开启,一样抢执行权运行,只有在结束时,有区别,当前台线程都运行结束后,后台线程会自动结束。
join()方法:什么意思?等待该线程结束。当A线程执行到了B的.join方法时,A就会处于冻结状态。A什么时候运行呢?当B运行结束后,A就会具备运行资格,继续运行。
interrupt()方法:目的是线程强制从冻结状态恢复到运行状态。但是会发生InterruptedException异常。
yield():临时暂停,可以让线程是释放执行权。
------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------