java多线程

68 阅读15分钟

概述

进程:

所有运行中的任务通常对应一个进程(process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。

进程的三个特征:

1、独立性: 进程是系统中独立存在的实体,它可以用于自己独立的资源,每一个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。

2、动态性: 进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中都不具备。

3、并发性: 多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

并发性与并行性:

1、并发性:(concurrency) 指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

2、并行性:(parallel) 指同一时刻,有多条指令在多个处理器上同时执行。

线程:

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的钱不资源。因为多个线程共享父进程里的全部资源,因此编程更加方便,但必须更加小心,因为需要确保线程不会妨碍同一进程里的其他线程。

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

单线程:安全性高,但是效率低

多线程:安全性低,效率高

主线程:执行主方法(main方法)的线程

单线程程序:java程序中只有一个线程,执行从main方法开始,从上到下依次执行。

硬盘:永久存储 ,只读存储器(Read-Only Memory)ROM

内存:临时存储, 随机存取存储器(Random Access Memory) RAM

多线程的优势:

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

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

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

线程的创建和启动

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

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

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

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

public class FirstThreadDemo extends Thread {
    private int i;
    public void run(){
        for (;i<100;i++) {
            System.out.println(getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if(i == 20){
                new FirstThreadDemo().start();
                new FirstThreadDemo().start();
            }
        }
    }
}

注意:线程开启不一定立即执行,由cpu调度执行

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

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

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

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

public class SecondThreadDemo implements Runnable {
    private int i;
    @Override
    public void run() {
        for(;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if(i == 20){
                SecondThreadDemo threadDemo = new SecondThreadDemo();
                new Thread(threadDemo,"新线程1").start();
                new Thread(threadDemo,"新线程2").start();
            }
        }
    }
}

三、使用Callable来创建并启动线程的步骤:

1、创建Callable接口的实现类,并实现call()方法,该方法将作为线程执行体,且该call()方法有返回值,在创建Callable实现类的实例。

2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

3、使用FutureTask对象作为Thread对象的target创建并启动新线程。

4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

public class ThirdThreadDemo {
    public static void main(String[] args) {
        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>) () -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
            }
            return i;
        });
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
            if(i == 20){
                new Thread(task,"有返回值的线程").start();
            }
        }
        try{
            System.out.println("子线程的返回值:"+task.get());
        }catch(Exception e){
        	e.printStackTrace();
        }
    }
}

创建线程的三种方式对比

通过继承Thread类或实现Runnable、Callable接口都可以实现多线程,不过实现Runnable接口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable接口实现Callable接口归为一种方式,这种方式与继承Thread方式之间的主要差别如下

采用实现Runnable。Callable接口的方式创建多线程的优缺点:

1、线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

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

3、劣势是,编程稍微复杂,如果需要访问当前线程,则必须是呀Thread.currentThread()方法,

采用继承Thread类的方式创建多线程的优缺点:

1、劣势是,因为线程类已经继承了Thread类,所以不能在继承其他父类。

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

总结: 一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。

线程的声明周期

新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead) 5种状态。

线程的声明周期.JPG

新建:

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态。

就绪:

当线程对象调用了start()方法之后,该线程处于就绪状态。

运行:

如果处于就绪状态的线程获得了cpu资源,开始执行run()方法的线程执行体,则该线程处于运行状态。

阻塞:

当发生如下情况时,线程将会进入阻塞状态

1、线程调用sleep()方法主动放弃所占用的处理器资源。

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

3、线程视图获得一个同步监视器,但该同步监视器正被其他线程所持有。

4、线程在等待某个通知。

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

当发生如下情况时,可以解除阻塞,线程重新进入就绪状态

1、调用sleep()方法的线程经过了指定时间。

2.线程调用的阻塞式IO方法已经返回。

3、线程成功地获得了视图取得的同步监视器。

4、线程正在等待某个通知时,其他线程发出了一个通知。

5、处于挂起状态的线程被调用了resume()恢复方法。

死亡:

线程会如下三种方式结束,结束后处于死亡状态

1、run()或call()方法执行完成,线程正常结束。

2、线程抛出一个未捕获的Exception或Error。

3、直接调用该方法的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用、

**注意:**不要对处于死亡状态的线程调用start()方法,该程序只能对新建状态的线程调用start()方法,对新建状态的线程两次调用start()方法也是错误的,这都会引发IllegalThreadStateException异常。

控制线程

线程等待

让一个线程等待另一个线程完成,当在某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join方法加入的join线程执行完为止。

后台线程

在后台运行的线程,它的任务是为其他的线程提供服务,这种线程被称为后台线程(Daemon Thread),又称为“守护线程”或“精灵线程”。jvm的垃圾回收线程就是典型的后台线程。如果所有的前台线程都死亡了,后台线程会自动死亡。setDaemon(true)方法

前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。

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

线程睡眠

如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。在其睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行。

线程让步

就是调用Thread类的yield()方法让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。让系统的线程调度器重新调度一次。

改变线程优先级

每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会,每个线程默认的优先级都与创建它的父线程的优先级相同。Thread提供了setPriority(int newPriority)、getPriority()方法来设置和返回指定线程的优先级。

线程同步

线程安全

1、同步代码块

synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

synchronized(同步锁){ 
  需要同步操作的代码
}
//同步锁就是同步监视器,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
public class DrawThreadDemo extends Thread {
    private Account account;
    private double drawAmount;

    public DrawThreadDemo(String name, Account account, double drawAmount) {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }

    public void run() {
        synchronized (account) {
            if (account.getBalance() >= drawAmount) {
                System.out.println(this.getName() + "取钱成功!:" + drawAmount);
                try {
                    Thread.sleep(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                account.setBalance(account.getBalance() - drawAmount);
                System.out.println("\t 余额为:" + account.getBalance());
            } else {
                System.out.println(getName() + "取钱失败,余额不足");
            }
        }
    }
}

2、同步方法

使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

public synchronized void method(){ 
   可能会产生线程安全问题的代码
}
public class RunnableImpl implements Runnable{
    //定义一个多个线程共享的票源
    private static int ticket = 100;
    //设置线程任务:卖票
    @Override
    public void run() {        				System.out.println("this:"+this);//this:com.itheima.demo08.Synchronized.RunnableImpl@58ceff1
        //使用死循环,让卖票操作重复执行
        while(true){
            payTicketStatic();
        }
    }
    /*
        静态的同步方法
        锁对象是谁?
        不能是this
        this是创建对象之后产生的,静态方法优先于对象
        静态方法的锁对象是本类的class属性-->class文件对象(反射)
     */
    public static /*synchronized*/ void payTicketStatic(){
        synchronized (RunnableImpl.class){
            //先判断票是否存在
            if(ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
    /*
        定义一个同步方法
        同步方法也会把方法内部的代码锁住
        只让一个线程执行
        同步方法的锁对象是谁?
        就是实现类对象 new RunnableImpl()
        也是就是this
     */
    public /*synchronized*/ void payTicket(){
        synchronized (this){
            //先判断票是否存在
            if(ticket>0){
                //提高安全问题出现的概率,让程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,卖票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
                ticket--;
            }
        }
    }
}



public class Demo01Ticket {
    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        RunnableImpl run = new RunnableImpl();
        System.out.println("run:"+run);//run:com.itheima.demo08.Synchronized.RunnableImpl@58ceff1
        //创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        //调用start方法开启多线程
        t0.start();
        t1.start();
        t2.start();
    }
}

释放同步监视器的锁定

程序无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放同步监视器:

1、当线程的同步方法、同步代码块执行结束。

2、当前线程在同步代码块、同步方法中遇到bread、return终止了该代码块、该方法的继续执行。

3、当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时。

4、当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停。

3、同步锁(Lock)

在实现线程安全控制中,比较常用的是ReentrantLock(可重入锁)。使用该lock对象可以显示低加锁、释放锁。

通常建议使用finally块来确保在必要时释放锁。

//定义锁对象
private final ReentrantLock lock = new ReentrantLock()

4、死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,java虚拟机没有检测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现,一旦出现死锁,整个程序即不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

线程通信

1、传统的线程通信

对于使用synchronize修饰的同步方法,因为该类默认实例(this)就是同步监视器,所以可以在同步方法中直接调用这三个方法。

对于使用synchronize修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用者三个方法。

wait()

notity()

notityAll()

2、使用Condition控制线程通信

await()

signal()

signalAll()

3、使用阻塞队列控制线程通信

当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞,当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。

put() 放元素

take() 取元素

线程状态

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,

有几种状态呢?在API中 java.lang.Thread.State 这个枚举中给出了六种线程状态:

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
TimedWaiting(计时等待)同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

等待唤醒:线程之间的通信

void wait() 在其他线程调用此对象的 notify()方法或 notifyAll() 方法前,导致当前线程等待。

void notify() 唤醒在此对象监视器上等待的单个线程。

void nottifyAll() 唤醒在此对象监视器上等待的所有线程。

/*
    等待唤醒案例:线程之间的通信
        创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
        创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子

    注意:
        顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
        同步使用的锁对象必须保证唯一
        只有锁对象才能调用wait和notify方法

    Obejct类中的方法
    void wait()
          在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
    void notify()
          唤醒在此对象监视器上等待的单个线程。
          会继续执行wait方法之后的代码
 */
public class Demo01WaitAndNotify {
    public static void main(String[] args) {
        //创建锁对象,保证唯一
        Object obj = new Object();
        // 创建一个顾客线程(消费者)
        new Thread(){
            @Override
            public void run() {
               //一直等着买包子
               while(true){
                   //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                   synchronized (obj){
                       System.out.println("告知老板要的包子的种类和数量");
                       //调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
                       try {
                           obj.wait();
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                       //唤醒之后执行的代码
                       System.out.println("包子已经做好了,开吃!");
                       System.out.println("---------------------------------------");
                   }
               }
            }
        }.start();

        //创建一个老板线程(生产者)
        new Thread(){
            @Override
            public void run() {
                //一直做包子
                while (true){
                    //花了5秒做包子
                    try {
                        Thread.sleep(5000);//花5秒钟做包子
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                    synchronized (obj){
                        System.out.println("老板5秒钟之后做好包子,告知顾客,可以吃包子了");
                        //做好包子之后,调用notify方法,唤醒顾客吃包子
                        obj.notify();
                    }
                }
            }
        }.start();
    }
}

线程池

其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

线程池的使用步骤:

​ 1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池

​ 2.创建一个类,实现Runnable接口,重写run方法,设置线程任务

​ 3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法

​ 4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)

public class Demo01ThreadPool {
    public static void main(String[] args) {
        //1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
        es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行
        //线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
        es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行
        es.submit(new RunnableImpl());//pool-1-thread-2创建了一个新的线程执行

        //4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
        es.shutdown();

        es.submit(new RunnableImpl());//抛异常,线程池都没有了,就不能获取线程了
    }
}

public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"创建了一个新的线程执行");
    }
}