黑马程序员——Java中多线程技术
来源:互联网 发布:明道办公软件登陆 编辑:程序博客网 时间:2024/05/18 15:23
一、多线程概念
学习多线程首先需要掌握一些基本概念:
2、线程
线程是指进程中一个负责程序执行的控制单元(执行路径)。一个进程中可以运行多个线程,多个线程可共享数据。
3、多进程
多线程是指一个进程中可以有多条执行路径,即一个进程中可以运行多个线程,实现多个程序的并发运行,这样每个线程都可以运行自己的内容,这个内容成为线程执行的任务。
多线程的好处:解决了多任务同时运行的问题。
多线程的弊端:线程太多造成运行效率降低。
4、多线程的内在原理
单核CPU在某个瞬间只能运行一个线程,而实现多线程是CPU在不同的线程间进行非常快速的切换,虽然看起来程序在同时运行,但是每一次的结果不一样,这是因为多线程具有随机性,多线程在运行时,各个线程在抢CPU资源。
二、创建线程的方式
1、继承Thread类
步骤:
a) 定义一个类继承Thread类;
b) 覆盖Thread类中的run方法;
c) 直接创建Thread的子类对象同时创建线程;
d) 调用start方法开启线程并调用run方法执行线程的任务。
覆盖run方法:Thread类用于描述线程。线程的运行需要有任务,而Thread类中的run方法就是封装自定义线程运行任务的函数,只要继承Thread类,复写run方法,将运行的任务代码定义在run方法中就可以运行。
run方法和start方法的区别:run方法是在本线程内调用该对象的run()方法,是做了一件事,可以重复多次调用;start方法是启动一个线程,调用该该对象的run()方法,是做了两件事,不能多次启动一个线程。
范例:
//通过继承Thread类创建线程class Demo extends Thread { privateString name; // 定义构造函数获取name Demo(Stringname) { //super(name); this.name= name; } // 复写run方法 public voidrun() { // 打印自定义线程运行情况 for(int x = 0; x < 10; x++) { System.out.println(name+ "....x=" + x + ".....name=" +Thread.currentThread().getName()); } }} class ThreadDemo { publicstatic void main(String[] args) { // 创建自定义线程 Demod1 = new Demo("my"); Demod2 = new Demo("your"); // 开启线程,调用run方法 d1.start(); d2.start(); // 打印主线程的运行情况 for(int x = 0; x < 5; x++) { System.out.println(x+ "...." + Thread.currentThread().getName()); } }}
输出结果:
0....main
1....main
2....main
3....main
4....main
your....x=0.....name=Thread-1
your....x=1.....name=Thread-1
your....x=2.....name=Thread-1
your....x=3.....name=Thread-1
your....x=4.....name=Thread-1
my....x=0.....name=Thread-0
my....x=1.....name=Thread-0
my....x=2.....name=Thread-0
my....x=3.....name=Thread-0
your....x=5.....name=Thread-1
your....x=6.....name=Thread-1
your....x=7.....name=Thread-1
your....x=8.....name=Thread-1
your....x=9.....name=Thread-1
my....x=4.....name=Thread-0
my....x=5.....name=Thread-0
my....x=6.....name=Thread-0
my....x=7.....name=Thread-0
my....x=8.....name=Thread-0
my....x=9.....name=Thread-0
由结果可知, 可以通过Thread的getName获取线程的名称:Thread-编号(从0开始).主线程的名字是main。每一个线程是执行是随机、交替执行的,每一次运行的结果都会不同。
2、实现Runnable接口
步骤:
a) 定义类实现Runnable接口;
b) 覆盖接口中的run方法,将线程的任务代码封装到run方法中;
c) 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递;
d) 调用线程对象的start方法开启线程。
这种创建线程方式的好处:将线程的任务进行单独封装;避免了Java单继承的局限性。因此,这种方式比较常用。
范例:
//实现Runnable接口创建线程class Demo1 implements Runnable { privateString name; // 定义构造函数获取name Demo1(Stringname) { //super(name); this.name= name; } // 复写run方法 public voidrun() { // 打印自定义线程运行情况 for(int x = 0; x < 10; x++) { System.out.println(name+ "....x=" + x + ".....name=" +Thread.currentThread().getName()); } }} class ThreadDemo1 { publicstatic void main(String[] args) { // 创建自定义线程 Demo1d1 = new Demo1("my"); Demo1d2 = new Demo1("your"); Threadt1 = new Thread(d1); Threadt2 = new Thread(d2); // 开启线程,调用run方法 t1.start(); t2.start(); // 打印主线程的运行情况 for(int x = 0; x < 5; x++) { System.out.println(x+ "...." + Thread.currentThread().getName()); } }}
输出结果为:
0....main
1....main
2....main
3....main
4....main
my....x=0.....name=Thread-0
my....x=1.....name=Thread-0
my....x=2.....name=Thread-0
my....x=3.....name=Thread-0
my....x=4.....name=Thread-0
my....x=5.....name=Thread-0
my....x=6.....name=Thread-0
my....x=7.....name=Thread-0
my....x=8.....name=Thread-0
my....x=9.....name=Thread-0
your....x=0.....name=Thread-1
your....x=1.....name=Thread-1
your....x=2.....name=Thread-1
your....x=3.....name=Thread-1
your....x=4.....name=Thread-1
your....x=5.....name=Thread-1
your....x=6.....name=Thread-1
your....x=7.....name=Thread-1
your....x=8.....name=Thread-1
your....x=9.....name=Thread-1
三、线程的状态
线程的五种状态:创建、运行、阻塞、冻结、消亡。
四、线程安全问题分析
1、产生原因
多个线程在运行时,它们操作的数据是相互共享的,并且操作共享数据的线程代码有多条。这时当一个线程在执这些代码过程中,其他线程若参与执行,就会导致线程安全问题的产生。(多线程访问延迟、线程随机性)
2、解决办法
若要避免线程安全问题的产生,就需要将多条操作共享数据的代码封装起来,当有线程执行这些代码时,不让其他线程参与执行。
Java中用“同步”就可以解决这个问题。
五、同步
1、同步的前提和利弊
当多个线程同时存在并且在使用同一个锁时才能使用同步。同步能够解决线程的安全问题,但是由于要判断锁,降低了效率。
2、同步代码块
代码格式:
synchronized(对象)
{需要被同步的代码}
同步代码块的锁为任意对象,建议使用同步代码块。
范例:
//同步代码块演示//实现Runnable接口创建多线程class TestThreadimplements Runnable { private int tickets = 20; //覆盖run方法 public void run() { while (true) {//无限循环保证程序一直运行 synchronized (this) {//同步代码块,this代表本类对象 if (tickets > 0) { //调用Thread.sleep()方法实现线程的切换 try { Thread.sleep(100); } catch(Exception e) { } System.out.println(Thread.currentThread().getName()+ "出售票" +tickets--); } } } }} public classSynchroBlockDemo { public static void main(String[] args) { TestThread t = new TestThread(); // 启动了四个线程,实现了资源共享的目的 new Thread(t).start(); new Thread(t).start(); new Thread(t).start(); new Thread(t).start(); }}
输出结果为:
Thread-1出售票20
Thread-1出售票19
Thread-1出售票18
Thread-1出售票17
Thread-1出售票16
Thread-1出售票15
Thread-2出售票14
Thread-0出售票13
Thread-3出售票12
Thread-3出售票11
Thread-3出售票10
Thread-0出售票9
Thread-0出售票8
Thread-2出售票7
Thread-2出售票6
Thread-1出售票5
Thread-1出售票4
Thread-1出售票3
Thread-1出售票2
Thread-1出售票1
3、同步函数
同步函数的定义只需在需要同步的函数定义前加上synchronized关键字即可。
同步函数的锁为固定的this。
范例:
//同步函数演示//实现Runnable接口创建多线程class MyThread implements Runnable { privateint tickets = 20; //覆盖run方法 publicvoid run() { while(true) {// 无限循环保证程序一直运行 sale(); } } //定义同步函数 publicsynchronized void sale() { if(tickets > 0) { //调用Thread.sleep()方法实现线程的切换 try{ Thread.sleep(100); }catch (Exception e) { } System.out.println(Thread.currentThread().getName()+ "出售票" +tickets--); } }} public class SynchroFunctionDemo { publicstatic void main(String[] args) { MyThreadt = new MyThread(); //启动了四个线程,实现了资源共享的目的 newThread(t).start(); newThread(t).start(); newThread(t).start(); newThread(t).start(); }}
输出结果:
Thread-0出售票20
Thread-2出售票19
Thread-2出售票18
Thread-3出售票17
Thread-1出售票16
Thread-3出售票15
Thread-3出售票14
Thread-3出售票13
Thread-3出售票12
Thread-3出售票11
Thread-3出售票10
Thread-2出售票9
Thread-2出售票8
Thread-2出售票7
Thread-0出售票6
Thread-0出售票5
Thread-2出售票4
Thread-2出售票3
Thread-3出售票2
Thread-1出售票1
4、静态同步函数
静态的同步函数使用的锁是该函数所属字节码文件对象,可以用 getClass方法
范例:
//验证静态同步函数的锁class Ticket implements Runnable {privatestatic int num = 10;booleanflag = true;// 标志用于同步代码快和同步函数的切换 // 覆盖run方法publicvoid run() { //同步代码块执行 if(flag) { while(true) { //将同步代码块的锁设置为Ticket.class,用于验证静态同步函数的锁 synchronized(Ticket.class)// 该锁也可用this.getClass()获取 { if(num > 0) { try{ Thread.sleep(10); }catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() +".....block...." + num--); } } } }else { //同步函数执行 while(true) Ticket.show(); }} // 静态同步函数publicstatic synchronized void show() { if(num > 0) { try{ Thread.sleep(10); }catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() +".....function...." + num--); }}} class StaticSynchroFunctionDemo {publicstatic void main(String[] args) { Tickett = new Ticket(); //创建并启动线程 Threadt1 = new Thread(t); Threadt2 = new Thread(t); t1.start(); //切换同步函数执行 try{ Thread.sleep(10); }catch (InterruptedException e) { } t.flag= false; t2.start();}}
输出结果:
Thread-0.....block....10
Thread-0.....block....9
Thread-1.....function....8
Thread-1.....function....7
Thread-1.....function....6
Thread-1.....function....5
Thread-1.....function....4
Thread-1.....function....3
Thread-1.....function....2
Thread-1.....function....1
5、死锁情况
多个进程争夺多个锁的访问权时就有可能发生死锁。即同步的嵌套。最常见的形式是当线程1持有对象A上的锁,而正在等待对象B上的锁;而线程2持有对象B上的锁,却正在等待对象A上的锁。这样,两个线程都不会获得锁或者释放锁,会永远等待。
范例:
//死锁情况演示class Testimplements Runnable { private boolean flag; Test(boolean flag) { this.flag = flag; } // 复写run方法 public void run() { // 同步嵌套 if (flag) { while (true) // 锁locka synchronized(MyLock.locka) { System.out.println(Thread.currentThread().getName() +"..if locka...."); // 锁lockb synchronized(MyLock.lockb) { System.out.println(Thread.currentThread().getName() +"..if lockb...."); } } } else { while (true) // lockb锁 synchronized(MyLock.lockb) { System.out.println(Thread.currentThread().getName() +"..else lockb...."); // locka锁 synchronized(MyLock.locka) { System.out.println(Thread.currentThread().getName() +"..else locka...."); } } } } } // 设置同步代码块的锁class MyLock { public static final Object locka = newObject(); public static final Object lockb = newObject();} classDeadLockDemo { public static void main(String[] args) { Test a = new Test(true); Test b = new Test(false); // 创建并启动线程 Thread t1 = new Thread(a); Thread t2 = new Thread(b); t1.start(); t2.start(); }}
输出结果:
Thread-0..if locka....
Thread-0..if lockb....
Thread-0..if locka....
Thread-0..if lockb....
Thread-0..if locka....
Thread-1..else lockb....
此时,程序形成死锁,等待但不会向下执行。
六、线程间通信
1、输入输出两个线程操作同一资源问题
假设资源区有存储着人的姓名和性别两个属性,并有输入线程对其赋值,输出线程对其取值,这时由于CPU切换线程是随机的,就可能会出现人的姓名和性别不对应、赋值和取值不是交替进行的情况。那么该怎么解决呢?
范例:
//多线程通信class Resource { private String name; private String sex; private boolean flag = false;// 标志用于线程切换 // 设置姓名和性别,使用同步函数解决线程安全问题 public synchronized void set(String name,String sex) { if (flag) try { this.wait();// 线程冻结 } catch(InterruptedException e) { } this.name = name; this.sex = sex; flag = true; this.notify();// 唤醒线程 } // 输出姓名和性别,使用同步函数解决线程安全问题 public synchronized void out() { if (!flag) try { this.wait();// 线程冻结 } catch(InterruptedException e) { } System.out.println(name +"---->" + sex); flag = false; notify();// 唤醒线程 }} // 输入线程class Inputimplements Runnable { Resource r; Input(Resource r) { this.r = r; } // 复写run方法 public void run() { int x = 0;// 用于 切换赋值 while (true) { if (x == 0) { r.set("小明", "男"); } else { r.set("小红", "女"); } x = (x + 1) % 2; } }} // 输出线程class Outputimplements Runnable { Resource r; Output(Resource r) { this.r = r; } public void run() { while (true) { r.out(); } }} classThreadCommunation { public static void main(String[] args) { // 创建资源 Resource r = new Resource(); // 创建任务 Input in = new Input(r); Output out = new Output(r); // 创建线程 Thread t1 = new Thread(in); Thread t2 = new Thread(out); // 开启线程 t1.start(); t2.start(); }}
部分输出结果:
小明---->男
小红---->女
小明---->男
小红---->女
小明---->男
小红---->女
说明:
等待唤醒机制:
wait(): 让线程处于冻结状态,被wait的线程会被存储到线程池中。
notify():唤醒线程池中一个线程(任意)。
notifyAll():唤醒线程池中的所有线程,高优先级的线程被首先唤醒。
这些方法都必须定义在同步中。因为这些方法是用于操作线程状态的方法。
必须要明确到底操作的是哪个锁上的线程。为什么操作线程的方法wait notify notifyAll定义在了Object类中? 因为这些方法是监视器的方法。监视器其实就是锁。锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中。
另外,wait 和 sleep 有什么区别呢?
wait:Object类中的方法,使线程处于“不可运行”状态,可以指定时间也可以不指定,用在同步中时释放CPU执行权,释放锁。
sleep:Thread类中的方法,使线程处于“非运行”状态,必须指定时间,用在同步中时释放CPU执行权,不释放锁。
2、多生产者多消费者问题
多生产者,多消费者,即多个输入线程,多个输出线程操作资源,若还是按照上述程序使用if判断标记,结果只判断一次,会导致不该运行的线程运行,出现操作资源数据错误的。如果用while判断标记,就解决了这种情况,这时线程获取执行权后,一定会运行!
另外,同样若继续使用notify方法,只能唤醒一个线程,对于多消费者多生产者情况,就会出现本方线程唤醒了本方的情况,没有意义。而while判断标记加notify方法会导致死锁。因此需要用notifyAll方法,这时本方线程一定会唤醒对方线程。
范例:
//多生产者多消费者情况演示class Resource { privateString name; privateint count = 1; privateboolean flag = false;// 标志用于多线程切换 //多生产者同步函数 publicsynchronized void set(String name) { while(flag) //while判断标记 try{ this.wait(); }catch (InterruptedException e) { } this.name= name + count;// 不同生产者生产不同产品 count++; System.out.println(Thread.currentThread().getName()+ "...生产者..." +this.name); flag= true; notifyAll();//唤醒所有线程 } //多消费者同步函数 publicsynchronized void out() { while(!flag) try{ this.wait(); }catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName()+ "...消费者........" +this.name); flag= false; notifyAll(); }} // 生产者线程类class Producer implements Runnable { privateResource r; Producer(Resourcer) { this.r= r; } //复写run方法 publicvoid run() { while(true) { r.set("产品");// 生产产品 } }} // 消费者线程类class Consumer implements Runnable { privateResource r; Consumer(Resourcer) { this.r= r; } publicvoid run() { while(true) { r.out();//消费产品 } }} class ProducerConsumerDemo { publicstatic void main(String[] args) { //创建资源 Resourcer = new Resource(); //创建任务 Producerpro = new Producer(r); Consumercon = new Consumer(r); //创建线程 Threadt0 = new Thread(pro); Threadt1 = new Thread(pro); Threadt2 = new Thread(con); Threadt3 = new Thread(con); //开启线程 t0.start(); t1.start(); t2.start(); t3.start(); }}
部分输出结果:
Thread-3...消费者........产品36031
Thread-0...生产者...产品36032
Thread-3...消费者........产品36032
Thread-1...生产者...产品36033
Thread-2...消费者........产品36033
Thread-1...生产者...产品36034
Thread-3...消费者........产品36034
Thread-0...生产者...产品36035
Thread-3...消费者........产品36035
Thread-1...生产者...产品36036
Thread-2...消费者........产品36036
Thread-1...生产者...产品36037
Thread-3...消费者........产品36037
3、JDK1.5版本多生产多消费问题解决办法
JDK1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。
Lock接口:替代了同步代码块或者同步函数,将同步的隐式锁操作变成现实锁操作,更为灵活,可以一个锁上加上多组监视器。
lock():获取锁。
unlock():释放锁,通常需要定义finally代码块中。
Condition接口:出现替代了Object中的wait、notify、notifyAll方法,为await()、signal()、signalAll()。将这些监视器方法单独进行了封装,变成Condition监视器对象,可以任意锁进行组合。
上个例子中的部分程序可以修改如下:
//JDK1.5版本多线程新特性import java.util.concurrent.locks.*; class Resource { privateString name; privateint count = 1; privateboolean flag = false; //创建一个锁对象。 Locklock = new ReentrantLock(); //通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。 Conditionproducer_con = lock.newCondition(); Conditionconsumer_con = lock.newCondition(); //生产者 publicvoid set(String name) { lock.lock();//获取锁 try{ while(flag) //try{lock.wait();}catch(InterruptedException e){} try{ producer_con.await();//await方法相当于wait方法 }catch (InterruptedException e) { } this.name= name + count; count++; System.out.println(Thread.currentThread().getName() +"...生产者5.0..." + this.name); flag= true; //notifyAll(); //con.signalAll(); consumer_con.signal(); }finally { lock.unlock();//释放锁 } } //消费者 publicvoid out() { lock.lock(); try{ while(!flag) //try{this.wait();}catch(InterruptedException e){} //t2 t3 try{ consumer_con.await(); }catch (InterruptedException e) { } System.out.println(Thread.currentThread().getName() +"...消费者.5.0......." + this.name); flag= false; //notifyAll(); //con.signalAll(); producer_con.signal(); }finally { lock.unlock(); } }}
七、多线程其他内容总结
1、停止线程:stop()方法结束和run()方法结束。一般使用后者,因为stop()方法会导致数据的不完整。用run()方法结束线程,可以通过控制任务中的循环来结束任务。如果线程处于冻结状态,可以使用interrupt()方法使线程强制恢复运行状态,再停止线程,不过需要处理InterruptException。
2、Thread.currentThread.getName():获取当前线程的名称
3、setPriority():设置线程的优先级。MAX_PRIORITY最高优先级10。MIN_PRIORITY最低优先级1。NORM_PRIORITY 分配给线程的默认优先级。
4、join():临时加入一个线程。
5、isAlive():判断线程是否启动。
6、setDaemon(true);设置线程为后台线程(守护线程)。
- 黑马程序员——Java中多线程技术
- 黑马程序员—— JAVA 多线程技术
- 黑马程序员—多线程技术
- 黑马程序员————Java多线程技术详解
- 黑马程序员--java技术--多线程
- 黑马程序员—java多线程
- 黑马程序员—JAVA多线程
- 黑马程序员—java多线程
- 黑马程序员—Java多线程
- 黑马程序员—java多线程
- 黑马程序员—Java多线程
- 黑马程序员——多线程技术
- 黑马程序员——Java中 多线程笔记
- 黑马程序员---回顾之java多线程技术
- 黑马程序员 Java基础 多线程技术
- 黑马程序员----JAVA基础----多线程技术1
- 黑马程序员-多线程技术
- 黑马程序员—ASP.net中多线程
- BaseHTTPServer构建基本服务器
- 算法竞赛入门经典第五章
- Android Touch事件传递机制通俗讲解
- C++学习笔记之erase
- 个人使用的Android Studio快捷键(MAC版)
- 黑马程序员——Java中多线程技术
- MySQL索引背后的数据结构及算法原理
- UVALive 5815 Pair of Touching Circles
- 读取jar包中资源文件的两种方法
- MySQL jdbc增删改查
- 【图论,树上路径倍增算法】NOIP2013货车运输
- 005求一个字符串中出现相同且长度最长的字符串,输出它及其首字符位置
- 简单字符串处理方法:
- sdut-1351-Max Sum-hdu-1003