java核心知识java多线程基础面试技术文

Java基础知识复习笔记(3)--线程基础

2016-05-16  本文已影响962人  TyiMan

一、线程概念

1. 操作系统中的线程
现在的操作系统是多任务操作系统,多线程是实现多任务的一种方式,在操作系统中,每一个进程都有操作系统分配给它的独立内存空间,线程是进程中的一个执行流程,一个进程中可以启动多个线程。线程总是属于某一个进程,进程中的多线程共享进程的内存。
2. Java中的线程
在Java中,线程指两件不同的事情:一是java.lang.Thread类的一个实例,二是线程的执行。

线程栈:
Java为了实现平台无关性, 必须解决不同操作系统中进程,线程的差异,因此Java建立了一套自己的进程与线程机制。 这套机制与windows系统的颇为相似,但是底层实现确实根据不同平台的机制进行实现。
线程栈存储的信息是指某时刻线程中方法调度的信息,当前调用的方法总是位于栈顶。 当某个方法被调用时,此方法的相关信息压入栈顶。

守护线程的作用是为其他线程的运行提供服务,比如说GC线程。其实用户线程和守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果用户线程全部撤离,那么守护线程也就没啥线程好服务的了,所以虚拟机也就退出了。

二、Java线程的创建和启动

1. 线程的创建
Java创建线程有两种方法,
第一种:继承java.lang.Thread类,重写run方法;
第二种,实现java.lang.Runnable接口,实现run方法。

两种生成线程对象的区别:
1.两种方法均需执行线程的start方法为线程分配必须的系统资源、调度线程运行并执行线程的run方法。
2.在具体应用中,采用哪种方法来构造线程体要视情况而定。通常,当一个线程已继承了另一个类时,就应该用第二种方法来构造,即实现Runnable接口。

2. 线程的实例化
第一种方法的创建的线程,直接new该线程类即可。

//Thread的构造函数
public Thread( );
public Thread(Runnable target);
public Thread(String name);
public Thread(Runnable target, String name);
public Thread(ThreadGroup group, Runnable target);
public Thread(ThreadGroup group, String name);
public Thread(ThreadGroup group, Runnable target, String name);
public Thread(ThreadGroup group, Runnable target, String name, long stackSize);

第二种方法创建的线程,需要调用Thread的构造方法进行实例化。

//实现runnable接口的类,调用Thread的构造方法进行实例化
Thread(Runnable target) 
Thread(Runnable target, String name) 
Thread(ThreadGroup group, Runnable target) 
Thread(ThreadGroup group, Runnable target, String name) 
Thread(ThreadGroup group, Runnable target, String name, long stackSize)

3. 启动线程

4. 创建线程例子
通过继承Thread创建线程

package tym.ThreadBase.create;
/** *Created by TyiMan on 2016/5/14. */
public class MyThread extends Thread {   
  public MyThread() {       
    super();    
  }    
  public MyThread(String name) {       
    super(name);    
  }    
  public void run() {        
    for (int i = 0; i < 3; i++)                    
      System.out.println(Thread.currentThread().getName() + " " + i);    
  }
}

通过实现Runnable接口创建线程

package tym.ThreadBase.create;
/** * Created by TyiMan on 2016/5/14. */
public class MyRunnable implements Runnable {    
  @Override    
  public void run() {        
    for(int i = 0;i<3;i++)        
      System.out.println(Thread.currentThread().getName()+" "+i);     
  }
}

主函数实例化线程并且运行

package tym.ThreadBase.create;
/** * Created by TyiMan on 2016/5/14. */
public class Main {    
  public static void main(String[]args){       
    Thread thread1 = new MyThread();        
    Thread thread2 = new Thread(new MyRunnable());        
    Thread thread3 = new MyThread("MyThread");        
    Thread thread4 = new Thread(new MyRunnable(),"MyRunnable");       

    thread1.start();        
    thread2.start();       
    thread3.start();        
    thread4.start();    
  }
}

执行结果:

//执行结果是按照顺序的,如果循环次数更多,他们的输出就会这么有序了
Thread-0 0
Thread-0 1
Thread-0 2
Thread-1 0
Thread-1 1
Thread-1 2
MyThread 0
MyThread 1
MyThread 2
MyRunnable 0
MyRunnable 1
MyRunnable 2

一些常见问题
1、线程的名字,名字有两个来源,一个是虚拟机自己给的名字,一个是你自己的定的名字(构造函数传线程名字或者setName方法)。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是main,非主线程的名字Thread-number(该number将是自动增加的,并被所有的Thread对象所共享,因为它是static的成员变量)。
2、获取当前线程的对象的方法是:Thread.currentThread()
3、想成的运行,只能保证:每个线程都将启动,每个线程都将运行直到完成。一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。
4、当线程目标run()方法结束时该线程完成。
5、一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。一个可运行的线程或死线程可以被重新启动。
6、线程的调度是JVM的一部分,在一个CPU的机器上上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程。众多可运行线程中的某一个会被选中做为当前线程。可运行线程被选择运行的顺序是没有保障的。

三、线程的生命周期

与人有生老病死一样,线程要经历新建、就绪、运行、死亡和阻塞这5种不同的状态。

线程的生命周期
新建(new Thread)
当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。例如:Thread t1=new Thread();

就绪(runnable)
线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。例如:t1.start();

运行(running)
线程获得CPU资源正在执行任务(调用run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。

死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
自然终止:正常运行run()方法后终止
异常终止:调用stop()方法让一个线程终止运行

阻塞(blocked)
由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入阻塞状态。
正在睡眠:sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。
正在等待:调用wait()方法。(调用notify()方法回到就绪状态)
被另一个线程所阻塞:调用suspend()方法。(调用resume()方法恢复)

下面给出了Thread类中和各个状态相关的方法。

// 开始线程
public void start( );
public void run( );
// 挂起和唤醒线程
public void resume( );     // 不建议使用
public void suspend( );    // 不建议使用
public static void sleep(long millis);
public static void sleep(long millis, int nanos);
public static void yied() //可以对当前线程进行临时暂停(让线程将资源释放出来)
// 终止线程
public void stop( );       // 不建议使用
public void interrupt( );
// 得到线程状态
public boolean isAlive( );
public boolean isInterrupted( );
public static boolean interrupted( );
//join方法让线程加入执行,执行某一线程join方法的线程会被冻结,
//等待某一线程执行结束,该线程才会恢复到可运行状态
public void join( ) throws InterruptedException;

四、线程的同步与锁

线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。
Java线程锁的原理

Java线程同步的synchronized关键字的使用

关于同步与锁的要点:
1)只能同步方法,而不能同步变量和类;
2)每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?
3)不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
4)如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
5)如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
6)线程睡眠时,它所持的任何锁都不会释放。
7)线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
8)同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
9)在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。要保证多个线程的同步,被锁定的对象,在它们之间是共享的(就是多个线程使用的同一个对象的锁)

(1)同步方法
使用synchronized关键字修饰方法。 当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

public class TestSynClass extends Thread{{

 public synchronized void synMethod(){
  //some codes
 }
}

(2)同步代码块
使用synchronized关键字修饰代码语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。使用synchronized关键字将需要互斥的代码包含起来,并上一把锁。并且锁定的对象必须是多个线程之间共享的对象。(如下面实例中第三个就是无效的同步代码块)

public class TestSynClass extends Thread{
 private Object object = new Object();

 public void synThisClass(){
  synchronized(this){
   //some code
  }
 }
 
 public void synOtherObject(){
  synchronized(object){
   //some code
  }
 }

  public void synDifferentObject(){
    //这个方法是不能实现同步的,因为每次运行都会生成一个新的Object对象
    //不同调用者调用的是不同对象
    synchronized(new Object){
      //some code
    }
  }
}

(3)synchronized作用于static 函数
要同步静态方法,一是在静态方法上加synchronized关键字,另一个是在整个类对象的锁,这个对象是就是这个类(XXX.class)。
public class TestSynClass extends Thread{{

 public synchronized static void methodA()      { 
   //some code
  }    
  public static void methodB()    {       
    synchronized(TestSynClass.class) {
      //some code
    }  
  }
}

线程同步小结
1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,并不一定好使。但是,一旦程序发生死锁,程序将死掉。

五、线程的交互与调度(线程状态转换实例)

1. 线程的交互
线程交互的方法

关于线程/通知要关键点
(1)必须从同步环境内调用wait()、notify()、notifyAll()方法。线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁。
(2)wait()、notify()、notifyAll()都是Object的实例方法。与每个对象具有锁一样,每个对象可以有一个线程列表,他们等待来自该信号(通知)。线程通过执行对象上的wait()方法获得这个等待列表。从那时候起,它不再执行任何其他指令,直到调用对象的notify()方法为止。如果多个线程在同一个对象上等待,则将只选择一个线程(不保证以何种顺序)继续执行。如果没有线程等待,则不采取任何特殊操作。

线程交互的实例

当在对象上调用wait()方法时,执行该代码的线程立即放弃它在对象上的锁。然而调用notify()时,并不意味着这时线程会放弃其锁。如果线程荣然在完成同步代码,则线程在移出之前不会放弃锁。因此,只要调用notify()并不意味着这时该锁变得可用。

主函数实例化线程,然后调用线程的wait()函数,等待线程计算1到100的和的结果。

package tym.ThreadBase.waitAndnotify;
/** * Created by TyiMan on 2016/5/16. */
public class TestWaitNotifyMain {   
  public static void main(String[] args){        
    TestThread thread = new TestThread();        
    thread.start();        
    synchronized (thread){            
      try{                
        System.out.println("等待对象b完成计算……");               
        thread.wait();           
      } catch (InterruptedException e) {                
        e.printStackTrace();            
      }            
      System.out.println("线程计算结果为 total is "+thread.total);        
    }    
  }
}

TestThread类的run方法计算1到100的和,计算完后,调用notify()函数。

package tym.ThreadBase.waitAndnotify;
/** * Created by TyiMan on 2016/5/16. */
public class TestThread extends Thread {    
  int total ;    
  @Override    
  public void run(){        
    synchronized (this){           
      for(int i = 0;i<=100;i++){                
       total +=i;            
      }            
      notify();        
    }    
  }
}

Calculator计算1到100的和,计算完唤醒所有其他线程。

package tym.ThreadBase.waitAndnotify;
/** * Created by TyiMan on 2016/5/16. */
public class Calculator extends Thread {    
  int total;   
  @Override    
  public void run() {        
    synchronized (this) {            
      for (int i = 0; i < 101; i++) {                
        total += i;            
      } 
      notifyAll();        
    }   
  }
}

ResultReader类等待结果,被唤醒后显示Calculator类的计算结果。

package tym.ThreadBase.waitAndnotify;

/**
 * Created by TyiMan on 2016/5/16.
 */
public class ReaderResult extends Thread {
    Calculator calculator;

    public ReaderResult(Calculator c) {
        this.calculator = c;
    }

    @Override
    public void run() {
        synchronized (calculator) {
            try {
                System.out.println(Thread.currentThread() + "等待计算结果。。。");
                calculator.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread() + "计算结果为:" + calculator.total);
        }
    }

    public static void main(String[] args) {
        Calculator calculator = new Calculator();

        //启动三个线程,分别获取计算结果
        new ReaderResult(calculator).start();
        new ReaderResult(calculator).start();
        new ReaderResult(calculator).start();
        //启动计算线程
        calculator.start();
     }
}

最后输出结果:

Thread[Thread-1,5,main]等待计算结果。。。
Thread[Thread-2,5,main]等待计算结果。。。
Thread[Thread-3,5,main]等待计算结果。。。
Thread[Thread-3,5,main]计算结果为:5050
Thread[Thread-2,5,main]计算结果为:5050
Thread[Thread-1,5,main]计算结果为:5050

问题注意
实际上,上面代码中,我们期望的是读取结果的线程在计算线程调用notifyAll()之前等待即可。 但是,如果计算线程先执行,并在读取结果线程等待之前调用了notify()方法,那么又会发生什么呢?
问题分析
这种情况是可能发生的。因为无法保证线程的不同部分将按照什么顺序来执行。幸运的是当读取线程运行时,它只能马上进入等待状态,它没有做任何事情来检查等待的事件是否已经发生。因此,如果计算线程已经调用了notifyAll()方法,那么它就不会再次调用notifyAll(),并且等待的读取线程将永远保持等待。这当然是开发者所不愿意看到的问题。
问题解决
当等待的事件发生时,需要能够检查notifyAll()通知事件是否已经发生。通常是利用某种循环,该循环检查某个条件表达式,只有当正在等待的事情还没有发生的情况下,它才继续等待。

2. 线程的调度——休眠
线程休眠的目的是使线程让出CPU的最简单的做法之一,线程休眠时候,会将CPU资源交给其他线程,以便能轮换执行,当休眠一定时间后,线程会苏醒,进入准备状态等待执行。
线程休眠的方法是Thread.sleep(long millis)Thread.sleep(long millis, int nanos),均为静态方法,那调用sleep休眠的哪个线程呢?简单说,哪个线程调用sleep,就休眠哪个线程。
sleep()实例代码

package tym.ThreadBase.waitAndnotify;

/**
 * Created by TyiMan on 2016/5/16.
 */
public class Test {
  public static void main(String[] args) {
    Thread t1 = new MyThread1();
    Thread t2 = new Thread(new MyRunnable());
    t1.start();
    t2.start();
  }
}

class MyThread1 extends Thread {
  public void run() {
    for (int i = 0; i < 3; i++) {
        System.out.println("线程1第" + i + "次执行!");
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  }
}

class MyRunnable implements Runnable {
  public void run() {
    for (int i = 0; i < 3; i++) {
        System.out.println("线程2第" + i + "次执行!");
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  }
}

输出结果:

线程2第0次执行!
线程1第0次执行!
线程1第1次执行!
线程2第1次执行!
线程2第2次执行!
线程1第2次执行!

3. 线程调度——优先级
void setPriority(int newPriority)函数设置线程优先级
与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的并非没机会执行。

setPriority()代码实例

package tym.ThreadBase.waitAndnotify;

/**
 * Created by TyiMan on 2016/5/16.
 */
public class Test {
  public static void main(String[] args) {
    Thread t1 = new MyThread1();
    Thread t2 = new Thread(new MyRunnable());
    t1.setPriority(10);
    t2.setPriority(1);

    t2.start();
    t1.start();
  }
}

class MyThread1 extends Thread {
  public void run() {
    for (int i = 0; i < 10; i++) {
        System.out.println("线程1第" + i + "次执行!");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  }
}

class MyRunnable implements Runnable {
  public void run() {
    for (int i = 0; i < 10; i++) {
        System.out.println("线程2第" + i + "次执行!");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  }
}

执行结果:

线程1第0次执行! 
线程2第0次执行! 
线程2第1次执行! 
线程1第1次执行! 
线程2第2次执行! 
线程1第2次执行! 
线程1第3次执行! 
线程2第3次执行! 
线程2第4次执行! 
线程1第4次执行! 
线程1第5次执行! 
线程2第5次执行! 
线程1第6次执行! 
线程2第6次执行! 
线程1第7次执行! 
线程2第7次执行! 
线程1第8次执行!
线程2第8次执行!
线程1第9次执行!
线程2第9次执行! 

4. 线程的调度——让步

线程的让步含义就是使当前运行着线程让出CPU资源,但是然给谁不知道,仅仅是让出,线程状态回到可运行状态。

yield()代码实例

package tym.ThreadBase.waitAndnotify;

/**
 * Created by TyiMan on 2016/5/16.
 */
public class Test {
  public static void main(String[] args) {
    Thread t1 = new MyThread1();
    Thread t2 = new Thread(new MyRunnable());

    t2.start();
    t1.start();
  }
}

class MyThread1 extends Thread {
  public void run() {
    for (int i = 0; i < 10; i++) {
        System.out.println("线程1第" + i + "次执行!");
    }
  }
}

class MyRunnable implements Runnable {
  public void run() {
    for (int i = 0; i < 10; i++) {
        System.out.println("线程2第" + i + "次执行!");
        Thread.yield();
    }
  }
}

执行结果:

线程2第0次执行!
线程1第0次执行!
线程2第1次执行!
线程1第1次执行!
线程2第2次执行!
线程1第2次执行!
线程2第3次执行!
线程1第3次执行!
线程2第4次执行!
线程1第4次执行!
线程2第5次执行!
线程1第5次执行!
线程2第6次执行!
线程1第6次执行!
线程2第7次执行!
线程1第7次执行!
线程2第8次执行!
线程1第8次执行!
线程2第9次执行!
线程1第9次执行!

在使用synchronized关键字时候,应该尽可能避免在synchronized方法或synchronized块中使用sleep或者yield方法,因为synchronized程序块占有着对象锁,sleep()让程序睡眠还不释放锁,不但严重影响效率,也不合逻辑。在同步程序块内调用yeild()方法让出CPU资源也没有意义,因为它占用着锁,其他互斥线程还是无法访问同步程序块。当然与同步程序块无关的线程可以获得更多的执行时间。

5. 线程的调度——合并

线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

join()代码实例

package tym.ThreadBase.waitAndnotify;

/**
 * Created by TyiMan on 2016/5/16.
 */
public class Test {
  public static void main(String[] args) {
    Thread t1 = new MyThread1();
    t1.start();

    System.out.println("主线程开始执行!");
    try {
        //t1线程合并到主线程中,主线程停止执行过程,转而执行t1线程,直到t1执行完毕后继续。
        t1.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("主线程执行完毕!");
  }
}

class MyThread1 extends Thread {
public void run() {
    for (int i = 0; i < 2; i++) {
        System.out.println("线程1第" + i + "次循环!");
    }
  }
}

执行结果:

主线程第0次执行!
线程1第0次执行!
线程1第1次执行!
主线程第1次执行!

6. 守护线程

守护线程与普通线程写法上基本么啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。

setDaemon方法的详细说明:
public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。
该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
参数: on - 如果为 true,则将该线程标记为守护线程。
抛出
IllegalThreadStateException - 如果该线程处于活动状态。
SecurityException - 如果当前线程无法修改该线程。

setDaemon()代码实例

package tym.ThreadBase.waitAndnotify;

/**
 * Created by TyiMan on 2016/5/16.
 */
public class Test {
  public static void main(String[] args) {
    Thread t1 = new MyCommon();
    Thread t2 = new Thread(new MyDaemon());
    t2.setDaemon(true);        //设置为守护线程

    t2.start();
    t1.start();
  }
}

class MyCommon extends Thread {
  public void run() {
    for (int i = 0; i < 5; i++) {
        System.out.println("线程1第" + i + "次执行!");
        try {
            Thread.sleep(7);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  }
}

class MyDaemon implements Runnable {
  public void run() {
    for (long i = 0; i < 9999999L; i++) {
        System.out.println("后台线程第" + i + "次执行!");
        try {
            Thread.sleep(7);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
  }
}

执行结果:守护进程随着主进程结束而结束

后台线程第0次执行!
线程1第0次执行!
线程1第1次执行!
后台线程第1次执行!
后台线程第2次执行!
线程1第2次执行!
后台线程第3次执行!
线程1第3次执行!
后台线程第4次执行!
线程1第4次执行!
后台线程第5次执行!

六、线程基础总结

该部分为Java线程比较基础的部分,接下来会继续整理一些更加深入的知识。想到Java多线程基础,我们应该知道自己应该了解:

七、参考引用

http://lavasoft.blog.51cto.com/62575/27069/
http://blog.csdn.net/csh624366188/article/details/7318245
http://www.cnblogs.com/riskyer/p/3263032.html

上一篇下一篇

猜你喜欢

热点阅读