Java基础--------多线程


 多线程         

  个人理解:要想理解多线程,就得先理解线程,要想明白线程,那么就要掌握进程啦,下面我们从进程开始,来一步步学习多线程吧~~

   进程     

           是一个正在执行的程序。

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

   线程    

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

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


    多线程

                在java虚拟机启动的时候会有一个进程java.exe,该进程中至少有一个线程负责java程序的执行。

                而且这个线程运行的代码存在于main方法中。该线程称之为主线程。JVM启动除了执行一个主线程,

                还有负责垃圾回收机制的线程。像这种在一个进程中有多个线程执行的方式,就叫做多线程。

     

 举例:迅雷下载,360管理界面。


我们如何实现多线程程序呢?

     由于线程是依赖于进程存在,而进程是由操作系统创建的,并且java语言是不能直接调用操作系统的功能。
所以,为了方便对多线程程序的时候,java就提供了线程的API对应的类-----线程类:Thread。


通过查看API,我们知道创建线程的方式有2种。


 
方式1:继承Thread类

      A:定义一个类继承Thread类。


        B:子类要重写Thread类的run()方法。

                     目的:将线程要执行的代码储存在run方法中,让程序运行。


        C:让线程启动并执行。

                       1.   创建类的实例对象,就相当于创建了一个线程。

                        2   调用线程的start方法,该方法有两个作用


                              a.   启动线程


                              b.   调用run方法


                              start:调用到底层让控制单元去执行的动作。


                              run:封装线程要运行的代码

 

                    注意:调用线程的start方法是开启线程并执行该线程的run方法。


                                而直接调用run方法仅仅是对象调用方法,而线程创建了,并没有运行

 


下面写一个小例子帮助理解:

package cn.itcast01;
/*
 * 为了看到每次确实在变化,我们要是能够知道线程对象的名字有多好呢?
 * 在Thread类中提供了一个方法:
 * 		public final String getName():获取线程对象的名称。默认情况下,名字的组成 Thread-编号(编号从0开始)
 * 		public final void setName(String name):设置线程名称。
 */
public class MyThread extends Thread{
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(getName()+"-----Hello "+i);
		}
	}
}
public class ThreadDemo {
	public static void main(String[] args) {
		MyThread mt1=new MyThread();
		MyThread mt2=new MyThread();

		mt1.setName("QQ聊天");
		mt2.setName("微信");

		mt1.start();
		mt2.start();

	}
}

运行后结果:

                            


不知道有没有人和我一样问过:为什么要覆盖run方法呢?

                 Thread类用于描述线程,Thread类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。

                       也就是说Thread类中的run方法,用于存储线程要运行的代码。


 方式2:


     A:创建一个类实现Runnable接口
    B:重写run()方法
     C:创建类的实例
    D:把类的实现作为Thread的构造参数传递,创建Thread对象


这时候肯定会有人问:既然有了继承Thread类的方式,为什么还要有实现Runnable接口的方式?

    A:避免单继承的局限性,如果你继承了Thread类,那么你就不能再继承其他的类,这样就会有好多局限性。
    B:实现接口的方式,只创建了一个资源对象,更好的实现了数据和操作的分离。
  一般我们选择第二种方式。


下面用第二种方式实现:

package cn.itcast01;
/*
 * public static Thread <strong>currentThread():</strong>返回当前正在执行的线程对象引用
 */
public class MyRunnable implements Runnable{
	@Override
	public void run() {
		for (int i = 0; i <100; i++) {
			System.out.println(Thread.currentThread().getName()+"-----Hello "+i);
		}
	}
}
public class MyRunnableDemo {
	public static void main(String[] args) {
		MyRunnable mr=new MyRunnable();

		Thread t1=new Thread(mr);
		Thread t2=new Thread(mr);

		t1.setName("QQ聊天");
		t2.setName("微信");

		t1.start();
		t2.start();
	}
}


运行后结果和第一种方式一样,在这儿就不截图占用空间啦。


注意:启动线程并执行,是不能使用run()方法的。这个时候,必须使用另外的一个方法。

      这个方法名是start()。这个方法其实做了两件事情。

       第一,让线程启动。第二,自动调用run()方法。

学到这儿,肯定会有人问学习多线程就这些好处啊?下面我们来总结一下多线程的意义:

   多线程的出现能让程序产生同时运行效果。可以提高程序执行效率。



         例如:在java.exe进程执行主线程时,如果程序代码特别多,在堆内存中产生了很多对象,而同时对象调用完后,就成了垃圾。


如果垃圾过多就有可能是堆内存出现内存不足的现象,只是如果只有一个线程工作的话,程序的执行将会很低效。


而如果有另一个线程帮助处理的话,如垃圾回收机制线程来帮助回收垃圾的话,程序的运行将变得更有效率。


线程的生命周期(几种状态): 


                   被创建:等待被start方法被调用启动。


                    运行状态:具有执行资格和执行权。


                    临时状态(阻塞状态):具备执行资格,但是没有执行权。


                   冻结状态:有两个状态


                                      a.   睡眠:sleep(time)方法 


                                      b.   等待:wait()方法。放弃了执行资格。


                       当sleep方法时间到或者调用到notify()方法时,获得执行资格,变为临时状态。


                        变为临时状态后,如果CPU空闲再执行。


                  消亡状态:run()方法结束,或者遇到stop()方法。


      在这儿需要注意的是: 总结:没有执行资格是冻结状态,有执行资格没有执行权是阻塞状态,执行权执行资格都有是运行状态。


线程的状态图如下:

 


线程的安全问题:


谈到线程安全问题,我想先模拟一下火车站窗口卖票的场景,来让大家看一下出现的问题:

public class TicketRunnable implements Runnable {
	private int tickets = 100;
	@Override
	public void run() {
		while (true) {
			//t1,t2,t3,t4过来了
			//tickets = 1
			if (tickets > 0) {
				try {


					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}


				System.out.println(Thread.currentThread().getName() + "正在出售第"
						+ (tickets--) + "张票");	


			}
		}
	}
}
public class RunnableTest {
	public static void main(String[] args) {
		
   TicketRunnable tr=new TicketRunnable();
   
     Thread t1=new Thread(tr);
     Thread t2=new Thread(tr);
     Thread t3=new Thread(tr);
     Thread t4=new Thread(tr);
     
		t1.setName("窗口1");
		t2.setName("窗口2");
		t3.setName("窗口3");
		t4.setName("窗口4");


		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}
运行后结果:

                                    


我们发现竟然票数还有负的,目前这个代码符合真实的买票程序,但是有问题,居然出现了负数票的情况。

那么,产生的原因是什么呢?

   线程的随机性和延迟性,导致了线程访问共享数据出现了问题。

怎么解决呢?

            在多线程程序中,一般来说,不会是所有的代码都有问题。

            所以,我们只需要找到那些可能出现问题的代码,然后把它们包起来,看做一个整体;

            只有这个整体完毕,别人才能继续访问,为了安全,把这个整体包起来的代码加个锁,给锁起来。


那么,怎么找呢?(多线程出问题的判断条件)

                  A:看有没有共享数据

                  B:看对共享数据的操作是不是多条语句

                  C:看是不是在多线程程序中

    

           找到后,就把同时满足着三个条件的代码给锁起来。


怎么锁嘞?

              java提供了一种锁机制方式:

                     synchronized(锁对象)

          {

              需要被锁的代码;

           }

     锁对象:怎么做呢? 反正不知道,所以,我们就用Object类的实例。


     注意:多个线程必须使用同一把锁。


把上面那个代码改进后:

public class TicketRunnable implements Runnable {


	private int tickets = 100;
	private Object obj = new Object();


	@Override
	public void run() {
		while (true) {
			//tickets=1
			//t1,t2,t3,t4都来了
			//假设t1抢到,看到是开的状态
			synchronized (obj) {
				//锁对象的状态:开,关
				//t1进来了,给外界了一个关的状态。
				if (tickets > 0) {
					try {
						//t1睡了
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "正在出售第"
							+ (tickets--) + "张票");
				}
			} //t1把状态修改为开
		}
	}

同步代码块的锁对象是什么?
  同步代码块的锁是任意对象。 

 同步方法:就是把锁加在方法上
格式:在方法上加synchronized关键字
 
同步方法的锁对象是谁?    this对象
 
 静态方法的锁对象是谁呢?
    是当前类的字节码文件对象。
    类名.class - Class类型的对象
 
  以后我们用同步代码块还是同步方法呢?
  被同步的内容越少越好,所以,一般使用同步代码块。
      如果一个方法内部全部都被同步了,那么,可以考虑使用同步方法。
 
public class TicketRunnable implements Runnable {

	private static int tickets = 100;
	private Object obj = new Object();
	private Demo d = new Demo();

	@Override
	public void run() {
		int x = 0;
		while (true) {
			if (x % 2 == 0) {
//				synchronized (this) {
				synchronized (TicketRunnable.class) {
					if (tickets > 0) {
						try {
							Thread.sleep(100);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						System.out.println(Thread.currentThread().getName()
								+ "正在出售第" + (tickets--) + "张票");
					}
				}
			} else {
				check();
			}
			x++;
		}
	}

	private static synchronized void check() {
		if (tickets > 0) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "正在出售第"
					+ (tickets--) + "张票");
		}
	}

}

线程代码的死锁问题


  死锁问题一般是在当同步中嵌套同步时,就有可能出现死锁现象。

   最经典就是哲学家就餐问题了。

下面是一个死锁的例子:

package cn.itcast_07;
/*
 * 死锁问题:
 * 		5个哲学家的故事。
 * 		5个哲学家去吃饭,菜饭都上齐了,筷子也上了,但是,一人只有一只筷子,每个人,先思考一会,把筷子借给别人,
 * 		然后,别人吃完了,自己在吃。假如这5个人都饿了,他们就会拿起自己的筷子,但是只有一只,都等待这个别人
 * 		放下那一只筷子,然后好拿过来吃饭,而没有任何一个人愿意先放下筷子,所以,就出现了死锁。
 */
public class DieLock extends Thread {
         
            //  定义一个布尔型的标记
	private boolean flag;
  
          //建立对象时接收boolean类型的数据  
	public DieLock(boolean flag) {
		this.flag = flag;
	}


	@Override
	public void run() {
		if (flag) {    
                              //    建立外锁A
			synchronized (MyLock.objA) {
				System.out.println("true -- objA");
                                     //  建立内锁 <strong>B</strong>
				synchronized (MyLock.objB) {
					System.out.println("true -- objB");
				}
			}
		} else {      
                               //   建立外锁B
			synchronized (MyLock.objB) {
 				System.out.println("false -- objA");
                                    //  建立内锁A  
				synchronized (MyLock.objA) {
					System.out.println("false -- objB");
				}
			}
		}
	}


}
//创建一个类用于放锁 
 class MyLock {
	public static final Object objA = new Object();
	public static final Object objB = new Object();
}


public class DieLockDemo {
	public static void main(String[] args) {


		DieLock d1 = new DieLock(true);
		DieLock d2 = new DieLock(false);
                   
                //开启线程
		d1.start();
		d2.start();
	}
}
运行后结果为:

                           

从上面的结果可以看到,两个人一人拿了一支筷子,想要对方的筷子,然后先吃饭,最后两人都不放,从而产生了死锁。


 线程间的通信

    

  线程间的通信问题,即不同种类的线程间对共享数据的操作问题;

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


举例:以学生作为资源举例。

      学生是资源,我们就可以对学生的属性进行赋值,也可以获取学生的属性值使用。
那么,我们要写哪些内容呢?

                             学生类

                              设置学生属性的类

                               获取学生属性的类

                               测试类

/*
 * 线程间的通信问题。刚开始的代码出现的问题都有:
 * 现在问题就产生了:
 * 		出现了:
 * 			张三 20
 * 			李四 23
 * 			李四 20
 * 			张三 23
 * 
 * 		我们先分析问题什么有这个问题,然后再解决问题。
 * 由于线程的随机性产生的问题。
 * 
 * 然后我们回到上面的那个总结:
 * 		A:是否有共享数据
 * 		B:是否有多条语句操作共享数据
 * 		C:是否在多线程环境中
 * 
 * 出问题的原因我们知道了,那么怎么解决呢?	
 * 		用同步解决。
 * 我们把setStudent给加同步了,但是,还是有问题。原因是需要对多个线程都要加同步。
 * 我给两个操作都加同步了,还是出问题,这一次的原因是:两种操作的锁对象不一致。
 * 当我们把所有的操作都加同步,并且锁用同一个以后,我们的数据就没有问题了。
 * 
 * 这个时候,我们看到了一个不好的现象,就是同一个数据一大片一大片的输出,原因就是每一次获取到CPU的执行权,就足够输出很多次数据。
 * 我想,能不能输出一个张三,然后输出一个李四,然后再输出一个张三,再输出一个李四...
 * 
 * 如何能够做到这个效果呢?
 * 
 * 做这个效果前,我们先分析一个问题,问题是:我们的程序其实是有一点点逻辑问题的。
 * 如果没有设置数据,就直接获取数据了。这应该不符合逻辑。
 * 正常逻辑是:
 * 		针对输出:
 * 			判断是否有数据,如果有就输出。否则,就等待设置数据。
 * 		针对设置:
 * 			判断是否有数据,如果有就等待输出数据,否则,就输出。
 * 等待唤醒机制:
 * 
 * Object类中:
 * 		wait() 让线程处于等待状态
 * 		notify() 唤醒等待的线程
 */
class Student {      //定义一个学生类
	String name;
	int age;
	// flag作为数据标识存在,true表示有数据,false表示没有数据。
	boolean flag = false;
}
   //设置学生属性的类
class SetStudent implements Runnable {
	private Student s;

	public SetStudent(Student s) {
		this.s = s;
	}

	@Override
	public void run() {
		// t1过来了
		int x = 0;
		while (true) {
			synchronized (s) {
				//如果有数据,就等待。
				if(s.flag){
					try {
						s.wait();//t1等待了,
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				
				if (x % 2 == 0) {
					s.name = "李四"; // t2抢到;
					s.age = 26;
				} else {
					s.name = "张三";// t2抢到;
					s.age = 29;
				}
				
				//修改标记
				s.flag = true;
				s.notify();//唤醒了t2,这个时候,可能是t1继续,或者t2抢到。
			}
			x++;// x=1,t2抢到;x=2,t2抢到;
		}
	}

}
      //        获取学生属性的类
class GetStudent implements Runnable {
	private Student s;

	public GetStudent(Student s) {
		this.s = s;
	}

	@Override
	public void run() {
		while (true) {
			synchronized (s) {
				if(!s.flag){
					try {
						s.wait();//t2等待了。wait()的线程,被唤醒后,继续执行。wait()方法出现后,对应的线程就释放了锁对象
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				
				System.out.println(s.name + "***" + s.age);
				
				//修改标记
				s.flag = false;
				s.notify(); //t1被唤醒,但是不一定会立马执行。
			}
		}
	}

}
     //测试类
public class StudentTest {
	public static void main(String[] args) {
		Student s = new Student();// 资源类

		SetStudent ss = new SetStudent(s);
		GetStudent gs = new GetStudent(s);

		Thread t1 = new Thread(ss);
		Thread t2 = new Thread(gs);

		t1.start();
		t2.start();
	}
}

线程的几个常用方法:


1)优先级

       通过查API,我们知道  线程默认优先级是5。范围是1-10。

             public final int getPriority() :  获取线程优先级
             public final void setPriority(int newPriority) :  更改线程的优先级。  

          

                  MAX_PRIORITY  最高优先级10

                  MIN_PRIORITY   最低优先级1

                  NORM_PRIORITY  分配给线程的默认优先级5

       在这儿需要注意的是:优先级可以在一定的程度上,让线程获较多的执行机会。


2)暂停线程  


       public static void yield() :  暂停当前正在执行的线程对象,并执行其他线程。

          是为了让线程更和谐一些的运行,但是你不要依赖这个方法保证,如果要真正的实现数据依次输出,请使用等待唤醒机制。


3)加入线程

           public final void join():等待该线程终止。 
             一旦有join()线程,那么,其他的线程必须都等待,一直到该线程结束。


4)守护线程

           public final void setDaemon(boolean on):设置线程为守护线程,一旦前台(主线程),结束,守护线程就结束了。

      在这要知道的是:main方法本身就是一个线程。


最后总结一些面试中经常遇到的关于线程方面的题:


 1.   wait(); notify() ; notifyAll()  都使用在同步中,因为要对持有监视器(锁)的线程操作。

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

                              因为这些方法在操作同步线程时,都必须要标识他们所操作的线程持有的锁。

                              只有同一个锁上的被等待线程,可以被同一个锁notify()唤醒,不可以对不同锁中的线程进行等待唤醒。

                              也就是说,等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object中。


 2.   wait(),sleep(time)有什么区别?


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


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


  3.   为什么要定义notifyAll()?


                        因为在需要唤醒对方线程时,如果只使用notify(),会容易出现只唤醒本方线程的情况,导致程序所有的线程都等待。


                        notify()只通知队列中的第一个线程,而notifAll()通知的是等待队列中的所有线程

        

      JDK1.5中提供了多线程的升级解决方案


            将同步synchronized替换成显示的Lock操作,将Object中的wait() ; notify() ; notifyAll()替换成condition对象,该对象可以通过Lock锁进行获取。

            

到此,线程的知识点儿就总结到这儿吧,有些理解的不对的地方,真诚地希望大家帮我指正出来,谢谢!!!


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值