Java核心技术--多线程篇

4. 多线程

4.1 概述

4.1.1 线程和进程

进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

线程: 线程是进程中的一个执行单元,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

4.2 实现多线程的三种方式

4.2.1 继承Thread类

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

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程
public class threadTest extends Thread{
     

    //线程执行体
    @Override
    public void run() {
     
        for(int i = 1;i<=20;i++){
     
            System.out.println("在学习"+i);
        }
    }

    public static void main(String[] args) {
     
        new threadTest().start();
        for(int i = 1;i<=20;i++){
     
            System.out.println("在听歌"+i);
        }
    }
}
4.2.2 实现Runnable接口(最常用)

采用java.lang.Runnable 是非常常见的一种,我们只需要重写run方法即可。
步骤如下

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正
    的线程对象。
  3. 调用线程对象的start()方法来启动线程。
public class runnableTest implements Runnable {
     
    @Override
    public void run() {
     
        for(int i = 1;i<=20;i++){
     
            System.out.println(Thread.currentThread().getName()+"在学习"+i);
        }
    }

    public static void main(String[] args) {
     
        runnableTest rt = new runnableTest();
        new Thread(rt,"张三").start();
        new Thread(rt,"李四").start();

        for(int i = 1;i<=20;i++){
     
            System.out.println(Thread.currentThread().getName()+"在听歌"+i);
        }
    }
}
4.2.3 实现Callable接口

前面实现多线程的两种方式有一种很明显的缺点就是没有返回值

实现多线程的第三种方式: 实现Callable接口,重写call方法

public class CallableTest implements Callable {
     

    /**
     * 实现Callable接口必须重写call方法
     */
    @Override
    public Object call() throws Exception {
     
        String [] str= {
     "apple","pear","banana","orange","grape"};
        int i=(int)(Math.random()*5);
        return str[i];
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
     

        //创建任务
        CallableTest ct = new CallableTest();

        /**FutureTask同时实现了Runnable,Future接口。
         * 它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
         */
        //交付任务管理
        //可以看成FutureTask实现了runnable接口
        FutureTask<String> futureTask = new FutureTask<>(ct);
        Thread t=new Thread(futureTask);
        t.start();
        System.out.println("获取结果:"+futureTask.get());
        System.out.println("任务是否完成:"+futureTask.isDone());
    }
}

4.2.4 多线程执行的原理

以第一个程序为例:

程序启动运行main方法时,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用Thread子类对象的start方法时,另外一个新的线程也启动了,这样,整个应用就在多线程下运行。

Java核心技术--多线程篇_第1张图片

通过上面这张图我们可以很清晰的看到多线程的执行流程,但是为什么可以完成并发执行呢?多线程执行时,到底在内存中是如何运行的呢?
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间

4.2.5 内部类、Lambda表达式方式实现多线程的创建
public class innerClassLambdaTest{
     

    //静态内部类
    /*static class test implements Runnable{
        @Override
        public void run() {
            for(int i = 1;i<=20;i++){
                System.out.println(Thread.currentThread().getName()+"在学习"+i);
            }
        }
    }*/

    public static void main(String[] args) {
     
        
        //局部内部类
        /*
        class test implements Runnable{
            @Override
            public void run() {
                for(int i = 1;i<=20;i++){
                    System.out.println(Thread.currentThread().getName()+"在学习"+i);
                }
            }
        }
        
        new Thread(new test()).start();
        for(int i = 1;i<=20;i++){
            System.out.println(Thread.currentThread().getName()+"在听歌"+i);
        }*/

        //匿名内部类
        /*new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 1;i<=20;i++){
                    System.out.println("在学习"+i);
                }
            }
        }).start();*/

        //jdk1.8新特性 lambda表达式  只需要关注线程体,往往适用于单个简单线程
        new Thread(()->{
     
                for(int i = 1;i<=20;i++){
     
                    System.out.println("在学习"+i);
                }
            }
        ).start();


    }

}

4.2.6 Thread和Runnable的区别

4.3 线程池

4.3.1 概述

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

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。在Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。一般建议使用Executors工程类来创建线程池对象。

几种常见的线程池:

4.3.2 newCachedThreadPool

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。

4.3.3 newFixedThreadPool

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

4.3.4 newScheduledThreadPool

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); scheduledThreadPool.schedule(new Runnable(){
      
    @Override 
    public void run() {
      
        System.out.println("延迟三秒"); 
    } 
}, 3, TimeUnit.SECONDS); 
scheduledThreadPool.scheduleAtFixedRate(new Runnable(){
      
    @Override 
    public void run() {
      
        System.out.println("延迟1秒后每三秒执行一次"); 
    } 
},1,3,TimeUnit.SECONDS);
4.3.5 newSingleThreadExecutor

Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去

4.3.6 使用线程池创建线程的步骤

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象
  3. 提交Runnable接口子类对象
  4. 关闭线程池(一般不做)。
4.3.7 参考示例

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads)返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

使用线程池对象的方法如下:

  • public Future submit(Runnable task)获取线程池中的某一个线程对象,并执行

    Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

/**
 * 使用线程池的方式创建线程
 */
public class ThreadPool implements Runnable {
     

    @Override
    public void run() {
     
        System.out.println("申请使用小黄车 ");
        try {
     
            //模拟出票操作
            Thread.sleep(1000);
        } catch (InterruptedException e) {
     
            e.printStackTrace();
        }
        System.out.println("成功使用到小黄车"+Thread.currentThread().getName());
        System.out.println("将小黄车归还");
    }
}

class ThreadPoolTest{
     
    public static void main(String[] args) {
     

        //创建一个线程池,里面包含的线程对象最大为2个
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        ThreadPool th = new ThreadPool();

        //从线程池中获取线程并执行
        /**
         * submit()方法执行完后,程序并不会终止,因为线程池控制了线程的关闭
         * submit()方法执行完后就直接将线程归还给了线程池
         */
        executorService.submit(th);
        executorService.submit(th);

        //将线程池关闭
        //executorService.shutdown();


    }
}

4.4 线程安全

问题引入:模拟卖票

public class SellTickets implements Runnable{
     

    private int tickets = 100;

    @Override
    public void run() {
     
        while(true){
     
            if(tickets>0){
     
                //使用睡眠时间模拟一下出票操作
                try {
     
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
                //获取当前对象的名称
                String name = Thread.currentThread().getName();
                System.out.println(name+"正在卖"+(tickets--)+"号票");
            }
        }
    }
}

//测试类
public class SellTicketsTest {
     

    public static void main(String[] args) {
     

        new Thread(new SellTickets(),"窗口1").start();
        new Thread(new SellTickets(),"窗口2").start();
        new Thread(new SellTickets(),"窗口3").start();
    }
}


运行结果部分截图:

Java核心技术--多线程篇_第2张图片

显而易见:相同号码的票被重复售卖,这是绝对不允许出现的情况 。这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写
操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,
否则的话就可能影响线程安全。

4.4.1 线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了**同步机制(synchronized)**来解决。

解决思想:

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

4.4.2 实现同步机制的三种方式
4.4.2.1 同步代码块

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

格式:

synchronized(同步锁){
     
    需要同步操作的代码
}

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等(BLOCKED)。

参考示例:

/**
 * 同步代码块实现线程同步问题
 */
public class SyncCodeBlock implements Runnable{
     

    private int tickets = 10;

    /**
     *  创建锁对象
     */
    Object lock = new Object();

    @Override
    public void run() {
     
        while (true){
     
            synchronized (lock){
     
                if(tickets>0){
     
                    try {
     
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName()+"正在卖"+(tickets--)+"号票");
                    } catch (InterruptedException e) {
     
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

/**
 * 测试类
 */
class SyncCodeBlockTest{
     
    public static void main(String[] args) {
     

        SyncCodeBlock syn = new SyncCodeBlock();
        Thread t1 = new Thread(syn,"窗口1");
        Thread t2 = new Thread(syn,"窗口2");
        Thread t3 = new Thread(syn,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
4.4.2.2 同步方法

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

格式:

public synchronized void method(){
     
    需要同步操作的代码
}

同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

参考示例:

/**
 * 同步方法实现同步操作
 */
public class SyncCodeMethod implements Runnable {
     

    private int tickets = 10;

    @Override
    public void run() {
     
        //调用同步方法
        this.sellTickets();
    }

    /**
     * 需要进行同步操作的代码
     */
    public synchronized void sellTickets(){
     
        while (true){
     
            if(tickets>0){
     
                try {
     
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName()+"正在卖"+(tickets--)+"号票");
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }
    }
}

/**
 * 测试类
 */
class SyncCodeMethodTest{
     
    public static void main(String[] args) {
     

        SyncCodeMethod syn = new SyncCodeMethod();
        new Thread(syn,"窗口1").start();
        new Thread(syn,"窗口2").start();
        new Thread(syn,"窗口3").start();


    }
}
4.4.2.3 锁机制

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,
同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。Lock锁也称同步锁,同步锁的加锁和释放锁的方法

返回值 方法 含义
void lock() 加同步锁
void unlock() 释放同步锁

参考示例:

/**
 * 同步锁实现同步操作
 */
public class SyncLock implements Runnable {
     

    private int tickets = 10;

    /**
     *创建锁对象
     */
    Lock lock = new ReentrantLock();

    @Override
    public void run() {
     
        while(true){
     

            //加锁
            lock.lock();

            if(tickets>0){
     
                try {
     
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName()+"正在卖"+(tickets--)+"号票");
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }finally {
     
                    //释放锁
                    lock.unlock();
                }
            }
        }
    }
}

class SyncLockTest{
     
    public static void main(String[] args) {
     
        SyncLock sync = new SyncLock();
        new Thread(sync,"窗口1").start();
        new Thread(sync,"窗口2").start();
        new Thread(sync,"窗口3").start();
    }
}

4.5 线程状态

4.5.1 概述

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

线程状态 描述
NEW(新建状态) 线程刚被创建,但是并未启动。还没调用start方法
RUNNABLE(就绪状态) 调用start方法,在JVM中运行的状态
RUNNING(运行状态) 获得了CPU,开始执行run()方法的线程执行体
BLOCKED(阻塞状态) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态
WAITING(无限等待状态) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒
TIMED_WAITING(休眠状态) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait
TERMINATED(死亡状态) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡

线程状态转换图

Java核心技术--多线程篇_第3张图片

4.5.2 TIMED_WAITING(计时等待)

以卖票操作为例:

@Override
    public void run() {
     
        while (true){
     
            synchronized (lock){
     
                if(tickets>0){
     
                    try {
     
                        Thread.sleep(1000);  //线程睡眠1秒
                        System.out.println(Thread.currentThread().getName()+"正在卖"+(tickets--)+"号票");
                    } catch (InterruptedException e) {
     
                        e.printStackTrace();
                    }
                }
            }
        }
    }

注意事项:

  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协
    作关系。
  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程
    中会睡眠
  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
4.5.3 WAITING(无限等待状态)
public class WaitAndNotify {
     
    public static void main(String[] args) {
     
        Object lock = new Object();

        //模拟生产者消费者

        //创建一个消费者线程
        new Thread(new Runnable() {
     
            @Override
            public void run() {
     
                while (true){
     
                    System.out.println("消费者要购买东西A!");
                    synchronized (lock){
     
                        try {
     
                            //等待生产者生产
                            lock.wait();
                        } catch (InterruptedException e) {
     
                            e.printStackTrace();
                        }
                    }
                    System.out.println("生产者已经生产好A");
                    System.out.println("-------------------");
                }
            }
        }).start();

        //创建一个生产者线程
        new Thread(new Runnable() {
     
            @Override
            public void run() {
     
                while (true){
     
                    System.out.println("生产者正在生产A");
                    synchronized (lock){
     
                        //生产A需要5秒
                        try {
     
                            Thread.sleep(5000);
                        } catch (InterruptedException e) {
     
                            e.printStackTrace();
                        }
                        //唤醒消费者,告知产品已经做好
                        lock.notify();
                    }
                }
            }
        }).start();

    }
}

由上可知:一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的Object.notify()方法 或 Object.notifyAll()方法。waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
  2. wait方法与notify方法是属于Object类的方法。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

你可能感兴趣的:(JavaSE,java,多线程)