多线程讲解
作者:gqk
1,本次预计讲解的知识点:
1、 多线程的操作中,对于线程功能的开发并不要求,但是其基本概念及各个操作语句必须熟练
2、 掌握多线程的两种实现方式及区别
3、 了解多线程的主要操作方法
4、 掌握线程的同步与死锁的概念
2、具体内容:
2.1、进程与线程(了解)
传统的 DOS 系统有一个很明显的特点,一旦程序中出现了病毒的话,则整个电脑将处于瘫痪的状态,这是因为传统 的 DOS 操作系统采用的是单进程的处理方式,即:在同一个时间段上只能有一个进程在运行着。
到了 windows 时代可以发现电脑中即使有了病毒也照样可以使用。因为 windows 本身属于多进程的操作系统,可以 在同一个时间段上运行多个程序,所有的程序都是并发运行的,但是在同一个时间点上只能有一个进程在执行。
线程实际上是在进程基础上的进一步划分,一个进程可以划分成多个线程。
举例:
计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。等工厂供电不足时:
进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人。一个进程可以包括多个线程,车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存
操作系统的设计,因此可以归结为三点:
(1)以多进程形式,允许多个任务同时运行;
(2)以多线程形式,允许单个任务分成不同的部分运行;
(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
2.2、Java 的线程实现:
在 Java 中如果要想进行多线程代码的实现有两种方式:
1,继承 Thread 类
2,实现 Runnable接口 下面通过代码分别来验证以上的两种方式及区别。
2.2.1、继承 Thread 类:
当一个类需要按照多线程的方式处理时,可以让这个类直接继承自 Thread 类即可,而且继承的时候要覆写好 Thread 类中提供的run()方法:
public void run(){}
范例:按照要求定义一个线程类
package com.xkrj.gqk; public class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("i=="+i); } } }
如果 要想启动一个线程并不是依靠run()方法而是 start()方法。
package com.xkrj.gqk; public class MyThread extends Thread{ private String name; public MyThread(String name){ this.name = name; } @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName()+"i=="+i); } } public static void main(String[] args) { MyThread thread1 = new MyThread("线程A"); MyThread thread2 = new MyThread("线程B"); MyThread thread3 = new MyThread("线程C"); thread1.start(); thread2.start(); thread3.start(); } }
那个线程抢到了 CPU 资源,那个线程就执行:
2.2.2、实现 Runnable 接口 :
package com.xkrj.gqk; public class MyThread implements Runnable{ private String name; public MyThread(String name){ this.name = name; } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(this.name+"运行,i="+i); } } }
调用线程:观察Thread的构造方法:
此时,通过 Thread 类进行了线程的启动。
package com.xkrj.gqk; public class MyThread implements Runnable{ private String name; public MyThread(String name){ this.name = name; } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(this.name+"运行,i="+i); } } public static void main(String[] args) { MyThread mt1 = new MyThread("线程 A") ; MyThread mt2 = new MyThread("线程 B") ; MyThread mt3 = new MyThread("线程 C") ; Thread t1 = new Thread(mt1); Thread t2 = new Thread(mt2); Thread t3 = new Thread(mt3); t1.start(); t2.start(); t3.start(); } }
2.2.3、两种实现方式的区别:
对于 Thread类和 Runnable 接口本身都是可以进行多线程的实现,那么两者到底该使用谁更好呢?
1,继承局限:使用 Runnable接口可以避免单继承的局限,而 Thread 类则有此局限;
2,资源共享:使用 Runnable 接口实现多线程,可以实现资源(对象属性)的共享,而 Thread 类却无法实现
范例:观察资源共享
继承Thread类资源不共享
现在的程序中每一个线程都各自占有各自的 count 属性,所以并没有达到资源共享的目的
package com.xkrj.gqk; public class MyThread extends Thread{ private int count = 5; @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("count===="+this.count--); } } public static void main(String[] args) { new MyThread().start(); new MyThread().start(); new MyThread().start(); } }
实现Runable接口资源共享:
package com.xkrj.gqk; public class MyThread implements Runnable{ private int count = 5; @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName()+"count===="+this.count--); } } public static void main(String[] args) { MyThread mt = new MyThread(); new Thread(mt).start(); new Thread(mt).start(); new Thread(mt).start(); } }
2.2.4、线程的状态:
1. 新建状态(New):新创建了一个线程对象。
2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
3.3、线程的操作方法(理解)
3.3.1、命名和取得
每一个线程实际上都可以为其设置名字,而且也可以取得每一个线程的名字: ·
设置线程名称:public final void setName(String name) ·
取得线程名称:public final String getName() 但是有一点也非常的麻烦,由于线程的操作不属于固定的状态,所以对于取得线程名称的操作,应该是取得的当前 正在运行的线程名称才合适,那么可以通过如下的方法取得一个当前正在运行的线程对象: ·
取得当前线程:public static Thread currentThread() 除了以上的设置名称的方法外,在 Thread 类中也提供了两个构造方法: · public Thread(String name) · public Thread(Runnable target,String name) 注意:一般都在线程启动前设置好名字,当然也可以为已经启动的线程修改名字或设置重名线程,不过这样不好。
那么既然是多线程的处理机制,实际上主方法是在一个 JVM 上产生的一个线程而已,那么一个 JVM 启动的时候至 少启动几个线程呢?两个:main、GC。
3.3.2、线程的休眠(重点)
漫天繁星
package com.xkrj.gqk; import java.awt.Color; import java.awt.Graphics; import javax.swing.JFrame; import javax.swing.JPanel; public class MyThread extends JPanel implements Runnable{ @Override public void run() { while(true){ try { Thread.sleep(1000);//让图像休眠x毫秒 } catch (InterruptedException e) { e.printStackTrace(); } repaint();//重新画出图片 } } @Override public void paint(Graphics g) { super.paint(g); System.out.println("11111"); g.setColor(Color.BLACK);//给下面的矩形区域上颜色 /*g.fillRect(0, 0,1000, 1000);*/ for(int i=1;i<=250;i++){ g.setColor(Color.white);//给星星上颜色 int x=(int )(Math.random()*1000);//星星在屏幕上的位置坐标取随机数 int y=(int )(Math.random()*1000); g.drawString("*", x, y);//调用这个函数将星星画在画板上 } g.fillOval(650, 200,75,75);//画一个黑圆,一个白园相交成一个白色的月亮 g.setColor(Color.BLACK); g.fillOval(675, 200, 75, 75); } public static void main(String[] args) { JFrame jf = new JFrame("漫天繁星"); jf.setSize(1000, 1000); jf.setLocation(300, 0); MyThread m = new MyThread(); m.setBackground(Color.black); jf.add(m); Thread t = new Thread(m); t.start(); jf.setVisible(true); jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } }
3.3.3、线程的优先级
实际上所有的线程启动之后并不是立刻运行的,都需要等待 CPU 进行调度,但是调度的时候本身也是存在“优先” 级的,如果优先级高则有可能最先被执行。 如果要想设置优先级可以使用:public final void setPriority(int newPriority) 这个优先级需要接收一个整型的数字,这个数字只能设置三个内容:
最 高优先级:public static final int MAX_PRIORITY
中 等 优先级:public static final int NORM_PRIORITY
最 低优先级:public static final int MIN_PRIORITY
3.4、线程的同步与死锁(重点)
由于多个线程可以对同一个资源进行操作,那么就必须进行同步的处理,但是如果过多的引入了同步的处理,也可 能造成死锁
当多个线程同时进行一种资源操作,为了保证操作的完整性,引入了同步处理
package com.xkrj.gqk; public class MyThread implements Runnable{ private int tickets = 10;//定义票数 public static void main(String[] args) { MyThread m = new MyThread(); new Thread(m,"西安").start(); new Thread(m,"咸阳").start(); new Thread(m,"宝鸡").start(); } @Override public void run() { for (int i = 0; i < 20; i++) { if(this.tickets>0){ System.out.println(Thread.currentThread().getName()+"卖票,剩余:"+this.tickets--); } } } }
但是,从实际的操作来看,都是使用网络卖票,既然是网络卖票的话,则有可能出现延迟
本程序中可以发现一旦加入了延迟(不加延迟也可能有问题)之后发现有的人卖出的票数是负数。 由于现在是有多个操作,那么最好的解决方法是加入一个锁的标记,锁的标记中将判断和修改同时进行,要想完成 这种锁的程序可以通过两种语句实现:同步代码块、同步方法。
package com.xkrj.gqk; public class MyThread implements Runnable{ private int tickets = 10;//定义票数 public static void main(String[] args) { MyThread m = new MyThread(); new Thread(m,"西安").start(); new Thread(m,"咸阳").start(); new Thread(m,"宝鸡").start(); } @Override public void run() { for (int i = 1; i < 20; i++) { synchronized (this) {//将当前对象锁定 if(this.tickets>0){ try { Thread.sleep(100); System.out.println(Thread.currentThread().getName()+"卖票,剩余:"+this.tickets--); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } } }
同步方法也可以完成:
package com.xkrj.gqk; public class MyThread implements Runnable{ private int tickets = 10;//定义票数 public static void main(String[] args) { MyThread m = new MyThread(); new Thread(m,"西安").start(); new Thread(m,"咸阳").start(); new Thread(m,"宝鸡").start(); } @Override public void run() { for (int i = 1; i < 20; i++) { this.sale(); } } public synchronized void sale(){ if(this.tickets>0){ try { Thread.sleep(300); System.out.println(Thread.currentThread().getName()+"卖票,剩余:"+this.tickets--); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }