多线程

引言

上一篇安卓面试系列–线程和线程池我们只是简单的介绍了一下线程,重点放在了线程池上。我们都知道线程是程序运行的最小单元,但是它不能无限的创建,因为线程的创建和销毁都需要消耗系统资源,但是我们可以创建有限的线程来提高我们程序的效率,所以这一篇我们来重点讲一下安卓中的多线程。

在那之前,我们先来了解一下进程和线程的区别:

  • 进程:每一个进程(程序)都有独立的代码和数据空间(进程上下文),进程间的切换开销巨大,一个进程中可以有多个线程;
  • 线程:线程是程序运行的最小单元,同一类线程拥有相同的代码和数据空间,每一个线程都有自己的运行栈,线程间的切换开销很小。

多线程就是指操作系统能同时运行多个程序。

一、实现多线程的两种方式

java中想要实现多线程,有两种手段,一种是继承Thread类,另外一种就是实现Runnable接口

1、继承Thread类

class Thread1 extends Thread{  
    private String name;  
    public Thread1(String name) {  
       this.name=name;  
    }  
    public void run() {  
        for (int i = 0; i < 5; i++) {  
            System.out.println(name + "运行  :  " + i);  
            try {  
                sleep((int) Math.random() * 10);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  

    }  
}  
public class Main {  

    public static void main(String[] args) {  
        Thread1 mTh1=new Thread1("A");  
        Thread1 mTh2=new Thread1("B");  
        mTh1.start();  
        mTh2.start();  

    }  

}  

执行:

输出:
A运行  :  0
B运行  :  0
A运行  :  1
A运行  :  2
A运行  :  3
A运行  :  4
B运行  :  1
B运行  :  2
B运行  :  3
B运行  :  4

再执行一下:

A运行  :  0
B运行  :  0
B运行  :  1
B运行  :  2
B运行  :  3
B运行  :  4
A运行  :  1
A运行  :  2
A运行  :  3
A运行  :  4

从运行结果可以看到,多线程是乱序执行的。只有乱序执行的代码才有必要设计为多线程。

Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。

而且线程是不能重复start的,如果重复start,会报java.lang.IllegalThreadStateException异常。

Thread1 mTh1=new Thread1("A");  
Thread1 mTh2=mTh1;  
mTh1.start();  
mTh2.start(); 

输出:

Exception in thread "main" java.lang.IllegalThreadStateException
    at java.lang.Thread.start(Unknown Source)
    at com.multithread.learning.Main.main(Main.java:31)

2、实现Runnable接口

这也是一种非常常见的实现多线程的方法,只需要重写run方法即可。

class Thread2 implements Runnable{  
    private String name;  

    public Thread2(String name) {  
        this.name=name;  
    }  

    @Override  
    public void run() {  
          for (int i = 0; i < 5; i++) {  
                System.out.println(name + "运行  :  " + i);  
                try {  
                    Thread.sleep((int) Math.random() * 10);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  

    }  

}  
public class Main {  

    public static void main(String[] args) {  
        new Thread(new Thread2("C")).start();  
        new Thread(new Thread2("D")).start();  
    }  

} 

运行:

C运行  :  0
D运行  :  0
D运行  :  1
C运行  :  1
D运行  :  2
C运行  :  2
D运行  :  3
C运行  :  3
D运行  :  4
C运行  :  4

Thread2类通过实现Runnable接口,使得该类有了多线程的特征。run()方法是多线程的约定,所有的代码都在run()方法中执行。在启动多线程的时候,我们需要通过Thread类的构造方法Thread(Runnable target)构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

3、Thread和Runnable的区别?

如果一个类继承Thread,则不适合资源共享;如果一个类实现Runnable,则很容易实现资源共享。

总结一下实现Runnable接口比继承Thread类的优势:

  • 适合多个相同的程序代码的线程去处理同一个资源;
  • 可以避免java中单继承的限制;
  • 代码和线程分离,逻辑更加清晰;
  • 线程池只能放入实现Runnable接口或者callable类线程,不能直接放入继承Thread的类。

二、线程状态切换

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

这里写图片描述

上面这张图基本上就可以涵盖线程状态而定所有内容,我们来分析分析。

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、优先级:java线程有优先级,优先级高的线程获得较多的运行机会。优先级一般用整数表示,取值1~10。

static int MAX_PRIORITY  
          线程可以具有的最高优先级,取值为10static int MIN_PRIORITY  
          线程可以具有的最低优先级,取值为1static int NORM_PRIORITY  
          分配给线程的默认优先级,取值为5

主线程的默认优先级为Thread.NORM_PRIORITY。继承关系中的两个线程具有相同的优先级。Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。

2、线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。

3、线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法。

4、线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

wait和sleep区别
共同点:
1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
2. wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException
不同点:
1、sleep()是线程的方法,而wait()是Object的方法。
2、每一个对象都可以通过锁来控制同步。 sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3、 wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 。

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

四、线程同步(重点)

1、synchronized关键字的作用域有二种:

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

2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/区块/},它的作用域是当前对象;

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

总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。

在这里还要多提几句:

  • 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁,而且同步方法很可能还会被其他线程的对象访问。
  • 每个对象只有一个锁与之相关联;
  • 实现同步对系统开销很大,甚至可能造成死锁,所以用的时候需要谨慎。

代码示例

加入p1、p2是同一个类的两个不同的对象。

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

Public synchronized void methodAAA()  
{  
//…  
} 

这儿就是同步方法,这个时候synchronized 关键字锁住的是哪个对象呢?它锁住的是调用方法methodAAA()的那个对象,也就是说,如果p1在不同的线程中调用方法methodAAA(),它们之间会形成互斥,达到同步的效果。但是此时p2还是可以访问这个methodAAA()方法的,因为p1、p2是不同的对象,synchronized 关键字只能锁住一个对象。

上面的代码跟下面这个等同:

public void methodAAA()  
{  
synchronized (this)      //  (1)  
{  
       //…..  
}  
} 

那么此处的this指的是什么?this指的就是调用methodAAA()方法的对象,比如p1。可见同步方法实质是将synchronized作用于object reference。那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干。

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

 public void method3(SomeObject so)  {  
       synchronized(so)  {  
       //…..  
    }  
}  

这时,锁就是so这个对象,谁拿到锁谁就可以控制那段代码。

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

Class Foo{  
    public synchronized static void methodAAA(){  // 同步的static 函数   
//… 
}  
public void methodBBB()  {  
       synchronized(Foo.class)   //  class literal(类名称字面常量)  
    }  
}  

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

总结一下:
1、线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏;
2、线程同步通过锁来实现,每个对象都有且仅有一个锁。这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法;
3、对于静态同步方法,锁是针对整个类的,锁住的对象是该类的Class对象。静态锁和非静态锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
5、死锁是线程间相互等待锁造成的,在实际中发生的概率非常的小。一旦程序发生死锁,程序将死掉。

五、线程数据传递

在传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。

1、通过构造方法传递数据:
在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。下面的代码演示了如何通过构造方法来传递数据:

public class MyThread1 extends Thread   {   
    private String name;   
public MyThread1(String name)   {   
    this.name = name;   
}   
public void run()   {   
    System.out.println("hello " + name);   
}   
public static void main(String[] args)   {   
    Thread thread = new MyThread1("world");   
    thread.start();   
    }   
}   

由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就就已经到位了,这样就不会造成数据在线程运行后才传入的现象。如果要传递更复杂的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就会造成很多不便。由于Java没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又会使构造方法在数量上大增。因此,要想避免这种情况,就得通过类方法或类变量来传递数据。

2、通过变量和方法传递数据

向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。下面的代码是对MyThread1类的改版,使用了一个setName方法来设置 name变量:

public class MyThread2 implements Runnable   {   
    private String name;   
public void setName(String name)   {   
    this.name = name;   
}   
public void run()   {   
    System.out.println("hello " + name);   
}   
public static void main(String[] args)   {   
    MyThread2 myThread = new MyThread2();   
    myThread.setName("world");   
    Thread thread = new Thread(myThread);   
    thread.start();   
    }   
}   

3、通过回调函数传递数据
上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回value之前,必须要得到三个随机数。也就是说,这个 value是无法事先就传入线程类的。

class Data   {   
    public int value = 0;   
}   
class Work   {   
    public void process(Data data, Integer numbers){   
        for (int n : numbers){   
        data.value += n;   
        }   
    }   
}   
public class MyThread3 extends Thread{   
    private Work work;   
    public MyThread3(Work work){   
        this.work = work;   
    }   
    public void run(){   
        java.util.Random random = new java.util.Random();   
        Data data = new Data();   
        int n1 = random.nextInt(1000);   
        int n2 = random.nextInt(2000);   
        int n3 = random.nextInt(3000);   
        work.process(data, n1, n2, n3); // 使用回调函数   
        System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"   
+ String.valueOf(n3) + "=" + data.value);   
}   
public static void main(String[] args){   
    Thread thread = new MyThread3(new Work());   
    thread.start();   
    }   
} 

好了,关于多线程的知识就是这么多了。

总结

实现多线程有两种方式,一种是继承Thread类,一种是实现Runnable接口。两者的区别需要搞清楚,实现Runnable接口比继承Thread类更好,因为它更有利于资源的共享,也避免了java中单继承的特性,同时使得代码和线程分离开来,逻辑更加清晰。然后就是简单了解一下线程的五种状态:创建、就绪、运行、阻塞、终止。其中重点是阻塞状态,进入阻塞状态的两种方式slee()和wait()两者的区别,sleep是Thread的方法,可以用于任何地方,没有释放对象锁,wait()是Object类的方法,只能用于同步方法或者同步代码块中,释放了对象锁。然后就是重点synchronized 对象锁,注意它锁住的是对象。我们主要讲了同步方法、同步代码块以及静态同步方法,注意,静态同步方法锁住的是Class,这个有点特别。最后就是线程间的通信,一共说了三种方式,第一种是通过构造方法传值,第二种是通过变量和方法传递数据,第三种是通过回调函数的方式传值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值