Java20多线程1:多线程相关概念和创建方式、Thread类的常用方法

1.多线程相关概念

现在使用场景经常存在,比如“听歌、打游戏、QQ聊天同时进行”,那么怎么设计?要解决这些问题,需要使用多进程 或者多线程来解决。

1.1 程序、进程、线程

  • 程序(Program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

  • 进程(Process)程序的一次执行过程,或是正在内存中运行的应用程序。如:运行中的QQ,运行中的浏览器,音乐播放器等

    • 每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程。(生命周期)
    • 程序是静态的,进程是动态的
    • 进程作为操作系统调度和分配资源的最小单位(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。
    • 现代的操作系统,大都是支持多进程的,支持同时运行多个程序。比如:现在我们使用电脑时一边浏览网页,一边聊天,同时还运行着 ide等软件。
  • 线程(Thread)进程可进一步细化成线程,它是程序内部的一条执行路径。一个进程中至少有一个线程。

    • 一个进程同一时间若并行执行多个线程,就是支持多线程的。
    • 线程是CPU调度和执行的最小单位
    • 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使的线程间通信更简单、高效。但多个线程操作共享的系统资源可能带来安全的隐患
    • 下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。
      JVM

注意:
不同进程之间不需要共享内存的
进程之间的数据交换和通信的成本很高

一个进程包含多个线程

核心概念:

  • 线程就是独立的执行路径;
  • 在程序执行时,即使没有自己创建线程,后台也会有多个线程,比如:主线程,GC线程;
  • main() 称之为主线程,是系统的入口,用于执行整个程序;
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密想关的,先后顺序是不能人为干预的;
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
  • 线程会带来额外的开销,比如 cpu调度时间,并发控制开销。
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

1.2 线程调度

1.2.1 分时调度

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

1.2.2 抢占式调度

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

1.3 多线程程序的优点

  1. 背景:以单核CPU为例,只使用单个线程先后完成多个任务(调度多个方法),肯定比用多个线程来完成的用时更短,为何仍须多线程呢?
  2. 多线程的优点:
    ①提高应用程序的响应。对图形化界面更具有意义;
    ②提高计算机系统 CPU的利用率;
    ③该善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。

1.4 补充概念

1.4.1 单核CPU和多核CPU

  1. 单核CPU:在一个时间单元内,只能执行一个线程的任务。例如:将CPU看作医生诊室,一段时间内只能给一个病人诊断治疗。所以单核CPU就是代码经过前面一系列的前导操作(类似于医院挂号。10个挂号窗口),然后就只有一个CPU(医生),大家排队执行。
  2. 提升系统性能:要么提升CPU性能(医生看病快一些),要么多加几个CPU(多个医生),即为多核CPU。
  3. *Question:多核的效率一定比单核的倍数高吗?*如:4核A53的CPU,性能一定是单核A53的4倍吗?理论上是,但实际上不可能会,至少有两方面的损耗。
    • 一个是多核CPU的其他共用资源限制。如:4核CPU对应的内存、Cache、寄存器并没有同步扩充4倍。这就像虽然现在有多个医生,但是B超检查机器只有一台,性能瓶颈就从医生转到B超检查了。
    • 另一个是多核CPU之间的协调管理损耗。如:多个核心同时运行两个相关的任务,需要考虑任务同步,这也需要消耗额外的性能。就像是在公司工作,一个人的时候不会在开会上浪费时间,两个人就要开会同步工作,协调分配,所以效率绝对不可能达到2倍。

1.4.2 并行与并发

  • 并行(Parallel):指两个或多个事件在同一时刻同时发生。指在同一时刻,有多条指令多个CPU同时执行。比如:多个人同时做不同的事情。如下图,图 1。
    并行与并发
  • 并发(Concurrency):指两个或多个事件在同一个时间段内发生。即在一段时间内,有多条指令单个CPU快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果。如上图,图 2。

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

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

2.多线程创建方式

2.1 概述

  • Java语言的 JVM允许程序运行多个线程,使用java.lang.Thread类代表线程,所有的线程对象都必须是 Thread类或是其子类的实例。
  • Thread类的特性:
    • 每个线程都是通过某个特定 Thread对象的 run()方法来完成操作的,因此把 run()方法体称为线程执行体;
    • 通过该 Thread对象的 start()方法来启动这个线程,而非直接调用 run();
    • 要想实现多线程,必须在主线程中创建新的线程对象。

两种方法对比:
继承 Thread类:

  • 子类继承 thread类, 子类具备多线程能力
  • 启动线程:子类对象.start();
  • 不建议使用:避免OOP单继承局限性

实现 Runnable接口:

  • 实现 Runnable接口,具有多线程能力
  • 启动线程:传入目标对象+ Thread对象.start();
  • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

2.2 方式1:继承Thread类

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

  1. 定义Thread类的子类;
  2. 重写该类的 run()方法 --> 将此线程要执行的操作,声明在此方法体中;
  3. 创建当前 Thread子类的实例,即创建了线程对象;
  4. 调用线程对象的 start()方法来启动线程:作用:①启动线程;②调用当前线程的 run()方法。
package com.mMultithreading;

/**
 * 多线程创建方式1: 继承 Thread类
 * 例题:创建一个分线程1,用于遍历100以内的偶数。
 * 拓展:再创建一个分线程2,用于遍历100以内的偶数。
 * 
 * 注意:t1.start()和main方法中的for循环是两个线程
 */
public class Thread1 {
    public static void main(String[] args) {
        //③创建当前 Thread的子类的对象
        PrintNumber t1 = new PrintNumber();

        //④通过对象调用 start()方法
        t1.start();
        //t1.start();IllegalThreadStateException

		// 拓展:再创建一个分线程2,用于遍历100以内的偶数。
		PrintNumber t2 = new PrintNumber();
		t2.start();

        //main() 所在的线程 执行的操作
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName()+": "+i);
            }
        }
    }
}

//①创建一个继承于 Thread类的子类
class PrintNumber extends Thread {
    /*②重写 Thread类的 run()方法,
    将此线程要执行的操作声明在此方法中*/
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName()+": "+i);
            }
        }
    }
}

问题一:能否使用 t1.run()替换 t1.start()的调用,实现分线程的创建和调用?

不能。

  1. 调用 t1.run()方法没有使用线程的方法,而是使用主线程的 run()方法,也就是说上面示例使用的是同一个线程;
  2. 调用 t1.start()方法的作用是:①启动线程;②调用当前线程的 run()方法。

问题二:再创建一个分线程2,用于遍历100以内的偶数。

注意:不能让已经 start()的线程再次执行 start(),否则报非法线程状态异常 IllegalThreadStateException。

PrintNumber t2 = new PrintNumber();
t2.start(); 

练习:创建两个分线程,其中一个线程遍历 100以内的偶数,另一个遍历 100以内的奇数。

package com.mMultithreading;
/**
 * 方式一
 */
public class Thread2Test {
    public static void main(String[] args) {
        PrintEven t1 = new PrintEven();
        PrintOdd t2 = new PrintOdd();

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

//输出偶数
class PrintEven extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

//输出奇数
class PrintOdd extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 1) {
                System.out.println(Thread.currentThread().getName() + ": " + i+"***");
            }
        }
    }
}
package com.mMultithreading;

import org.junit.Test;
/**
 * 方式二:创建 Thread类的匿名子类的匿名对象
 */
public class Thread2Test {
    public static void main(String[] args) {
        //偶数
        new Thread(){
            public void run() {
                for (int i = 0; i <= 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + ": " + i);
                    }
                }
            }
        }.start();
        //奇数
        new Thread(){
            public void run() {
                for (int i = 0; i <= 100; i++) {
                    if (i % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + "---->" + i);
                    }
                }
            }
        }.start();
    }
}

线程是程序中执行的线程。 Java虚拟机允许应用程序同时执行多个执行线程。
每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。

2.3 方式2:实现Runnable接口

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

  1. 创建一个实现 Runnable接口的类;
  2. 实现接口中的 run()方法 --> 将此线程要执行的操作,声明在此方法体中;
  3. 创建当前实现类的对象;
  4. 将此对象作为参数传递到 Thread类的构造器中,创建 Thread的实例;
  5. Thread类的实例调用 start():作用:①启动线程;②调用当前线程的 run()方法
package com.mMultithreading;

/**
 * 实现 Runnable接口 来创建并启动多线程
 *
 * 例题:创建一个分线程1,用于遍历100以内的偶数。
 * 拓展:再创建一个分线程2,用于遍历100以内的偶数。
 */
public class Runnable1 {
    public static void main(String[] args) {
        //③创建当前实现类的对象;
        EvenNumberPrint p = new EvenNumberPrint();
        //④将此对象作为参数传递到 Thread类的构造器中,创建 Thread的实例;
        Thread t1 = new Thread(p);
        /*⑤Thread类的实例调用 start().
        start()作用:①启动线程;②调用当前线程的 run()方法*/
        t1.start();

        //main()方法对应的主线程执行的操作
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }

        /*再创建一个分线程2,用于遍历100以内的偶数。*/
        Thread t2 = new Thread(p);
        t2.start();
    }
}

/**
 * ①创建一个实现 Runnable接口的类;
 */
class EvenNumberPrint implements Runnable {
    /*②实现接口中的 run()方法 --> 将此线程要执行的操作,声明在此方法体中;*/
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + " --> " + i);
            }
        }
    }
}
package com.mMultithreading;
/**
 * 方式三:实现 Runnable接口 的匿名子类的匿名对象
 */
public class Runnable2Test {
    public static void main(String[] args) {
        //偶数
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + ": " + i);
                    }
                }
            }
        }).start();

        //奇数
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 100; i++) {
                    if (i % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + " --》" + i);
                    }
                }
            }
        }).start();
    }
}

2.4 两种方式对比

  1. 共同点:
    • ① 启动线程,使用的都是 Thread类中定义的 start()方法;
    • ② 创建的线程对象,都是 Thread类 或其子类的实例。
  2. 不同点:一个是类的继承,一个是接口的实现。
  3. 联系:public class Thread implements Runnable (代理模式)
  4. 启示:建议使用实现 Runnable接口的方式。好处:
    • ① 实现的方式,避免了类的单继承的局限性;
    • ② 更适合处理有共享数据的问题;
    • ③ 实现了代码和数据的分离。

2.5 思考题:判断各自调用的是哪个 run()?

情景一

package com.mMultithreading;

/** 5
 * 思考题:判断各自调用的是哪个 run()?
 */
public class Test {
    public static void main(String[] args) {
        A a = new A();
        a.start();//①启动线程;②调用 Thread类的 run()r方法

        B b = new B(a);
        b.start();
    }
}
//创建线程类 A
class A extends Thread{
    @Override
    public void run() {
        System.out.println("线程 A的--> run()");
    }
}
//创建线程类 B
class B extends Thread{
    private A a;
    /*构造器中直接传入 A类对象。下面两种方法结果相同
    public B(A a) {this.a = a;}*/
    public B(A a) {super(a);}

    /**
     * 注意:当这里没有重写 run()方法,那么就会调用 A类中的 run()方法
     * 前提:上面的构造器要使用 super(a)的这个
     */
    @Override
    public void run() {
        System.out.println("线程 B的--> run()");
    }
}

情景二

package com.mMultithreading;

/** 6
 * 判断各自调用的是哪个 run()?
 */
public class Test2 {
    public static void main(String[] args) {
        BB b = new BB();
        //调用的就是 形参对象的 run()方法
        new Thread(b){}.start();//BB

        //这里使用的就是 自己的 run()方法
        new Thread(b){
            @Override
            public void run() {
                System.out.println("CC");//CC
            }
        }.start();
    }
}
class AA extends Thread{
    @Override
    public void run() {
        System.out.println("AA");
    }
}
class BB implements Runnable{
    @Override
    public void run() {
        System.out.println("BB");
    }
}

3.Thread类的常用结构

3.1 构造器

//分配一个新的线程对象
public Thread();
//分配一个指定名字的新线程对象
public Thread(String name);
//指定创建线程的目标对象,它实现了 Runnable接口中的 Run()方法
public Thread(Runnable target);
//分配一个带有指定目标的新线程对象,并指定名字
public Thread(Runnable target, String name);
package com.mMultithreading;
/**
 * 线程中的构造器
 */
public class Test3Construct {
    public static void main(String[] args) {
        EvenNumber p1 = new EvenNumber("线程_1:");
        p1.start();

        EvenNumber p2 = new EvenNumber(p1,"线程_2:");
        p2.start();
    }
}
class EvenNumber extends Thread{
    public EvenNumber() {}
    public EvenNumber(String name) {super(name);}
    public EvenNumber(Runnable target, String name) {super(target, name);}

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i%2 ==0){
                System.out.println(Thread.currentThread().getName()+"-->"+i);
            }
        }
    }
}

3.2 常用方法_系列1

public void run();//此线程要执行的任务,需要在此处定义代码。
public void start();//开始执行此线程;Java虚拟机调用此线程的 run()方法
public String getName();//获取当前线程的名称
public void setName(String name);//设置当前线程的名称
public static native Thread currentThread();//返回当前正在执行的线程对象的引用。在Thread子类中就是 this,通常用于主线程和 Runnable实现类
public static void sleep(long millis);//是当前正在运行的线程暂停 millis毫秒(暂时停止执行)
public static native void yield();//yield只是让当前线程暂停一下,让系统的线程调度器从新调度一次,希望优先级

3.3 常用方法_系列2

  • public final boolean isAlive();//判断当前线程是否存活
  • void join(); 等待该线程终止
    void join(long millis); 等待该线程终止的时间最长是 millis毫秒。时间到,就不再等待
    void join(long millis, int nanos); 等待该线程终止的时间最长是 millis毫秒 +nanos纳秒
  • public final void stop();已过时,不建议使用。强行结束一个线程的执行,直接进入死亡状态。run()方法即刻停止可能会导致一些清理性的工作不能完成,如文件、数据库的关闭等,同时会立刻释放掉该线程持有的所有锁,导致数据不能同步,出现数据不一致的问题。
  • void suspend() / void resume(); 已过时不建议使用。这两个操作好比播放器的暂停和恢复,二者必须成对出现,否则非常容易发生死锁。调用suspend()会导致线程暂停,但不会释放掉任何锁资源,导致其他线程都无法访问被它占用的锁,直到调用resume()。
package com.mMultithreading;
public class Test4Method {
    public static void main(String[] args) {
        PrintNumberTest t1 = new PrintNumberTest("线程_1:");
        //设置线程名称
        t1.setName("子线程1:");
        //①启动线程 ②调用线程的run()
        t1.start();

        Thread.currentThread().setName("主线程");

        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                //getName():获取线程名称
                System.out.println(Thread.currentThread().getName() + "--> " + i);
            }
            if (i % 20 == 0) {
                try {
                    //在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,
                    // 直到线程b执行结束,线程a才结束阻塞状态,继续执行。
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        //isAlive():判断当前线程是否存活
        System.out.println("子线程1是否还存活?" + t1.isAlive());
    }
}

class PrintNumberTest extends Thread {
    public PrintNumberTest() {
    }

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

    /**
     * 此线程要执行的任务,需要在此处定义代码。
     */
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {

            try {
                //静态方法,调用时,调用时可以使当前线程睡眠指定的毫秒数
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i % 2 == 0) {
                //currentThread():获取当前执行代码对应的线程
                System.out.println(Thread.currentThread().getName() + "-->" + i);
            }

            if (i % 20 == 0) {
                //一旦执行此方法,就释放CPU的执行权
                Thread.yield();
            }
        }
    }
}

3.4 线程的优先级

每个线程都有一定的优先级,同优先级线程组成先进先出队列(FIFO:先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。

  • Thread内部类声明的三个优先级常量:
    • MIN_PRIORITY (1) :最低优先级
    • NORM_PRIORITY (5) :普通优先级,默认情况下 main线程具有普通优先级。
    • MAX_PRIORITY (10) :最高优先级
  • public final int getPriority():获取线程的优先级
  • public final int setPriority(int newPriority):改变线程的优先级,范围在[1, 10]之间。
package com.mMultithreading;

/**
 * 线程方法_3:
 *  getPriority():获取线程的优先级
 */
public class Test5Method {
    public static void main(String[] args) {
        PrintNumberTest5 t1 = new PrintNumberTest5("线程_1:");
        t1.setPriority(Thread.MIN_PRIORITY);
        t1.start();
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);

        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                //getName():获取线程名称
                System.out.println(Thread.currentThread().getName() + ":" +
                        //getPriority():获取线程优先级
                        Thread.currentThread().getPriority()+ i);
            }
        }
    }
}
class PrintNumberTest5 extends Thread {
    public PrintNumberTest5(String name) {
        super(name);
    }

    /**
     * 此线程要执行的任务,需要在此处定义代码。
     */
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" +
                        //getPriority():获取线程优先级
                        Thread.currentThread().getPriority()+ i);
            }
        }
    }
}

4.多线程的生命周期

Java语言使用 Thread类及其子类的对象来表示线程,在它的一个完整生命周期内通常要经历一下状态

4.1 JDK1.5 之前:5种状态

它们分别是:新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。CPU需要在多条线程之间切换,于是线程会多次在运行、阻塞、就绪之间切换

JDK1.5 之前:5种状态

4.1 JDK1.5 及之后:6种状态

线程的生命周期jdk5.0之后

5.线程安全问题及解决

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

举例:
对一个账户进行取钱操作;

5.1统一资源问题和线程安全问题

案例:
模拟火车站卖票的过程。如果本次列车的座位共100个(即只能出售100张火车票)。这里模拟售票窗口,实现多个窗口同时售票的过程。注意:不能出现错票、重票。

package com.mMultithreading.synBlock;

/** 出现了错票、重票,同步代码块和同步方法中的代码解决了这些问题!!!
 * 模拟车站售票:模拟车站售票:实现Runnable接口方式,实现卖票
 * 3个窗口,共100张票
 */
class SaleTicket implements Runnable {
    static int ticket = 100;
    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "售票,票号为 " + ticket);
                ticket--;
            } else {
                break;
            }
        }
    }
}

public class Test1WindowIM {
    public static void main(String[] args) {
        SaleTicket s = new SaleTicket();
        Thread t1 = new Thread(s,"售票窗口1");
        Thread t2 = new Thread(s,"售票窗口2");
        Thread t3 = new Thread(s,"售票窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
  1. 多线程卖票,出现的问题:
    出现了重票和错票

  2. 什么原因导致的?
    线程1操作 ticket 时,其他进程也参与进来,并对 ticket 进行操作。

  3. 解决思路?
    必须保证一个线程a在操作 ticket 时,其他线程必须等待,直到线程a操作ticket结束后,其他线程才可以进入操作 ticket。

  4. Java如何解决线程的安全问题?
    使用线程的同步机制。① 同步代码块;② 同步方法

5.2 同步代码块

synchronized(同步监视器){
//需要被同步的代码
}

说明:

共享数据:即多个线程需要操作的数据。比如:ticket
需要被同步的代码,即为操作共享数据的代码,它被synchronized包裹以后,就使得一个线程在操作这些代码的过程时,其他线程必须等待。
同步监视器,俗称。哪个线程获得了锁,哪个线程就能执行需要被同步的代码;它可以使用任何一个类的对象充当。但是,多个线程必须保证同步监视器唯一

package com.mMultithreading.synBlock;

/**
 * 模拟车站售票:实现Runnable接口方式,实现卖票
 * 使用同步代码块解决买票中的线程安全问题。
 * 3个窗口,共100张票
 */
class Dog {
}

class Sale implements Runnable {
    static int ticket = 100;
    Object obj = new Object();
    Dog dog = new Dog();

    @Override
    public void run() {
//        synchronized (this) {//当前类是唯一的?yes,就是题中的s
        while (true) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
//            synchronized (obj){//obj是唯一的?yes
//            synchronized (dog) {//dog是唯一的?yes
            synchronized (this) {//当前类是唯一的?yes,就是题中的
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号为 " + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

public class Test3WindowIM {
    public static void main(String[] args) {
        Sale s = new Sale();
        Thread t1 = new Thread(s, "售票窗口1");
        Thread t2 = new Thread(s, "售票窗口2");
        Thread t3 = new Thread(s, "售票窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

package com.mMultithreading.synBlock;

/**
 * 模拟车站售票:继承Thread类方式
 * 使用同步代码块解决买票中的线程安全问题。
 * 3个窗口,共100张票
 */
class WatchDog {}

class Ticket extends Thread {
    static int ticket = 100;
    WatchDog dog = new WatchDog();
    static Object obj=new Object();
    @Override
    public void run() {
        while (true) {
//            synchronized (this) {//this此时表示t1,t2,t3
//            synchronized (obj) {//static 修饰后就能保证使其唯一
//            synchronized (dog) {
            synchronized (Ticket.class) {//反射结构:Class cla = Ticket.class;类只加载一次所以它是唯一的。
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号为 " + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

public class Test4WindowEX {
    public static void main(String[] args) {
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();
        Ticket t3 = new Ticket();
        t1.setName("售票窗口1");
        t2.setName("售票窗口2");
        t3.setName("售票窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
  • 注意:
    • 在实现Runnable接口的方式中,同步监视器可以考虑使用:this ;
    • 在继承Thread类的方式中,同步监视器 慎用 this,可以考虑使用:当前类.class 。

5.3 同步方法

  • 如果操作共享数据的代码完整的声明在了一个方法中,那么就可以将此方法声名为同步方法即可。
  • 非静态的同步方法,默认同步监视器是 this(要考虑是否唯一?);
  • 静态的同步方法,默认同步监视器是当前类本身。

5.3.1 非静态方法加锁

package com.mMultithreading.synMethod;

/**
 * 模拟车站售票:实现Runnable接口方式,实现卖票
 * 使用同步方法解决买票中的线程安全问题。
 * 3个窗口,共100张票
 */
class Sale implements Runnable {
    static int ticket = 100;
    static boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            show();
        }
    }

    /**
     * 方法由synchronized修饰后就变成了非静态同步方法
     * 此时的同步监视器默认是:this。此题目中即为`s`,它是唯一的。
     */
    public synchronized void show() {
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售票,票号为 " + ticket);
            ticket--;
        } else {
            flag = false;
        }
    }
}

public class Test5WindowIM {
    public static void main(String[] args) {
        Sale s = new Sale();
        Thread t1 = new Thread(s, "售票窗口1");
        Thread t2 = new Thread(s, "售票窗口2");
        Thread t3 = new Thread(s, "售票窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

5.3.2 静态方法加锁

package com.mMultithreading.synMethod;

/**
 * 模拟车站售票:继承Thread类方式
 * 使用同步方法解决买票中的线程安全问题。
 * 3个窗口,共100张票
 */
class Ticket extends Thread {
    static int ticket = 100;
    static boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            show();
        }
    }

    /**
     * 方法由synchronized修饰后就变成了静态同步方法
     * 此时的同步监视器默认是:Ticket.class(是一个对象),它是唯一的。
     */
    //public synchronized void show(){此时的同步监视器默认是:this。此题目中即为`t1,t2,t3`,它不是唯一的。
    public static synchronized void show(){
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售票,票号为 " + ticket);
            ticket--;
        } else {
            flag = false;
        }
    }
}

public class Test6WindowEX {
    public static void main(String[] args) {
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();
        Ticket t3 = new Ticket();
        t1.setName("售票窗口1");
        t2.setName("售票窗口2");
        t3.setName("售票窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

5.4 synchronized优弊

  • 优点:
    • 解决了现成的安全问题。
  • 弊端:
    • 在操作共享数据时,多线程其实是串行执行的,性能低。

5.5 练习

甲乙两人往一个账户各存3000元,分三次,每次存完输出余额,是否有线程安全问题?

package com.mMultithreading;

/**
 * 账户
 */
class Account {
    //余额
    private double balance;

    /**
     * @param amt 存款额
     * @description 存款。这里默认是this,本题中是acc,故是唯一
     */
    public synchronized void deposit(double amt) {
        if (amt > 0) {
            balance += amt;
        }
        System.out.println(Thread.currentThread().getName() +
                "存款" + amt + "元,余额为:" + balance);
    }
}

class Customer extends Thread {
    Account account;
    public Customer(Account acct) {
        this.account = acct;
    }
    public Customer(Account acct, String name) {
        super(name);
        this.account = acct;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.deposit(1000);
        }
    }
}

public class Test3Account {
    public static void main(String[] args) {
        Account acc = new Account();
        Customer c1 = new Customer(acc, "甲");
        Customer c2 = new Customer(acc, "乙");
        c1.start();
        c2.start();
    }
}

6.线程同步

线程安全的懒汉式、死锁

6.1单例模式之懒汉式的线程安全问题

饿汉式:不存在线程安全问题;
懒汉式:存在线程安全问题,需要使用同步机制来处理。

注意:方法3中,有指令重排问题

  • men = allocate(); 给单例对象分配内存空间
  • instance = mem; instance引用现在非空,但还未初始化
  • ctorSingleton( instance); 单例对象通过instance调用构造器
  • 从JDK2开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,然后复制到主存储区。但是需要volatile关键字,避免指令重排
package com.mMultithreadOther;

/**
 * 实现线程安全的 懒汉式
 */
class Bank { //银行
    private Bank() {
    }

    //instance:共享数据,地址值
    //volatile关键字作用:避免指令重排
    private static volatile Bank instance = null;

    //是先线程安全方式1:同步监视器,默认为Bank.class
    /*public static synchronized Bank getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Bank();
        }
        return instance;
    }*/

    //是先线程安全方式2:同步监视器,默认为Bank.class
    /*public static Bank getInstance() {
        //synchronized (this) {静态方法不能使用this
        synchronized (Bank.class) {
            if (instance == null) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                instance = new Bank();
            }
        }
        return instance;
    }*/

    /*是先线程安全方式3:相较于方式1和2,相率更高一些
    为了避免指令重排,将 instance 声明为 volatile */
    public static Bank getInstance() {
        if (instance==null){
            synchronized (Bank.class) {
                if (instance == null) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

public class SingletonLazy {
    static Bank b1 = null;
    static Bank b2 = null;

    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                b1 = Bank.getInstance();
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                b2 = Bank.getInstance();
            }
        };
        t1.start();
        t2.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1 == b2);
    }
}

6.2 同步机制带来的问题:死锁

  1. 如何为看待死锁?
    不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
    一旦出现死锁,整个程序既不会发生异常,也不会给出提示,只是所有线程处于阻塞状态,无法继续。

  2. 出现死锁的原因?
    以下四个条件,同时出现就会出现死锁

  • 互斥条件
  • 占用且等待
  • 不可抢夺(或不可抢占)
  • 循环等待
  1. 如何避免死锁?
    死锁一旦出现,基本很难人为干预,只能尽量规避,可以打破任意一个条件。为保证同步需要,只能对后面 3个条件进行破坏。
  • 针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题;
  • 针对条件2:可以考虑一次申请所有需要的资源,就不会出现等待的问题;
  • 针对条件3:占用部分资源的线程,进一步申请其他资源时,若申请不到,就主动释放掉已占用的资源
  • 针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样就避免了循环等待的问题

6.3.JDK5 Lock锁的使用

  • java.util.concurrent.locks(简称JUC,并发编程使用的包)
  • JDK5.0的新增功能,保证线程的安全。与采用synchronized相比,Lock可以提供多种锁方案,更灵活、更强大。Lock通过显式定义同步锁对象实现同步。同步锁使用Lock对象充当
  1. 面试题:synchronized同步的方式,与Lock的对比?
  • synchronized不管是同步代码块还是同步方法,都需要在结束一对“{}”之后,释放对同步监视器的调用;
  • Lock是通过调用两个方法控制需要被同步的代码,更灵活。
  • Lock作为接口,提供了多种实现类,适合更多复杂的场景,效率更高。
  1. 使用步骤:

① 创建Lock的实例,需要确保多个线程共用一个Lock实例。这里是继承Thread,需要考虑使用static修饰;
② 执行Lock方法,将共享资源加锁;
③ unlock()的调用,将共享资源释放

package com.mMultithreadOther;

import java.util.concurrent.locks.ReentrantLock;

/**
 * java.util.concurrent.locks
 * 模拟车站售票:实现Runnable接口方式,实现卖票
 * 使用 JDK1.5Lock(锁)解决买票中的线程安全问题。
 * 3个窗口,共100张票
 */
class Sale extends Thread {
    static int ticket = 100;
    //1.创建Lock的实例,需要确保多个线程共用一个Lock实例。这里是继承Thread,需要考虑使用static修饰
    private static final ReentrantLock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                //2.执行Lock方法,将共享资源加锁
                reentrantLock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号为 " + ticket);
                    ticket--;
                } else {
                    break;
                }
            } finally {
                //3.unlock()的调用,将共享资源释放
                reentrantLock.unlock();
            }
        }
    }
}

public class Test4Lock {
    public static void main(String[] args) {
        Sale s1 = new Sale();
        Sale s2 = new Sale();
        Sale s3 = new Sale();
        s1.setName("窗口1");
        s2.setName("窗口2");
        s3.setName("窗口3");
        s1.start();
        s2.start();
        s3.start();
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值