黑马程序员——Java基础---多线程详解

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

一,多线程概念

         要掌握多线程,首先你需要了解进程和线程的概念,以及两者之间的关系。

         1,进程

         进程是程序在处理机中的一次运行。一个进程既包括其所要执行的指令,也包括了执行指令所需的系统资源,不同进程所占用的系统资源相对独立。所以进程是重量级的任务,它们之间的通信和转换都需要操作系统付出较大的开销。

         简单来说,进程就是一个正在执行的程序。

         每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。

         2,线程

         线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程自己基本上不拥有系统资源,但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。所以线程是轻量级的任务,它们之间的通信和转换只需要较小的系统开销。

       Java支持多线程编程,因此用Java编写的应用程序可以同时执行多个任务。

         3,线程和进程之间的关系

         线程就是进程中的一个独立的控制单元。线程在控制着进程的执行。只要进程中有一个线程在执行,进程就不会结束。

       一个进程中至少有一个线程。

         4,结论

         了解了进程和线程以及两者之间的关系,我们就可以得出这样的结论:

         单线程:一个进程中只有一个执行路径,即一个线程,那么这个程序称为单线程。

         多线程:一个进程中有多个执行路径,即多个线程,那么这个程序就被称为多线程。

         5,多线程的好处(意义)

         为什么出现多线程?

         举个例子:从磁盘读取一个文件需要5秒,处理一个文件需要2秒。

                   单线程执行顺序:读取文件A、处理文件A、读取文件B、处理文件B。

                   单线程实现这样的操作共需要14秒,可以说效率非常低,因为从磁盘中读取文件的时候,大部分的CPU时间用于等待磁盘去读取数据。在这段时间里,CPU非常的空闲,可以让它可以做一些别的事情。

                   通过改变操作的顺序,就能够更好的使用CPU资源。如:

                   读取文件A、读取文件B(A还未读取完)、处理文件A(B未读取完)、处理文件B。

                   按照这样的顺序执行只需要12秒。

         这样我们就得出了结论:多线程的出现能让程序产生"同时运行"效果。可以提高程序执行效率。

         注意:随机性原理,CPU在某一个时刻只能执行一个程序,多个程序执行只是CPU快速的切换完成的。

二,java实现多线程

         讲完了多线程的概念,你应该对多线程有了初步的了解,下面说一下java是怎么实现多线程的。

         java中创建线程有两种方式:继承Thread类和实现Runnable接口。

         1,继承Thread类

                   步骤:

                   a,定义类继承Thread类。

                   b,覆盖Thread类中的run方法,将需要被多线程执行的代码定义到该run方法当中。

                   c,建立Thread类的子类创建线程对象。

                   d,调用start方法,开启线程并调用该线程的run方法。

         下面有个示例来让你直观的了解怎么用继承Thread类的方式来创建线程。
<span style="white-space:pre">	</span>/*
	 * 示例:创建三个线程,每过2秒打印一下线程的名称,打印三次
	 */
	public class Thread1 extends Thread{
		private final int MAX = 3;//最大打印次数
		private int COUNT = 1;//计数
		private final int TIME = 2;//间隔时间
	
		//接收线程名称
		public Thread1(String name) {
			super(name);
		} 
		//覆盖run方法,在里面写我们要执行的代码
		public void run() {
			while(COUNT<= MAX){
				System.out.println(this.getName());
				COUNT++;
				//每次打印后,在一段时间后再打印
				try {
					Thread.sleep(TIME*1000); 
				} catch (InterruptedException e) {
					e.printStackTrace(); 
				} 
			}
		} 

		public static void main(String[] args) {
			Thread1 t1 = new Thread1("线程1");//创建线程
			Thread1 t2 = new Thread1("线程2");
			Thread1 t3 = new Thread1("线程3");
			t1.start(); //开启线程
			t2.start(); 
			t3.start(); 
			//也可以使用下面这种方式书写
			//new Thread1("线程4").start();
		} 
	}

下图是执行结果:观察可知,多线程的执行是随机交替执行的,每次运行的结果都可能不同。



看到这里,有些朋友可能会有疑问:为什么要继承Thread?为什么要覆盖run方法?

        其实直接建立Thread类对象即可。并开启线程执行就可以了。但是虽然线程执行了,可是执行的代码是该线程默认的代码,该代码就存放在run方法中。我们定义线程的目的是为了执行自定义的代码,实现自己想要的功能,而线程运行代码都存储在run方法中,所以只有覆盖了run方法,才可以运行自定义的内容,想要覆盖,必须先要继承。

         主线程运行的代码都在main函数中,自定义线程运行的代码都在run方法中。

2,实现Runnable接口

使用继承方式有一个弊端,那就是如果该类本来就继承了其他父类,那么就无法通过Thread类来创建线程了,这样就有了第二种创建线程的方式:实现Runnable接口。

步骤:

a, 定义类实现Runnable的接口。

b, 覆盖Runnable接口中的run方法。目的也是为了将线程要运行的代码存放在该run方法中。

c, 通过Thread类创建线程对象。

d, 将Runnable接口的子类对象作为参数传递给Thread类的构造方法。

e, 调用Thread类中start方法启动线程。start方法会自动调用Runnable接口子类的run方法。

实现Runnable接口好处:java是单继承多实现,使用实现Runnable接口的方式避免了单继承的局限性。在定义线程时,建议使用实现Runnable接口方式。


       使用实现Runnable接口的方式实现相同的功能。

/*
 * 示例:创建三个线程,每过2秒打印一下线程的名称,共打印三次
 * */
class ThreadTestWin implements Runnable{
	private final int MAX = 3;//最大打印次数
	private int COUNT = 1;//计数
	private final int TIME = 2;//间隔时间
	private String NAME;
	
	public ThreadTestWin(String name) {
		this.NAME = name;
	} 
	public String getName() {
		return this.NAME;
	}
	//覆盖run方法,在里面写我们要执行的代码
	public void run() {
		while(COUNT<= MAX){
			System.out.println(this.getName());
			COUNT++;
			//每次打印后,在间隔时间后再打印
			try {
				Thread.sleep(TIME*1000); 
			} catch (InterruptedException e) {
				e.printStackTrace(); 
			} 
		}
	}
}

class Thread2{
	public static void main(String[] args) {
		Thread t1 = new Thread(new ThreadTestWin("线程1"));//创建线程
		Thread t2 = new Thread(new ThreadTestWin("线程2"));
		Thread t3 = new Thread(new ThreadTestWin("线程3"));
		t1.start(); //开启线程
		t2.start(); 
		t3.start(); 
		//也可以使用下面这种方式书写
		//new Thread(new Thread2("线程4")).start();
	} 
}


执行结果如图:


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

因为要被多线程执行的代码都存放了Runnable接口的子类中,所以必须要明确线程对象要执行的run方法所属的对象。

        

3,这两种方式的区别:

a,继承Thread:线程代码存放在Thread子类run方法中; 实现Runnable:线程代码存放在接口子类run方法中。

        b,实现Runnable接口是可以将资源共享。

        c,实现Runnable接口避免了单继承的局限性 。

        所以建议使用实现Runnable接口的方式来创建线程。

三,线程状态

创建状态:通过建立Thread类的对象或者Thread类的子类对象,来完成线程创建。

        运行状态:线程具备执行资格,具备CPU执行权,表示该线程正在被CPU执行。

        临时阻塞状态:只具备执行资格,不具备执行权。

        冻结状态:释放了线程执行权和线程的执行资格,

        线程执行到sleep方法或wait方法时,线程就会进入这种状态。

        消亡状态:当线程调用了stop方法(过时),或者线程执行的代码已经结束了,

                   这时该线程结束,该执行路径在进程中消失。

四,多线程的缺点

a,等候使用共享资源时造成程序的运行速度变慢。这些共享资源主要是独占性的资源,如打印机等。

b,对线程进行管理要求额外的CPU开销。线程的使用会给系统带来上下文切换的额外负担,当这种负担超过一定程度时,多线程的特点主要表现在其缺点上,比如用独立的线程来更新数组内每个元素。

C,线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状。

d,对公有变量的同时读或写。当多个线程需要对公有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,从而使前一个线程的参数被修改;另外,当公用变量的读写操作是非原子性时,在不同的机器上,中断时间的不确定性,会导致数据在一个线程内的操作产生错误,从而产生莫名其妙的错误,而这种错误是程序员无法预知的。

五,线程同步

为了解决多线程引发的安全问题,java引入了线程同步synchronized。

同步原理:通过一个对象锁,将多条操作共享数据的代码进行了封装并加锁,持有这个锁的线程才能进入同步去执行,执行期间,其他线程无法获得执行权。

Java中线程同步有两种方式:同步代码块和同步函数。

1,同步代码块

使用方法:

synchronized(对象){

需要被同步的代码

}

同步代码块之所以能够解决安全问题,原因就在那个对象上。这个对象就像一把钥匙,拥有钥匙的线程可以打开锁执行同步代码,没有钥匙的线程即使获取了cpu的执行权,也不能执行同步代码。

同步代码块示例:

/*
 * 多线程打印1-100,并打印当前线程名称
 * */
class ThreadTestWin2 implements Runnable{

	private int count = 1;
	private int MAX = 100;
	Object Obj = new Object();
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		
		while(true){
			synchronized (Obj) {
				if(count<=MAX){
				System.out.println(Thread.currentThread().getName()+"____"+count);
					count++;
					try {
						Thread.sleep(30);
					} catch (Exception e) {
						// TODO: handle exception
						e.printStackTrace(); 
					}
				}
			}
		}
	}
	
}

class Thread3{
	public static void main(String[] args) {
		ThreadTestWin2 t = new ThreadTestWin2();//创建线程
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		
		t1.start(); //开启线程
		t2.start(); 
		t3.start(); 
	}
}

2,同步函数

将同步关键字(synchronized)修饰在函数上即可。

同步函数示例:

/*
 * 多线程打印1-100,并打印当前线程名称
 * */
class ThreadTestWin3 implements Runnable{

	private int count = 1;
	private int MAX = 100;
	Object Obj = new Object();
	
	@Override
	public void run() {
		// TODO Auto-generated method stub
		
		while(true){
			show();
		}
	}
	public synchronized void show(){
		if(count<=MAX){
			System.out.println(Thread.currentThread().getName()+"..。"+count);
			count++;
			try {
				Thread.sleep(30);
			} catch (Exception e) {
				// TODO: handle exception
				e.printStackTrace(); 
			}
		}
	}
	
}

class Thread4{
	public static void main(String[] args) {
		ThreadTestWin3 t = new ThreadTestWin3();//创建线程
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		Thread t3 = new Thread(t);
		
		t1.start(); //开启线程
		t2.start(); 
		t3.start(); 
	}
}

3,同步的前提

a,必须是两个或者两个以上的线程。

b,必须保证多个线程使用的是同一个锁。

    注意:如果你发现多线程存在安全问题,而且加上同步后,安全问题没有解决,那么要查看一下这两个前提是否符合。

4,同步具体体现区别:

同步函数使用的锁是this。

        同步代码块使用的锁可以是任意对象。

特殊:静态同步函数使用的锁是 该函数所属类对象字节码文件对象。

六,线程间通信

         线程间通信其实就是多个线程操作同一个资源。

面的示例采用同步操作同一资源:

/*
 * 例子:两个卖方共同经营一家手机店(资源),生意很兴隆,每到货一部手机,就能立即卖出去
 * */
public class Thread5 {
	public static void main(String[] args) 
	{
		Res r = new Res();//表示操作的是同一个资源
		Pro p = new Pro(r);
		Cus c = new Cus(r);

		Thread t1 = new Thread(p);//t1,t2是卖方
		Thread t2 = new Thread(p);
		Thread t3 = new Thread(c);//t3,t4是买方
		Thread t4 = new Thread(c);
		
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

class Res
{
	private String name;
	private int count  = 0;
	private boolean b = false;
	public synchronized void set(String name)
	{
		while(b)
			try{this.wait();}catch(Exception e){} //有手机等待手机卖出
												
		this.name = name+"_"+count;

		count++;

		System.out.println(Thread.currentThread().getName()+".....买方...."+this.name);

		b = true; //切换为有手机
		this.notifyAll();//唤醒
	}

	public synchronized void out()
	{
		while(!b)
			try{this.wait();}catch(Exception e){} //无手机,等待到货
		System.out.println(Thread.currentThread().getName()+".....卖方...."+this.name);
		b = false; //切换为无手机状态
		this.notifyAll();
	}
}

//实现Runnable接口实现多线程
class Pro implements Runnable
{
	private Res r;
	Pro(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
		{
			r.set("phone");
		}
	}
}

class Cus implements Runnable
{
	private Res r;
	Cus(Res r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
		{
			r.out();
		}
	}
}

输出结果(部分截图):


关于上面的代码,解释几点:

1,wait(),sleep()有什么区别?

sleep()是线程类(Thread)的方法,让出cpu,让cpu去执行其他线程,在指定的时间过后,cpu回到这个线程继续执行。

wait()是Object类的方法,调用wait方法会导致线程放弃对象锁,进入等待锁定池,只有发出notify方法或notifyAll后线程才进入对象锁定池准备获得对象锁进入运行状态。

总结来说:

wait():释放cpu执行权,释放锁。

        sleep():释放cpu执行权,不释放锁。

2,notify()和notifyAll()的区别?

notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。

notify(): 唤醒一个正在等待该对象的线程。

notifyAll(): 唤醒所有正在等待该对象的线程。

两者的最大区别在于:

notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。

notify他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁,此时如果该对象没有再次使用notify语句,即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。

 

七,单线程、多线程什么时候使用?

多线程效率确实比单线程效率高,但使用也是要分情况的,一些情况下并不适合用多线程。

1.耗时的操作使用线程,提高应用程序响应。如读取数据库文件、导入数据。

2.并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求。

3.多CPU系统中,使用线程提高CPU利用率

4.改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

其他情况都使用单线程。


                                              

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


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值