09-java多线程

目录

第9章 多线程

9.1 相关概念(了解)

9.1.1 线程与进程

9.1.2 查看进程和线程

9.1.3 并发与并行

9.1.4 线程调度

9.2 另行创建和启动线程

9.2.1 继承Thread类

9.2.2 实现Runnable接口

9.2.3 使用匿名内部类对象来实现线程的创建和启动

9.3 Thread类

9.3.1 构造方法

9.3.2 常用方法系列1

9.3.3 常用方法系列2

9.3.4 如何让线程提前结束

9.3.5 守护线程(了解)

9.4 线程安全

9.4.1 同一个资源问题和线程安全问题

1、局部变量不能共享

2、不同对象的实例变量不共享

3、静态变量是共享的

4、同一个对象的实例变量共享

5、抽取资源类,共享同一个资源对象

9.4.2 尝试解决线程安全问题

1、同步机制的原理

2、同步代码块和同步方法

3、同步锁对象的选择

4、同步代码的范围选择

5、代码演示

示例一:静态方法加锁

示例二:非静态方法加锁

示例三:同步代码块

9.4.6 单例设计模式的线程安全问题

1、饿汉式没有线程安全问题

2、懒汉式线程安全问题

第9章 多线程

我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?

要解决上述问题,咱们得使用多进程或者多线程来解决.

9.1 相关概念(了解)

9.1.1 线程与进程

  • 程序:为了完成某个任务和功能,选择一种编程语言编写的一组指令的集合。

  • 软件1个或多个应用程序+相关的素材和资源文件等构成一个软件系统。

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

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

    简而言之:一个软件中至少有一个应用程序,应用程序的一次运行就是一个进程,一个进程中至少有一个线程。

  • 面试题:

    • 进程是操作系统调度和分配资源的最小单位,线程是CPU调度的最小单位。

    • 不同的进程之间是不共享内存的。进程之间的数据交换和通信的成本是很高。

    • 不同的线程是共享同一个进程的内存的。当然不同的线程也有自己独立的内存空间。

    • 对于方法区,堆中的同一个对象的内存,线程之间是可以共享的,但是栈的局部变量永远是独立的。另外进程之前切换的复杂度要远远高于线程之间的切换调度。

9.1.2 查看进程和线程

我们可以再电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程:

1、每个应用程序的运行都是一个进程

2、一个应用程序的多次运行,就是多个进程

3、一个进程中包含多个线程

9.1.3 并发与并行

  • 并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令在多个处理器上同时执行。

  • 并发(concurrency):指两个或多个事件在同一个时间段内发生。指在同一个时刻只能有一条指令执行,但多个进程的指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

在操作系统中,启动了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一个程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

假设你需要洗衣服和做饭(两个任务)

  1. 并行 : 将洗衣盆拿到厨房,左手炒菜,右手洗衣服。

  2. 并发 : 一会洗衣,一会做饭,但疾如闪电切换。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

单核CPU:只能并发

多核CPU:并行+并发

9.1.4 线程调度

  • 分时调度

    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

    • 抢占式调度详解

      大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

      实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

9.2 另行创建和启动线程

当运行Java程序时,其实已经有一个线程了,那就是main线程。

那么如何创建和启动main线程以外的线程呢?

9.2.1 继承Thread类

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

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

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

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

public class CreateThread2 {
    public static void main(String[] args) {
        MyThread2 th1 = new MyThread2();

        th1.start();

        //打印1-20奇数
        for (int i = 1; i <= 20; i += 2) {
            System.out.println("main主进程" + i);
        }
    }
}

class MyThread2 extends Thread{
    @Override
    public void run() {
        //打印1-20偶数
        for (int i = 2; i <= 20; i += 2) {
            System.out.println(this.getName()+"自定义线程" + i);
        }
    }
}

9.2.2 实现Runnable接口

Java有单继承的限制,当我们无法继承Thread类时,那么该如何做呢?在核心类库中提供了Runnable接口,我们可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行我们的线程体run()方法

步骤如下:

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

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

  3. 调用线程对象的start()方法来启动线程。 代码如下:

public class CreateRunnable1 {
    public static void main(String[] args) {
        ThreadRunnable ru1 = new ThreadRunnable();
        Thread th = new Thread(ru1);
        th.start();

        //打印1-20奇数
        for (int i = 1; i <= 20; i += 2) {
            System.out.println("main主进程" + i);
        }
    }
}

class ThreadRunnable implements Runnable {
    @Override
    public void run() {
        //打印1-20偶数
        for (int i = 2; i <= 20; i += 2) {
            System.out.println("自定义主进程" + i);
        }
    }
}

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程 代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现 Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。 而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

9.2.3 使用匿名内部类对象来实现线程的创建和启动

public class NoNameThread2 {
    public static void main(String[] args) {
        //1.实现Runnable接口  2.创建子实现类的实例对象
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //打印1-20偶数
                for (int i = 2; i <= 20; i += 2) {
                    System.out.println("自定义主进程" + i);
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //打印1-20奇数
                for (int i = 1; i <= 20; i += 2) {
                    System.out.println("自定义主进程" + i);
                }
            }
        });
        t2.start();
    }
}

9.3 Thread类

9.3.1 构造方法

  • public Thread() :分配一个新的线程对象。

  • public Thread(String name) :分配一个指定名字的新的线程对象。

  • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。

  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

  • public String getName() :获取当前线程名称。

  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

public class ThreadTh1 {
    public static void main(String[] args) {
        MyThreadMy th1 = new MyThreadMy();
        MyThreadMy th2 = new MyThreadMy("线程2");
        th1.start();
        th2.start();

        //2.分配一个指定名字的新的线程对象
        new Thread("线程3") {
            @Override
            public void run() {
                //打印1-10偶数
                for (int i = 1; i <= 10; i += 2) {
                    System.out.println(getName() + "自定义主进程" + i);
                }
            }
        }.start();

        //3.分配一个带有指定目标新的线程对象
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+":匿名内部类,实现Runnable接口的");
            }
        }).start();

        //4.分配一个带有指定目标新的线程对象并指定名字
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+":匿名内部类,实现Runnable接口的");
            }
        }, "线程4").start();

    }
}

class MyThreadMy extends Thread {
    public MyThreadMy() {
    }

    public MyThreadMy(String name) {
        super(name);
    }

    @Override
    public void run() {
        //打印1-10奇数
        for (int i = 1; i <= 10; i += 2) {
            System.out.println(getName() + "自定义主进程" + i);
        }
    }
}

9.3.2 常用方法系列1

  • public void run() :此线程要执行的任务在此处定义代码。

  • public final int getPriority() :返回线程优先级

  • public final void setPriority(int newPriority) :改变线程的优先级

    • 每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。Thread类提供了setPriority(int newPriority)和getPriority()方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:

    • MAX_PRIORITY(10):最高优先级

    • MIN _PRIORITY (1):最低优先级

    • NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。

示例:

  • 获取main线程对象的名称和优先级。

  • 声明一个匿名内部类继承Thread类,重写run方法,在run方法中获取线程名称和优先级。设置该线程优先级为最高优先级并启动该线程。

public class ThreadPriority {
    public static void main(String[] args) {
        SubThread sub1 = new SubThread();
        SubThread sub2 = new SubThread();

        //main函数的当前线程优先级
        System.out.println(Thread.currentThread().getPriority());
        sub1.start();
        sub2.start();
        sub2.setPriority(10);
        sub1.setPriority(Thread.MIN_PRIORITY);

        System.out.println(sub1.getPriority());
        System.out.println(sub2.getPriority());
    }
}

class SubThread extends Thread {
    public SubThread() {
    }

    public SubThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        //打印1-10奇数
        for (int i = 1; i <= 10; i += 2) {
            System.out.println(getName() + "自定义主进程:" + i);
        }
    }
}

9.3.3 常用方法系列2

  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。

  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。

  • public static void yield():yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。

  • void join() :等待该线程终止。

    void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。

    void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。

    public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。

案例:

  • 声明一个匿名内部类继承Thread类,重写run方法,实现打印[1,100]之间的偶数,要求每隔1秒打印1个偶数。

  • 声明一个匿名内部类继承Thread类,重写run方法,实现打印[1,100]之间的奇数,

    • 当打印到5时,让奇数线程暂停一下,再继续。

    • 当打印到5时,让奇数线程停下来,让偶数线程执行完再打印。

    • 当打印到5时,让奇数线程停下来,让偶数线程先执行10秒完再打印。

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

        Thread t1 = new Thread("偶数线程:"){
            @Override
            public void run() {
                for (int i=2;i<20;i+=2){
                    System.out.println(getName()+i);
                    //异常  ctrl+alt+t
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        Thread t2 = new Thread("奇数线程:"){
            @Override
            public void run() {
                for (int i=1;i<20;i+=2){
                    System.out.println(getName()+i);
                    //异常  ctrl+alt+t
                    if (i==5){
                        Thread.yield();
                    }
                }
            }
        };

        t1.start();
        t2.start();
    }
}
public class ThreadAlive4 {
    public static void main(String[] args) {
        SubThread sub = new SubThread();
        System.out.println("没有启动时:"+sub.isAlive());
        sub.start();
        System.out.println("启动后:"+sub.isAlive());

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("此时:"+sub.isAlive());
    }
}

class SubThread extends Thread {
    @Override
    public void run() {
        for (int i = 2; i < 50; i += 2) {
            System.out.println(i);
        }
    }
}

9.3.4 如何让线程提前结束

一个线程如何让另一个线程提前结束呢?

线程的死亡有两种:

自然死亡:当一个线程的run方法执行完,线程自然会停止。

意外死亡:当一个线程遇到未捕获处理的异常,也会挂掉。

我们肯定希望是让线程自然死亡更好。

  • public final void stop():强迫线程停止执行。 该方法具有固有的不安全性,已经标记为@Deprecated(已过时、已废弃)不建议再使用,那么我们就需要通过其他方式来停止线程了,其中一种方式是使用变量的值的变化来控制线程是否结束。

  • 标记法

案例:

声明一个PrintEvenThread线程类,继承Thread类,重写run方法,实现打印[1,100]之间的偶数,要求每隔1毫秒打印1个偶数。

声明一个PrintOddThread线程类,继承Thread类,重写run方法,实现打印[1,100]之间的奇数。

在main线程中:

(1)创建两个线程对象,并启动两个线程

(2)当打印奇数的线程结束了,让偶数的线程也停下来,就算偶数线程没有全部打印完[1,100]之间的偶数。

public class ThreadStop {
    public static void main(String[] args) {
        PrintEvenThread even = new PrintEvenThread();
        PrintOddThread odd = new PrintOddThread();

        even.start();
        odd.start();

        //当奇数线程停止
        try {
            odd.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //当奇数线程停止,设置偶数线程的标记变量为false
        even.setFlag(false);
    }
}

class PrintEvenThread extends Thread {
    //设置标记变量
    private boolean flag = true;

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        for (int i = 2; i <= 100 && flag; i += 2) {
            System.out.println("偶数线程" + i);
//            try {
//                Thread.sleep(100);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
        }
    }
}

class PrintOddThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 50; i += 2) {
            System.out.println("奇数线程:" + i);
        }
    }
}

9.3.5 守护线程(了解)

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

守护线程有个特点,就是如果所有非守护线程都死亡,那么守护线程自动死亡。

调用setDaemon(true)方法可将指定线程设置为守护线程。必须在线程启动之前设置,否则会报IllegalThreadStateException异常。

调用isDaemon()可以判断线程是否是守护线程。

public class ThreadDaemon {
    public static void main(String[] args) {
        ThDaemon daemon = new ThDaemon();
        //将指定线程,设置为守护线程
        daemon.setDaemon(true);
        daemon.start();

        for (int i=1;i<=100;i++){
            System.out.println("main线程"+i);
        }
    }
}

class ThDaemon extends Thread{
    @Override
    public void run() {
        while(true){
            if (Thread.currentThread().isDaemon()){
                System.out.println("你放心飞吧,我会守护你的,我是守护线程");
            }else{
                System.out.println("普通线程,是一个死循环");
            }
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

9.4 线程安全

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题,但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。

我们通过一个案例,演示线程的安全问题: 电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个 (本场电影只能卖100张票)。 我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)

9.4.1 同一个资源问题和线程安全问题

1、局部变量不能共享
public class Unsafe1 {
    public static void main(String[] args) {
        Window w1 = new Window("窗口1");
        Window w2 = new Window("窗口2");
        Window w3 = new Window("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}

class Window extends Thread{
    public Window(String name) {
        super(name);
    }

    @Override
    public void run() {

        //1.局部变量不共享
        int  total = 100;
        while(total>=1){
            total--;
            System.out.println(getName()+"卖出一张票,剩余"+total+"张票");
        }
    }
}
2、不同对象的实例变量不共享
public class Unsafe2 {
    public static void main(String[] args) {
        Window1 w1 = new Window1("窗口1");
        Window1 w2 = new Window1("窗口2");
        Window1 w3 = new Window1("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}

class Window1 extends Thread{
    //2.实例化成员也不可以共享
    private int total = 100;

    public Window1(String name) {
        super(name);
    }

    @Override
    public void run() {
        while(total>=1){
            total--;
            System.out.println(getName()+"卖出一张票,剩余"+total+"张票");
        }
    }
}
3、静态变量是共享的
public class Unsafe3 {
    public static void main(String[] args) {
        Window2 w1 = new Window2("窗口1");
        Window2 w2 = new Window2("窗口2");
        Window2 w3 = new Window2("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}

class Window2 extends Thread{
    //3.静态变量是共享的
    private static int total = 100;

    public Window2(String name) {
        super(name);
    }

    @Override
    public void run() {
        while(total>=1){
            total--;
            System.out.println(getName()+"卖出一张票,剩余"+total+"张票");
        }
    }
}
4、同一个对象的实例变量共享
public class Unsafe4 {
    public static void main(String[] args) {
        //一部电影,只有一个线程对象
        TicketThread t = new TicketThread();
        Thread w1 = new Thread(t,"窗口1");
        Thread w2 = new Thread(t,"窗口2");
        Thread w3 = new Thread(t,"窗口3");
        w1.start();
        w2.start();
        w3.start();

    }
}

class TicketThread implements Runnable{
    //4.同一个对象的实例变量可以共享
    //实例成员
    private  int total = 100;

    @Override
    public void run() {
        while(total>=1){
            total--;
            System.out.println(Thread.currentThread().getName()+"卖出一张票,剩余"+total+"张票");
        }
    }

}

9.4.2 尝试解决线程安全问题

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制 (synchronized)来解决。

根据案例简述:

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

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

1、同步机制的原理

同步解决线程安全的原理:

同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”,我们称为它同步锁。因为Java对 象在堆中的数据分为分为对象头、实例变量、空白的填充。而对象头中包含:

  • Mark Word:记录了和当前对象有关的GC、锁标记等信息。

  • 指向类的指针:每一个对象需要记录它是由哪个类创建出来的。

  • 数组长度(只有数组对象才有)

哪个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的ID,这样其他线程就只能等待了,除非这个线程”释放“了锁对象,其他线程才能重新获得/占用”同步锁“对象。

2、同步代码块和同步方法

同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。

public synchronized void method(){
    可能会产生线程安全问题的代码
}

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

synchronized(同步锁){
     需要同步操作的代码
}
3、同步锁对象的选择

同步锁对象可以是任意类型,但是必须保证竞争“同一个共享资源”的多个线程必须使用同一个“同步锁对象”。

对于同步代码块来说,同步锁对象是由程序员手动指定的,但是对于同步方法来说,同步锁对象只能是默认的,

  • 静态方法:当前类的Class对象

  • 非静态方法:this

4、同步代码的范围选择

锁的范围太小:不能解决安全问题

锁的范围太大:因为一旦某个线程抢到锁,其他线程就只能等待,所以范围太大,效率会降低,不能合理利用CPU资源。

5、代码演示
示例一:静态方法加锁
public class SafeThread {
    public static void main(String[] args) {
        //一部电影,只有一个线程对象
        TicketThread t = new TicketThread();
        Thread w1  = new Thread(t,"窗口1");
        Thread w2  = new Thread(t,"窗口2");
        Thread w3  = new Thread(t,"窗口3");
        w1.start();
        w2.start();
        w3.start();
    }
}

//线程类
class TicketThread implements Runnable {
    //实例成员
    private  int total = 10;
    @Override
    public void run() {
        while (total >= 1) {
            saleOneTicket();
        }
    }

    //1.同步方法
    public synchronized void saleOneTicket() {
       //加上判断条件
        //不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
        if (total >= 1) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            total--;
            System.out.println(Thread.currentThread().getName() + ",卖出1张票,剩余" + total + "张票");
        }
    }
}
示例二:非静态方法加锁
public class SafeThread2 {
    public static void main(String[] args) {
        WindowThree w1 = new WindowThree("窗口1");
        WindowThree w2 = new WindowThree("窗口2");
        WindowThree w3 = new WindowThree("窗口3");
        w1.start();
        w2.start();
        w3.start();
    }
}

//线程类
class WindowThree extends Thread {
    //静态成员
    private static int total = 10;

    public WindowThree(String name) {
        super(name);
    }

    @Override
    public void run() {
        while (total >= 1) {
            saleOneTicket();
        }
    }

    //2.同步方法
    public static synchronized void saleOneTicket() {
        //加上判断条件
        if (total >= 1) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            total--;
            System.out.println(Thread.currentThread().getName() + ",卖出1张票,剩余" + total + "张票");
        }
    }
}
示例三:锁了run方法,会导致,只有一个窗口卖票
public class SafeThread3 {
    public static void main(String[] args) {
        Window t = new Window();
        Thread w1  = new Thread(t,"窗口1");
        Thread w2  = new Thread(t,"窗口2");
        Thread w3  = new Thread(t,"窗口3");
        w3.start();
        w2.start();
        w1.start();
    }
}

//线程类
class Window implements Runnable {
    //静态成员
    private static int total = 10;

    //3.不正常,不能锁run,因为锁了run方法,会导致,只有一个窗口卖票
    @Override
    public synchronized void run() {
        while (total >= 1) {
//            try {
//                Thread.sleep(1000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            total--;
            System.out.println(Thread.currentThread().getName() + ",卖出1张票,剩余" + total + "张票");
        }
    }

}

9.4.6 单例设计模式的线程安全问题

1、饿汉式没有线程安全问题

饿汉式:在类初始化时就直接创建单例对象,而类初始化过程是没有线程安全问题的

形式一:

/*
public class HungryOne{
    public static final HungryOne INSTANCE = new HungryOne();
    private HungryOne(){}
}*/
public enum HungryOne{
    INSTANCE
}

形式二:

public class HungrySingle {
    private static final HungrySingle INSTANCE = new HungrySingle();
    private HungrySingle(){}
    public static HungrySingle getInstance(){
        return INSTANCE;
    }
}

测试类:

public class TestHungry {
    public static void main(String[] args) {
        HungryOne h1 = HungryOne.INSTANCE;
        HungryOne h2 = HungryOne.INSTANCE;
        System.out.println(h1 == h2);

        System.out.println("----------------------");
        HungrySingle s1 = HungrySingle.getInstance();
        HungrySingle s2 = HungrySingle.getInstance();
        System.out.println(s1 == s2);
    }
}
2、懒汉式线程安全问题

懒汉式:延迟创建对象,第一次调用getInstance方法再创建对象

public class LazyOne {
    private static LazyOne instance;

    private LazyOne(){}

    public static synchronized LazyOne getInstance(){
        if(instance == null){
            instance = new LazyOne();
        }
        return instance;
    }

    //有指令重排问题
/*    public static LazyOne getInstance(){
        if(instance == null){
            synchronized (LazyOne.class) {
                try {
                    Thread.sleep(10);//加这个代码,暴露问题
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(instance == null){
                    instance = new LazyOne();
                }
            }
        }

        return instance;
    }*/
}
3、二者区别
1.实例化时机:
  饿汉式:在类加载时立即实例化.
  懒汉式:在第一次调用获取实例的方法时实例化.
2.线程安全性:
  饿汉式:天然线程安全,因为实例在类加载时已经创建,这是由JVM的类加载机制保证的。
  懒汉式:非线程安全,需要通过额外的同步措施(如synchronized关键字)来保证线程安全,这可能会带来性能开销。
3.资源使用:
  饿汉式:可能会浪费-一些资源,因为如果该类从未被使用,那么创建的单例实例就会白白占用内存。
  懒汉式:节省了资源,只有在真正需要时才创建实例.
4.性能:
  饿汉式:由于实例在类加载时已经创建,所以第一次调用获取实例的方法时速度更快.
  懒汉式:第一次调用获取实例的方法时需要进行实例化操作,可能会有一些性能开销.
  • 37
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值