Java多线程面试攻略(一)

一、多线程相关概念

1、线程:①每个线程都有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在进程中

②线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源,如程序计数器,一组寄存器和线程栈,但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

③多个线程可以共享同一段代码。

2、线程栈:线程栈是线程独有的,保存其运行状态和局部变量,线程栈在线程开始的时候初始化,每个线程的栈互相独立。

3、进程:每个进程都有独立的代码和数据空间(进程上下文,也叫堆内存),一个进程包含1— N个线程。堆内存在操作系统对进程进行初始化的时候分配,运行过程中也可以向系统申请额外的内存空间,但是记得用完了要还给操作系统,要不然就会发生著名的“内存泄漏”现象。

4、并行:操作系统(也可以说多核CPU)同时执行多个程序,是真正的同时,也就是同一时间。

5、并发:通过cpu调度算法,在用户看来是同时执行,实际上从cpu调度层面不是真正的同时。

6、同步:Java中的同步指的是通过人为的控制和调度,保证对于共享资源的多线程访问是线程安全的,来保证结果的准确。通常在代码上加入synchronized关键字来实现同步。

7、线程安全:经常用来描述一段代码的状态。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响执行结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。

8、监视器:java会为每个object对象分配一个monitor,当一个线程调用一个对象的同步方法时,JVM会检查该对象的monitor。如果monitor没有被占用,那么这个线程就得到了monitor的占有权,可以继续执行该对象的同步方法;如果monitor被其他线程所占用,那么该线程将被挂起,直到monitor被释放。当线程退出同步方法调用时,该线程会释放monitor,这将允许其他等待的线程获得monitor以使对同步方法的调用执行下去。

9、守护(后台)线程:指为其他线程提供服务的线程,比如JVM的垃圾回收线程就是一个守护线程。守护线程会随着主线程的结束而结束。

10、死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

java 死锁产生的四个必要条件:

  • 1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。下面用java代码来模拟一下死锁的产生。

解决死锁问题的方法是:一种是用synchronized,一种是用Lock显式锁实现。

二、线程状态转换

下面的这个图非常重要!你如果看懂了这个图,那么对于多线程的理解将会更加深刻!

 

1、新建状态(New):新创建了一个线程对象。

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)

(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

 

三、线程常用函数(也可以叫线程调度方法)

1、setPriority()和getPriority():Thread类可以设置和获取线程的优先级,Java线程有优先级,优先级高的线程会获得较多的运行机会。

Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:

static int MAX_PRIORITY:线程可以具有的最高优先级,取值为10。

static int MIN_PRIORITY: 线程可以具有的最低优先级,取值为1。

static int NORM_PRIORITY: 分配给线程的默认优先级,取值为5,主线程默认的优先级。

线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。

JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

 

2、sleep(long millis)方法:sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会。在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。 

共同点: 

  • 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。 
  • wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。 

   如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。 
   需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。


不同点: 

  • Thread类的方法:sleep(),yield()等    ;  Object的方法:wait()和notify()等 
  • 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。 
  •  wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 
  • sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

所以sleep()和wait()方法的最大区别是:
    sleep()睡眠时,保持对象锁,仍然占有该锁;
    而wait()睡眠时,释放对象锁。
但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
 

3、wait()方法,notify()方法 :Object类的wait()方法,对象方法Obj.wait()的作用是让需要Obj锁的线程等待,直到其他线程调用Obj的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。

Obj.wait()与Obj.notify()必须要与synchronized(Obj)一起使用,也就是说wait和notify必须是对已经被获取了的Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内,否则会在时扔出”java.lang.IllegalMonitorStateException“异常。

从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续让线程继续执行。

Obj.notify()就是对某个需要Obj锁的线程进行唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在需要对应对象锁的等待队列的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。

/* 
经典面试题:建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。
这个问题用Object的wait(),notify()就可以很方便的解决
*/
 class Thread1 implements  Runnable {

     private String name;
     private Object prev;
     private Object self;

     private Thread1(String name, Object prev, Object self) {
         this.name = name;
         this.prev = prev;
         this.self = self;
     }

     @Override
     public void run() {

         int count = 10;
         while (count > 0) {
             synchronized (prev) {
                 synchronized (self) {
                     System.out.print(name);
                     count--;

                     self.notify();
                 }
                 try {
                     prev.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }

         }
     }

         public static void main(String[] args) throws Exception {
             Object a = new Object();
             Object b = new Object();
             Object c = new Object();
             Thread1 pa = new Thread1("A", c, a);
             Thread1 pb = new Thread1("B", a, b);
             Thread1 pc = new Thread1("C", b, c);


             new Thread(pa).start();
             Thread.sleep(100);  //确保按顺序A、B、C执行
             new Thread(pb).start();
             Thread.sleep(100);
             new Thread(pc).start();
             Thread.sleep(100);
         }

     }
/*结果:ABCABCABCABCABCABCABCABCABCABC*/

4、yield() 方法:Thread.yield() 方法将暂停当前正在执行的线程对象,把CPU 占有权让给相同或者更高优先级的线程,但是实际中无法保证yield()达到让步的目的,因为让步的线程还有可能被线程调度程序再次选中。

class Thread1 extends Thread {
     public Thread1(String name) {
         super(name);
     }

     @Override
     public void run() {
         for (int i = 1; i <= 5; i++) {
             System.out.println("" + this.getName() + "-----" + i);
             // 当i为3时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
             if (i ==3) {
                 this.yield();
             }
         }

     }
 }
public class thread {

    public static void main(String[] args) {
        Thread1 yt1 = new Thread1("张三");
        Thread1 yt2 = new Thread1("李四");
        yt1.start();
        yt2.start();
    }

}

 

5、join()方法:thread1.join()方法阻塞调用此方法的线程(calling thread),直到线程thread1完成,此线程再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程。简单点说就是一个线程阻塞另一个线程,等我运行完了你再运行,如果你正好在运行那就先挂起,好好呆着,看我运行。

为什么要用join()方法?

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

 class Thread1 extends Thread {
        private String name;
        public Thread1(String name) {
            super(name);
            this.name=name;
        }
        public void run() {
            System.out.println(Thread.currentThread().getName() + " 线程运行开始!");
            for (int i = 0; i < 5; i++) {
                System.out.println("子线程"+name + "运行 : " + i);
                try {
                    sleep((int) Math.random() * 10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " 线程运行结束!");
        }
}

public class thread {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
        Thread1 mTh1=new Thread1("A");
        Thread1 mTh2=new Thread1("B");
        mTh1.start();
        mTh2.start();
        System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");
     /*   System.out.println(Thread.currentThread().getName()+"主线程运行开始!");
        Thread1 mTh1=new Thread1("A");
        Thread1 mTh2=new Thread1("B");
        mTh1.start();
        mTh2.start();
        try {
            mTh1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            mTh2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");*/
    }

}
/*            加join:                             不加join:
        main主线程运行开始!                    main主线程运行开始!
        A 线程运行开始!                        main主线程运行结束!
        B 线程运行开始!                        B 线程运行开始!
        子线程B运行 : 0                        子线程B运行 : 0
        子线程A运行 : 0                        A 线程运行开始!
        子线程B运行 : 1                        子线程B运行 : 1
        子线程A运行 : 1                        子线程A运行 : 1
        子线程A运行 : 2                        子线程B运行 : 2
        子线程A运行 : 3                        子线程A运行 : 2
        子线程B运行 : 2                        子线程B运行 : 3
        子线程A运行 : 4                        子线程A运行 : 3
        子线程B运行 : 3                        子线程B运行 : 4
        A 线程运行结束!                        子线程A运行 : 4
        子线程B运行 : 4                        B 线程运行结束!
        B 线程运行结束!                        A 线程运行结束!
        main主线程运行结束!
        */

 

6、interrupt()方法 :强制中断某个线程,这种结束方式比较粗暴,如果t线程打开了某个资源还没来得及关闭,也就是run方法还没有执行完就强制结束线程,会导致资源无法关闭,所以很少用。

7、currentThread()方法 :Thread.currentThread()可以获取当前线程的引用,一般都是在没有线程对象又需要获得线程信息时通过Thread.currentThread()获取当前代码段所在线程的引用。

四、线程同步

1、synchronized关键字的作用域有二种: 
1)作用域是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法。
2)作用域是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。 

2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个代码块中。

3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法 。

 

总的说来,synchronized关键字可以作为函数的修饰符,也可修饰函数内的代码块,也就是平时说的同步方法和同步代码块 。synchronized可以把实例对象或类名(我怀疑类也是个对象,牵扯到了反射)作为锁。

在进一步阐述之前,我们需要明确几点:

A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。

B.每个对象只有一个锁(lock)与之相关联。

C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

 

接着来讨论synchronized用到不同地方对代码产生的影响:

 

假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都可以调用它们。

 

1.  把synchronized当作函数修饰符时,示例代码如下:

Public synchronized void methodAAA()     {      //…..    }

这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。

上边的示例代码等同于如下代码:

public void methodAAA()   {

synchronized (this)   {       //…..       }

}

这里的this指的是什么呢?它指的就是调用这个方法的对象,假设是P1。可见同步方法实质是将synchronized作用于实例。那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱!!

2.同步块,示例代码如下:

public void method3(SomeObject so)  {  

synchronized(so){    //…..    }

}

这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:

class Foo implements Runnable  {

    private byte[] lock = new byte[0];  // 特殊的实例变量

    Public void methodA(){

       synchronized(lock) { //… }

}

//…..

}

注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

3.将synchronized作用于static 函数,示例代码如下:

Class Foo{

public synchronized static void methodAAA()   {    //这是同步静态函数    }

public void methodBBB(){     synchronized(Foo.class) { //… }        }

}

   代码中的methodBBB()方法是把class作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。

 

总结:

1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。

2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法。

3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。

4、对于同步,要时刻清醒在哪个对象上同步,这是关键。

5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。

6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。

7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。

 

 

 

  • 4
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值