结合实例分析线程及多线程的使用
Java中线程(Thread)的知识很重要,没有它,我们项目中的很多功能都无法实现。跟线程有关的是进程,日常生活中我们听的比较多的是进程,通常我们的电脑卡了,我们就会说要杀进程。进程跟线程是不同的概念,两者有区别也有联系。进程,通俗的讲就是我们电脑中运行中的程序,程序的概念是静态的,进程是动态的概念。像我们电脑运行的视频播放器,音乐播放器都是进程。线程,是运行在进程中的顺序控制流,只能使用分配给进程的资源和环境。一个进程中至少会有一个线程。
了解线程的相关概念后,我们现在来将如何实现线程。线程的实现方式有两种。一种是通过继承Thread类,并重写run()方法实现;另一种是通过实现Runnable接口并实现其run()方法。下面通过例子来分析两种实现的区别。
1、通过继承Thread类
1 package thread; 2 /** 3 * 4 * @author CIACs 5 *线程的生成通过继承Thread类实现 6 */ 7 public class ThreadTest { 8 public static void main(String[] args) { 9 MyThread1 t1 = new MyThread1(); 10 t1.start(); 11 MyThread2 t2 = new MyThread2(); 12 t2.start(); 13 } 14 15 } 16 17 class MyThread1 extends Thread 18 { 19 20 @Override 21 public void run() { 22 for(int i=0;i<50;i++) 23 { 24 System.out.println("MyThread1 running: "+i); 25 } 26 } 27 } 28 class MyThread2 extends Thread 29 { 30 @Override 31 public void run() { 32 for(int i=0;i<50;i++) 33 { 34 System.out.println("MyThread2 running: "+i); 35 } 36 } 37 }
控制台输出结果:
在这里我们可以看到两个线程会交叉执行,并不是一个先执行完后,另一个再执行。这就是说当线程启动后我们是不能控制执行顺序的。(当然是在还没用synchronized、wait()、notify()的时候)
2、通过实现Runnable接口
1 package thread; 2 /** 3 * 4 * @author CIACs 5 *线程通过实现Runnable接口生成 6 */ 7 public class ThreadTest2 { 8 public static void main(String[] args) { 9 Thread1 t1 = new Thread1(); 10 new Thread(t1).start(); 11 Thread2 t2 = new Thread2(); 12 new Thread(t2).start(); 13 } 14 15 } 16 17 class Thread1 implements Runnable 18 { 19 @Override 20 public void run() { 21 for(int i=0;i<50;i++) 22 { 23 System.out.println("Thread1 running "+i); 24 } 25 26 } 27 } 28 29 class Thread2 implements Runnable 30 { 31 @Override 32 public void run() { 33 for(int i=0;i<50;i++) 34 { 35 System.out.println("Thread2 running "+i); 36 } 37 38 } 39 }
控制台输出结果:
在这里我只上传了部分结果的截图,我们所要的在这部分截图中就可以看出了。
在编写程序时我们把希望线程执行的代码放到run()方法中,然后通过调用start()方法来启动线程,start()方法会为线程的执行准备好资源,之后再去调用run()方法,当某个类继承了Thread类后,该类就是线程类。线程的消亡不能通过调用stop()方法,而是让run()方法自然结束。
每个线程有其优先级,最高为10(MAX_PRIORITY),最低为1(MIN_PRIORITY),设置优先级是为了在多线程环境中便于系统对线程的调度,同等情况下,优先级高的会先比优先级低的执行。当然操作系统也不是完全按照优先级高低执行的,否则有可能优先级低的会一直处于等待状态,操作系统有自己的调度算法(这里就先不展开讨论了)。当运行中的线程调用了yield()方法,就会让出cpu的占用;调用sleep()方法会使线程进入睡眠状态,此时其他线程也就可以占用cpu资源了;有另一个更高优先级的线程出现也会导致运行中的线程让出cpu资源。有些调度算法是分配固定的时间片给线程执行,在这段时间内可以占用cpu,一旦用完就必须让出cpu资源。
线程的生命周期有如下四个
1、创建状态,当用new创建一个新的线程对象时,该线程处于创建状态,但此时系统没有分配资源给它。
2、可运行状态,执行线程的start()方法后系统将会分配线程运行所需的系统资源,并调用线程体run()方法,此时线程处于可运行状态。
3、不可运行状态,当线程调用了sleep()方法,或者wait()方法时,线程处于不可运行状态,线程的输入输出阻塞时也会导致线程不可运行。
4、消亡状态,当线程的run()方法执行结束后,就进入消亡状态。
下图是普通线程的状态转换图:
当使用了synchronized关键字时,线程的状态转换图会有点不同,如下图
控制多线程同步,使用wait()和notify()的线程状态转换图如下:
对于单核cpu来说,某一时刻只能有一个线程在执行,但在宏观上我们会看到多个进程在执行,这就是微观串行,宏观上并行。现在单核的电脑基本上已经没有了。多核的电脑就可以实现微观并行。多线程编程就是为了最大限度的利用cpu资源。例如当某一个线程和外设打交道时,此时它不需要用到cpu资源,但它仍然占着cpu,其他的线程就不能利用,多线程编程就可以解决该问题。多线程是多任务处理的一种特殊形式。但是多线程可能会造成冲突,两个或多个线程要同时访问一个共同资源。
下面以银行取款为例,有一个账号,里面存1000元,然后创建两个线程模拟从银行柜台和ATM同时取700元。这里我们的银行卡不能透支。
1 package thread; 2 /** 3 * 4 * @author CIACs 5 * 6 */ 7 public class BankAccount { 8 private int MyMoney = 1000; 9 10 public void getMoney(int money) 11 { 12 if(MyMoney<=0||(MyMoney-money)<0) 13 { 14 System.out.println("余额不足"); 15 } 16 else 17 { 18 try { 19 Thread.sleep((long)(Math.random()*1000)); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 System.out.println("取"+money); 24 MyMoney = MyMoney-money; 25 } 26 System.out.println("账户剩余的钱"+MyMoney); 27 } 28 }
1 package thread; 2 /** 3 * 4 * @author CIACs 5 * 6 */ 7 public class Thread1 extends Thread{ 8 private BankAccount MyBank; 9 public Thread1(BankAccount ba) 10 { 11 this.MyBank = ba; 12 } 13 @Override 14 public void run() { 15 16 MyBank.getMoney(700); 17 } 18 19 }
1 package thread; 2 3 public class Client { 4 public static void main(String[] args) { 5 BankAccount MyBank = new BankAccount(); 6 Thread1 t1 = new Thread1(MyBank); 7 Thread1 t2 = new Thread1(MyBank); 8 t1.start(); 9 t2.start(); 10 11 } 12 }
控制台输出结果:
我们取了1400,账户余额未-400,很明显银行是不允许我们这样做的。这就说,可能当一个线程进入到取钱代码部分时先进行了睡眠,另一个也进来了,且也进入睡眠。当其中一个醒来时,取完钱后,另一个也醒来继续取钱。我们如何解决这个问题呢?这时我们就要用synchronized关键字来解决。只需在取钱的方法处加上synchronized关键字修饰就可以解决该问题。
加上后控制台输出结果:
synchronized关键字是同步的意思,当synchronized修饰一个方法时,该方法叫做同步方法。Java中的每个对象都有一个锁(lock)或者叫监视器(monitor),当我们使用synchronized关键字修饰一个对象的方法时,就会把这个对象上锁,当该对象上锁时,其他的线程就无法再访问该对象的方法,直到线程结束或抛出异常,该对象的锁就会释放掉。看似在方法前加上synchronized关键字修饰好像完美的解决了多线程访问同一资源的冲突问题,但是由于synchronized是粗粒度的,也就是使用了该关键字会把方法所对应的类也会锁上,该类的任何其他方法都不会被其他线程所访问,这样就导致了资源利用率降低。为了解决这个方法,我们还是要利用synchronized关键字。此时synchronized关键字锁的是我们创建的任何一个对象。synchronized块代码如下
1 private obj = new Object(); 2 synchronized(obj) 3 { 4 //线程要执行的代码 5 }
此时我们锁的是我们创建的一个任一对象,synchronized块外面的方法是没有被锁的,也就是说其他线程可以访问synchronized块外面的方法。
wait()跟notify()方法可以实现线程的等待和唤醒操作,这两个方法都是定义在Object类中的,而且是final的,因此会被所有的java类所继承,且无法重写。调用这两个方法要求线程已经获得了对象的锁,因此会把这两个方法放在synchronized方法或块中,且是成对出现的。执行wait()时,线程会释放对象的锁,还有一个方法也会使线程暂停执行,那就是sleep()方法,不过该方法不会释放对象的锁。
生产者,消费者问题在每一本学习线程的书上都会有提到,大概就是说生产者生产了商品就通知消费者消费商品,当消费者消费了商品酒通知生产者生产商品,这里涉及到了线程间的通信。我们用输出0和1来模拟生产者消费者问题。当number为1时就要减一,当number为0时就要加一。要解决该问题就要用wait()跟notify()方法结合synchronized的使用。
Number类
1 package thread; 2 /** 3 * 4 * @author CIACs 5 * 6 */ 7 public class Number 8 { 9 private int number = 1; 10 public synchronized void AddNumber() 11 { 12 if(number > 0) 13 { 14 try 15 { 16 //当不符合条件时进入等待状态,让出cpu资源 17 wait(); 18 } catch (InterruptedException e) 19 { 20 e.printStackTrace(); 21 } 22 } 23 //执行加操作 24 number++; 25 System.out.println(number); 26 //唤醒减操作的线程 27 notify(); 28 } 29 30 public synchronized void SubNumber() 31 { 32 if(number == 0) 33 { 34 try 35 { 36 //等于0时进入等待状态 37 wait(); 38 } catch (InterruptedException e) 39 { 40 e.printStackTrace(); 41 } 42 } 43 //执行减操作 44 number--; 45 System.out.println(number); 46 //唤醒加操作的线程 47 notify(); 48 } 49 }
AddThread类
1 package thread; 2 /** 3 * 4 * @author CIACs 5 * 6 */ 7 public class AddThread extends Thread 8 { 9 //设置线程执行的标志,初始为true 10 boolean flag = true; 11 private Number number; 12 public AddThread(Number number) 13 { 14 this.number = number; 15 } 16 //改变判断标志,使线程停止 17 public void setFlag() 18 { 19 this.flag = false; 20 } 21 22 @Override 23 public void run() 24 { 25 while(flag) 26 { 27 try 28 { 29 Thread.sleep((long)(Math.random()*1000)); 30 this.number.AddNumber(); 31 } catch (InterruptedException e) 32 { 33 e.printStackTrace(); 34 } 35 } 36 } 37 }
SubThread类
1 package thread; 2 /** 3 * 4 * @author CIACs 5 * 6 */ 7 public class SubThread extends Thread 8 { 9 //设置线程执行的标志,初始为true 10 boolean flag = true; 11 private Number number; 12 public SubThread(Number number) 13 { 14 this.number = number; 15 } 16 //改变标志的值,使线程停止 17 public void setFlag() 18 { 19 this.flag = false; 20 } 21 22 @Override 23 public void run() 24 { 25 while(flag) 26 { 27 try 28 { 29 Thread.sleep((long)(Math.random()*1000)); 30 this.number.SubNumber(); 31 } catch (InterruptedException e) 32 { 33 e.printStackTrace(); 34 } 35 } 36 } 37 }
客户端
1 package thread; 2 /** 3 * 4 * @author CIACs 5 * 6 */ 7 public class Client 8 { 9 public static void main(String[] args) 10 { 11 Number num = new Number(); 12 AddThread t1 = new AddThread(num); 13 SubThread t2 = new SubThread(num); 14 t2.start(); 15 t1.start(); 16 try 17 { 18 //过了三秒后使其中一个线程停止,一个线程停止后,另一个也不能执行,因为另一个线程处于等待状态。 19 Thread.sleep((long)(Math.random()*3000)); 20 t1.setFlag(); 21 22 } catch (InterruptedException e) 23 { 24 e.printStackTrace(); 25 } 26 27 } 28 }
控制台输出结果:
在这个例子中我利用flag标志来控制线程的停止和执行,我们不会用stop()方法去控制线程的停止。
在线程的使用中我们还要注意的是如果线程中有控制的是局部变量则每个线程对局部变量的改变互不影响,如果是成员变量则会影响到其他线程。使用好线程,我们能提高做事的效率。