黑马程序员-【多线程】

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

一、什么是多线程

 

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

    线程:是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里

执行多任务。通常由操作系统负责多个线程的调度和执行。
   

   多线程是这样一种机制,它允许在程序中并发执行多个指令流,每个指令流都称为一个线程,彼此间互相独立。线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度。

   

     进程中至少要有一个线程,开启多个线程是为了同时运行多部分代码,每个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。

    多线程的好处:解决了多部分同时运行的问题,多线程的弊端:线程太多会导致效率降低。

    JVM启动时就启动了多个线程,1、执行main函数的线程,该线程的任务代码都定义在main函数中   2、负责垃圾回收的线程

   

   返回当前线程的名称:Thread.currentThread().getName()
   

   线程的名称是由:Thread-编号定义的。编号从0开始,也可以在线程初始化时,自定义线程的名字
    

   JVM创建主线程的任务都定义在了主函数中,自定义线程要运行的代码都统一存放在了run方法中。
    

   线程要运行必须要通过类中指定的方法开启。start方法。(启动后,就多了一条执行路径)
    

    start方法:1)、启动了线程;2)、让jvm调用了run方法。

二、多线程的实现

 

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

 

1、继承Thread类

      1)定义一个类并且继承Thread类

      2)复写Thread类的run方法,在run方法中写入要运行的代码

      3)创建Thread子类的实例对象

     4)调用子类的start方法,开启线程并执行run方法中的代码

 

class MyThread extends Thread{  
    public MyThread() {  
        super();  
    } 
    //声明带有参数的构造方法
    public MyThread(String name) {  
        super(name);  
        // TODO Auto-generated constructor stub  
    }  
     //覆写Thread类的run方法
      public void run() {  

            System.out.println("这是:"+Thread.currentThread().getName());  

    }  
}  
public class ThreadDemo {  
    public static void main(String[] args) {  
        //实例化MyThread类并调用start方法开启线程
        MyThread demo1 = new MyThread("线程一");  
        demo1.start();  
        MyThread demo2 = new MyThread("线程二");  
        demo2.start();  
                  
    }  
}  

 

2、实现Runnable接口

 

      1)定义一个类实现Runnable接口

      2)覆盖接口中的run方法,写入自己需要执行的代码

      3)通过Thread类创建线程对象,将实现了Runnable接口的子类对象作为实际参数传递给Thread类中的构造函数

      4)调用Thread对象的start方法,开启线程,执行run方法中的代码

 

class Ticket implements Runnable{
	private  int tick = 100;
	//覆写Runnable接口的run方法  
   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 t1 = new Thread(t);//创建了一个线程;
		Thread t2 = new Thread(t);//创建了一个线程;
		t1.start();//开启第一个线程
		t2.start();//开启第二个线程
	}
}

 

3、两个实现方式的比较

 

    1)因为Java是单继承的,通过实现Thread类来开启多线程,那么这个类就不能再继承其他类了,同样如果该类是每个类的子类,但是还想开启多线程的话只能通过实现Runnable接口的方式实现,所以实现Runnable接口可以很好的避免单继承的局限性

    2)Runnable接口将线程任务从线程的子类中分离出来,进行了单独封装,按照面向对象的思想将任务封装成了对象

 

三、线程的状态

 

      NEW-------新建状态,至今尚未启动的线程处于这种状态。
    

      RUNNABLE-------运行状态,正在 Java 虚拟机中执行的线程处于这种状态。
    

      BLOCKED-------阻塞状态,受阻塞并等待某个监视器锁的线程处于这种状态。
    

      WAITING-------冻结状态,无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
    

      TIMED_WAITING-------等待状态,等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
    

       TERMINATED-------已退出的线程处于这种状态。

黑马程序员-【多线程】_第1张图片

四、线程的控制及优先级

 

     join方法:调用join方法的线程对象强制运行,该线程强制运行期间,其他线程无法运行,必须等到该线程结束后其他线程才可以运行。

 有人也把这种方式成为联合线程

   SetDaemon方法:设置后台线程,处于后台运行,任务是为其他线程提供服务。也称为“守护线程”或“精灵线程”。JVM的垃圾回收就是典型的后台线程。
  

    特点:若所有的前台线程都死亡,后台线程自动死亡。
  

    设置后台线程:Thread对象setDaemon(true);
  

     setDaemon(true)必须在start()调用前。否则出现IllegalThreadStateException异常;

   sleep:线程休眠,让执行的线程暂停一段时间,进入阻塞状态,使用方法为Thread.sleep(long milllis).

    
    线程礼让:暂停当前正在执行的线程对象,并执行其他线程;Thread的静态方法,可以是当前线程暂停,但是不会阻塞该线程,而是进入就绪状态。所以完全有可能:某个线

程调用了yield()之后,线程调度器又把他调度出来重新执行。

    每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关。并非线程优先级越高的就一定先执行,哪个线程的先运行取决于CPU的调度;
   

   默认情况下main线程具有普通的优先级,而它创建的线程也具有普通优先级。
  

    Thread对象的setPriority(int x)和getPriority()来设置和获得优先级。  MAX_PRIORITY : 值是10    MIN_PRIORITY : 值是1    NORM_PRIORITY : 值是5(主方法默认优先级)

五、线程安全问题

 

  1、线程安全问题出现的原因:

 

        一个线程执行多条语句,并计算出某一数据时,该线程还没有执行完,其他线程就参与进来了,并操作了这个数据,导致计算的数据产生了错误

   由此可以看出,当多个线程在操作共享数据,并且线程操作有随机性且操作出现延迟时就有可能造成安全性的问题,例如:

 

class SellDemo implements Runnable{
	private int num = 50;
	@Override
	public void run() {
		for (int i = 0; i < 200; i++) {
		
				if(num > 0){	
					try {
				  //在程序中加入延迟
            Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				System.out.println(Thread.currentThread().getName()+"卖出第"+num--+"张票!");
			}
		}
	}
}

public class Demo3 {
	public static void main(String[] args) {
		SellDemo s = new SellDemo();
		new Thread(s,"A").start();
		new Thread(s,"B").start();
		new Thread(s,"C").start();
	}
}

 

以上的例子可能造成有负数票的产生


2、线程安全的解决方案

     只要将操作共享数据的语句在某一时段只让一个线程执行,在执行过程中,其他线程不能进来执行就可以解决这个问题,在Java中可以通过同步机制解决,主要的同步机制包括同步代码块,同步函数,Lock锁来实现

  

 1)同步代码块

   用法:

 

       synchronized(对象){需要被同步的代码}

 

   同步的好处:解决了线程安全问题,同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁,同步的前提:同步中必须有多个线程并使用同一个锁,这个锁就是对象,没有持有锁的线程无法进入代码块中进行执行。

 

class SellDemo implements Runnable{
	private int num = 50;
	Object obj = new Object(); 
	public void run() {
		for (int i = 0; i < 200; i++) {
	            synchronized(obj){
			 if(num > 0){	
					try {
					//在程序中加入延迟 
            Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				System.out.println(Thread.currentThread().getName()+"卖出第"+num--+"张票!");
			 }                                                                                                                           }
		}
	}
}

public class Demo3 {
	public static void main(String[] args) {
		SellDemo s = new SellDemo();
		new Thread(s,"A").start();
		new Thread(s,"B").start();
		new Thread(s,"C").start();
	}
}


 2)同步函数

 

用法:直接在方法上加上synchronized修饰符,让方法具备了同步性

 

同步方法使用的锁是this

synchronized 返回值类型 方法名(参数列表)
{
                   /**
                          TODO SOMETHING
                   */
}

 

class SellDemo implements Runnable{
	private int num = 50;
	Object obj = new Object(); 
	public void run() {
		show();
	}
	//在方法中加入关键字synchronized
	public synchronized void show() {
		for (int i = 0; i < 200; i++) {
	          
			 if(num > 0){	
					try {
			//在程序中加入延迟 
            Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				System.out.println(Thread.currentThread().getName()+"卖出第"+num--+"张票!");                                                                                                                      }
		}
	}
}

public class Demo3 {
	public static void main(String[] args) {
		SellDemo s = new SellDemo();
		new Thread(s,"A").start();
		new Thread(s,"B").start();
		new Thread(s,"C").start();
	}
}


3)lock同步锁

 

jkd1.5后的另一种同步机制:通过显示定义同步锁对象来实现同步,这种机制,同步锁应该使用Lock对象充当。

 

使用方法如下

 

public class X {
	private final ReentrantLock lock = new ReentrantLock();
	//定义需要保证线程安全的方法
	public void method(){
		//加锁
		lock.lock();
		try{
			//... method body
		}finally{
			//在finally释放锁
			lock.unlock();
     }
   }
}

 

3、线程同步的利弊

 

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

弊端:相对降低性能,因为判断锁需要消耗资源,产生了死锁。

 

定义同步是有前提的:

1,必须要有两个或者两个以上的线程,才需要同步。

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

4、死锁的产生

 

当同步发生嵌套时,就有可能发生死锁,一个外部同步锁等待另一内部同步锁的释放,这两个同步都不会释放自己的锁,就会导致两个线程相互等待都不会继续向下进行,如下

 

public class DeadLockDemo {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
			// TODO Auto-generated method stub
			Ticket1 ticket1 = new Ticket1();
			Thread thread1 = new Thread(ticket1);
			thread1.start();
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			Thread thread2 = new Thread(ticket1);
			ticket1.flag = false;
			thread2.start();
	}
}
class Ticket1 implements Runnable{

	private static int ticket1 = 10;
	private Object obj = new Object();
	boolean flag = true;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		if(flag){
			while(true){
				
				synchronized (obj) {
					show();
				}
			}
		}else{
			while(true){
				show();
			}
		}
	}
	public synchronized void show(){
		synchronized (obj) {
			if(ticket1>0){
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+":"+ticket1--);
			}
		}
	}
}

 

六、线程间的通信

 

1、生产者和消费者:

 

   有一个存储空间,由两部分组成,一部分用于存储人的姓名,另一部分用于存储人的电话;

   现在有两个线程,一个线程不停向数据存储空间添加数据(生产者),另一个线程从数据空间取出数据(消费者);
 

   因为线程的不确定性,存在于以下两种情况:
 

   若生产者线程刚向存储空间添加了人的姓名还没添加人的性别,CPU就切换到了消费者线程,消费者线程把姓名和上一个人的性别联系到一起;
 

   生产者放了若干数据,消费者才开始取数据,或者是消费者取完一个数据,还没等到生产者放入新的数据,又重复的取出已取过的数据;



2、问题的解决

 

  要解决生产者和消费者的问题需要利用线程间的通信,让两个线程之间可以互相控制对方的状态

 

  wait():让当前线程放弃监视器进入等待,直到其他线程调用同一个监视器并调用notify()或notifyAll()为止。
 

  notify():唤醒在同一对象监听器中调用wait方法的第一个线程。
 

  notifyAll():唤醒在同一对象监听器中调用wait方法的所有线程。
 

 以上方法都是Object的方法,这些方法必须有对象监视器对象调用,同步的方法使用this对象,同步代码块可以使用括号内的对象调用这些方法。

 

当使用Lock锁时,Condition代替了同步监视器的功能,可以通过Lock()对象的newCondition()方法创建,

 

Condition的主要方法包括:

       await():  等价于同步监听器的wait()方法;
       signal(): 等价于同步监听器的notify()方法; 
       signalAll(): 等价于同步监听器的notifyAll()方法;

 

例1,一个线程想数据中写入姓名和电话,另一个线程分别取出,写一个取出一个

 

class Person
{
	String name;
	String phone;
	boolean flag = false;
}
class Input implements Runnable
{
	private Person p;
	Input(Person p)
	{
		this.p = p;
	}
	public void run()
	{
		int x = 0;
		while(true)
		{
			synchronized(p)
			{

				if(p.flag)
					try{p.wait();}catch(Exception e){}
				if(x==0)
				{
					p.name="匪警";
					p.phone="110";
				}
				else
				{
					p.name="急救";
					p.phone = "120";
				}
				x = (x+1)%2;
				p.flag = true;
				p.notify();
			}
		}
	}
}
class Output implements Runnable
{
	private Person p ;
	
	Output(Person p)
	{
		this.p = p;
	}
	public void run()
	{
		while(true)
		{
			synchronized(p)
			{
				if(!p.flag)
					try{p.wait();}catch(Exception e){}
				System.out.println(p.name+"...."+p.phone);
				p.flag = false;
				p.notify();
			}
		}
	}
}
class  InputOutputDemo
{
	public static void main(String[] args) 
	{
		Person p = new Person();
		Input in = new Input(p);
		Output out = new Output(p);

		Thread t1 = new Thread(in);
		Thread t2 = new Thread(out);
		t1.start();
		t2.start();
	}
}

 

例2子线程循环10次,接着主线程循环100,接着又回到子线程循环10次,接着再回到主线程又循环100,如此循环50次,请写出程序。

public class ThreadTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		new ThreadTest().init();

	}

	public void init()
	{
		final Business business = new Business();
		new Thread(
				new Runnable()
				{

					public void run() {
						for(int i=0;i<50;i++)
						{
							business.SubThread(i);
						}						
					}
					
				}
		
		).start();
		
		for(int i=0;i<50;i++)
		{
			business.MainThread(i);
		}		
	}
	
	private class Business
	{
		boolean bShouldSub = true;//这里相当于定义了控制该谁执行的一个信号灯
		public synchronized void MainThread(int i)
		{
			if(bShouldSub)
				try {
					this.wait();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}		
				
			for(int j=0;j<5;j++)
			{
				System.out.println(Thread.currentThread().getName() + ":i=" + i +",j=" + j);
			}
			bShouldSub = true;
			this.notify();
		
		}
		
		
		public synchronized void SubThread(int i)
		{
			if(!bShouldSub)
				try {
					this.wait();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}	
				
			for(int j=0;j<10;j++)
			{
				System.out.println(Thread.currentThread().getName() + ":i=" + i +",j=" + j);
			}
			bShouldSub = false;				
			this.notify();			
		}
	}
}

 

七、线程池及其使用

 

   当有多个任务时,如果每个认为都创建一个线程,那么会产生大量的线程,而大量的线程会大大的增大处理器的负担,这时可以定义一个线程池,池中可以包含几个线程,让线程自己去取任务


     1、线程池的定义


            1)第一种定义方式(固定大小线程池):
                    ExecutorsService threadPool = Executors.newFixedThreadPool(3);
                   线程池中建立三个线程
           

              2)第二种定义方式(缓存线程池):
                     ExecutorsService threadPool = Executors.newFixedThreadPool();
           

              3)第三种定义方式(单个线程池):
                   ExecutorsService threadPool = Executors.newThreadPool();


     2、定时器的使用


                1)Executors.newScheduledThreadPool(3).schedule(Runnable,delay,unit)
                  Runnable为run方法
                  delay为多长时间后线程启动
                  unit为时间的单位


               2)scheduleAtFixedRate固定时间频率线程启动


八、常见的问题

 

1、sleep() 和 wait() 有什么区别?


     (网上的答案:sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。 wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。)

sleep就是正在执行的线程主动让出cpu,cpu去执行其他线程,在sleep指定的时间过后,cpu才会回到这个线程上继续往下执行,如果当前线程进入了同步锁,sleep方法并不会释放锁,即使当前线程使用sleep方法让出了cpu,但其他被同步锁挡住了的线程也无法得到执行。wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果notify方法后面的代码还有很多,需要这些代码执行完后才会释放锁,可以在notfiy方法后增加一个等待和一些代码,看看效果),调用wait方法的线程就会解除wait状态和程序可以再次得到锁后继续向下运行


2、top()和suspend()方法为何不推荐使用?

 

反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。

 

 3、同步和异步有何异同

 

如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。

  

 

你可能感兴趣的:(黑马程序员—java)