zoukankan      html  css  js  c++  java
  • Java温故而知新(2)多线程详解

    多线程指的是在单个程序中可以同时运行多个同的线程执行不同的任务。线程是程序内的顺序控制流,只能使用分配给序的资源和环境。

        一、线程与进程的区别

    多个进程的内部数据和状态都是完全独立的,而多线程是共享一块内存空间和一组系统资源,有可能互相影响。 线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换的负担要小。

     多线程编程的目的,就是"最大限度地利用CPU资源",当某一线程的处理不需要占用CPU而只和I/O等资源打交道时,让需要占用CPU资源的其它线程有机会获得CPU资源。从根本上说,这就是多线程编程的最终目的。

    二、了解一下java在多线程中的基础知识

    1.Java中如果我们自己没有产生线程,那么系统就会给我们产生一个线程(主线程,main方法就在主线程上运行),我们的程序都是由线程来执行的。

     2. 进程:执行中的程序(程序是静态的概念,进程是动态的概念)。 

    3. 线程的实现有两种方式,第一种方式是继承Thread类,然后重写run方法;第二种是实现Runnable接口,然后实现其run方法。 

    1、继承java.lang.Thread类

    一个Thread类实例只是一个对象,像Java中的任何其他对象一样,具有变量和方法,生死于堆上。

    1. public class ThreadInstance extends Thread{  
    2.       @Override  
    3.       public void run() {  
    4.              for(int i=0;i<5;i++){  
    5.                     System.out.println(name+":"+"第"+i+"次执行");  
    6.              }  
    7.       }  
    8.        
    9.       public ThreadInstance(String name){  
    10.              this.name = name;  
    11.       }  
    12.        
    13.       private String name ;  
    14. }  

     测试类:

    1. Thread threadA = new ThreadInstance("threadA");  
    2. Thread threadB = new ThreadInstance("threadB");  
    3. Thread threadC = new ThreadInstance("threadC");  
    4.   
    5. threadA.start();  
    6. threadB.start();  
    7. threadC.start();  

    打印结果:

    threadA:第0次执行

    threadC:第0次执行

    threadB:第0次执行

    threadC:第1次执行

    threadA:第1次执行

    threadC:第2次执行

    threadB:第1次执行

    threadB:第2次执行

    threadB:第3次执行

    threadB:第4次执行

    threadC:第3次执行

    threadA:第2次执行

    threadC:第4次执行

    threadA:第3次执行

    threadA:第4次执行

    注意:由于程序运行当时,CPU状态不同,线程调度器的工作状态不同,每次的打印结果并不一致。

    假如我们不调用start()方法,而是直接调用run()方法,会怎样呢?将测试代码作如下改动:

    1. Thread threadA = new ThreadInstance("threadA");  
    2. Thread threadB = new ThreadInstance("threadB");  
    3. Thread threadC = new ThreadInstance("threadC");  
    4.   
    5. threadA.run();  
    6. threadB.run();  
    7. threadC.run();  

    打印结果:

    threadA:第0次执行

    threadA:第1次执行

    threadA:第2次执行

    threadA:第3次执行

    threadA:第4次执行

    threadB:第0次执行

    threadB:第1次执行

    threadB:第2次执行

    threadB:第3次执行

    threadB:第4次执行

    threadC:第0次执行

    threadC:第1次执行

    threadC:第2次执行

    threadC:第3次执行

    threadC:第4次执行

    此次的打印结果是按照Thread实例的先后顺序执行的,这不是偶然的。从本质上讲,run()方法就是Thread实例的一个成员方法,如果我们直接调用,就跟调用其他成员方法一样,不会由于线程的的调度而产生阻塞、执行的状态,而会一直执行完毕,完毕之前后面的程序不会执行。先执行threadA.run(),完毕后再执行threadB.run(),完毕后再执行threadC.run()。所以它并不是一个多线程程序

    虽然start()也是调用run()方法来执行相关任务的,但是start()方法只是让线程进入可执行状态就绪状态),等待cpu分配给它时间片,并不一定会立刻执行。这时可能有多个线程处在可执行状态,线程调度器轮流分配给它们时间片。所以它是一个多线程程序。

    2、实现java.lang.Runnable接口

    必须实现Runnable接口中的run()方法,跟Thread类中的run()方法一样,线程执行的任务需要写在run()方法中。

    1. public class RunnableInstance implements Runnable{  
    2.    
    3.       @Override  
    4.       public void run() {  
    5.              for(int i=0;i<5;i++){  
    6.                     System.out.println(Thread.currentThread().getName()+":"+"第"+i+"次执行");  
    7.              }  
    8.               
    9.       }  
    10. }  

     

    Thread的构造方法可接受一个Runnable的实例,用Runnable的run()方法覆盖掉Thread类的run()方法。

       

    1. RunnableInstance r1 = new RunnableInstance();  
    2. Thread threadA = new Thread(r1,"threadA");  
    3.   
    4. RunnableInstance r2 = new RunnableInstance();  
    5. Thread threadB = new Thread(r2,"threadB");  
    6.   
    7. RunnableInstance r3 = new RunnableInstance();  
    8. Thread threadC = new Thread(r3,"threadC");  
    9.   
    10. threadA.start();  
    11. threadB.start();  
    12. threadC.start();  


    打印结果:

    threadA:第0次执行

    threadB:第0次执行

    threadA:第1次执行

    threadB:第1次执行

    threadA:第2次执行

    threadB:第2次执行

    threadC:第0次执行

    threadC:第1次执行

    threadC:第2次执行

    threadC:第3次执行

    threadC:第4次执行

    threadA:第3次执行

    threadA:第4次执行

    threadB:第3次执行

    threadB:第4次执行

    执行效果与Thread的效果类似。

    在测试程序中,每个Thread持有一个Runnable实例,互不干扰。试想,如果让Thread共享一个Runnable实例,会发生什么情况呢?

    1. RunnableInstance r1 = new RunnableInstance();  
    2. Thread threadA = new Thread(r1,"threadA");  
    3.   
    4. //RunnableInstance r2 = new RunnableInstance();  
    5. Thread threadB = new Thread(r1,"threadB");  
    6.   
    7. //RunnableInstance r3 = new RunnableInstance();  
    8. Thread threadC = new Thread(r1,"threadC");  
    9.   
    10. threadA.start();  
    11. threadB.start();  
    12. threadC.start();  

    打印结果:

    threadA:第0次执行

    threadA:第1次执行

    threadC:第0次执行

    threadB:第0次执行

    threadC:第1次执行

    threadA:第2次执行

    threadC:第2次执行

    threadB:第1次执行

    threadC:第3次执行

    threadA:第3次执行

    threadC:第4次执行

    threadB:第2次执行

    threadB:第3次执行

    threadB:第4次执行

    threadA:第4次执行

    可见,与原来的测试结果类似,并没有特别的地方。

    这是因为没有出现多线程竞争同一个资源的情况。将Runnable接口改动如下:

    1. public class RunnableInstance implements Runnable{  
    2.    
    3.       @Override  
    4.       public void run() {  
    5.              while(count > 0){  
    6.                     count = count-10;  
    7.                     System.out.println(Thread.currentThread().getName()+"取出10元,余额:"+count);  
    8.              }  
    9.       }  
    10.        
    11.       private int count = 100;  
    12.    
    13. }  


    在进行测试,打印结果如下:

    threadA取出10元,余额:80

    threadC取出10元,余额:70

    threadB取出10元,余额:80

    threadB取出10元,余额:40

    threadC取出10元,余额:50

    threadC取出10元,余额:20

    threadC取出10元,余额:10

    threadC取出10元,余额:0

    threadA取出10元,余额:60

    threadB取出10元,余额:30

    结果显然不正常。

    这是因为多个线程同时对同一实例中的统一数据进行了读取操作造成的。为了避免这种情况,使共享数据在同一时刻只能有一个线程进行读取,这就是线程的同步控制。

    3、补充

    1、一个运行中的线程总是有名字的,名字有两个来源,一个是虚拟机自己给的名字,一个是你自己的定的名字。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是mian,非主线程的名字不确定。

    2、线程都可以设置名字,也可以获取线程的名字,连主线程也不例外。

    3、获取当前线程的对象的方法是:Thread.currentThread()

    4、在上面的代码中,只能保证:每个线程都将启动,每个线程都将运行直到完成。一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。

    5、当线程目标run()方法结束时该线程完成。

    6、一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。一个可运行的线程或死线程可以被重新启动。

    7、线程的调度是JVM的一部分,在一个CPU的机器上上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。众多可运行线程中的某一个会被选中做为当前线程。可运行线程被选择运行的顺序是没有保障的。

    8、尽管通常采用队列形式,但这是没有保障的。队列形式是指当一个线程完成“一轮”时,它移到可运行队列的尾部等待,直到它最终排队到该队列的前端为止,它才能被再次选中。事实上,我们把它称为可运行池而不是一个可运行队列,目的是帮助认识线程并不都是以某种有保障的顺序排列成个一个队列的事实。

    9、尽管我们没有无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式,比如设置优先级,以及调用Thread.sleep(),wait(),yield()等方法。

    4. 将我们希望线程执行的代码放到run方法中,然后通过start方法来启动线程,start方法首先为线程的执行准备好系统资源,然后再去调用run方法。当某个类继承了Thread类之后,该类就叫做一个线程类。 

    5. 一个进程至少要包含一个线程。 

    6. 对于单核CPU来说,某一时刻只能有一个线程在执行(微观串行),从宏观角度来看,多个线程在同时执行(宏观并行)。 

    7. 对于双核或双核以上的CPU来说,可以真正做到微观并行。

    三、Thread源码研究: 

    1) Thread类也实现了Runnable接口,因此实现了Runnable接口中的run方法; 

    2) 当生成一个线程对象时,如果没有为其设定名字,那么线程对象的名字将使用如下形式:Thread-number,该number将是自动增加的,并被所有的Thread对象所共享(因为它是static的成员变量)。 

    3) 当使用第一种方式来生成线程对象时,我们需要重写run方法,因为Thread类的run方法此时什么事情也不做。

    4)当使用第二种方式生成线程对象时,我们需要实现Runnable接口的run方法,然后使用new Thread(new MyThread())(假如MyThread已经实现了Runnable接口)来生成线程对象,这时的线程对象的run方法或调就会MyThread类的run方法,这样我们自己编写的run方法就执行了。

    说明: 

    Public void run(){

    If(target!=null){

    Target.run();

    }}

    当使用继承Thread生成线程对象时,target为空,什么也不执行,当使用第二种方式生成时,执行target.run(),target为runnable的实例对象,即为执行重写后的方法

    总结:两种生成线程对象的区别:

    1.两种方法均需执行线程的start方法为线程分配必须的系统资源、调度线程运行并执行线程的run方法。 

    2.在具体应用中,采用哪种方法来构造线程体要视情况而定。通常,当一个线程已继承了另一个类时,就应该用第二种方法来构造,即实现Runnable接口。 

    四:线程的生命周期:

    由上图可以看出,一个线程由出生到死亡分为五个阶段:

    1).创建状态 

    •当用new操作符创建一个新的线程对象时,该线程处于创建状态。 

    •处于创建状态的线程只是一个空的线程对象,系统不为它分配资源 

    2). 可运行状态 

    •执行线程的start()方法将为线程分配必须的系统资源,安排其运行,并调用线程体—run()方法,这样就使得该线程处于可运行( Runnable )状态。 

    •这一状态并不是运行中状态(Running ),因为线程也许实际上并未真正运行。 

    3).不可运行状态 

    .当发生下列事件时,处于运行状态的线程会转入到不可运行状态。 

    调用了sleep()方法; 

    •线程调用wait方法等待特定条件的满足 

    •线程输入/输出阻塞 

    4)返回可运行状态: 

    •处于睡眠状态的线程在指定的时间过去后 

    •如果线程在等待某一条件,另一个对象必须通过notify()或notifyAll()方法通知等待线程条件的改变 

    •如果线程是因为输入/输出阻塞,等待输入/输出完成 

    5). 消亡状态 

    当线程的run方法执行结束后,该线程自然消亡。 

    注意:

    1.停止线程的方式:不能使用Thread类的stop方法来终止线程的执行。一般要设定一个变量,在run方法中是一个循环,循环每次检查该变量,如果满足条件则继续执行,否则跳出循环,线程结束。 

    2.不能依靠线程的优先级来决定线程的执行顺序。 

    五:多线程并发

    多线程并发是线程同步中比较常见的现象,java多线程为了避免多线程并发解决多线程共享数据同步问题提供了synchronized关键字

    synchronized关键字:当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。 

    1.Java中的每个对象都有一个锁(lock)或者叫做监视器(monitor),当访问某个对象的synchronized方法时,表示将该对象上锁,此时其他任何线程都无法再去访问该synchronized方法了,直到之前的那个线程执行方法完毕后(或者是抛出了异常),那么将该对象的锁释放掉,其他线程才有可能再去访问该synchronized方法。 

    2. 如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的。 

    3.如果某个synchronized方法是static的,那么当线程访问该方法时,它锁的并不是synchronized方法所在的对象,而是synchronized方法所在的对象所对应的Class对象,因为Java中无论一个类有多少个对象,这些对象会对应唯一一个Class对象,因此当线程分别访问同一个类的两个对象的两个static,synchronized方法时,他们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始执行。 

    4. synchronized块,写法: 

    synchronized(object) 

    表示线程在执行的时候会对object对象上锁。 

    5.synchronized方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行该synchronized方法;synchronized块则是一种细粒度的并发控制,只会将块中的代码同步,位于方法内、synchronized块之外的代码是可以被多个线程同时访问到的。 

    同步的线程状态图:

    六:wait与notify

    1.wait与notify方法都是定义在Object类中,而且是final的,因此会被所有的Java类所继承并且无法重写。这两个方法要求在调用时线程应该已经获得了对象的锁,因此对这两个方法的调用需要放在synchronized方法或块当中。当线程执行了wait方法时,它会释放掉对象的锁。 

    2. 另一个会导致线程暂停的方法就是Thread类的sleep方法,它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的。 

    3.notify():唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 

    直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。

    此方法只应由作为此对象监视器的所有者的线程来调用。通过以下三种方法之一,线程可以成为此对象监视器的所有者:

    o 通过执行此对象的同步实例方法。

    o 通过执行在此对象上进行同步的 synchronized 语句的正文。

    o 对于 Class 类型的对象,可以通过执行该类的同步静态方法。

    一次只能有一个线程拥有对象的监视器。

     关于成员变量与局部变量:如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作时,他们对该成员变量是彼此影响的(也就是说一个线程对成员变量的改变会影响到另一个线程)。  如果一个变量是局部变量,那么每个线程都会有一个该局部变量的拷贝,一个线程对该局部变量的改变不会影响到其他的线程。

    七:死锁的问题: 

    定义:线程1锁住了对象A的监视器,等待对象B的监视器,线程2锁住了对象B的监视器,等待对象A的监视器,就造成了死锁。

         导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性访问权。当线程访问对象时,线程会给对象加锁

    Java中每个对象都有一把锁与之对应。但Java不提供单独的lock和unlock操作。下面笔者分析死锁的两个过程“上锁”和“锁死” 。

    (1) 上锁
         许多线程在执行中必须考虑与其他线程之间共享数据或协调执行状态,就需要同步机制。因此大多数应用程序要求线程互相通信来同步它们的动作,在 Java 程序中最简单实现同步的方法就是上锁。在 Java 编程中,所有的对象都有锁。线程可以使用 synchronized 关键字来获得锁。在任一时刻对于给定的类的实例,方法或同步的代码块只能被一个线程执行。这是因为代码在执行之前要求获得对象的锁。

        为了防止同时访问共享资源,线程在使用资源的前后可以给该资源上锁和开锁。给共享变量上锁就使得 Java 线程能够快速方便地通信和同步。某个线程若给一个对象上了锁,就可以知道没有其他线程能够访问该对象。即使在抢占式模型中,其他线程也不能够访问此对象,直到上锁的线程被唤醒、完成工作并开锁。那些试图访问一个上锁对象的线程通常会进入睡眠状态,直到上锁的线程开锁。一旦锁被打开,这些睡眠进程就会被唤醒并移到准备就绪队列中。

    (2)锁死
         如果程序中有几个竞争资源的并发线程,那么保证均衡是很重要的。系统均衡是指每个线程在执行过程中都能充分访问有限的资源,系统中没有饿死和死锁的线程。当多个并发的线程分别试图同时占有两个锁时,会出现加锁冲突的情形。如果一个线程占有了另一个线程必需的锁,互相等待时被阻塞就有可能出现死锁。

        在编写多线程代码时,笔者认为死锁是最难处理的问题之一。因为死锁可能在最意想不到的地方发生,所以查找和修正它既费时又费力。例如,常见的例子如下面这段程序。print?

    1 public int sumArrays(int[] a1, int[] a2){  

    2   int value = 0;  

    3   int size = a1.length;  

    4   if (size == a2.length) {  

    5      synchronized(a1) { //1        

    6        synchronized(a2) { //2          

    7          for (int i=0; i<size; i++)  

    8             value += a1[i] + a2[i];  

    9        }    

    10      }    

    11   } return value;  

    12 }   

    这段代码在求和操作中访问两个数组对象之前锁定了这两个数组对象。它形式简短,编写也适合所要执行的任务;但不幸的是,它有一个潜在的问题。这个问题就是它埋下了死锁的种子。

    ThreadLocal类(这个类本人没用过,占时不太懂)

    首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。

    另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。

    如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。

  • 相关阅读:
    设计模式之工厂方法3
    Orchard CMS中如何打包不带源码的模块
    Dump Checking
    认识WinDbg
    Bootstrap3.0学习第九轮(CSS补充)
    SVN版本冲突解决详解
    windbg Symbol file path
    SVN下错误集锦
    MvcMovieStore mvc5.0,EF6.01
    SQL Server中的窗口函数
  • 原文地址:https://www.cnblogs.com/sdgf/p/4924257.html
Copyright © 2011-2022 走看看