黑马程序员 java基础回顾---多线程
来源:互联网 发布:php数组按id排序 编辑:程序博客网 时间:2024/05/19 22:28
---------------------- ASP.Net+Android+IOS开发、 .Net培训、期待与您交流! ----------------------
一、概述:
1、线程是什么呢?
我们先来说一说比较熟悉的进程吧,之后就比较容易理解线程了。所谓进程,就是一个正在执行(进行)中的程序。每一个进程的执行都有一个执行顺序,或者说是一个控制单元。简单来说,就是你做一件事所要进行的一套流程。线程,就是进程中的一个独立的控制单元;也就是说,线程是爱控制着进程的执行。一个进程至少有一个线程,并且线程的出现使得程序要有效率。打个比方说,在仓库搬运货物,一个人搬运和五个人搬运效率是不一样的,搬运货物的整个程序,就是进程;每一个人搬运货物的过程,就是线程。
2、java中的线程:
在java中,JVM虚拟机启动时,会有一个进程为java.exe,该程序中至少有一个线程负责java程序的执行;而且该程序运行的代码存在于main方法中,该线程称之为主线程。其实,JVM启动时不止有一个线程(主线程),由于java是具有垃圾回收机制的,所以,在进程中,还有负责垃圾回收机制的线程。
3、多线程的意义:
透过上面的例子,可以看出,多线程有两方面的意义:
1)提高效率。 2)清除垃圾,解决内存不足的问题。
二、自定义线程:
线程有如此的好处,那要如何才能通过代码自定义一个线程呢?其实,线程是通过系统创建和分配的,java是不能独立创建线程的;但是,java是可以通过调用系统,来实现对进程的创建和分配的。java作为一种面向对象的编程语言,是可以将任何事物描述为对象,从而进行操作的,进程也不例外。我们通过查阅API文档,知道java提供了对线程这类事物的描述,即Thread类。创建新执行线程有两种方法:
一)创建线程方式一:继承Thread类。
1、步骤:
第一、定义类继承Thread。
第二、复写Thread类中的run方法。
第三、调用线程的start方法。分配并启动该子类的实例。
start方法的作用:启动线程,并调用run方法。
2、运行特点:
A.并发性:我们看到的程序(或线程)并发执行,其实是一种假象。有一点需要明确:;在某一时刻,只有一个程序在运行(多核除外),此时cpu是在进行快速的切换,以达到看上去是同时运行的效果。由于切换时间是非常短的,所以我们可以认为是在并发进行。
B.随机性:在运行时,每次的结果不同。由于多个线程都在获取cpu的执行权,cpu执行到哪个线程,哪个线程就会执行。可以将多线程运行的行为形象的称为互相抢夺cpu的执行权。这就是多线程的特点,随机性。执行到哪个程序并不确定。
3、覆盖run方法的原因:
1)Thread类用于描述线程。该类定义了一个功能:用于存储线程要运行的代码,该存储功能即为run方法。也就是说,Thread类中的run方法用于存储线程要运行的代码,就如同main方法存放的代码一样。
2)复写run的目的:将自定义代码存储在run方法中,让线程运行要执行的代码。直接调用run,就是对象在调用方法。调用start(),开启线程并执行该线程的run方法。如果直接调用run方法,只是将线程创建了,但未运行。
示例:
class Demo extends Thread{public void run(){for(int x = 0;x<60;x++){System.out.println("demo run----"+x);}}}class ThreadDemo{public static void main(String[] args) {Demo d = new Demo();d.start();for(int i = 0;i<60;i++){System.out.println("Hello World!"+i);}}}
二)创建线程方式二:实现Runnable接口
1、步骤:
第一、定义类实现Runnable接口。
第二、覆盖Runnable接口中的run方法。
第三、通过Thread类建立线程对象。要运行几个线程,就创建几个对象。
第四、将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
第五、调用Thread类的start方法开启线程,并调用Runnable接口子类的run方法。
2、说明:
A.步骤2覆盖run方法:将线程要运行的代码存放在该run方法中。
B.步骤4:为何将Runnable接口的子类对象传给Thread构造函数。因为自定义的run方法所属对象为Runnable接口的子类对象,所以让线程指定对象的run方法,就必须明确该run方法所属的对象。
示例:
class Demo implements Runnable{public void run(){for(int x = 0;x<60;x++){System.out.println("demo run----"+x);}}}class ThreadDemo{public static void main(String[] args) {Demo d = new Demo();Thread t = new Thread(d);t.start();for(int i = 0;i<60;i++){System.out.println("Hello World!"+i);}}}
三)实现方式与继承方式有何区别:
1、实现方式:避免了单继承的局限性。
在定义线程时,建议使用实现方式。
2区别:
继承Thread:线程代码存放在Thread子类的run方法中。
实现Runnable:线程代码存在接口的子类run方法中。
需要注意的是:局部变量在每一个线程中都独有一份。
四)多线程的同步
多线程的运行出现了安全问题。
问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来进行执行,导致共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完,在执行的过程中,其他线程不可以参与执行。
同步的前提:
1、必须有两个或者两个以上的线程;
2、必须是多个线程使用同一个锁。
同步的两种方法:
1、同步代码块
格式:
synchronized(obj){...}
其中obj是一个对象,这个对象可以是任意的,在同步功能中充当一个锁。
2、同步函数
格式:
public synchronized void Test(...){...}
注意:同步函数的锁是 this 如果是静态同步函数,那么锁是该静态函数所在类的Class对象
多线程同步的应用--单例设计模式(懒汉式)
class Single{private static Single s = null;private Single(){}public static Single getInstance(){if(s==null){synchronized(Single.class)//静态函数里面不能用this{if(s==null)s = new Single();}}return s;}}
注意:懒汉式加同步锁比较低效,因为线程较多时每个调用的都要判断锁,这个问题可以用两个if 判断语句来解决。
3、死锁
死锁产生的原因:同步中嵌套同步,两个同步代码中都在等待对方的锁造成的。
在编码过程中我们要尽量避免死锁。
五)线程间通信
多线程间通信是线程之间进行交互的方式,简单说就是存储资源和获取资源。比如说仓库中的货物,有进货的,有出货的。还比如生产者和消费者的例子。这些都可以作为线程通信的实例。那么如何更好地实现通信呢?先看下面的代码:
class Rec{private String name;private String sex;public boolean flag;public void setName(String name){this.name = name;}public void setSex(String sex){this.sex = sex;}public String getName(){return this.name;}public String getSex(){return this.sex;}}class Input implements Runnable{private Rec r;public Input(Rec r){this.r = r;}public void run(){int i = 0;while(true){synchronized(r){if(i == 0){r.setName("mark");r.setSex("male");}else{r.setName("丽丽");r.setSex("女");}i = (i+1)%2;}}}}class Output implements Runnable{private Rec r;public Output(Rec r){this.r = r;}public void run(){while(true){synchronized(r){System.out.println(r.getName()+"..."+r.getSex());}}}}class InputOutputDemo {public static void main(String[] args) {Rec r = new Rec();Thread input = new Thread(new Input(r));Thread output = new Thread(new Output(r));input.start();output.start();}}
在编写过程中遇到了一些问题:打印出了一些名字和性别不相符的记录;
原因:取数据和存数据两个线程同步没有用相同的锁;
解决办法:在主函数中传入相同的锁 r 。
等待唤醒机制
1、显式锁机制和等待唤醒机制:
在JDK 1.5中,提供了改进synchronized的升级解决方案。将同步synchronized替换为显式的Lock操作,将Object中的wait,notify,notifyAll替换成Condition对象,该对象可对Lock锁进行获取。这就实现了本方唤醒对方的操作。在这里说明几点:
1)、对于wait,notify和notifyAll这些方法都是用在同步中,也就是等待唤醒机制,这是因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁。
2)、而这些方法都定义在Object中,是因为这些方法操作同步中的线程时,都必须表示自己所操作的线程的锁,就是说,等待和唤醒的必须是同一把锁。不可对不同锁中的线程进行唤醒。所以这就使得程序是不良的,因此,通过对锁机制的改良,使得程序得到优化。
3)、等待唤醒机制中,等待的线程处于冻结状态,是被放在线程池中,线程池中的线程已经放弃了执行资格,需要被唤醒后,才有被执行的资格。
代码示例:
import java.util.concurrent.locks.*; class ProducerConsumerDemo{ public static void main(String[] args){ Resouse r = new Resouse(); Producer p = new Producer(r); Consumer c = new Consumer(r); Thread t1 = new Thread(p); Thread t2 = new Thread(c); Thread t3 = new Thread(p); Thread t4 = new Thread(c); t1.start(); t2.start(); t3.start(); t4.start(); } } class Resouse{ private String name; private int count = 1; private boolean flag = false; private Lock lock = new ReentrantLock(); private Condition condition_P = lock.newCondition(); private Condition condition_C = lock.newCondition(); //要唤醒全部,否则都可能处于冻结状态,那么程序就会停止。这和死锁有区别的。 public void set(String name)throws InterruptedException{ lock.lock(); try{ while(flag)//循环判断,防止都冻结状态 condition_P.await(); this.name = name + "--" + count++; System.out.println(Thread.currentThread().getName() + "..生成者--" + this.name); flag = true; condition_C.signal(); }finally{ lock.unlock();//释放锁的机制一定要执行 } } public void out()throws InterruptedException{ lock.lock(); try{ while(!flag)//循环判断,防止都冻结状态 condition_C.await(); System.out.println(Thread.currentThread().getName() + "..消费者." + this.name); flag = false; condition_P.signal();//唤醒全部 }finally{ lock.unlock(); } } } class Producer implements Runnable{ private Resouse r; Producer(Resouse r){ this.r = r; } public void run(){ while(true){ try{ r.set("--商品--"); }catch (InterruptedException e){} } } } class Consumer implements Runnable{ private Resouse r; Consumer(Resouse r){ this.r = r; } public void run(){ while(true){ try{ r.out(); }catch (InterruptedException e){} } } }
2、对于上面的程序,有两点要说明:
1)、为何定义while判断标记:
原因是让被唤醒的线程再判断一次。
避免未经判断,线程不知是否应该执行,就执行本方的上一个已经执行的语句。如果用if,消费者在等着,两个生成着一起判断完flag后,cpu切换到其中一个如t1,另一个t3在wait,当t1唤醒冻结中的一个,是t3(因为它先被冻结的,就会先被唤醒),所以t3未经判断,又生产了一个。而没消费。
2)这里使用的是signal方法,而不是signalAll方法。是因为通过Condition的两个对象,分别唤醒对方,这就体现了Lock锁机制的灵活性。可以通过Contidition对象调用Lock接口中的方法,就可以保证多线程间通信的流畅性了。
小结:
多线程编程在实际应用中十分广泛,因为一个系统不可能每次都只为允许一个用户使用,这样的效率太低;一个好的系统必须允许很多人同时访问,这样就需要使用到多线程,但是多线程又具有安全隐患,当多个人同时访问系统中的数据时会造成数据错乱,产生“脏数据”,这时就需要用到线程同步技术,当把某些代码设置成同步后,每次就只能有一个线程对其进行访问了。所以我们需要掌握线程同步的方法,这里介绍的三种线程同步的方法在开发过程中各有用途,都必须掌握。
---------------------- ASP.Net+Android+IOS开发、 .Net培训、期待与您交流! ----------------------
- 黑马程序员 java基础回顾---多线程
- 黑马程序员-----java基础回顾
- 黑马程序员---回顾之java多线程技术
- 黑马程序员--java 知识回顾--多线程
- 黑马程序员 java基础回顾---IO流
- 黑马程序员 java基础回顾---集合框架
- 黑马程序员 java基础回顾---网络编程
- 黑马程序员 java基础回顾---枚举
- 黑马程序员 java基础回顾---正则表达式
- 黑马程序员 java基础回顾---异常
- 黑马程序员 Java基础 --->多线程
- 黑马程序员--java基础多线程
- 黑马程序员JAVA基础-多线程
- 黑马程序员---java基础多线程
- 黑马程序员--java基础--多线程
- 黑马程序员--Java基础--多线程
- 黑马程序员-----java基础 多线程
- 黑马程序员-->Java基础-->多线程
- 16-循环控制
- linux 下串口命令
- javaScript用正则验证QQ
- 堆排序
- STM32复位时钟控制模块RCC
- 黑马程序员 java基础回顾---多线程
- CodeForces:305B Continued Fractions
- Opencv图像显示出现倒立
- IE和FF在对js支持的不同及解决方法
- 【原创】Java与数据结构(下篇:图)
- js中的事件触发
- Matroska文件格式标准
- Picture
- eclipse 导入工程后,注释为乱码解决方法