第九章 多线程

线程概述

什么是进程?什么是线程?它们的区别?
进程是指操作系统中的一段程序,它是一个正在执行中的程序实例,具有独立的内存空间和系统资源,如文件、网络端口等。在计算机程序执行时,先创建进程,再在进程中进行程序的执行。一般来说,一个进程可以包含多个线程。
线程是指进程中的一个执行单元,是进程的一部分,它负责在进程中执行程序代码。每个线程都有自己的栈和程序计数器,并且可以共享进程的资源。多个线程可以在同一时刻执行不同的操作,从而提高了程序的执行效率。
现代的操作系统是支持多进程的,也就是可以启动多个软件,一个软件就是一个进程。称为:多进程并发。
通常一个进程都是可以启动多个线程的。称为:多线程并发。
多线程的作用?
提高处理效率。(多线程的优点之一是能够使 CPU 在处理一个任务时同时处理多个线程,这样可以充分利用 CPU 的资源,提高 CPU 的利用效率。)
JVM规范中规定:
堆内存、方法区 是线程共享的。
虚拟机栈、本地方法栈、程序计数器 是每个线程私有的。
关于Java程序的运行原理
“java HelloWorld”执行后,会启动JVM,JVM的启动表示一个进程启动了。
JVM进程会首先启动一个主线程(main-thread),主线程负责调用main方法。因此main方法是在主线程中运行的。
除了主线程之外,还启动了一个垃圾回收线程。因此启动JVM,至少启动了两个线程。
在main方法的执行过程中,程序员可以手动创建其他线程对象并启动。

并发与并行

并发

使用单核CPU的时候,同一时刻只能有一条指令执行,但多个指令被快速的轮换执行,使得在宏观上具有多个指令同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干端,使多个指令快速交替的执行。


如上图所示,假设只有一个CPU资源,线程之间要竞争得到执行机会。图中的第一个阶段,在A执行的过程中,B、C不会执行,因为这段时间内这个CPU资源被A竞争到了,同理,第二阶段只有B在执行,第三阶段只有C在执行。其实,并发过程中,A、B、C并不是同时进行的(微观角度),但又是同时进行的(宏观角度)。
在同一个时间点上,一个CPU只能支持一个线程在执行。因为CPU运行的速度很快,CPU使用抢占式调度模式在多个线程间进行着高速的切换,因此我们看起来的感觉就像是多线程一样,也就是看上去就是在同一时刻运行。

并行

使用多核CPU的时候,同一时刻,有多条指令在多个CPU上同时执行。


如图所示,在同一时刻,ABC都是同时执行(微观、宏观)。

并发编程与并行编程

在CPU比较繁忙(假设为单核CPU),如果开启了很多个线程,则只能为一个线程分配仅有的CPU资源,
这些线程就会为自己尽量多抢时间片,这就是通过多线程实现并发,线程之间会竞争CPU资源争取执行机会。
在CPU资源比较充足的时候,一个进程内的多个线程,可以被分配到不同的CPU资源,这就是通过多线程实现并行。 
至于多线程实现的是并发还是并行?上面所说,所写多线程可能被分配到一个CPU内核中执行,也可能被分配到不同CPU执行,分配过程是操作系统所为,不可人为控制。所以,如果有人问我我所写的多线程是并发还是并行的?我会说,都有可能。 
总结:单核CPU上的多线程,只是由操作系统来完成多任务间对CPU的运行切换,并非真正意义上的并发。随着多核CPU的出现,也就意味着不同的线程能被不同的CPU核得到真正意义的并行执行,故而多线程技术得到广泛应用。
不管并发还是并行,都提高了程序对CPU资源的利用率,最大限度地利用CPU资源,而我们使用多线程的目的就是为了提高CPU资源的利用率。

线程的调度策略

线程的调度模型

  1. 如果多个线程被分配到一个CPU内核中执行,则同一时刻只能允许有一个线程能获得CPU的执行权,那么进程中的多个线程就会抢夺CPU的执行权,这就是涉及到线程调度策略。
  2. 分时调度模型
  • 所有线程轮流使用CPU的执行权,并且平均的分配每个线程占用的CPU的时间。
  1. 抢占式调度模型
  • 让优先级高的线程以较大的概率优先获得CPU的执行权,如果线程的优先级相同,那么就会随机选择一个线程获得CPU的执行权,而Java采用的就是抢占式调用。

实现线程

第一种方式

  1. 编写一个类继承Thread,重写run方法。
  2. 创建线程对象:Thread t = new MyThread();
  3. 启动线程:t.start();
/**
 * 在Java语言中,实现线程,有两种方式,第一种方式:
 *      第一步:编写一个类继承 java.lang.Thread
 *      第二步:重写run方法
 *      第三步:new线程对象
 *      第四部:调用线程对象的start方法来启动线程。
 */
public class ThreadTest02 {
    public static void main(String[] args) {
        // 创建线程对象
        //MyThread mt = new MyThread();
        Thread t = new MyThread();

        // 直接调用run方法,不会启动新的线程。
        // java中有一个语法规则:对于方法体当中的代码,必须遵循自上而下的顺序依次逐行执行。
        // run()方法不结束,main方法是无法继续执行的。
        //t.run();

        // 调用start()方法,启动线程
        // java中有一个语法规则:对于方法体当中的代码,必须遵循自上而下的顺序依次逐行执行。
        // start()方法不结束,main方法是无法继续执行的。
        // start()瞬间就会结束,原因这个方法的作用是:启动一个新的线程,只要新线程启动成功了,start()就结束了。
        t.start();

        // 这里编写的代码在main方法中,因此这里的代码属于在主线程中执行。
        for (int i = 0; i < 100; i++) {
            System.out.println("main--->" + i);
        }
    }
}

// 自定义一个线程类
// java.lang.Thread本身就是一个线程。
// MyThread继承Thread,因此MyThread本身也是一个线程。
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("MyThread--->" + i);
        }
    }
}

调用run方法内存图

调用start方法内存图

第二种方式

  1. 编写一个类实现Runnable接口,实现run方法。
  2. 创建线程对象:Thread t = new Thread(new MyRunnable());
  3. 启动线程:t.start();

优先选择第二种方式:因为实现接口的同时,保留了类的继承。

第二种方式也可以使用匿名内部类。

/**
 * 在Java语言中,实现线程,有两种方式,第二种方式:
 *      第一步:编写一个类实现 java.lang.Runnable接口(可运行的接口)
 *      第二步:实现接口中的run方法。
 *      第三步:new线程对象
 *      第四部:调用线程的start方法启动线程
 *
 * 总结:实现线程两种方式:
 *      第一种:编写类直接继承Thread
 *      第二种:编写类实现Runnable接口
 *
 *      推荐使用第二种,因为实现接口的同时,保留了类的继承。
 */
public class ThreadTest03 {
    public static void main(String[] args) {
        // 创建Runnable对象
        //Runnable r = new MyRunnable();
        // 创建线程对象
        //Thread t = new Thread(r);

        // 创建线程对象
        Thread t = new Thread(new MyRunnable());

        // 启动线程
        t.start();

        // 主线程中执行的。
        for (int i = 0; i < 100; i++) {
            System.out.println("main----->" + i);
        }
    }
}

// 严格来说,这个不是一个线程类
// 它是一个普通的类,只不过实现了一个Runnable接口。
class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("t----->" + i);
        }
    }
}

匿名内部类方式实现

public class ThreadTest04 {
    public static void main(String[] args) {
        /*// 创建线程对象
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("t ---> " + i);
                }
            }
        });

        // 启动线程
        t.start();*/

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("t ---> " + i);
                }
            }
        }).start();

        for (int i = 0; i < 100; i++) {
            System.out.println("main ---> " + i);
        }
    }
}

第三种方式

实现Callable接口,实现call方法。

/**
 * 实现线程的第三种方式:实现Callable接口,实现call方法。
 * 这种方式实现的线程,是可以获取到线程返回值的。
 */
public class ThreadTest {
    public static void main(String[] args) {
        // 创建“未来任务”对象
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                // 处理业务......
                Thread.sleep(1000 * 5);
                return 1;
            }
        });

        // 创建线程对象
        Thread t = new Thread(task);
        t.setName("t");

        // 启动线程
        t.start();

        try {
            // 获取“未来任务”线程的返回值
            // 阻塞当前线程,等待“未来任务”结束并返回值。
            // 拿到返回值,当前线程的阻塞才会解除。继续执行。
            Integer i = task.get();
            System.out.println(i);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

第四种方式

线程池技术

/**
 * 创建线程的第四种方式:使用线程池技术。
 * 线程池本质上就是一个缓存:cache
 * 一般都是服务器在启动的时候,初始化线程池,
 * 也就是说服务器在启动的时候,创建N多个线程对象,
 * 直接放到线程池中,需要使用线程对象的时候,直接从线程池中获取。
 */
public class ThreadTest {
    public static void main(String[] args) {

        // 创建一个线程池对象(线程池中有3个线程)
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 将任务交给线程池(你不需要触碰到这个线程对象,你只需要将要处理的任务交给线程池即可。)
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + "--->" + i);
                }
            }
        });

        // 最后记得关闭线程池
        executorService.shutdown();
    }
}

t.start()和t.run()的本质区别?

  • 本质上没有区别,都是普通方法调用。只不过两个方法完成的任务不同。
  • t.run()是调用run方法。执行run方法中的业务代码。
  • t.start()是启动线程,只要线程启动了,start()方法就执行结束了。

线程常用三个方法

  • 实例方法:
    • String getName(); //获取线程名字
    • void setName(String name);//给线程起名字
  • 静态方法:static Thread currentThread();//获取当前线程

线程生命周期的七大状态

  • 线程生命周期指的是:从线程对象新建,到最终线程死亡的整个过程。
  • 线程生命周期包括七个重要阶段:
    • 新建状态(NEW)
    • 就绪状态(RUNNABLE)
    • 运行状态(RUNNABLE)
    • 超时等待状态(TIMED_WAITING)
    • 等待状态(WAITING)
    • 阻塞状态(BLOCKED)
    • 死亡状态(TERMINATED)

关于sleep

/**
 * 关于线程的sleep方法:
 *      1. static void sleep(long millis)
 *          静态方法,没有返回值,参数是一个毫秒。1秒 = 1000毫秒
 *      2. 这个方法作用是:
 *          让当前线程进入休眠,也就是让当前线程放弃占有的CPU时间片,让其进入阻塞状态。
 *          意思:你别再占用CPU了,让给其他线程吧。
 *          阻塞多久呢?参数毫秒为准。在指定的时间范围内,当前线程没有权利抢夺CPU时间片了。
 *      3. 怎么理解“当前线程”呢?
 *          Thread.sleep(1000); 这个代码出现在哪个线程中,当前线程就是这个线程。
 *      4. run方法在方法重写的时候,不能在方法声明位置使用 throws 抛出异常。
 *      5. sleep方法可以模拟每隔固定的时间调用一次程序。
 */
public class ThreadTest {
    public static void main(String[] args) {
        try {
            // 让当前线程睡眠5秒
            // 这段代码出现在主线程中,所以当前线程就是主线程
            // 让主线程睡眠5秒
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "===>" + i);
        }

        // 启动线程
        Thread t = new Thread(new MyRunnable());
        t.setName("t");
        t.start();
    }
}

class MyRunnable implements Runnable {

    @Override
    public void run(){
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "===>" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

面试题

public class ThreadTest {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.setName("t");
        t.start();

        try {
            // 这行代码并不是让t线程睡眠,而是让当前线程睡眠。
            // 当前线程是main线程。
            t.sleep(1000 * 5); // 等同于:Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "===>" + i);
        }
    }
}

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

中断线程睡眠

/**
 * 怎么中断一个线程的睡眠。(怎么解除线程因sleep导致的阻塞,让其开始抢夺CPU时间片。)
 */
public class ThreadTest {
    public static void main(String[] args) {
        // 创建线程对象并启动
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "===> begin");
                try {
                    // 睡眠一年
                    Thread.sleep(1000 * 60 * 60 * 24 * 365);
                } catch (InterruptedException e) {
                    // 打印异常信息
                    //e.printStackTrace();
                    System.out.println("知道了,这就起床!");
                }
                // 睡眠一年之后,起来干活了
                System.out.println(Thread.currentThread().getName() + " do some!");
            }
        });

        // 启动线程
        t.start();

        // 主线程
        // 要求:5秒之后,睡眠的Thread-0线程起来干活
        try {
            Thread.sleep(5 * 1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        // Thread-0起来干活了。
        // 这行代码的作用是终止 t 线程的睡眠。
        // interrupt方法是一个实例方法。
        // 以下代码含义:t线程别睡了。
        // 底层实现原理是利用了:异常处理机制。
        // 当调用这个方法的时候,如果t线程正在睡眠,必然会抛出:InterruptedException,然后捕捉异常,终止睡眠。
        t.interrupt();

    }
}

线程的终止

/**
 * 一个线程 t 一直在正常的运行,如何终止 t 线程的执行!!!!
 */
public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.setName("t");
        t.start();

        // 5秒之后,t线程停止!
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        // 终止线程t的执行。
        // 从java2开始就不建议使用了,因为这种方式是强行终止线程。容易导致数据丢失。
        // 没有保存的数据,在内存中的数据一定会因为此方式导致丢失。
        t.stop();
    }
}

class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "===>" + i);
        }
    }
}

这种方式会导致数据丢失,不建议使用

停止线程的合适方法

/**
 * 如何合理的,正常的方式终止一个线程的执行?
 *      一般我们在实际开发中会使用打标记的方式,来终止一个线程的执行。
 */
public class ThreadTest {
    public static void main(String[] args) {
        // 创建线程
        MyRunnable mr = new MyRunnable();
        Thread t = new Thread(mr);
        t.setName("t");
        // 启动线程
        t.start();

        // 5秒之后终止线程t的执行
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //终止线程t的执行。
        mr.run = false;
    }
}

class MyRunnable implements Runnable {
    /**
     * 是否继续执行的标记。
     * true表示:继续执行。
     * false表示:停止执行。
     */
    boolean run = true;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if(run){
                System.out.println(Thread.currentThread().getName() + "==>" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }else{
                return;
            }
        }
    }
}

守护线程

/**
 * 1. 在Java语言中,线程被分为两大类:
 *      第一类:用户线程(非守护线程)
 *      第二类:守护线程(后台线程)
 *
 * 2. 在JVM当中,有一个隐藏的守护线程一直在守护者,它就是GC线程。
 *
 * 3. 守护线程的特点:所有的用户线程结束之后,守护线程自动退出/结束。
 *
 * 4. 如何将一个线程设置为守护线程?
 *      t.setDaemon(true);
 */
public class ThreadTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.setName("t");

        // 在启动线程之前,设置线程为守护线程
        myThread.setDaemon(true);

        myThread.start();

        // 10s结束!
        // main线程中,main线程是一个用户线程。
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "===>" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        int i = 0;
        while(true){
            System.out.println(Thread.currentThread().getName() + "===>" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

定时任务

/**
 * 1. JDK中提供的定时任务:
 *      java.util.Timer         定时器
 *      java.util.TimerTask     定时任务
 * 2. 定时器 + 定时任务:可以帮我们在程序中完成:每间隔多久执行一次某段程序。
 * 3. Timer的构造方法:
 *      Timer()
 *      Timer(boolean isDaemon) isDaemon是true表示该定时器是一个守护线程。
 */
public class ThreadTest {
    public static void main(String[] args) throws Exception{
        // 创建定时器对象(本质上就是一个线程)
        // 如果这个定时器执行的任务是一个后台任务,是一个守护任务,建议将其定义为守护线程。
        Timer timer = new Timer(true);

        // 指定定时任务
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime = sdf.parse("2024-01-27 10:22:00");
        //timer.schedule(new LogTimerTask(), firstTime, 1000);


        // 匿名内部类的方式
        timer.schedule(new TimerTask() {
            int count = 0;
            @Override
            public void run() {
                // 执行任务
                Date now = new Date();
                String strTime = sdf.format(now);
                System.out.println(strTime + ": " + count++);
            }
        }, firstTime, 1000 * 5);


        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
        }
    }
}
/**
 * 定时任务类:专门记录日期的定时任务类。
 */
public class LogTimerTask extends TimerTask {

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
    int count = 0;

    @Override
    public void run() {
        // 执行任务
        Date now = new Date();
        String strTime = sdf.format(now);
        System.out.println(strTime + ": " + count++);
    }

}

join方法

/**
 * 线程合并
 *      1. 调用join()方法完成线程合并。
 *
 *      2. join()方法是一个实例方法。(不是静态方法) t.join
 *
 *      3. 假设在main方法(main线程)中调用了 t.join(),后果是什么?
 *          t线程合并到主线程中。主线程进入阻塞状态。直到 t 线程执行结束。主线程阻塞解除。
 *
 *      4. t.join()方法其实是让当前线程进入阻塞状态,直到t线程结束,当前线程阻塞解除。
 *
 *      5. 和sleep方法有点类似,但不一样:
 *          第一:sleep方法是静态方法,join是实例方法。
 *          第二:sleep方法可以指定睡眠的时长,join方法不能保证阻塞的时长。
 *          第三:sleep和join方法都是让当前线程进入阻塞状态。
 *          第四:sleep方法的阻塞解除条件?时间过去了。 join方法的阻塞解除条件?调用join方法的那个线程结束了。
 */
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.setName("t");
        t.start();

        System.out.println("main begin");

        // 合并线程
        // t合并到main线程中。
        // main线程受到阻塞(当前线程受到阻塞)
        // t线程继续执行,直到t线程结束。main线程阻塞解除(当前线程阻塞解除)。
        //t.join();

        // join方法也可以有参数,参数是毫秒。
        // 以下代码表示 t 线程合并到 当前线程,合并时长 10 毫秒
        // 阻塞当前线程 10 毫秒
        //t.join(10);

        // 调用这个方法,是想让当前线程受阻10秒
        // 但不一定,如果在指定的阻塞时间内,t线程结束了。当前线程阻塞也会解除。
        t.join(1000 * 10);

        // 当前线程休眠10秒。
        //Thread.sleep(1000 * 10);

        // 主线程
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "==>" + i);
        }

        System.out.println("main over");
    }
}

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

线程优先级

/**
 * 关于线程生命周期中的JVM调度:
 *      1. 优先级
 *      2. 线程是可以设置优先级的,优先级较高的,获得CPU时间片的总体概率高一些。
 *      3. JVM采用的是抢占式调度模型。谁的优先级高,获取CPU时间片的总体概率就高。
 *      4. 默认情况下,一个线程的优先级是 5.
 *      5. 最低是1,最高是10.
 */
public class ThreadTest {
    public static void main(String[] args) {
        /*System.out.println("最低优先级:" + Thread.MIN_PRIORITY);
        System.out.println("最高优先级:" + Thread.MAX_PRIORITY);
        System.out.println("默认优先级:" + Thread.NORM_PRIORITY);

        // 获取main线程的优先级
        Thread mainThread = Thread.currentThread();

        System.out.println("main线程的优先级:" + mainThread.getPriority()); // 5

        // 设置优先级
        mainThread.setPriority(Thread.MAX_PRIORITY);

        System.out.println("main线程的优先级:" + mainThread.getPriority()); // 10*/

        // 创建两个线程
        Thread t1 = new MyThread();
        t1.setName("t1");

        Thread t2 = new MyThread();
        t2.setName("t2");

        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MIN_PRIORITY);

        t1.start();
        t2.start();
    }
}

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

线程让位(yield方法)

/**
 * 关于JVM的调度:
 *      1. 让位
 *      2. 静态方法:Thread.yield()
 *      3. 让当前线程让位。
 *      4. 注意:让位不会让其进入阻塞状态。只是放弃目前占有的CPU时间片,进入就绪状态,继续抢夺CPU时间片。
 *      5. 只能保证大方向上的,大概率,到了某个点让位一次。
 */
public class ThreadTest {
    public static void main(String[] args) {
        Thread t1 = new MyThread();
        t1.setName("t1");

        Thread t2 = new MyThread();
        t2.setName("t2");

        t1.start();
        t2.start();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            if(Thread.currentThread().getName().equals("t1") && i % 10 == 0){
                System.out.println(Thread.currentThread().getName() + "让位了,此时的i下标是:" + i);
                // 当前线程让位,这个当前线程一定是t1
                // t1会让位一次
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName() + "==>" + i);
        }
    }
}

线程安全问题

/**
 * 线程安全专题:
 *      1. 什么情况下需要考虑线程安全问题?
 *          条件1:多线程的并发环境下
 *          条件2:有共享的数据
 *          条件3:共享数据涉及到修改的操作
 *
 *      2. 一般情况下:
 *          局部变量不存在线程安全问题。(尤其是基本数据类型不存在线程安全问题【在栈中,栈不是共享的】,如果是引用数据类型,就另说了!)
 *          实例变量可能存在线程安全问题。实例变量在堆中。堆是多线程共享的。
 *          静态变量也可能存在线程安全问题。静态变量在堆中。堆是多线程共享的。
 *
 *      3. 大家找一个现实生活中的例子,来说明一下,线程安全问题:比如同时取钱!
 *
 *      4. 以上多线程并发对同一个账户进行取款操作的时候,有安全问题?怎么解决?
 *          让线程t1和线程t2排队执行。不要并发。要排队。
 *          我们把线程排队执行,叫做:线程同步机制。(t1和t2线程,t1线程在执行的时候必须等待t2线程执行到某个位置之后,t1线程才能执行。只要t1和t2之间发生了等待,就认为是同步。)
 *          如果不排队,我们将其称为:线程异步机制。(t1和t2各自执行各自的,谁也不需要等对方。并发的,就认为是异步)
 *          异步:效率高。但是可能存在安全隐患。
 *          同步:效率低。排队了。可以保证数据的安全问题。
 *
 *      5. 以下程序存在安全问题。t1和t2线程同时对act一个账号进行取款操作。数据是错误的。
 */

线程同步

/**
 * 使用线程同步机制,来保证多线程并发环境下的数据安全问题:
 *      1. 线程同步的本质是:线程排队执行就是同步机制。
 *      2. 语法格式:
 *          synchronized(必须是需要排队的这几个线程共享的对象){
 *              // 需要同步的代码
 *          }
 *
 *          “必须是需要排队的这几个线程共享的对象” 这个必须选对了。
 *          这个如果选错了,可能会无故增加同步的线程数量,导致效率降低。
 *      3. 原理是什么?
 *          synchronized(obj){
 *              // 同步代码块
 *          }
 *          假设obj是t1 t2两个线程共享的。
 *          t1和t2执行这个代码的时候,一定是有一个先抢到了CPU时间片。一定是有先后顺序的。
 *          假设t1先抢到了CPU时间片。t1线程找共享对象obj的对象锁,找到之后,则占有这把锁。只要能够占有obj对象的对象锁,就有权利进入同步代码块执行代码。
 *          当t1线程执行完同步代码块之后,会释放之前占有的对象锁(归还锁)。
 *          同样,t2线程抢到CPU时间片之后,也开始执行,也会去找共享对象obj的对象锁,但由于t1线程占有这把锁,t2线程只能在同步代码块之外等待。
 *
 *      4. 注意同步代码块的范围,不要无故扩大同步的范围,同步代码块范围越小,效率越高。
 */
/**
 * 在实例方法上也可以添加 synchronized 关键字:
 *      1. 在实例方法上添加了synchronized关键字之后,整个方法体是一个同步代码块。
 *      2. 在实例方法上添加了synchronized关键字之后,共享对象的对象锁一定是this的。
 *
 * 这种方式相对于之前所讲的局部同步代码块的方式要差一些:
 *      synchronized(共享对象){
 *          // 同步代码块
 *      }
 *
 *      这种方式优点:灵活
 *          共享对象可以随便调整。
 *          同步代码块的范围可以随便调整。
 */

synchronized面试题

/**
 * 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
 *      不需要。
 */
public class ThreadTest {
    public static void main(String[] args) {
        MyClass mc = new MyClass();
        Thread t1 = new Thread(new MyRunnable(mc));
        Thread t2 = new Thread(new MyRunnable(mc));

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t2.start();
    }
}

class MyRunnable implements Runnable {

    private MyClass mc;

    public MyRunnable(MyClass mc) {
        this.mc = mc;
    }

    @Override
    public void run() {
        if("t1".equals(Thread.currentThread().getName())){
            mc.m1();
        }
        if("t2".equals(Thread.currentThread().getName())){
            mc.m2();
        }
    }
}

class MyClass {
    public synchronized void m1(){
        System.out.println("m1 begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("m1 over");
    }

    public void m2(){
        System.out.println("m2 begin");
        System.out.println("m2 over");
    }
}

/**
 * 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
 *      需要
 */
public class ThreadTest {
    public static void main(String[] args) {
        MyClass mc = new MyClass();
        Thread t1 = new Thread(new MyRunnable(mc));
        Thread t2 = new Thread(new MyRunnable(mc));

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t2.start();
    }
}

class MyRunnable implements Runnable {

    private MyClass mc;

    public MyRunnable(MyClass mc) {
        this.mc = mc;
    }

    @Override
    public void run() {
        if("t1".equals(Thread.currentThread().getName())){
            mc.m1();
        }
        if("t2".equals(Thread.currentThread().getName())){
            mc.m2();
        }
    }
}

class MyClass {
    public synchronized void m1(){
        System.out.println("m1 begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("m1 over");
    }

    public synchronized void m2(){
        System.out.println("m2 begin");
        System.out.println("m2 over");
    }
}

/**
 * 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
 *      不需要
 */
public class ThreadTest {
    public static void main(String[] args) {
        MyClass mc1 = new MyClass();
        MyClass mc2 = new MyClass();
        Thread t1 = new Thread(new MyRunnable(mc1));
        Thread t2 = new Thread(new MyRunnable(mc2));

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t2.start();
    }
}

class MyRunnable implements Runnable {

    private MyClass mc;

    public MyRunnable(MyClass mc) {
        this.mc = mc;
    }

    @Override
    public void run() {
        if("t1".equals(Thread.currentThread().getName())){
            mc.m1();
        }
        if("t2".equals(Thread.currentThread().getName())){
            mc.m2();
        }
    }
}

class MyClass {
    public synchronized void m1(){
        System.out.println("m1 begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("m1 over");
    }

    public synchronized void m2(){
        System.out.println("m2 begin");
        System.out.println("m2 over");
    }
}

/**
 * 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
 *      需要等待。
 *
 * 在静态方法上添加synchronized之后,线程会占有类锁。
 * 类锁是,对于一个类来说,只有一把锁。不管创建了多少个对象,类锁只有一把。
 *
 * 静态方法上添加synchronized,实际上是为了保证静态变量的安全。
 * 实例方法上添加synchronized,实际上是为了保证实例变量的安全。
 */
public class ThreadTest {
    public static void main(String[] args) {
        MyClass mc1 = new MyClass();
        MyClass mc2 = new MyClass();
        Thread t1 = new Thread(new MyRunnable(mc1));
        Thread t2 = new Thread(new MyRunnable(mc2));

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        t2.start();
    }
}

class MyRunnable implements Runnable {

    private MyClass mc;

    public MyRunnable(MyClass mc) {
        this.mc = mc;
    }

    @Override
    public void run() {
        if("t1".equals(Thread.currentThread().getName())){
            mc.m1();
        }
        if("t2".equals(Thread.currentThread().getName())){
            mc.m2();
        }
    }
}

class MyClass {
    public static synchronized void m1(){
        System.out.println("m1 begin");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("m1 over");
    }

    public static synchronized void m2(){
        System.out.println("m2 begin");
        System.out.println("m2 over");
    }
}

死锁

/**
 * 写一个死锁。
 */
public class ThreadTest {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();
        Thread t1 = new Thread(new MyRunnable(o1, o2));
        Thread t2 = new Thread(new MyRunnable(o1, o2));
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}

class MyRunnable implements Runnable {

    private Object o1;
    private Object o2;

    public MyRunnable(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        if("t1".equals(Thread.currentThread().getName())){
            synchronized (o1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (o2){

                }
            }
        }else if("t2".equals(Thread.currentThread().getName())){
            synchronized (o2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (o1){

                }
            }
        }

    }
}

模拟多线程卖票

/**
 * 模拟三个窗口卖票。
 */
public class SellTicket {
    public static void main(String[] args) {

        // 创建一个对象,这样让多个线程共享同一个对象
        MyRunnable mr = new MyRunnable();

        // 创建三个线程,模拟三个窗口
        Thread t1 = new Thread(mr);
        t1.setName("1");
        Thread t2 = new Thread(mr);
        t2.setName("2");
        Thread t3 = new Thread(mr);
        t3.setName("3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class MyRunnable implements Runnable {

    // 实例变量(多线程共享)
    private int ticketTotal = 100;

    @Override
    public void run() {
        // 实现核心业务:卖票
        while(true){
            // synchronized 是线程同步机制。
            // synchronized 又被称为互斥锁!
            synchronized (this){
                if(ticketTotal <= 0){
                    System.out.println("票已售完...");
                    break; // 停止售票
                }
                // 票还有(ticketTotal > 0)
                // 一般出票都需要一个时长,模拟一下
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 出票了
                System.out.println("窗口"+ Thread.currentThread().getName() +"售出一张票,还剩" + (--ticketTotal) + "张票");
            }
        }
    }
}

线程通信

线程通信涉及到三个方法:

* wait()、notify()、notifyAll()

  1. wait(): 线程执行该方法后,进入等待状态,并且释放对象锁。
  2. notify(): 唤醒优先级最高的那个等待状态的线程。【优先级相同的,随机选一个】。被唤醒的线程从当初wait()的位置继续执行。
  3. notifyAll(): 唤醒所有wait()的线程
  4. 需要注意的:
    1. 以上三个方法在使用时,必须在同步代码块中或同步方法中。
    2. 调用这三个方法的对象必须是共享的锁对象。
    3. 这三个方法都是Object类的方法。
  1. wait()和sleep的区别?
    1. 相同点:都会阻塞。
    2. 不同点:
      1. wait是Object类的实例方法。sleep是Thread的静态方法。
      2. wait只能用在同步代码块或同步方法中。sleep随意。
      3. wait方法执行会释放对象锁。sleep不会。
      4. wait结束时机是notify唤醒,或达到指定时间。sleep结束时机是到达指定时间。
/**
 * 1. 内容是关于:线程通信。
 *
 * 2. 线程通信涉及到三个方法:
 *      wait()、notify()、notifyAll()
 *
 * 3. 以上三个方法都是Object类的方法。
 *
 * 4. 其中wait()方法重载了三个:
 *      wait():调用此方法,线程进入“等待状态”
 *      wait(毫秒):调用此方法,线程进入“超时等待状态”
 *      wait(毫秒, 纳秒):调用此方法,线程进入“超时等待状态”
 *
 * 5. 调用wait方法和notify相关方法的,不是通过线程对象去调用,而是通过共享对象去调用。
 *
 * 6. 例如调用了:obj.wait(),什么效果?
 *      obj是多线程共享的对象。
 *      当调用了obj.wait()之后,在obj对象上活跃的所有线程进入无期限等待。直到调用了该共享对象的 obj.notify() 方法进行了唤醒。
 *      而且唤醒后,会接着上一次调用wait()方法的位置继续向下执行。
 *
 * 7. obj.wait()方法调用之后,会释放之前占用的对象锁。
 *
 * 8. 关于notify和notifyAll方法:
 *      共享对象.notify(); 调用之后效果是什么?唤醒优先级最高的等待线程。如果优先级一样,则随机唤醒一个。
 *      共享对象.notifyAll(); 调用之后效果是什么?唤醒所有在该共享对象上等待的线程。
 *

class MyRunnable implements Runnable {

    // 实例变量,多线程共享的。
    private int count = 0;

    private Object obj = new Object();

    @Override
    public void run() {
        while(true){
            //synchronized (this){
            synchronized (obj) {

                // 记得唤醒t1线程
                // t2线程执行过程中把t1唤醒了。但是由于t2仍然占用对象锁,所以即使t1醒了,也不会往下执行。
                //this.notify();
                obj.notify();

                if(count >= 100) break;
                // 模拟延迟
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 程序执行到这里count一定是小于100
                System.out.println(Thread.currentThread().getName() + "-->" + (++count));

                try {
                    // 让其中一个线程等待,这个等待的线程可能是t1,也可能是t2
                    // 假设是t1线程等待。
                    // t1线程进入无期限的等待,并且等待的时候,不占用对象锁。
                    //this.wait();
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

三个线程交替输出

public class ThreadTest {

    // 共享对象(t1 t2 t3线程共享的一个对象,都去争夺这一把锁)
    private static final Object lock = new Object();
    // 给一个初始值,这个初始值表示第一次输出的时候,t1先输出。
    private static boolean t1Output = true;
    private static boolean t2Output = false;
    private static boolean t3Output = false;

    public static void main(String[] args) {
        // 创建三个线程

        // t1线程:负责输出A
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    for (int i = 0; i < 10; i++) {
                        while(!t1Output){ // 只要不是t1线程输出
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                        // 程序到这里说明:该t1线程输出了,并且t1线程被唤醒了。
                        System.out.println(Thread.currentThread().getName() + " ---> A");
                        // 该布尔标记的值
                        t1Output = false;
                        t2Output = true;
                        t3Output = false;
                        // 唤醒所有线程
                        lock.notifyAll();
                    }
                }
            }
        }).start();

        // t2线程:负责输出B
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    for (int i = 0; i < 10; i++) {
                        while(!t2Output){
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                        System.out.println(Thread.currentThread().getName() + " ---> B");
                        // 该布尔标记的值
                        t1Output = false;
                        t2Output = false;
                        t3Output = true;
                        // 唤醒所有线程
                        lock.notifyAll();
                    }
                }
            }
        }).start();

        // t3线程:负责输出C
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock){
                    for (int i = 0; i < 10; i++) {
                        while(!t3Output){
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                        System.out.println(Thread.currentThread().getName() + " ---> C");
                        // 该布尔标记的值
                        t1Output = true;
                        t2Output = false;
                        t3Output = false;
                        // 唤醒所有线程
                        lock.notifyAll();
                    }
                }
            }
        }).start();

    }
}

懒汉式单例模式

class SingletonTest {

    // 静态变量
    private static Singleton s1;
    private static Singleton s2;

    public static void main(String[] args) {

        // 获取某个类。这是反射机制中的内容。
        /*Class stringClass = String.class;
        Class singletonClass = Singleton.class;
        Class dateClass = java.util.Date.class;*/

        // 创建线程对象t1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                s1 = Singleton.getSingleton();
            }
        });

        // 创建线程对象t2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                s2 = Singleton.getSingleton();
            }
        });

        // 启动线程
        t1.start();
        t2.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        // 判断这两个Singleton对象是否一样。
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s1 == s2);

    }
}

/**
 * 懒汉式单例模式
 */
public class Singleton {
    private static Singleton singleton;

    private Singleton() {
        System.out.println("构造方法执行了!");
    }

    // 非线程安全的。
    /*public static Singleton getSingleton() {
        if (singleton == null) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            singleton = new Singleton();
        }
        return singleton;
    }*/

    // 线程安全的:第一种方案(同步方法),找类锁。
    /*public static synchronized Singleton getSingleton() {
        if (singleton == null) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            singleton = new Singleton();
        }
        return singleton;
    }*/

    // 线程安全的:第二种方案(同步代码块),找的类锁
    /*public static Singleton getSingleton() {
        // 这里有一个知识点是反射机制中的内容。可以获取某个类。
        synchronized (Singleton.class){
            if (singleton == null) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                singleton = new Singleton();
            }
        }
        return singleton;
    }*/

    // 线程安全的:这个方案对上一个方案进行优化,提升效率。
    /*public static Singleton getSingleton() {
        if(singleton == null){
            synchronized (Singleton.class){
                if (singleton == null) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }*/

    // 使用Lock来实现线程安全
    // Lock是接口,从JDK5开始引入的。
    // Lock接口下有一个实现类:可重入锁(ReentrantLock)
    // 注意:要想使用ReentrantLock达到线程安全,假设要让t1 t2 t3线程同步,就需要让t1 t2 t3共享同一个lock。
    // Lock 和 synchronized 哪个好?Lock更好。为什么?因为更加灵活。
    private static final ReentrantLock lock = new ReentrantLock();

    public static Singleton getSingleton() {
        if(singleton == null){

            try {
                // 加锁
                lock.lock();
                if (singleton == null) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    singleton = new Singleton();
                }
            } finally {
                // 解锁(需要100%保证解锁,怎么办?finally)
                lock.unlock();
            }

        }
        return singleton;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java老狗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值