java 多线程
来源:互联网 发布:centos升级kde 编辑:程序博客网 时间:2024/06/06 10:02
线程概述
多任务操作系统,即能够同时执行多个应用程序,最常见的有 Windows、Linux、UNIX等。
在一个操作系统中,每个独立运行的程序都可以称为一个进程,即“正在运行的程序”。
在多任务操作系统中,例如可以一边听音乐一边聊天,但实际上这些进程并不是同时运行的。在计算机中,所有的应用程序都是由 CPU 执行的,对于一个 CPU 而言,在某个时间点只能运行一个程序,即只能执行一个进程。由于 CPU 运行速度很快,能在极短的时间内,在不同的进程之间进行切换,所以给人以同时运行多个程序的感觉。
每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序运行的一条条线索,被称为线程。
操作系统中的每一个进程中都至少存在一个线程。当一个 Java 程序启动时,就会产生一个进程,该进程会默认创建一个线程,在这个线程上会运行 main() 方法中的代码。
单线程程序:代码都是按照调用顺序依次往下执行,没有出现两段程序代码交替运行的效果。
多线程程序:多段程序代码交替运行的效果。
多线程和进程一样,也是由 CPU 轮流执行的,只不过 CPU 运行速度很快,所以给人同时执行的感觉。
线程的创建
第一种方式:继承 java.lang 包下的 Thread 类,覆写 Thread 类的 run() 方法,在 run() 方法中实现运行在线程上的代码
示例代码如下:
class MyThread extends Thread { public void run() { while(true) { System.out.println("MyThread类的 run() 方法在运行"); } }}public class Example { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); while() { System.out.println("main() 方法在运行"); } }}
第二种方式:实现 java.lang.Runnable 接口,同样是在 run() 方法中实现运行在线程上的代码
class MyThread implements Runnable { public void run() { //线程的代码段,当调用 start() 方法时,线程从此处开始执行 while(true) { System.out.println("main() 方法在运行"); } }}public class Example { public static void main(String[] args) { MyThread myThread = new MyThread(); //创建 MyThread 的实例对象 Thread thread = new Thread(myThread); //创建线程对象 thread.start(); //开启线程,执行线程中的 run() 方法 while(true) { System.out.println("main() 方法正在运行"); } }}
实现 Runnable 接口相对于继承 Thread 类来说,有以下优点:
- 适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好地体现了面向对象的设计思想。
- 可以避免由于 Java 的单继承带来的局限性。在开发中经常碰到这样一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类,所以不能用继承 Thread 类的方式,只能采用实现 Runnable 接口的方式
- 事实上,大部分的应用程序都会采用第二种方式来创建多线程,即实现 Runnable 接口。
前台线程与后台线程
前台线程:新创建的线程默认都是前台线程。
后台线程:若某个线程对象在启动之前(即调用 start() 方法之前)调用了 setDaemon(true) 语句,这个线程就是一个后台线程。
注意:要将某个线程设置为后台线程,必须在该线程启动之前,也就是说 setDaemon() 方法必须在 start() 方法之前,否则会引发 IllegalThreadStateException 异常。
对 Java 程序来说,只要还有一个前台线程在运行,这个进程就不会结束。若一个进程中只有后台线程运行,这个进程就会结束。
示例代码如下:
演示当程序只有后台线程时就会结束的情况
class DamonThread implements Runnable { public void run() { while(true) { System.out.println(Thread.currentThread().getName() + "-----is running."); } }}public class Example { Public static void main(String[] args) { System.out.println("main 线程就是后台线程么?" + Thread.currentThread().isDaemon()); DamonThread dt = new DamonThread(); //创建一个 DamonThread 对象 dt Thread t = new Thread(dt, "后台线程"); //创建线程 t 共享 dt 资源 System.out.println("t 线程默认是后台线程么?" + t.isDaemon()); //判断是否为后台线程 t.setDaemon(true); t.start(); for(int i=0; i<10; i++) { System.out.println(i); } } }
该程序说明,当开启线程 t 后,会执行死循环中的打印语句,但我们将线程 t 设置为后台线程后,当前台线程死亡后,JVM 会通知后台线程。由于后台线程从接受指令,到做出响应,需要一定的时间。因此,打印了几次 “后台线程—is running” 语句后,后台也结束了。所以当只有后台线程运行时,进程就会结束。
线程的调度
线程的调度:程序中的多个线程是并发执行的,某个线程若想被执行必须要得到 CPU 的使用权,Java 虚拟机会按照特定的机制为程序中的每个线程分配 CPU 的使用权,这种机制被称作线程的调度。
在计算机中,线程调度有两种模型,即分时调度模型和抢占式调度模型。Java 虚拟机默认采用抢占式调度模型。
分时调度模型:是指让所有的线程轮流获得 CPU 的使用权,并且平均分配每个线程占用的 CPU 的时间片。
抢占式调度模型:是指让可运行池中优先级高的线程优先占用 CPU,而对于优先级相同的线程,随机选择一个线程使其占用 CPU,当它失去了 CPU 的使用权后,再随机选择其他线程获取 CPU 使用权。
线程的优先级
在应用程序中,若要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得 CPU 执行的机会越大。线程的优先级用 1~10 之间的整数来表示,数字越大优先级越高。除了数字表示线程的优先级,还可以使用 Thread 类中提供的三个静态常量表示线程的优先级,如下表所示:
示例代码如下:
//定义类 MaxPriority 实现 Runnable 接口class MaxPriority implements Runnable { public void run() { for(int i=0; i<10; i++){ System.out.println(Thread.currentThread().getName() + "正在输出:" + i); } }}//定义类 MinPriority 实现 Runnable 接口class MinPriority implements Runnable { public void run(){ for(int i=0; i<10; i++){ System.out.println(Thread.currentThread().getName() + "正在输出:" + i); } }}public class Example { public static void main(String[] args){ //创建两个线程 Thread minPriority = new Thread(new MinPriority, "优先级较低的线程"); Thread maxPriority = new Thread(new MaxPriority, "优先级较高的线程"); minPriority.setPriority(Thread.MIN_PRIORITY); //设置线程的优先级为 1 maxPriority.setPriority(10); //设置线程的优先级为 10 //开启两个线程 maxPriority.start(); minPriority.start(); }}
注意:虽然 Java 中提供了 10 个线程优先级,但不同的操作系统对优先级的支持是不一样的,不能很好地和 Java 中线程优先级一一对应。因此,在设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,只能把线程优先级作为一种提高程序效率的手段
线程休眠
若要人为地控制线程,使正在执行的线程暂停,将 CPU 让给别的线程,可以使用静态方法 sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。当前线程调用 sleep(long millis) 方法后,在指定时间内是不会执行的,这样其他的线程就可以得到执行的机会了。
注意:第一点:sleep(long millis) 方法声明抛出 InterruptedException 异常,因此在调用该方法时,应该捕获异常,或者声明抛出该异常。
第二点:sleep() 是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠时间结束后,线程就会返回到就绪状态,而不是立即开始运行。
示例代码如下:
class SleepThread implements Runnable{ public void run() { for(int i =1; i<10; i++){ if(i==3){ try{ Thread.sleep(2000); //当前线程休眠 2 秒 }catch(InterruptedException e){ e.printStackTrace(); } } System.out.println("线程一直在输出:" + i); try{ Thread.sleep(500); //当前线程休眠 500 毫秒 }catch(Exception e){ e.printStackTrace(); } } }}public class Example { public static void main(String[] args) throws Exception{ //创建一个线程 new Thread(new SleepThread()).start(); for(int i=1; i<=10; i++){ if(i==5){ Thread.sleep(20000); //当前线程休眠 2 秒 } System.out.println("主线程正在输出:" + i); Thread.sleep(500); //当前线程休眠 500 毫秒 } }}
线程让步
在校园中,经常会看到同学互相抢篮球,当某个同学抢到篮球后就可以拍一会,之后他会把篮球让出来,大家重新开始抢篮球,这个过程就相当于 Java 程序中的线程让步。
线程让步可以通过 yield() 方法来实现,该方法和 sleep() 方法有点相似,都可以让当前正在运行的线程暂停,区别在于 yield() 方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用 yield() 方法后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。
示例代码如下:
//定义 YieldThread 类继承 Thread 类class YieldThread extends Threads { //定义一个有参的构造方法 public YieldThread(String name){ super(name); //调用父类的构造方法 } public void run(){ for(int i=0; i<5; i++){ System.out.println(Thread.currentThread().getName() + "---" + i); if(i==3){ System.out.print("线程让步:"); Thread.yield(); //线程运行到此,做出让步 } } }}public class Example{ public static void main(String[] args){ //创建两个线程 Thread t1 = new YieldThread("线程 A"); Thread t2 = new YieldThread("线程 B"); //开启两个线程 t1.start(); t2.start(); }}
线程插队
现实生活中经常碰到“插队”的情况,同样,在 Thread 类中也提供了一个 join() 方法来实现这个“功能”。
当在某个线程中调用其他线程的 join() 方法时,调用的线程将被阻塞,知道被 join() 方法加入的线程执行完成后,它才会继续运行。
示例代码如下:
class EmergencyThread implements Runnable{ public void run(){ for(int i=1; i<6; i++){ System.out.println(Thread.currentThread().getName() + "输入:" + i); try{ Thread.sleep(500); //线程休眠 500 毫秒 }catch(InterruptedException e){ e.printStackTrace(); } } }}public class Example{ public static void main(String[] args) throws Exception{ Thread t = new Thread(new EmergencyThread(), "线程一"); t.start(); //开启线程 for(int i=1; i<6; i++){ System.out.println(Thread.currentThread().getName() + "输入:" + i); if(i==2){ t.join(); //调用 join() 方法 } Thread.sleep(500); //线程休眠 500 毫秒 } }}
多线程同步
线程安全问题是由多个线程同时处理共享资源所导致的。因此必须能保证共享资源在任何时刻只能有一个线程访问。
为了实现这种限制,Java 中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用 synchronized 关键字来修饰,被称作同步代码块,其语法结构如下:
synchronized(lock){ 操作共享资源代码块}
其中,lock 是一个锁对象,它是同步代码块的关键。当线程执行同步代码块时,首先会检查所对象的标志位,默认情况下标志位为 1,此时线程会执行同步代码块,同时将锁对象的标志位置为 0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为 0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位被置为 1,新线程才能进入同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程类似于一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以打。
示例代码如下:
//定义 Ticket 类实现 Runnable 接口class Ticket implements Runnable{ private int ticket = 10; //定义变量 tickets,并赋值 10 object lock = new Object(); //定义任意一个对象,用作同步代码块的锁 public void run(){ while(true){ synchronized(lock){ //定义同步代码块 try{ Thread.sleep(10); //经过的线程休眠 10 毫秒 }catch(InterruptedException e){ e.printStackTrace(); } if(tickets>0){ System.out.println(Thread.currentThread().getName() + "---卖出的票" + tickets--); }else{ break; } } } }}public class Example{ public static void main(String[] args){ Ticket ticket = new Ticket(); //创建 Ticket 对象 //创建并开启四个线程 new Thread(ticket, "线程一").start(); new Thread(ticket, "线程二").start(); new Thread(ticket, "线程三").start(); new Thread(ticket, "线程四").start(); }}
注意:同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。还有一点,锁对象的创建代码不能放到 run() 方法中,否则每个线程运行到 run() 方法都会创建一个新对象,这样每个线程都有一个不同的锁,每个锁都有自己的标志位。线程之间便不能产生同步的效果。
同步方法
在方法前,使用 synchronized 关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:
synchronized 返回值类型 方法名 ([参数1, 参数2, ...]){}
注意:被 synchronized 修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行方法。
示例代码如下:
//定义 Ticket 类实现 Runnable 接口class Ticket implements Runnable{ private int ticket = 10; public void run(){ while(true){ saleTicket(); if(ticket<=0){ //调用售票方法 break; } } } //定义一个同步方法 saleTicket() if(ticket>0){ try{ Thread.sleep(10); //经过的线程休眠 10 毫秒 }catch(InterruptedException e){ e.printStackTrace(); } System.out.println( Thread.currentThread().getName() + "---卖出的票" + tickets-- ); }}public class Example{ public static void main(String[] args){ Ticket ticket = new Ticket(); //创建 Ticket 对象 //创建并开启四个线程 new Thread(ticket, "线程一").start(); new Thread(ticket, "线程二").start(); new Thread(ticket, "线程三").start(); new Thread(ticket, "线程四").start(); }}
多线程同步小结:
同步代码块的锁是自己定义的任意类型的对象
同步方法的锁是当前调用该方法的对象,也就是 this 指向的对象。
多线程通信
为了更好地理解线程间通信,可以模拟这样一种应用场景,假设有两个线程同时去操作同一个存储空间,其中一个线程负责向存储空间中存入数据,另一个线程负责取出数据。
示例代码如下:
class Storage{ private int[] cells = new int[10]; //数据存储数组 private int[] inPos, outPos; //inPos存入时数组下标,outPos取出时数组下标 private int count; //存入或者取出数据的数量 public synchronized void put(int num){ try{ //如果放入数据等于 cells 的长度,此线程等待 while(count==cells.length){ this.wait(); } cells[inPos] = num; //向数组中放入数据 System.out.println("在 cells["+inPos+"]中放入数据---" + cells[inPos]); inPos++; //存完元素让位置加 1 this.notify(); }catch(Exception e){ e.printStackTrace(); } } public synchronized void get(){ try{ while(count==0){ //如果 count 为 0,此线程等待 this.wait(); } int data = cells[outPos]; //从数组中取出数据 System.out.println("从 cells["+outPos+"] 中取出数据" + data); cells[outPos] = 0; //取出后,当前位置的数据置 0 outPos++; //取完元素让位置加 1 if(outPos==cells.length){ //当从 cells[9] 取完数据后再从 cells[0] 开始 outPos = 0; count--; this.notify(); }catch(Exception e){ e.printStackTrace(); } }}
//讲解待续
- 【Java多线程】多线程死锁
- Java 多线程
- java 多线程
- java多线程
- JAVA多线程
- java多线程
- JAVA多线程
- java多线程
- JAVA 多线程
- Java多线程
- java多线程
- JAVA 多线程
- Java 多线程
- Java 多线程
- java多线程
- Java 多线程
- Java多线程
- java 多线程
- P1136 迎接仪式
- 2016-2017 ACM-ICPC Pacific Northwest Regional Contest (Div. 2)
- 源码托管平台
- input输入子系统
- Python编程入门到实践:(看书笔记总结9-7)
- java 多线程
- 进程与线程的区别,简单的图片理解+面试简要回答
- ccf认证网络延时100分
- C++顺序容器之deque初探
- [ターゲット] 最近在搞的东东
- request.getAttribute()和request.getParameter()的区别
- UVA
- JS总结——运算符
- java中的反射