Java——线程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java——线程相关的知识,希望对你有一定的参考价值。

线程


进程和线程的区别:

  进程:每一个进程都具有独立的代码和数据空间。进程是系统进行资源分配和调度对的一个独立单位。

  线程:同一类线程共享代码和数据空间。每一个线程都有自己的堆栈。

总结:操作系统可以有同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。

注意:并发性和并行性是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。


 再来说说多线程的优势:

  1.进程之间不能共享内存,但线程之间共享内存非常容易。

  2.系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小很多,因此使用多线程来实现多任务并发比多进程的效率高。

  3. Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。

 


一、通过继承 Thread 类来实现多线程

   通过继承 Thread 类来创建并启动多线程的步骤如下:

  》定义一个Thread 类的子类,并重写该类的 run() 方法,该 run() 方法的方法体就代表了线程需要完成的任务。因此把 run() 方法称为线程执行体。

  》创建 Thread 子类的实例,即创建了线程对象。

  》调用线程该对象的 start() 方法来启动该线程。

public class ThreadTest {

    public static void main(String[] args) {
        //创建线程
        Thread t = new Processor();
        //启动线程
        t.start();  //这段代码执行瞬间结束。告诉jvm再分配一个新的栈给t线程
                    //run不需要程序员手动调用,系统线程启动    之后自动调用 run 方法
        //这段代码在主线程中运行
        for (int i = 0; i < 10; i++) {
            System.out.println("main--->" + i);
        }
    }
}

//定义一个线程
class Processor extends Thread{
    
    //重些run方法
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("run--->" + i);
        }
    }
}

 三次执行结果:

程序启动运行main()方法的时候,主线程main也随之被创建,随着两个对象调用 strar()方法,另外两个线程也被启动,这样整个应用就在多线程下运行。

注意:start()方法不是调用就立即执行多线程代码,而是是该线程变为可运行状态(Runnable),什么时候执行是由系统决定的。

从上面的执行结果可以看出多线程的执行顺序是无序的,每次执行顺序都是随机的。


 二、通过实现 Runnable 接口来实现多线程

 实现 Runnable 接口来创建并启动多线程的步骤如下:

  》定义 Runnable 接口的实现类,并重写该接口 run() 方法,该 run() 方法的方法体同样是该线程的线程执行体。

  》创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真的线程对象。

public class RunnableTest {

    public static void main(String[] args) {
        
        Thread t1 =new Thread(new SecondThread());
        Thread t2 =new Thread(new SecondThread());
        
        t1.start();
        t2.start();
    }
}

class SecondThread implements Runnable{
    
    @Override
    public void run() {
        for (int i=0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
    
}

  SecondThread 类通过实现 Runnable 接口,让该类有了多线程的特性。 run() 方法是多线程的一个约定,所有的多线程的代码都在 run 方法了里面。Thread 类实际上也是实现了 Runnable 接口类。

在启动多线程的时候,首先要通过 new Thread(Runn able target) 构造出对象,然后调用 Thread 对象的 start() 方法来运行多线程。实际上不管是继承 Thread 类或者 实现 Runnable 接口,都要通过Thread 对象的 start() 方法来是实现多线程。


三、Thread 和 Runnable 的区别:

Thread :

  》劣势是:线程类继承了 Thread 类,所以不能再继承其他父类。

  》优势是:编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用this 即可获得当前线程。

Runnable :

  》线程类只是实现了 Runnable 接口,还可以继承其它类。

  》在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理通一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

  》劣势是:编程稍稍有点复杂,如果需要访问当前线程,则必需使用 Thread.currentThread() 方法。


四、线程的生命周期:

            

创建状态:当程序时用 new 关键字创建了一个线程之后,该线程就处于新建状态。此时线程对象没有表现出任何线程的动态特征,程序也不会执行线程执行体

就绪状态:当线程对象调用了 start() 方法后,该线程处于就绪状态。处于这个状态中的线程并没有开始运行,只是表示该程序可以运行了,至于该线程何时开始运行,取决与 JVM 里的线程调度器的调度。

运行状态:处于就绪状态的线程活得了CPU,开始执行 run() 方法的线程执行体,则该状态处于运行状态。

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

  》线程调用了一个sleep() 方法主动放弃所占用的处理器资源。

  》线程调用了一个阻塞式 IO 方法,在该方法返回之前,该线程被阻塞。

  》线程试图活得一个同步监视器,但该同步监视器正被其他线程所持有。

  》线程在等某个通知(notify)

  》程序调用了线程的 suspend() 方法将该线程挂起,但这个方法容易导致死锁,所以应该尽量避免使用该方法。

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


五、控制线程

》》线程的优先级

  每个线程执行时都具有一定的优先级,优先级高的线程会获得较多的执行机会,而优先级低的线程则获得较少的机会。每个线程默认的优先级都与创建它的父线程的优先级相同, main 线程具有普通优先级,由 main 线程创建的子线程也具有普通优先级。Thread 类提供了 setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级,其中 setPriority()的参数可以是一个整数,范围是1~10之间,也可以使用 Thread 类的如下三个静态常量。

  》MAX_PRIORITY :其值是 10。

  》MIN_PRIORITY:其值是 1。

  》NORM_PRIORITY:其值是 5。

public class PriorityTest {

    public static void main(String[] args) {
        PriorityThread pt =new PriorityThread();
        pt.start();
        pt.currentThread().setPriority(Thread.MIN_PRIORITY);
        for (int i = 0; i < 10; i++) {
            Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
            System.out.println(Thread.currentThread().getName() +" " + i);
        }
    }
}

class PriorityThread extends Thread{
    
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(this.currentThread().getName() +" " + i);
        }
    }
}

 


》》join线程

   join()方法有如下几种重载方式:

  》join():等待被 join 的线程执行完成。

  》join(long millis):等待被 join 的线程的时间最长为 millis 毫秒。如果在 millis 毫秒内被 join 的线程还没有执行结束,则不再等待。

public class JoinTest {
    public static void main(String[] args) throws Exception {
        JoinThread jt = new JoinThread("A");
        jt.start();
        jt.join();   //合并线程
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}

class JoinThread  extends Thread{

    public JoinThread(String name){
        super(name);
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(this.currentThread().getName()+""+i);
        }
    }    
}

  在线程 “A” 启动并调用了 join() 方法后,就不会和主线程 main 并发执行了,main 线程会一直等到 子线程 “A” 执行结束,才可以继续执行,在子线程 “A” 执行时,主线程 main 一直处于等待状态。

运行结果:


》》后台线程 :

  有一种线程是在后台运行的,它的任务是为其它的线程提供服务,这种线程被称为 “后台线程(Daemon Thread)”,又被称为 “守护线程” 或 “精灵线程”。JVM 的垃圾回收线程就是典型的后台线程。

  后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。

public class DemonTest {

    public static void main(String[] args) {
        DemonThread dt = new DemonThread();
        dt.setDaemon(true);
        dt.start();
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

class DemonThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(this.currentThread().getName()+" "+i);
        }
    }
}

 运行结果:

  上面代码中将 dt 线程设置为后台线程,然后启动线程,本来该线程该执行到 i 等于999时才会结束,但从运行结果不难看出,该后台线程不能运行到999,因为当主线程也就是线程中的唯一线程结束后,JVM 会退出,因而后台线程也就被结束了。

注意

  前台线程死亡后,JVM 会通知后台线程死亡,但从它接收到指令到做出响应,需要一定时间。而且要将某个线程设置成后台线程,必须在线程启动之前设置,也就是说,setDaemon(true) 必须在 start() 方法之前调用,否则会引发

IllegalThreadStateException 异常。


》》睡眠:sleep

  如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用 Thread 类的静态 sleep() 方法来实现。

当线程他调用sleep() 方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep() 方法常用来暂停程序的执行。

public class SleepTest {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            System.out.println("当前时间"+new Date());
            Thread.sleep(2000);
        }
    }
}

 运行结果:  

上面的程序调用 sleep() 方法来暂停主线程的执行,应为该线程只有一个主线程,当主线程进入睡眠后,暂停2秒后,系统没有别的线程可以执行,可以清楚的看见程序在 sleep() 出暂停。


 》》线程让步:yield

  yield() 方法是一个和 sleep() 方法有点相识的方法,它也是Thread 类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield() 只是让当前的线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了 yield() 方法暂停之后。线程调度器又将其调度出来重新执行。

  当某个线程调用了 yield() 方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行机会。

public class YieldTest {

    public static void main(String[] args) {
        YieldThread yt1 =new YieldThread("A");
        yt1.start();
        yt1.currentThread().setPriority(3);
        for (int i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

class YieldThread extends Thread{
    public YieldThread(String name){
        super(name);
    }
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(this.currentThread().getName() + " " + i);
            if(i%10==0){
                Thread.yield();
            }
        }
    }
}

 

运行结果: 

 当子线程调用 yield()方法让当前正在执行的线程暂停,并且给它的优先级改为3,它现在的优先级低于主线程,主线程 main 就会开始执行。

关于 sleep() 方法 和 yield() 方法的区别如下:

》sleep() 方法暂停当前线程后,会给其他线程机会,不会理会其他线程的优先级;但 yield() 方法只会给优先级相同,或优先级更高的线程执行机会。

》sleep() 方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而 yield() 方法不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用 yield()方法暂停后,立即再次获得处理器资源被执行。

》sleep() 方法声明抛出了 InterruptedException 异常,所以调用 sleep() 方法时要么捕捉该异常,要么显示声明抛出该异常;而 yield() 方法则没有声明抛出任何异常。

》sleep() 方法比 yield() 方法有更好的可移植性,通常不建议使用 yield() 方法来控制并发线程的执行。


线程同步:

  首先不说线程同步,让我们先用线程做一个去银行取钱的例子。

》首先创建一个类,模拟两个人使用同一个账户并发取钱
public class Account {

    private String accountNo;  //编号
    private double balance;  //账户余额
    
    public Account(){
        
    }
    public Account(String accountNo,double balance){
        this.accountNo = accountNo;
        this.balance = balance;
    }
   省略set和 get方法    
}

》接下来提供一个取钱的线程类,该线程类根据执行账户、取钱数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统吐出钞票,余额减少。

public class DrawThread extends Thread{

    private Account account;
    private double money;
    private String name;
    public DrawThread(String name,Account account, double money) {
        super(name);
        this.account = account;
        this.money = money;
    }

    @Override
    public void run() {
        if(account.getBalance() >= money){
            System.out.println(currentThread().getName() +"取钱成功!取出金额为:"+ money);
            account.setBalance(account.getBalance() - money);
            System.out.println("\\t剩余金额"+account.getBalance());
        }else{
            System.out.println(currentThread().getName() +"取钱失败!金额不足");
        }
    }
    
    public static void main(String[] args){
        Account account =new Account("123456",1000);
        new DrawThread("A",account,800).start();;
        new DrawThread("B",account,800).start();;
    }
}

多次运行上面程序,很有可能出现这种情况:

那么问题出现了:账户余额只有1000元时取出了1600元,而且账户余额出现了负数,这不是银行想要的结果。那么出现这种情况可以通过以下几种方式来解决,修改代码:


》》第一种:同步代码块

  为了解决上面代码中出现的问题,Java多线程支持引入同步监视器来解决这个问题,使用同步监视器通用方法就是同步代码块。

Java 程序允许使用任何对象来作为同步监视器,但想一下同步监视器的目的:阻止两条线程对同一个资源进行并发访问。因此推荐使用可能被并发访问共享资源充当同步监视器。

public class DrawThread extends Thread{

    private Account account;  //模拟用户账号
    private double money;    //需要取得钱数
    private String name;    //线程名    
    public DrawThread(String name,Account account, double money) {
        super(name);
        this.account = account;
        this.money = money;
    }

    @Override
    public void run() {
        //使用 account 作为同步监视器,任何线程进入下面代码块之前
        //必须先获得对 account 账户的锁定——其它线程无法获得锁,也就无法修改它了
        synchronized (account) {
            if(account.getBalance() >= money){
                System.out.print(currentThread().getName() +"取钱成功!取出金额为:"+ money);
                account.setBalance(account.getBalance() - money);
                System.out.println("\\t剩余金额"+account.getBalance());
            }else{
                System.out.println(currentThread().getName() +"取钱失败!金额不足");
            }
        }
    }
    
    public static void main(String[] args){
        Account account =new Account("123456",1000);
        new DrawThread("A",account,800).start();;
        new DrawThread("B",account,800).start();;
    }
}

  上面程序使用 synchronized 将 run 方法里方法体修改成同步代码块,该同步代码的同步监视器是 account 对象,这样符合“加锁-->修改完成-->释放锁”逻辑,任何线程想修改指定资源之前,首先要对资源加锁,在加锁期间其他线程无

法修改资源,当该线程修改完成,该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一天线程可以修改共享资源的代码区(也被称为临界区),所以同一时最多只有一条线程处于临界区内,从而保证了线程的安全。


》》第二种方法:同步方法

public class Account {

    private String accountNo;  //编号
    private double balance;  //账户余额

    public Account(){        
    }
    public Account(String accountNo,double balance){
        this.accountNo = accountNo;
        this.balance = balance;
    }
    
    //提供一个线程安全 drow 方法来完成取钱操作
    public synchronized void drow(double drowAmount){
        if(balance >= drowAmount){
            System.out.print(Thread.currentThread().getName() +"取钱成功!取出金额为:"+ drowAmount);
            //修改余额
            balance-=drowAmount;
            System.out.println("\\t剩余金额"+balance);
        }else{
            System.out.println(Thread.currentThread().getName() +"取钱失败!金额不足");
        }
    }
        
    //省率set和get方法    
}


public class DrawThread extends Thread{

    private Account account;  //模拟用户账号
    private double money;    //需要取得钱数
    private String name;    //线程名    
    public DrawThread(String name,Account account, double money) {
        super(name);
        this.account = account;
        this.money = money;
    }

    @Override
    public void run() {
        account.drow(money);
    }
    
    public static void main(String[] args){
        Account account =new Account("123456",1000);
        new DrawThread("A",account,800).start();;
        new DrawThread("B",account,800).start();;
    }
}

  上面程序中在账户类添加了一个代表取钱的方法,并使用 synchronized 关键字修饰该该方法,把该方法变成一个同步方法。同步方法的监视器是 this,因此对于同一个 Account 账户而言,任意时刻只能有一条线程获得对 Account对象的锁定,然后进入 drow() 方法执行取钱操作——这样就可以保证多线程并发取钱的线程安全。而在 run() 方法中只需调用该方法就可以了。

 

  

  

以上是关于Java——线程的主要内容,如果未能解决你的问题,请参考以下文章

Java工程师面试题,二级java刷题软件

java线程

Java——线程池

Java线程池详解

Java线程池详解

Java 线程池详解