黑马程序员ava学习笔记——多线程

------- android培训 java培训 、期待与您交流! ----------

多线程

    概念

    进程:是一个正在执行的程序,每一个进程都有一个执行顺序,或者叫一个控制单元。

    线程:就是进程中一个独立的控制到元,线程在操控着进程的的执行,一个进程中至少有一个线程。 

    多线程:一个程序里边有多条执行路径,就是说由程序是由多个线程组成的,多线程虽然降低了程序运行的效率,但一个程序中线程越多,获取到cpu执行权的概率就越大。

    创建线程的方式

    创建线程的方式有两种,一种是继承Thread类,另一种是实现Runnable接口。

    继承Thread类

    步骤:1,定义一个类继承Thread;

          2,复写Thread类中的run( )方法;

          3,调用线程的start( )方法,开启线程,并调用执行run( )方法。

    我们用一个小程序来说明:

class Demo extends Thread
{
	public void run()
	{
		for(int x=0; x<60; x++)
			System.out.println("demo run----"+x);
	}
}
class ThreadDemo 
{
	public static void main(String[] args) 
	{
		Demo d = new Demo();//创建好一个线程。
		d.start();//开启线程并执行该线程的run方法。
		//d.run();仅仅是对象调用方法。而线程创建了,并没有运行。	
		for(int x=0; x<60; x++)
			System.out.println("Hello World!--"+x);	
	}
}

    

    发现运行结果每一次都不同,因为多个线程都在获取cpu的执行权,cpu执行到谁,谁就运行,需要明确的是,在某一时刻,只有一个程序在运行(多核除外),cpu在做着快速的切换,,以达到看上去是同时在运行的结果,我们可以形象的把多线程的运行形容为,多个线程在抢夺cpu的执行权,这就是多线程的一个特性,随机性。谁抢到谁执行,至于执行多长时间,cpu说了算。

    多线程run方法和start方法的区别

    Thread类用于描述线程,该类定义了一个能用于储存线程运行代码的方法,这个方法就是run方法,也就是说Thread类中的run方法用于存储线程要运行的代码,主线程要运行的代码放在main方法中,虚拟机定义的。我们复写run方法的目的就是将自定义的代码放在run方法中,让线程运行。

    d.start():开启线程并执行该线程的run方法;

    d.run():仅仅是对象调用方法,线程创建了却并没有运行。

    实现Runnable接口

    步骤:1,定义一个类实现Rannable接口;

          2,覆盖Rannable接口中的run( )方法;

          3,通过Thread类创建线程对象;

          4,将Rannable接口的子类对象作为实际参数传递给Thread类的构造函数;

          5,调用Thread类的start方法开启线程,调用Runnable接口子类的run方法 。

    为什么要将Runnable接口的子类对象传递给Thread类的构造函数?

    因为自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去指定对象的run方法,就必须明确该run方法所属的对象。

    实现方式和继承方式的区别:我们用一个售票的例子来进行说明,用继承Thread类的方法创建线程的话,需要建立四个线程对象,运行四次,等于是卖了400张票,我们可以想到用静态,但是静态的生命周期太长,所以我们可以选择实现Runnable接口的方法来完成,代码如下:

/*
需求:简单的卖票程序。
多个窗口同时买票。
*/
class Ticket implements Runnable//extends Thread
{
	private  int tick = 100;
	public void run()
	{
		while(true)
		{
			if(tick>0)
			{
				System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
			}
		}
	}
}
class  TicketDemo
{
	public static void main(String[] args) 
	{
		Ticket t = new Ticket();//只需要建立一个对象,然后将这个对象传递给Thread类的构造函数;
		Thread t1 = new Thread(t);//创建了一个线程;
		Thread t2 = new Thread(t);//创建了一个线程;
		Thread t3 = new Thread(t);//创建了一个线程;
		Thread t4 = new Thread(t);//创建了一个线程;
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}


    运行结果是:

      

    实现方式和继承方式的区别

    实现方式好处:避免了单继承的局限性,在定义线程时,建立使用实现方式。
    两种方式区别:继承Thread:线程代码存放Thread子类run方法中。
    实现Runnable,线程代码存在接口的子类的run方法。

    

    线程运行状态

        

    如图所示,线程存在五种状态,分别是创建,运行,临时,冻结和消亡状态。

    获取线程对象及名称

    currentThread:这是一个静态方法,用于获取当前对象;

    getName:获取线程的名称;

    setName:设置线程的名称。

    

    多线程的安全问题

    增加窗口后的运行结果:


        


    通过上边的买票小程序,我们发现当买票的窗口增加后,可能会出现打印0,-1,-2这些错票的情况,为了验证,我们可以让程序在打票之前,用sleep方法让进入if语句的线程停一段时间,我们队if语句中的代码作如下改动,如图:

       

    分析一下出现这种问题的原因:在if语句中有可能,四个线程进来后,都停在sleep这条语句这里,我们假设现在tick=1,某一个线程获取执行权后,向下执行输出语句,执行完tick--,tick=0,这是,还在if语句中的其他线程已经判断过条件了,所以即使tick的值已经不满足if的条件表达式,这些线程还是会向下执行输出语句,所以就会出现打印出错票的情况,线程越多,有可能打印出的错票就越多。

    得出一个结论:当多条语句在执行同一个线程共享数据时,一个线程对象对于语句只执行了一部分,没有执行完,另一个线程参与进来执行,导致共享数据的错误。

    解决办法:对于多条操作共享数据的语句,只能让一个线程都执行完后在执行其他线程,执行过程中,其他线程不能参与执行。

    java对多线程安全问题提供了专门的解决方式:同步代码块和同步函数。

    同步代码块

    synchronized(对象)  //这个对象可以是任意对象;

   {

        要被同步的代码;

    }

    那些代码需要被同步,就看那些代码在操作共享数据。

    对于售票这个例子,加上同步代码快后的代码如下,就只有run方法的代码有变化,其他地方都不变,在这里为了说明问题,我只写run方法中的代码:

public void run()
	{
		while(true)
		{
			synchronized(obj)
			{
				if(tick>0)
				{
					try{Thread.sleep(10);}catch(Exception e){}//让进来的线程在这里停一会;
					System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
				}
			}
		}
	}

    

    对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程,及时获取了cpu的执行权,也进不去,因为没有获取锁。

    同步的前提:1,必须要有两个或者两个以上的线程;

                2,必须是多个线程使用同一个锁;

    必须保证同步中只有一个线程在运行。

    同步的好处与弊端

    好处:解决了多线程的安全问题。

    弊端:多个线程需要判断锁,较为消耗资源。

    同步函数

    同步函数:就是在函数中添加了synchronized修饰符的函数。

    如何找多线程中的安全问题?

     1,明确哪些代码是多线程运行代码;

     2,明确共享数据;

     3,明确多线程运行代码中哪些语句是操作共享数据的。

    同步代码快的锁是任意对象,那么同步函数的锁又是什么呢?

    对于同步函数的锁,我们可以去验证一下,创建两个线程,一个线程执行同步代码快中的代码,另一个线程执行同步函数中的代码,这个可以通过定义标记来实现,验证的原理就是同步的前提:必须是多个线程使用同一个锁;通过运行结果,我们发现,当同步代码快中的对象是Object类的对象时,还是会发生安全问题,这说明两个锁不一样,换成this后,安全问题就解决了,说明同步函数的锁是this。

    如果同步函数被静态修饰后,使用的锁是什么呢?

    我们也可以用上面的方法进行验证,发现不再是this,因为静态方法中也不可以定义this;静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象,类名.class  该对象的类型是Class
    静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class

    单例设计模式中的懒汉式在多线程,处理安全问题的用的就是用的静态同步函数。

    死锁

    同步中嵌套同步,而锁却不同。

/*
死锁
*/
class Dead implements Runnable
{
	private boolean flag;
	Dead(boolean flag)
	{
		this.flag = flag;
	}
	public void run()
	{
		if(flag)
		{
			synchronized(MyLock.locka)//a锁
			{
				System.out.println("if locka");
				synchronized(MyLock.lockb)//b锁
				{
					System.out.println("if lockb");
				}
			}
		}
		else
		{
			synchronized(MyLock.lockb)//b锁
			{
				System.out.println("if lockb");
				synchronized(MyLock.locka)//a锁
				{
					System.out.println("if locka");
				}
			}
		}
	}
}
class MyLock
{
	static Object locka = new Object();
	static Object lockb = new Object();
}
class DeadLockDemo 
{
	public static void main(String[] args)
	{
		Thread t1 = new Thread(new Dead(true));
		Thread t2 = new Thread(new Dead(false));
		t1.start();
		t2.start();
	}
}


出现这种结果:


        

    死锁就相当于吃饭时用筷子,筷子只有一双,两个人每人手里有一根,这时,两人一人一根筷子,但是两人都不愿意让别人先吃,不愿意将筷子送给对方,于是两人都吃不了饭,死锁也是一样,两个线程都想获取对方的锁,如果都这么僵持着,就会出现死锁,死锁就是在这样的情况下造成的。
     

    线程间通信

    线程间通信其实就是多个线程在操作同一个资源,但操作的动作不同。

    我们用一个小程序来提现什么是线程间通信:这个小程序我们只创建两个线程,已解决过安全问题,而且得到的结果是存一个打一个。

/*
线程间通信
这个小程序,创建了两个线程,一个线程负责设置名字,另一个线程负责打印名字。
*/
class Resource
{
	private String name,sex;
	private boolean flag;//定义一个标记;
	public synchronized void setName(String name,String sex)
	{
		if(flag)//如果这个标记为真
			try{this.wait();}catch(Exception e){}//让线程释放执行权;
		this.name = name;
		this.sex = sex;
		flag = true;//传完值后,把标记改为真;
		this.notify();//唤醒后,再回去判断标记久为真,那么,就会wait;
	}
	public synchronized void printName()
	{
		if(!flag)//如果这个标记为假
			try{this.wait();}catch(Exception e){}//让线程释放执行权;
		System.out.println(name+"............"+sex);//如果为假,就打印
		flag = false;//打印完,把标记改为假;
		this.notify();//唤醒另一个线程后,执行完再回去判断标记if中的条件表达式为真,就会wait;
	}
}
class Input implements Runnable
{
	private Resource res;
	Input(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		int x = 0;
		while(true)
		{
			if(x==0)
				res.setName("BaiYun","M M");
			else
				res.setName("黑土","G G");
			x = (x+1)%2;
		}
	}
}
class Output implements Runnable
{
	private Resource res;
	Output(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			res.printName();
		}
	}
}
class InputOutputDemo
{
	public static void main(String[] args)
	{
		Resource res = new Resource();
		new Thread(new Input(res)).start();//开启并执行线程
		new Thread(new Output(res)).start();
	}
}


结果是:


    

  

    在操作同一种资源时,对于不同的处理方式,可以用多线程来完成,为了让两个线程交替执行,可以定义一个标记,在一个线程执行完后,让该线程进入等待状态,同时,唤醒其他在等待中的线程,这就是我理解的等待唤醒机制。

    等待中的线程临时存放在线程池中,当我们notify的时候,唤醒的是线程池中的线程,如果有多个线程在线程池中,通常先唤醒第一个在等待的,如果要唤醒很多,可以用notifyAll()方法。

    wait和notify方法,是从Object中继承的方法,全都用在同步中,这时必须要指出wait所操作的那个线程所属的锁,wait方法会抛出异常。

    为什么这些操作线程的方法都定义在Object类中?

    因为这些方法在操作同步中线程时,都必须要标识他们所操作线程持有的那个锁,只有同一个锁上的被等待县城可以被一个锁上的notify唤醒,也就是说,等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象所调用的方法定义在Object类中。

    生产者消费者

    对于多个生产者,消费者,在多线程运行时,出现的问题,我们通过下面的代码进行说明,网页上的代码写太多注释,有时候会出问题,所以,我把分析写在后边。

class Resource
{
	private String name;
	private int count = 1;
	private boolean flag;
	public synchronized void set(String name)
	{
		if(flag)//这里用的是if语句来判断标记;
			try{wait();}catch(Exception e){}//t1,t2
		this.name = name+"-----"+count++;
		System.out.println(Thread.currentThread().getName()+"....生产者......"+this.name);
		flag = true;
		this.notify();//这里只唤醒了一个线程;
	}
	public synchronized void out()
	{
		if(!flag)
			try{wait();}catch(Exception e){}//t3,t4
		System.out.println(Thread.currentThread().getName()+"..........消费者.........."+this.name);
		flag = false;
		this.notify();
	}
}
class Pro implements Runnable
{
	private Resource res;
	Pro(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			res.set("+商品+");
		}
	}
}
class Consumer implements Runnable
{
	private Resource res;
	Consumer(Resource res)
	{
		this.res = res;
	}
	public void run()
	{
		while(true)
		{
			res.out();
		}
	}
}
class ProCon
{
	public static void main(String[] args)
	{
		Resource res = new Resource();
		Pro pro = new Pro(res);
		Consumer con = new Consumer(res);
		Thread t1 = new Thread(pro);//我们创建四个线程来说明问题;
		Thread t2 = new Thread(pro);
		Thread t3 = new Thread(con);
		Thread t4 = new Thread(con);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}


运行后出现了这样的问题:


    
    

    出现了生产一个商品却被消费了两次的情况,当然,也有可能出现生产两次,消费一次的情况,这是为什么呢?

    分析:t1、t2是生产者线程,t3、t4是消费者线程,我们假设t1先获取到执行权,这时标记为false,t1向下执行,生产一个商品,然后标记被设置为true,回来再读标记,t1进入等待状态,释放了执行权;现在t2、t3、t4都有可能获取到执行权,假设t2获取到了执行权,标记为true,它也进入等待状态,释放了执行权;接下来t3、t4中有一个获取到执行权,执行后消费了一次,标记被设置为假,t3进入等待状态,释放执行权;然后t3唤醒了t1,t1生产一个商品后,并没有去唤醒消费者线程,把t2唤醒了,这样,虽然标记已经被t1设置为true,t2它已经判断过标记,被唤醒后直接向下执行,所以又会生产一个商品,于是就出现了生产两次,消费一次的情况,原因就是t1在执行完后,唤醒了本方的线程。

    对于这种问题我们应该怎么处理呢?

    发生这个现象就是因为没有循环判断标记,所以,可以用while循环去循环判断标记,但是,新的问题出现了,while循环判断标记可能会造成所有线程都陷入等待状态,如图:

     

    那么,我们就需要在线程释放执行权之前,将其他线程都唤醒,可以使用notifyAll()方法,这样问题就解决了。

    然而,在开发中这么做是比较麻烦的,所以我们就想:能不能每次唤醒只唤醒对方的线程呢?答案是可以,java在JDK1.5之后给我们提供了 一些新特性,在java.util.concurrent.locks包中,有Condition和Lock接口,还有ReenTrantLock类给我们提供了方法,Lock替代了synchronized方法和语句的使用,Condition替代了 Object监视器方法的使用。接下来,我们就来使用这些新特性,将生产者消费者的代码重新体现。

import java.util.concurrent.locks.*;//包就不一个一个导了
class Res
{
	private String name;
	private int count;
	boolean flag;
	private Lock lock = new ReentrantLock();//ReentrantLock是非抽象的,要建立它的对象,调用这些方法。
	private Condition condition_pro = lock.newCondition();//获取condition对象,一个锁上可以有多个相关的condition;
	private Condition condition_con = lock.newCondition();//获取condition对象
	public void set(String name)throws InterruptedException
	{
		lock.lock();//synchronized同步换成了两个方法,拿锁,释放锁;
		try
		{
			while(flag)
				condition_pro.await();//wait换成了await方法
			this.name = name+"--"+count++;
			System.out.println(Thread.currentThread().getName()+"......生产者......"+this.name);
			flag = true;
			condition_con.signal();//notify换成了signal方法;
		}
		finally
		{
			lock.unlock();//释放锁的动作一定要做;这里没有catch处理,下边是一样的;
		}
		
	}
	public void out()throws InterruptedException
	{
		lock.lock();
		try
		{
			while(!flag)
				condition_con.await();
			System.out.println(Thread.currentThread().getName()+"............消费者..........."+this.name);
			flag = false;
			condition_pro.signal();	
		}
		finally
		{
			lock.unlock();
		}	
	}
}
class Producer implements Runnable
{
	private Res r;
	Producer(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
			try
			{
				r.set("+商品+");//run方法里只能try
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
	}
}
class Consumer implements Runnable
{
	private Res r;
	Consumer(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
			try
			{
				r.out();
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
			
	}
}
class ProConDemo
{
	public static void main(String[] args)
	{
		Res r = new Res();
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);
		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(pro);
		Thread t3 = new Thread(con);
		Thread t4 = new Thread(con);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}


运行后的结果:


    


    在jdk1.5版本后提供了显示的锁机制,以及显示的锁对象上的等待唤醒操作机制。
   

    Lock:替代了Synchronized。
    lock():上锁;

    unlock():释放锁;

    newCondition():获取Condition对象,一个锁可以对应多个Condition,而以前一个wait只能对应一个notify;
   

    Condition:替代了Object中的wait、notify、notifyAll。
    await():相当于wait;

    signal():相当于notify;

    signalAll():相当于notifyAll。

    

    停止线程

    因为stop方法已过时,那么我们想要停止线程要通过什么方法呢?

    只有一种,那就是等run方法结束,开启线程运行,运行代码通常都是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。

    步骤:1,定义循环标记;

          2,使用interrupt中断方法,该方法能结束现成的冻结状态,使线程回到运行状态。

    特殊情况:当线程处于冻结状态,就不会读取到标记,那么线程就不会结束,这是,当没有指定的方式让冻结的线程恢复到运行状态是,需要对冻结状态进行清除,强制让线程恢复到运行状态中来,这样就可以让线程结束,Thread类提供了该方法:interrupt方法,中断线程。

class StopThread implements Runnable
{
	private boolean flag =true;
	public  synchronized void run()
	{
		while(flag)
		{
			try
			{
				wait();
			}
			catch (InterruptedException e)
			{
				System.out.println(Thread.currentThread().getName()+"..........Exception");
				flag = false;//只要捕捉到异常,说明就有人在强制清除冻结状态;
			}
			
			System.out.println(Thread.currentThread().getName()+"....run");
		}
	}
	public void changeFlag()//这个方法就不再需要了;
	{
		flag = false;
	}
}
class  StopThreadDemo
{
	public static void main(String[] args) 
	{
		StopThread st = new StopThread();	
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
		t1.start();
		t2.start();
		int num = 0;
		while(true)
		{
			if(num++ == 60)
			{
				//st.changeFlag();
				t1.interrupt();
				t2.interrupt();
				break;
			}
			System.out.println(Thread.currentThread().getName()+"......."+num);
		}
		System.out.println("over");
	}
}


得到的结果是:


    

    注意:interrupt方法只是将冻结状态线程唤醒,如果没有冻结状态的线程,这个方法是没作用的。    

    

    守护线程 

    特点:1,当正在运行的线程都是守护线程时,java虚拟机退出;

          2,该方法必须在县城启动前调用 。

    setDemon(boolean):如果参数是true,则将该线程设置为守护线程。

    当所有的前台线程都结束,后台线程会自动结束,后台依赖于前台,开启运行都和前台线程没区别,结束时有区别。

    应用:比如有一个县城依赖于另一个线程,另外一个线程的数据如果不在运算了,这个线程存在是没意义的,输入线程不输入了,输出线程就不用输出了。

    

    join方法

    等待该线程终止。

    特点:当A线程执行到了B线程的join方法时,那么A线程就会等待,等B线程都执行完,A线程才会执行,join可以用来临时加入线程执行。


    优先级代表抢资源的频率,所有线程的默认优先级是5,包括主线程。

    setPriority(int num) :设置优先级。


    yeild方法:暂停当前正在执行的线程对象,并执行其他线程,就是临时释放一下执行权,稍微减缓一下线程执行的频率。

    










  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值