Java基础之多线程知识点总结
进程:当前正在执行的程序。代表一个应用程序在内存中的执行区域。 线程:是进程中的一个执行控制单元,执行路径。(线程就是进程中的一个执行者) 一个进程中至少有一个线程在负责控制程序的执行。 一个进程中如果只有一个执行路径,这个程序称为单线程。 一个进程中有多个执行路径时,这个程序成为多线程。 多线程的出现:可以有多条执行路径,让多部分代码可以完成同时执行。 以提高效率。本身也是对问题的一种解决方案。 比如:图形界面中的多个小程序的同时执行,例子:360管理软件。 JVM启动是单线程,还是多线程的呢? jvm的启动其实就是多线程程序。其中有一个程序负责从主函数开始执行,并控制程序运行的流程。同时为了提高效率,还启动了另一个控制单元(执行路径)专门负责堆内存中的垃圾回收。在程序正常执行过程中,如果出现了垃圾,这时另一个负责收垃圾的线程会在不定时间内进行垃圾的处理。这两个程序是同时执行的。所以说JVM启动时多线程的。 负责执行正常代码的线程,称为主线程。该线程执行的代码都存放于主函数中。 负责收垃圾代码执行的线程,称为垃圾回收线程。该线程要执行的代码在finalize中。
如何在我们自定义的程序中去创建一个线程(执行路径,控制单元)呢?如何让自定义的线程运行我们执行的代码,可以和主线程同时执行呢?
例子1.自定义的程序中创建一个线程。
在该示例中,只有一个主线程在控制代码执行的流程。 当d1.show()没有执行完,那么d2.show(),是不可能执行的。 如果d1执行时,遇到了较复杂的运算时,d2只有等d1结束。 可不可以完成一个效果,让 d1 和 d2同时执行呢? 这时就需要由一个线程控制d1,由另一个线程控制d2. 那如何在Java中创建一个线程呢? 其实Java中对线程这类事物已经进行了描述,并提供了相对应的对象。这个对象就是Thread。 通过API查阅,发现Thread类描述时,有两种创建线程的方式。 方式一:定义一个类继承Thread类,并覆盖Thread类中run方法。 问题:为什么要继承Thread,为什么要覆盖run? 其实直接建立Thread类对象即可。并开启线程执行就可以了。但是虽然线程执行了,可是执行的代码是该线程默认的代码,该代码就存放在run方法中。 可是定义线程的目的是为了执行自定义的代码。而线程运行代码都存储在run方法中,所以只有覆盖了run方法,才可以运行自定义的内容,想要覆盖,必须先要继承。主线程运行的代码都在main函数中,自定义线程运行的代码都在run方法中。 直接创建Thread类的子类对象就是创建了一个线程。 在内存中其实: 1.堆内存中产生了一个对象, 2.需要调用了底层资源,去创建执行路径。 如果直接调用该对象的run方法。 这时,底层资源并没有完成线程的创建和执行。 仅仅是简单的对象调用方法的过程。所以这时,执行控制流程的只有主线程 . 如果想要开启线程,需要去调用Thread类中另一个方法完成。 start方法完成:该方法做了两件事,1,开启线程,2,调用了线程的run方法。
例子2:创建两个线程。
当创建了两个线程对象d1,d2后,这时程序就有了三个线程在同时执行。当主函数执行完d1.start(),d2.start()后,这时三个线程同时打印,结果比较杂乱:这是因为线程的随机性造成的。 随机性的原理是: windows中的多任务同时执行,其实就是多个应用程序在同时执行。而每一个应用程序都有线程来负责控制的。所以window就是一个多线程的操作系统。那么cpu是负责提供程序运算的设备。 cpu特点:在某一个时刻,只能执行一个程序,所以多个程序执行并不是真正的同时执行。其实就是cpu做这快速的切换完成的。只是你感觉上同时而已. 能不能真正意义上的同时执行呢? 可以,要是计算机有多个CPU,就可以了。也就是现在所见多核。
线程中的几个方法。 多线程的创建,为了对各个线程进行标识,他们有一个自己默认的名称。格式:Thread-编号,编号从0开始。
例子3:获得程序中多线程中,各个线程的名字。
staticThread currentThread():获取当前线程对象。 StringgetName():获取线程名称。 void setName():设置线程的名称。 Thread(Stringname):构造函数,线程对象一建立就可以指定名称。
例子4:铁路售票,一共100张,通过四个窗口卖完。
例子5:修改例子4中的程序,使用实现Runnable接口的方法去完成铁路售票。
创建线程的两种方式:1,继承Thread类。步骤:1)定义类继承Thread。 2)覆盖Thread类中的run方法,run方法用于存储多线程要运行的代码。 3)创建Thread类的子类对象创建线程。 4)调用Thread类中的start方法开启线程,并执行子类中的run方法。 特点: 1.当类去描述事物,事物中有属性和行为。 如果行为中有部分代码需要被多线程所执行,同时还在操作属性。就需要该类继承Thread类,产生该类的对象作为线程对象,可是这样做会导致每一个对象中都存储一份属性数据。无法在多个线程中共享该数据。加上静态,虽然实现了共享但是生命周期过长。 2.如果一个类明确了自己的父类,那么很遗憾,它就不可以在继承Thread。 因为java不允许类的多继承。 2,实现Runnable接口: 步骤: 1)定义类实现Runnable接口。 2)覆盖接口中的run方法,将多线程要运行的代码定义在方法中。 3)通过Thread类创建线程对象,并将实现了Runnable接口的子类对象 作为实际参数传递给Thread类的构造函数。 为什么非要把Runnable接口的子类对象传递给Thread类的构造函数呢? 是因为线程对象在建立时,必须要明确自己要运行的run方法,而这个run方法定义在了Runnable接口的子类中,所以要将该run方法所属的对象传递给Thread类的构造函数。让线程对象一建立,就知道运行哪个run方法。 4)调用Thread类中的start方法,开启线程,并执行Runanble接口子类中的run方法。 特点: 1.描述事物的类中封装了属性和行为,如果有部分代码需要被多线程所执行。同时还在操作属性。那么可以通过实现Runnable接口的方式。因为该方式是定义一个Runnable接口的子类对象,可以被多个线程所操作实现了数据的共享。 2.实现了Runnable接口的好处,避免了单继承的局限性。 也就是说,一个类如果已经有了自己的父类是不可以继承Thread类的。 但是该类中还有需要被多线程执行的代码。这时就可以通过在该类上功能扩展的形式。即可以实现一个Runnable接口。 所以在创建线程时,建议使用第二种方式。 线程安全问题.因为线程的随机性,有可能会导致多线程在操作数据时发生数据错误的情况产生。 线程安全问题产生的原因: 当线程中多条代码在操作同一个共享数据时,一个线程将部分代码执行完,还没有继续执行其他代码时,被另一个线程获取cpu执行权,这时,共享数据操作就有可能出现数据错误。 简单说:多条操作共享数据的代码被多个线程分开执行造成的。 安全问题涉及的内容: 1,共享数据。 2,是否被多条语句操作。 这也是判断多线程程序是否存在安全隐患的依据。
例子6:在线程中实现同步代码块来解决线程安全的问题。
解决安全问题的方式: java中提供了一个同步机制。 解决原理;让多条操作共享数据的代码在某一时间段,被一个线程执行完,在执行过程中,其他线程不可以参与运算。 同步格式: 同步代码块: synchronized(对象)//该对象可以是任意对象 { 需要被同步的代码; } 同步的原理,通过一个对象锁,将多条操作共享数据的代码进行了封装并加锁。只有持有这个锁的线程才有机会进入同步中的去执行,在执行期间,即使其他线程获取到执行权,因为没有获取到锁,所以只能在外面等着。只有同步中的线程执行完同步代码块中的代码。出同步代码时,才会释放这个锁,那么其他程序线程才有机会去获取这个锁,并只能有一个获取到而且进入到同步中。 举例:火车上的卫生间。锁机制最好的体现。 同步好处: 同步的出现解决了多线程的安全问题。 同步弊端: 因为多个线程每次都要判断这个锁,所以效率会降低。 以后写同步你会发现这样一个问题,如果出现了安全问题后:加入了同步,安全问题依然存在。 因为同步是有前提的: 同步前提: 1.必须是两个或者两个以上的线程才需要同步。 2.必须要保证多个线程使用的是同一个锁,才可以实现多个线程被同步。 如果出现加上同步安全问题依然存在,就按照两个前提来排查问题。
例子7:有两个储户,到同一个银行存钱,每次存100,存3次,两个存储是随机存入的。
这个程序有没有安全隐患呢? 分析: 1,查看线程代码中是否有共享数据。 2,这个共享数据有没有被多条语句所操作。 发现,sum是共享数据。有两条语句在操作这个共享数据,如果这两条语句被多个线程分开执行。也就是一个线程没有执行完,其他线程就参与执行,就容易发生安全问题. 解决办法:加入同步机制。 将需要被一个线程一次执行完的代码存储在同步代码块中
例子8:修改例7,实现同步函数,来解决线程安全的问题。
发现,同步代码块是用于封装代码的。 而函数也是用封装代码的。所不同之处是同步带有锁机制。 那么如果让函数这个封装体具备同步的特性。不就可以取代同步代码块了吗? 怎么让函数具备同步性呢? 其实很简单,只要在函数上加上一个同步关键字(synchronized)修饰即可。这就是同步的另一个体现形式:同步函数。 示例代码: public synchronized void add(int num){ sum = sum + num; try {Thread.sleep(10);}catch(Exception e) {} System.out.println(Thread.currentThread().getName()+"-----sum="+sum); } 验证同步函数到底用的是哪个锁?
例子8:需求同样是卖票。 一个线程到同步函数中卖票。 一个线程到同步代码块中卖票。 保证他们卖的是同100张票. 如果同步函数和同步代码块使用的是同一个锁,就不会出现0号票这种错误的票情况。
运行结果:还是有0号票的存在。 修改例子8的部分代码:
测试结果运行正常。 通过该示例:验证结果:同步函数使用的锁是 this 同步函数和同步代码的区别: 同步代码块使用的锁可以是任意对象。 同步函数使用的锁是固定对象是 this 所以一般定义同步时,建议使用 同步代码块. 当然,如果对象可以使用this。那么可以简化同步函数的形式。 验证静态同步函数到底用的是哪个锁? 静态同步函数使用的锁肯定不是this。因为静态函数中不可以定义this。静态随着类的加载而加载,这时有可能内容还没有该类对象。但是一个类加载进内存,会先将这个类对应的字节码文件封装成对象。该对象的表示方式:类名.class 代表一个类字节码文件对象,该对象在内存中是唯一的。 结论:静态同步函数使用的锁就是该类对应字节码文件对象。也就是 类名.class
例子9:验证静态同步函数到底用的是:类名.class。
单例模式有两种体现形式: 1,饿汉式。 class Single{ privatestatic final Single s = new Single(); private Single(){} publicstatic Single getInstance(){ returns; } } 2,懒汉式。 class Single{ private static Single s = null; private Single(){} public static Single getInstance(){ if(s==null){ s = new Single(); } return s; } }
解决线程安全问题的修改代码:
相对于懒汉式的单例设计:当多个线程并发执行getInstance方法时,容易发生线程安全问题。因为s是共享数据,有多条语句在操作共享数据。解决方式很简单。只要让getInstance方法具备同步性即可.即在getInstance()方法中上使用synchronized就可以解决问题。这虽然解决了线程安全问题,但是多个线程每一次获取该实例,都要调用这个方法,每次调用都判断一次锁,所以效率会比较低.为了保证安全,同时为了提高效率.可以通过双重判断的形式来完成。原理:就是减少线程判断的锁次数。 虽然解决安全问题,也解决了效率问题,但是代码过多。 所以建议使用饿汉式体现单例设计模式。 但是面试时,考的都是懒汉式。 面试考懒汉式,可能会问到的问题? 1. 什么是延迟加载? 2. 如果是多线程怎么办?解决要加同步。 3. 效率低怎么办?利用双重判断解决问题。 4. 懒汉式用的锁是哪个锁?类名.class 虽然同步的出现解决了线程安全问题。 但是也带来了一些弊端: 1,效率会降低。 2,容易引发死锁。 死锁经常出现的状况为:同步嵌套。
例子10.线程产生死锁的情况。
例子11.线程产生死锁的例子。
例子12.线程间通信的例子。
例子13,让输出的数据间隔格式输出。
等待唤醒机制: 使用的方法是:wait() <-----> notity() wait(): 让线程等待,将线程存储到一个线程池中。 notity(): 唤醒被等待的线程。通常都唤醒线程池中的第一个。让被唤醒的线程处于临时阻塞状态。 notityAll():唤醒所有的线程。将线程池中的所有线程都唤醒,都让他们从冻结状态转到临时阻塞状态(所有的线程全都wait了,没 一个存活的了,这时,可以用notityAll唤醒全部wait的线程) 注意:等待唤醒机制通常都用在同步中。因为需要锁的支持。而且必须要明确wait()、notity()所作用的锁对象。 wait(),notity(),notityAll()这三个方法用于操作线程,可是都定义在Object类中,为什么? 因为,这三个方法在使用时dou需要定义在同步中,要明确这些方法所操作的线程所属的锁。简单说,就是在A锁中被wait的线程,只能被A锁的notity唤醒。所以必须要表示wait,notity方法所属的锁对象。而锁对象可以使任意的对象,可以被任意的对象调用的方法肯定定义在Object.
例子14:将例13代码进行优化,将Resource中的属性私有化。
生产者,消费者。 例子15:生产者,消费者的例子。
代码完成,加入同步和等待唤醒机制后,可以实现,生产一个,就消费一个。可是在实际开发中,生产者和消费者,并不是一个。有可能是多个也就是有多个生产者生产,有多个消费者消费。
例子16.多个生产者和多个消费者。
造成数据错误的原因:当生产者消费者多个时,本方的线程有可能唤醒本方的线程,而且,本方被唤醒后,没有判断标记,就进行了执行,会到导致原来本方的操作还没有被对方所操作就已经被覆盖了。生产者1,进行了生产后,将本方生产者2唤醒,生产者2没有判断标记直接继续生产,导致生产者1的产品还没有被消费就覆盖了。 解决方式:因为有本方唤醒本方的情况,所以必须每次的醒来都要判断一次标记。判断标记的动作要执行多次。所以不使用if,而是使用while.当进行while标记判断后,本方唤醒本方的动作还会发生,但是本方被唤醒后,继续判断标记,虽然没有将前一次操作覆盖,但是导致了程序中的线程都处于了等待状态。导致程序处于死锁状态。 到这里发现原因有两个: 1,是判断标记。通过循环判断比较搞定。 2,一定要唤醒对方。notify是唤醒一个,这个线程有可能是本方,也有可能是对方。干脆,无论是本方还是对方,全唤醒。通过notifyAll搞定。
在jdk1.5版本之后,出现了一些新的特性,将原理的线程进行了改良。 在java.util.concurrent.locks包中提供了一个接口Lock。替代了synchronized。而synchronized使用的是锁操作是隐式的。 Lock接口,使用的锁操作是显示的。 由两个方法来完成: lock():获取锁。 unlock():释放锁。 还有一个对象,Condition.该对象的出现替代了Object中的wait(), notify(), notifyAll()这些操作监视器的方法. 替代后的方式:await(),signal(),signalAll(). 接下来,把下列代码替换成JDK1.5版本只有的新对象。新功能最大好处,就是在一个Lock锁上,可以添加多组监视器对象。这样就可以实现本方只唤醒对方的线程.
例子17.把例子16换成JDK1.5版本的新对象操作锁的方法。
锁,是同步的机制.通过锁来控制同步.监视器是用于同步中对象的操作.比如wait(),notify(),notifyAll().每一组监视器方法对应一个锁. 到了jdk1.5以后,将监视器的方式从Object中,封装到了Condition对象中,每一个锁lock,可以对应多组监视器对象,这就可以实现本方只唤醒对方的操作。 代码实例:
sleep和wait有什么区别? 对时间的指定。 1,sleep方法必须指定时间。 2,wait方法有重载形式,可以指定时间,也可以不指定时间。 对于执行权和锁的操作. 1,sleep():释放执行权,不释放锁,因为肯定能醒,肯定可以恢复到临时阻塞状态。 2,wait():释放执行权,释放锁,因为wait不释放锁,如果没有指定时间,那么其他线程都进行不了同步,无法将其唤醒。 代码实例: public synchronized void show() { if() wait(); code....; notify(); code....; } 针对上面代码实例的结论:同步中可以有多个存活的线程,但是只能有一个执行同步的代码。因为只有一个线程会持有同步的锁。只有当该线程释放了锁,其他线程才会有机会获取到锁,而且只能有一个线程获取到锁,继续执行。 如何让线程停止。 停止线程有两种方式: 1,使用Thread类中的stop方法。很遗憾,该方法过时了。 2,线程执行的代码结束,也就是run方法结束。 通常定义线程代码都有循环,因为需要单独开辟一个执行路径去重复做很多事情。既然有循环,只要控制住循环,即可结束run方法。
例子18.使用定义标记的方式来停止线程。
定义标记可以结束线程,但是如果线程在运行过程中存储了冻结状态,没有执行到标记,这时,程序还能结束吗?
代码实例:
可以通过Thread类中的interrupt()方法中断线程的冻结状态。强制让其恢复到运行状态中来,就可以有机会执行标记,但是这种强制动作会发生InterruptedException异常。
例子19.使用Thread的interrupt()方法中断线程的冻结状态。
setDeamon(boolean):可以将线程标记为后台线程。 线程分前台线程和后台线程两种。运行方式都一样都会获取cpu的执行权执行。不同的在于,结束方式不同。前台线程只有run方法结束,才结束。后台线程,run方法结束,结束。还有,如果run方法没结束,而前台线程都结束了。后台线程一样自动结束。 所以一个进程是否结束参考的是:是否还有前台线程存活。如果前台线程都结束了,那么进程也就是结束了。 实例代码:
可以想想 圣斗士星矢的例子,雅典娜就是前台线程,,星矢哥五个就是守护线程,雅典娜挂了,那哥五个就自动结束了。失业了。 join:临时加入一个线程进行执行。 例如: 当主线程获取到了cpu的执行权,执行时,执行到了A线程的join方法。这时就知道A线程要加入进来进行,那么A执行就需要cpu的执行权。而这时cpu的执行权在主线程持有,主线程会释放自己的执行权。让A线程进行执行。 那么主线程什么时候执行呢? 只有等待A线程执行完以后,主线程才会执行,此时主线程就处于冻结状态。 能让线程处于冻结状态的方式有3种:wait(),sleep(),join().
例子20.线程中join()方法的使用。
一般使用情况,当在线程执行过程中,需要一个运算结果,可以通过加入一个临时线程,将该结果进行运算,这时需要的结果的线程处于冻结状态,等加入的线程执行完,该线程在继续执行。