首页 > 代码库 > 黑马程序员——多线程
黑马程序员——多线程
一、概述
(一)进程
正在执行中的程序,每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
(二)线程
进程中的一个独立的控制单元。线程在控制着进程的执行。一个进程中至少有一个线程。只要进程中有一个线程在执行,进程就不会结束。
(三)多线程
在java虚拟机启动的时候会有一个java.exe的执行程序,也就是一个进程。该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。JVM启动除了执行一个主线程,还有负责垃圾回收机制的线程。这种在一个进程中有多个线程执行的方式,就叫做多线程。
(四)多线程创建线程的目的和意义
1、创建目的:开启一条执行路径;运行的代码就是执行路径的任务。JVM创建的主线程的任务都定义在了主函数中。run方法中,定义的代码就是线程要运行的任务代码。
2、创建的意义:可以让程序产生同时运行效果;可以提高程序执行效率。
二、创建线程的方式
创建线程共有两种方式:继承方式和实现方式。
(一)“继承Thread类”创建多线程
Java API文档的java.lang包中提供了Thread类,通过继承Thread类,然后覆写其run方法来创建线程。
1、创建步骤
(1)定义类,继承Thread。
(2)覆写Thread类中的run方法。
(3)调用start方法,启动线程,调用run方法执行线程任务代码。
2、多线程run和start的特点
为什么要覆盖run方法呢?Thread类用于描述线程,该类定义了一个功能,用于存储要运行的代码,这些运行的代码就存储在run方法中。
3、练习:创建两线程,和主线程交替运行。示例代码如下:
1 //创建线程Test 2 class Test extends Thread{ 3 Test(String name){ 4 super(name); 5 } 6 //覆写run方法 7 public void run(){ 8 for(int x=0;x<150;x++)
9 System.out.println(Thread.currentThread().getName()+"...run..."+x);10 }11 }12 class MultiThread{13 public static void main(String[] args){14 new Test("Thread one").start();//开启第二个线程16 //主线程执行的代码17 for(int x=0;x<150;x++)18 System.out.println("Hello World!");19 }20 }
说明:上述代码每次运行的结果都不一样。因为多个线程都在获取CPU的执行权,CPU执行到谁谁就运行。更明确一点说,在某一时刻,只能有一个程序在运行(多核除外)。CPU是在做着快速的切换,已达到看上去是同时在运行的效果。
(二)“实现Runnable接口”创建多线程
使用Thread类继承方式有一个弊端,即如果该类本来就继承了其他父类,那么就无法通过继承Thread类来创建多线程。这样就有了第二种创建线程的方式:实现Runnable接口,并覆写其中的run方法来创建多线程。
1、创建步骤
(1)定义类实现Runnable接口。
(2)覆盖Runnable接口中的run方法。
(3)通过Thread类创建线程对象。
(4)将Runnable接口的子类对象作为实参传递给Thread类的构造方法。
(5)调用Thread类中start方法启动线程,start方法会自动调用Runnable接口子类的run方法。
2、细节问题
(1)为什么要将Runnable接口的子类对象传递给Thread类的构造函数?
因为自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去指定对象的run方法,就必须明确该run方法所属对象。
(2)实现Runnable的好处:将线程任务从线程的子类中分离出来,进行单独的封装。避免了Java单继承的局限性。在定义线程时,建议使用实现方式。
(三)“实现方式”和“继承方式”的区别
1、继承方式:线程代码存放在Thread子类的run方法中。
2、实现方式:线程代码存在接口的子类run方法中。
(四)线程运行状态结构图
运行状态结构图说明:
1、被创建:等待启动,调用start启动。
2、运行状态:具有执行资格和执行权。
3、临时状态(阻塞):有执行资格,但是没有执行权。
4、冻结状态:遇到sleep(time)方法和wait()方法时,失去执行资格和执行权,sleep方法时间到或者调用notify()方法时,获得执行资格,变为临时状态。
5、消忙状态:stop()方法,或者run方法结束。
(五)获取线程对象以及名称
1、原来的线程都有自己默认的名称,命名方式是Thread-编号,编号从0开始。
2、通过对象this.getName()、super(name),可以获取线程对象的名称。也可以通过Thread.currentThread().getName()标准方式获取线程的名称。说明:① static Thread currentThread():获取当前线程对象。② getName():获取线程名称。
3、设置线程名称:setName(String name)方法或者构造函数都可以实现。
4、局部变量在每一个线程区域中都有独立的一份。
(六)练习:售票例子
1 /* 简单卖票程序:多窗口同时卖票 */ 2 class Ticket implements Runnable//extends Thread{ 3 private int tick = 100; 4 public void run(){ 5 while(true){ 6 if(tick>0) 7 System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--); 8 } 9 }10 }11 class TicketDemo{12 public static void main(String[] args){13 Ticket t = new Ticket();//创建Runnable接口子类的实例对象14 //有多个窗口在同时卖票,创建三个线程15 Thread t1 = new Thread(t);// 16 Thread t2 = new Thread(t);17 Thread t3 = new Thread(t);18 //启动线程19 t1.start();20 t2.start();21 t3.start();22 }23 }
三、多线程的安全问题
(一)多个线程“安全问题”
1、问题的原因:当多条线程在操作同一个共享数据时,因为CPU的权限分配而出现一个线程对多条语句只执行一部分,还没有执行完,另外一条线程参与进来执行,导致共享数据的错误。
2、解决思路:对操作共享数据的多条语句,只能让一个线程都执行完,而其他线程不能执行。
(二)解决方式
java对于多线程的安全问题提供了专业的解决方式:为需要操作共享数据的多条语句添加synchronized(同步)修饰符。有两种解决方式:一种是同步代码块,另一种是同步函数。都是利用关键字synchronized来实现。
1、同步代码块
(1)格式:synchronized(对象){ 需要被同步的代码块 }
同步可以解决安全问题的根本原因就在那个对象上。对象如同一把锁,持有锁的线程可以在执行同步代码块,而没有持有锁的线程即使获取CPU执行权,也因为没有获取对象而无法执行同步代码块。
(2)同步代码块使用的锁是哪一个呢?任意对象。
(3)同步代码块示例:为多线程卖票程序加上同步代码块,代码如下:
1 class Ticket implements Runnable { 2 private int tick = 100; 3 Object obj = new Object(); 4 public void run() { 5 while (true) { 6 // 给程序加同步,即锁 7 synchronized (obj) { 8 if (tick > 0) { 9 try {// 使用线程中的sleep方法,模拟线程出现的安全问题10 // 因为sleep方法有异常声明,所以这里要对其进行处理11 Thread.sleep(10);12 } catch (Exception e) {13 }14 System.out.println(Thread.currentThread().getName()+ "..tick..=" + tick--);// 显示线程名及余票数15 }16 }17 }18 }19 }
2、同步函数
(1)格式:synchronized作为函数的修饰符,添加在函数上,就构成了同步函数。
(2)同步函数用的是哪一个锁呢?因为函数需要被对象调用,那么函数都持有一个所属对象引用,即this。所以同步函数使用的锁是this。
(3)同步函数示例
1 public synchronized void add(int num){2 sun = sum + num;3 try{4 Thread.sleep(10);5 }catch(InterruptedException e){6 }7 System.out.println(“sum = ”+ sum);8 }
3、静态同步函数
(1)格式:同步函数被static修饰后,就构成了静态同步函数。
(2)静态同步函数使用的锁是什么呢?Class对象。
说明:因为静态在进内存时,内存中没有本类对象,但一定有该类对应的字节码文件对象,因此,静态同步函数使用的锁是该函数所属字节码文件对象,即 类名.class,该对象的类型是Class,可以用getClass()方法获取,也可以用 类名.class获取。
(3)静态同步函数代码示例:
1 class StaticSynchDemo implements Runnable{ 2 private static int num = 100; 3 public void run(){ System.out.println(“num = ”+ num); }; 4 public static synchronized void show() { 5 if(num>0) { 6 try{ 7 Thread.sleep(10); 8 }catch(InterruptedException e){ }
9 System.out.println(Thread.currentThread().getName()+".....function...."+num--);10 }11 }12 }
(三)同步的前提和利弊
1、同步的前提
(1)必须要有两个或者两个以上的线程。
(2)必须是多个线程使用同一个锁。
(3)必须保证同步中,只能有一个线程在运行
2、同步的利弊
利:解决了多线的安全问题。
弊:多个线程需要判断锁,较为消耗资源。
(四)如何寻找多线程中的安全问题
(1)明确哪些代码是多线程运行代码。
(2)明确共享数据。
(3)明确多线程运行代码中,哪些语句是操作共享数据的。
四、死锁
(一)死锁:常见情景之一是“同步的嵌套”。
(二)“同步嵌套”出现的死锁示例:
1 /* 写一个死锁程序 */ 2 class DeadLock implements Runnable { 3 private boolean flag; 4 DeadLock(boolean flag) { 5 this.flag = flag; 6 } 7 public void run() { 8 if (flag){ 9 while (true)10 synchronized (MyLock.locka) {11 System.out.println(Thread.currentThread().getName()+ "..if locka....");12 synchronized (MyLock.lockb){13 System.out.println(Thread.currentThread().getName()+ "..if lockb....");14 }15 }16 } else {17 while (true)18 synchronized (MyLock.lockb) {19 System.out.println(Thread.currentThread().getName()+ "..else lockb....");20 synchronized (MyLock.locka) {21 System.out.println(Thread.currentThread().getName()+ "..else locka....");22 }23 }24 }25 }26 }27 28 class MyLock {29 public static final Object locka = new Object();30 public static final Object lockb = new Object();31 }32 33 class DeadLockTest {34 public static void main(String[] args) {35 DeadLock a = new DeadLock(true);36 DeadLock b = new DeadLock(false);37 38 Thread t1 = new Thread(a);39 Thread t2 = new Thread(b);40 t1.start();41 t2.start();42 }43 }
五、线程间通信
(一)线程间通信
多个线程在操作同一个资源,但是操作的动作不同。
(二)等待唤醒机制
1、涉及的方法
(1)wait(): 让线程处于冻结状态,被wait的线程会被存储到线程池中。
(2)notify():唤醒线程池中一个线程(任意)。
(3)notifyAll():唤醒线程池中的所有线程。
2、为什么这些方法都必须定义在同步中?
因为这些方法是用于操作线程状态的方法,必须要明确到底操作的是哪个锁上的线程。
3、为什么操作线程的方法wait、notify、notifyAll定义在了Object类中?
因为这些方法是监视器的方法。监视器其实就是锁,锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中。
4、wait 和 sleep 区别?
(1)wait可以指定时间也可以不指定;而sleep必须指定时间。
(2)在同步中时,各自对CPU的执行权和锁的处理不同:① wait释放执行权,释放锁。② sleep释放执行权,不释放锁。
5、为甚么要定义notifyAll ?
因为在需要唤醒对方线程时。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所以线程都等待。
1 * 练习:多线程通信示例(生产者——消费者) 2 class Resource{ 3 private String name; 4 private int count = 1; 5 private boolean flag = false; 6 public synchronized void set(String name){ 7 while(flag) 8 try{this.wait();}catch(InterruptedException e){} 9 this.name = name + count;10 count++;//2 3 411 System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);12 flag = true;13 notifyAll();14 }15 16 public synchronized void out(){17 while(!flag)18 try{this.wait();}catch(InterruptedException e){}19 System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);20 flag = false;21 notifyAll();22 }23 }24 25 class Producer implements Runnable{26 private Resource r;27 Producer(Resource r){28 this.r = r;29 }30 public void run(){31 while(true){32 r.set("烤鸭");33 }34 }35 }36 37 class Consumer implements Runnable{38 private Resource r;39 Consumer(Resource r){40 this.r = r;41 }42 public void run(){43 while(true){44 r.out();45 }46 }47 }48 49 class ProducerConsumerDemo{50 public static void main(String[] args){51 Resource r = new Resource();52 Producer pro = new Producer(r);53 Consumer con = new Consumer(r);54 55 Thread t0 = new Thread(pro);56 Thread t1 = new Thread(pro);57 Thread t2 = new Thread(con);58 Thread t3 = new Thread(con);59 t0.start();60 t1.start();61 t2.start();62 t3.start();63 }64 }
(三)关于同步和锁的JDK1.5新特性
1、jdk1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。
2、Lock接口:替代了同步代码块或同步函数,将同步的隐式锁操作变成显式的锁操作。同时更为灵活,可以一个锁上加上多组监视器。
3、Condition接口:替代了Object中的wait()、notify()、 notifyAll()方法,将这些监视器方法单独进行了封装,变成Condition监视器对象。Condition对象可以和任意锁进行组合,
4、接口Lock上的一些方法
(1)lock():获取锁。
(2)unlock():释放锁,通常需要定义finally代码块中。
5、接口Condition上的一些方法
(1)await():使当前线程在接到信号或被中断之前一直处于等待状态。
(2)signal():唤醒一个等待线程。
(3)signalAll():唤醒所有等待线程。
1 练习:多生产者多消费者问题(JDK1.5解决办法) 2 import java.util.concurrent.locks.*; 3 class Resource2 4 { 5 private String name; 6 private int count = 1; 7 private boolean flag = false; 8 9 // 创建一个锁对象。 10 Lock lock = new ReentrantLock(); 11 12 //通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。 13 Condition producer_con = lock.newCondition(); 14 Condition consumer_con = lock.newCondition(); 15 16 public void set(String name) 17 { 18 lock.lock(); 19 try 20 { 21 while(flag) 22 try{producer_con.await();}catch(InterruptedException e){} 23 24 this.name = name + count; 25 count++;//2 3 4 26 System.out.println(Thread.currentThread().getName()+"...生产者5.0..."+this.name); 27 flag = true; 28 consumer_con.signal(); 29 } 30 finally 31 { 32 lock.unlock(); 33 } 34 35 } 36 37 public void out() 38 { 39 lock.lock(); 40 try 41 { 42 while(!flag) 43 try{consumer_con.await();}catch(InterruptedException e){} 44 System.out.println(Thread.currentThread().getName()+"...消费者.5.0......."+this.name); 45 flag = false; 46 producer_con.signal(); 47 } 48 finally 49 { 50 lock.unlock(); 51 } 52 53 } 54 } 55 56 class Producer2 implements Runnable 57 { 58 private Resource r; 59 Producer2(Resource r) 60 { 61 this.r = r; 62 } 63 public void run() 64 { 65 while(true) 66 { 67 r.set("烤鸭"); 68 } 69 } 70 } 71 72 class Consumer2 implements Runnable 73 { 74 private Resource r; 75 Consumer2(Resource r) 76 { 77 this.r = r; 78 } 79 public void run() 80 { 81 while(true) 82 { 83 r.out(); 84 } 85 } 86 } 87 88 class ProducerConsumerDemo2 89 { 90 public static void main(String[] args) 91 { 92 Resource r = new Resource(); 93 Producer2 pro = new Producer2(r); 94 Consumer2 con = new Consumer2(r); 95 96 Thread t0 = new Thread(pro); 97 Thread t1 = new Thread(pro); 98 Thread t2 = new Thread(con); 99 Thread t3 = new Thread(con);100 t0.start();101 t1.start();102 t2.start();103 t3.start();104 }105 }
六、停止线程
(一)停止线程的方式
1、stop方法(已过时)。
2、run方法结束。
(二)run()方法结束线程
1、“定义标记方式”结束线程任务
怎么控制线程的任务结束呢? 开启多线程运行,运行代码通常都会有循环结构,只要控制住循环就可以让run方法结束,也就是线程结束。控制循环通常就用“定义标记”来完成。
2、“interrupt()方法”结束线程任务
如果线程处于了冻结状态,无法读取标记,如何结束呢?可以使用Thread对象的interrupt()方法将线程从冻结状态强制恢复到运行状态中来,让线程具备CPU的执行资格,再通过操作标记,让线程任务结束。但是强制动作会发生InterruptedException,记得要处理
(三)线程停止示例
1 class StopThread implements Runnable{ 2 private boolean flag = true; 3 public synchronized void run(){ 4 while(flag){ 5 try{ 6 wait();//t0 t1 7 }catch (InterruptedException e){ 8 System.out.println(Thread.currentThread().getName()+"....."+e); 9 flag = false;10 } 11 System.out.println(Thread.currentThread().getName()+"......++++");12 }13 }14 public void setFlag(){15 flag = false;16 }17 }18 19 class StopThreadDemo{20 public static void main(String[] args){21 StopThread st = new StopThread();22 Thread t1 = new Thread(st);23 Thread t2 = new Thread(st);24 t1.start();25 t2.setDaemon(true);//将该线程标记为守护线程(后台线程), 该方法必须在启动线程前调用。26 t2.start();27 28 int num = 1;29 for(;;){30 if(++num==50){31 t1.interrupt();32 break;33 }34 System.out.println("main...."+num);35 }36 System.out.println("over");37 }38 }
(四)守护线程(可理解为:后台线程)
void setDaemon(boolean on) :将该线程标记为守护线程或用户线程,可以理解为后台进程。
守护线程的特点:
1、该方法必须在启动线程前调用。
2、当正在运行的线程都是守护线程时,Java 虚拟机退出。
3、当所有的前台线程执行结束,后台线程无论处于什么状态,都自动结束。
(五)线程类的其他方法
1、setPriority(int newPriority)方法(用来设置线程的优先级)
(1)参数newPriority的取值有:MAX_PRIORITY(最高优先级10)、MIN_PRIORITY(最低优先级)、NORM_PRIORITY(默认优先级5)
(2)格式:线程.setPriority(Thread. MAX_PRIORITY)
2、join()方法
当A线程执行到了b线程的.join()方法时,A线程就会等待,等B线程都执行完,A线程才会执行。(此时B和其他线程交替运行。)join可以用来临时加入线程执行。
3、yield()方法
可以暂停当前线程,释放执行权,让其他线程执行。
4、toString()方法
返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
七、面试题
1 /*class Test implements Runnable{ 2 public void run(Thread t) 3 {} 4 }*/ 5 //如果错误 错误发生在哪一行?错误在第一行,应该被abstract修饰 6 7 class ThreadTest{ 8 public static void main(String[] args){ 9 new Thread(new Runnable(){10 public void run(){11 System.out.println("runnable run");12 }13 })14 {15 public void run(){16 System.out.println("subThread run");17 }18 }.start();19 }20 }21 运行结果:subThread run
黑马程序员——多线程