java学习笔记——多线程

程序、进程、线程

总结:

复制代码
1.程序、进程和线程的区分:
程序(program):为完成特定任务,用某种语言编写的`一组指令的集合`。即指一段静态的代码。
进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。程序是静态的,进程是动态的。
进程作为操作系统调度和分配资源的最小单位。
线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径线程.
            作为CPU调度和执行的最小单位
2.线程调度策略
分时调度:所有线程轮流使用`CPU`的使用权,并且平均分配每个线程占用CPU的时间。

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

3.了解
> 单核CPU与多核CPU
> 并行与并发

相关代码:

java 复制代码
单线程
public class SingleThread {
    public void method1(String str){
        System.out.println(str);
    }
    public void method2(String str){
        method1(str);
    }
    public static void main(String[] args) {
        SingleThread s = new SingleThread();
        s.method2("hello!");
    }
}

线程的创建和使用

总结:

复制代码
1.线程的创建方式一:继承Thread类
1.1 步骤:
① 创建一个继承于Thread类的子类
② 重写Thread类的run()--->将此线程要执行的操作,声明在此方法体中
③ 创建当前Thread的子类的对象
④ 通过对象调用start(): 1.启动线程 2.调用当前线程的run()

1.2 例题:创建一个分线程1,用于遍历100以内的偶数
【拓展】再创建一个分线程2,用于遍历100以内的偶数

2.线程的创建方式二: 实现Runnable接口
2.1 步骤:
① 创建一个实现Runnable接口的类
② 实现接口中的run()-->将此线程要执行的操作,声明在此方法体中
③ 创建当前实现类的对象
④ 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
⑤ Thread类的实例调用start():1.启动线程 2.调用当前线程的run()

2.2 例题:创建分线程遍历100以内的偶数

3.对比两种方式?
    共同点: ① 启动线程,使用的都是Thread类中定义的start()
           ② 创建的线程对象,都是Thread类或其子类的实例。

    不同点: 一个是类的继承,一个是接口的实现。
            建议:建议使用实现Runnable接口的方式。
            Runnable方式的好处:① 实现的方式,避免的类的单继承的局限性
                             ② 更适合处理有共享数据的问题
                             ③ 实现了代码和数据的分离。

    联系:public class Thread implements Runnable (代理模式)

线程的创建方式一:继承Thread类

相关代码1:

java 复制代码
package com.atguigu01.create.thread;

/**
 * ClassName: EvenNumberTest
 * Package: com.atguigu01.create.thread
 * Description:
 * 例题:创建一个分线程1,用于遍历100以内的偶数
 * @Author: lwfstart
 * @Create 2025-03-27 20:27
 * @Version: 1.0
 */
//① 创建一个继承于Thread类的子类
class PrintNumber extends Thread {
    private double money = 1000;
    //② 重写Thread类的run()--->将此线程要执行的操作,声明在此方法体中
    public void run(){
        for (int i = 1; i <= 100; i++){
            if(i%2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
public class EvenNumberTest {
    public static void main(String[] args) {
        //③ 创建当前Thread的子类的对象
        PrintNumber t1 = new PrintNumber();
        //④ 通过对象调用start()
        t1.start();

        /**
         * 问题1:能否使用t1.run()替换 t1.start()的调用,实现分线程的创建和调用?不能!
         */
//        t1.run();
        /**
         * 问题2:在提供一个分线程,用于100以内的偶数的遍历
         *
         * 注意:不能让已经start()的线程,再次执行start(),否则报异常IllegalThreadStateException
         */
//        t1.start();
        PrintNumber t2 = new PrintNumber();
        //④ 通过对象调用start()
        t2.start();

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

线程的创建方式二: 实现Runnable接口

相关代码2:

java 复制代码
package com.atguigu01.create.runnable;

/**
 * ClassName: EvenNumberTest
 * Package: com.atguigu01.create.runnable
 * Description:
 * ① 创建一个实现Runnable接口的类
 * ② 实现接口中的run()-->将此线程要执行的操作,声明在此方法体中
 * ③ 创建当前实现类的对象
 * ④ 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
 * ⑤ Thread类的实例调用start():1.启动线程 2.调用当前线程的run()
 * @Author: lwfstart
 * @Create 2025-03-27 21:36
 * @Version: 1.0
 */
//① 创建一个实现Runnable接口的类
class EvenNumberPrint implements Runnable {
    private double money = 1000;
//    ② 实现接口中的run()-->将此线程要执行的操作,声明在此方法体中
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++){
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
public class EvenNumberTest {
    public static void main(String[] args) {
//        ③ 创建当前实现类的对象
        EvenNumberPrint p = new EvenNumberPrint();
//        ④ 将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
        Thread t1 = new Thread(p);
//        ⑤ Thread类的实例调用start():1.启动线程 2.调用当前线程的run()
        t1.start();
        //main()方法对应的主线程执行的操作
        for (int i = 1; i <= 100; i++){
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
        /**
         * 拓展:在创建一个线程,用于遍历100以内的偶数
         */
        Thread t2 = new Thread(p);
        t2.start();
    }
}

练习题1:

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

相关代码:

java 复制代码
class EvenNumberPrint extends Thread{ //用于打印偶数
    @Override
    public void run() {
        for(int i = 1; i <= 100; i++){
            if(i%2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
class OddNumberPrint extends Thread{ //用于打印奇数
    @Override
    public void run() {
        for(int i = 1; i <= 100; i++){
            if(i%2 != 0){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }
}
public class PrintNumberTest {//打印偶数
    public static void main(String[] args) {
        //方式1:
//        EvenNumberPrint t1 = new EvenNumberPrint();
//        OddNumberPrint t2 = new OddNumberPrint();
//        t1.start();
//        t2.start();
        //方式2:创建Thread类的匿名子类的匿名对象
//        new Thread(){
//            public void run() {
//                for(int i = 1; i <= 100; i++){
//                    if(i%2 == 0){
//                        System.out.println(Thread.currentThread().getName() + ":" + i);
//                    }
//                }
//            }
//        }.start();
//        new Thread(){
//            public void run() {
//                for(int i = 1; i <= 100; i++){
//                    if(i%2 != 0){
//                        System.out.println(Thread.currentThread().getName() + "--->" + i);
//                    }
//                }
//            }
//        }.start();

        //方式3:使用Runnable接口的方式:(提供了Runnable接口匿名实现类的匿名对象)
        new Thread(new Runnable(){
            public void run(){
                for(int i = 1; i <= 100; i++){
                    if(i%2 == 0){
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            }
        }).start();
        new Thread(new Runnable(){
            public void run() {
                for(int i = 1; i <= 100; i++){
                    if(i%2 != 0){
                        System.out.println(Thread.currentThread().getName() + "--->" + i);
                    }
                }
            }
        }).start();
    }
}

思考题:判断各自调用的是哪个run

java 复制代码
Exer
/**
 * ClassName: Exer
 * Package: com.atguigu01.create.exer2
 * Description:
 * 思考题:判断各自调用的是哪个run()?
 * @Author: lwfstart
 * @Create 2025-03-27 22:22
 * @Version: 1.0
 */
public class Exer {
    public static void main(String[] args) {
        A a = new A();
        a.start(); //① 启动线程 ② 调用Thread类的run()
        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;
//    public B(A a){ //构造器中,直接传入A类对象
//        this.a = a;
//    }
    public B(A a){
        super(a);
    }

    /**
     * 注释前:
     * 线程A的run()...
     * 线程B的run()...
     * 注释后:
     *  线程A的run()...
     *  线程A的run()...
     */
    @Override
    public void run() {
        System.out.println("线程B的run()...");
//        a.run(); //不用这种写法来表示代理模式
    }
}

Exer_1 另外一个练习题
public class Exer_1 {
    public static void main(String[] args) {
        // CC 执行的是继承于Thread子类的run方法,如果匿名子类当中没有方法的话,那就执行BB当中的run方法
//        BB b = new BB();
//        new Thread(b){
//            public void run(){
//                System.out.println("CC");
//            }
//        }.start();
        new Thread(){}.start();
    }
}
class AA extends Thread {
    @Override
    public void run() {
        System.out.println("AA");
    }
}
class BB extends Thread {
    @Override
    public void run() {
        System.out.println("BB");
    }
}

线程中的常用方法

线程的调度

线程的优先级

线程的分类

复制代码
if(i % 20 == 0){
    Thread.yield();
}

线程的生命周期

线程的生命周期,这个图会更好,面试回答的话

总结:

复制代码
一、线程的常用结构
1.线程中的构造器
> Thread():创建新的Thread对象
> Thread(String threadname):创建线程并指定线程实例名
> Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
> Thread(Runnable target, String name):创建新的Thread对象,分配一个带有指定目标新的线程对象并指定名字。
2.线程中的常用方法:
> start():①启动线程 ②调用线程的run()
> run():将线程要执行的操作,声明在run()中
> currentThread():获取当前执行代码对应的线程。
> getName():获取线程名
> setName():设置线程名
> sleep(long millis):静态方法,调用时,可以使得当前线程睡眠指定的毫秒数
> yield():静态方法,一旦执行此方法,就释放CPU的执行权
> join():在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻塞状态,继续执行
> isAlive():判断当前线程是否存活
过时方法:
> stop():强行结束一个线程的执行,直接进入死亡状态。不建议使用
> void suspend()/void resume():可能造成死锁,所以也不建议使用
3.线程的优先级:
getPriority():获取线程的优先级
setPriority():设置线程的优先级。范围[1,10]

Thread类内部声明的三个常量:
- MAX_PRIORITY(10):最高优先级
- MIN_PRIORITY(1):最低优先级
- NORM_PRIORITY(5):普通优先级,默认情况下main线程具有普通优先级。

二、线程的生命周期
java 复制代码
class PrintNumber extends Thread {
    public PrintNumber() {
    }

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

    public void run(){
        for (int i = 1; i <= 100; i++){
//            try {
//                Thread.sleep(1000);
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
            if(i%2 == 0){
                System.out.println(Thread.currentThread().getName() +
                        ":" + Thread.currentThread().getPriority() +
                        ":" + i);
            }
            if(i % 20 == 0){
                Thread.yield();
            }
        }
    }
}
public class EvenNumberTest {
    public static void main(String[] args) {
        PrintNumber t1 = new PrintNumber("线程1");
        t1.setName("子线程1");
        t1.setPriority(Thread.MIN_PRIORITY);
        t1.start();
        Thread.currentThread().setName("主线程");
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);

        for (int i = 1; i <= 100; i++){
            if(i%2 == 0){
                System.out.println(Thread.currentThread().getName() +
                        ":" + Thread.currentThread().getPriority() +
                        ":" + i + "***********************");
            }
            if(i == 20){
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        System.out.println("子线程1是否存活?" + t1.isAlive()); //子线程1是否存活?false
    }
}

练习题:

复制代码
新年倒计时
模拟新年倒计时,每隔1秒输出一个数字,依次输出10,9,8....1,最后输出:新年快乐!

代码:

java 复制代码
public class HappyNewYear {
    public static void main(String[] args) {
        for (int i = 10; i >= 0; i--) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if(i > 0) {
                System.out.println(i);
            }else {
                System.out.println("Happy New Year!");
            }
        }
    }
}

面试题:

java 复制代码
package com.atguigu02.method_lifecycle.interview;

/**
 * ClassName: ThreadTest
 * Package: com.atguigu02.method_lifecycle.interview
 * Description:
 * 关于Thread.sleep()方法的一个面试题:如下的代码中sleep()执行后,到底是哪个线程进入阻塞状态了呢?
 * @Author: lwfstart
 * @Create 2025-03-28 10:47
 * @Version: 1.0
 */
public class ThreadTest {
    public static void main(String[] args) {
        //创建线程对象
        MyThread t = new MyThread();
        t.setName("线程1");
        t.start();
        //调用sleep方法
        try {
            t.sleep(1000*5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //5秒之后这里才会执行
        System.out.println("hello World!");
    }
}
class MyThread extends Thread {
    public void run(){
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

线程安全问题和解决

总结:

复制代码
线程的安全问题与线程的同步机制
1.多线程卖票,出现的问题:出现了重票和错票。
2.什么原因导致的? 线程1操作ticket的过程中,尚未结束的情况下,其他线程也参与进来,对ticket进行操作。
3.如何解决?必须保证一个线程a在操作ticket的过程中,其它线程必须等待,直到线程a操作ticket结束以后,其它线程才可以进来
    继续操作ticket。
4.Java是如何解决线程的安全问题的? 使用线程的同步机制。
方式1: 同步代码块
synchronized(同步监视器){
    //需要被同步的代码
}
说明:
> 需要被同步的代码,即为操作共享数据的代码。
> 共享数据:即多个线程多需要操作的数据。比如:ticket
> 需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其它线程必须等待。
> 同步监视器,俗称锁。哪个线程获取了锁,哪个线程就能执行需要被同步的代码。
> 同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须共用同一个同步监视器,
注意:在实现Runnable接口的方式中,同步监视器可以考虑使用:this。
    在继承Thread类的方式中,同步监视器要慎用this。可以考虑使用当前类.class

方式2: 同步方法

说明:
> 如果操作共享数据的代码完整的声明在了一个方法中,那么我们就可以将此方法声明为同步方法即可。
> 非静态的同步方法,默认同步监视器是this
    静态的同步方法,默认同步监视器是当前类本身。

5.synchronized好处:解决了线程的安全问题。
    弊端:在操作共享数据时,多线程其实是串行执行的,意味着性能低。

存在安全问题时:

java 复制代码
/**
 * ClassName: WindowTest1
 * Package: com.atguigu03.threadsafe.notsafe
 * Description:
 * 实现继承Thread类的方式,实现卖票
 * 使用同步代码块的方式解决线程安全问题
 *
 * @Author: lwfstart
 * @Create 2025-03-28 08:18
 * @Version: 1.0
 */
class Window extends Thread {
    static int ticket = 100;
    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
                ticket--;
            } else {
                break;
            }
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();
        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");
        w1.start();
        w2.start();
        w3.start();
    }
}

/**
 * ClassName: WindowTest1
 * Package: com.atguigu03.threadsafe.notsafe
 * Description:
 * 实现继承Thread类的方式,实现卖票
 * 使用同步代码块的方式解决线程安全问题
 *
 * @Author: lwfstart
 * @Create 2025-03-28 08:18
 * @Version: 1.0
 */
class Window extends Thread {
    static int ticket = 100;
    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
                ticket--;
            } else {
                break;
            }
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();
        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");
        w1.start();
        w2.start();
        w3.start();
    }
}

线程的同步

线程安全问题的解决:

runnable实现接口方式

java 复制代码
同步代码块
/**
 * ClassName: WindowTest
 * Package: com.atguigu03.threadsafe.runnablesafe
 * Description:
 *
 * @Author: lwfstart
 * @Create 2025-03-28 09:29
 * @Version: 1.0
 */
class SaleTicket implements Runnable {
    int ticket = 100;
    Object obj = new Object();
    Dog dog = new Dog();
    public void run() {
//        synchronized(this){ //这样线程不安全
        while (true) {
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
//            synchronized(obj){ //obj:是唯一的?yes
//            synchronized(dog){ //dog:是唯一的?yes
//            synchronized(this){ //this:是唯一的?yes,就是题目中的对象s
            synchronized(SaleTicket.class){ //结构:Class clz = Window.class  是唯一的
                if(ticket > 0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
                    ticket--;
                }else {
                    break;
                }
            }
        }
    }
}
public class WindowTest {
    public static void main(String[] args) {
        SaleTicket s = new SaleTicket();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        Thread t3 = new Thread(s);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Dog{

}


同步方法
/**
 * ClassName: WindowTest
 * Package: com.atguigu03.threadsafe.runnablesafe
 * Description:
 * 使用同步方法结局实现Runnable接口的线程安全问题
 *
 * @Author: lwfstart
 * @Create 2025-03-28 09:29
 * @Version: 1.0
 */
class SaleTicket1 implements Runnable {
    int ticket = 100;
    boolean isFlag = true;

    public void run() {
        while (isFlag) {
            show();
        }
    }

    public synchronized void show() { // 此时的同步监视器就是:this,此题目是this,是唯一的
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
            ticket--;
        } else {
            isFlag = false;
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        SaleTicket1 s = new SaleTicket1();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        Thread t3 = new Thread(s);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

继承Thread类的方式

java 复制代码
同步代码块
/**
 * ClassName: WindowTest
 * Package: com.atguigu03.threadsafe.threadsafe
 * Description:
 *
 * @Author: lwfstart
 * @Create 2025-03-28 09:36
 * @Version: 1.0
 */
class Window extends Thread {
    static int ticket = 100;
    static Object obj = new Object();
    @Override
    public void run() {
        while (true) {
//            synchronized (this) { //this:此时表示w1,w2,w3,不能保证锁的唯一性
//              synchronized (obj) { //obj:使用static修饰以后,就能保证其唯一性
            synchronized (Window.class) { //结构:Class clz = Window.class  是唯一的
                if(ticket > 0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
                    ticket--;
                }else {
                    break;
                }
            }
        }
    }
}
public class WindowTest {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();
        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");
        w1.start();
        w2.start();
        w3.start();
    }
}

同步方法
/**
 * ClassName: WindowTest1
 * Package: com.atguigu03.threadsafe.threadsafe
 * Description:
 * 使用同步方法解决继承Thread类中的线程安全问题
 * @Author: lwfstart
 * @Create 2025-03-28 10:02
 * @Version: 1.0
 */
class Window1 extends Thread {
    static int ticket = 100;
    static boolean isFlag = true;

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

    //    public synchronized void show(){ //此时同步监视器:this。此题目中this:w1,w2,w3,仍然是线程不安全的
    public static synchronized void show() { //此时同步监视器:当前类本身,即为Window1.class,是唯一的
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
            ticket--;
        } else {
            isFlag = false;
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w1 = new Window1();
        Window1 w2 = new Window1();
        Window1 w3 = new Window1();
        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");
        w1.start();
        w2.start();
        w3.start();
    }
}

练习题:

复制代码
银行有一个账户。
有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题:该程序是否有安全问题,如果有,如何解决?
【提示】
1,明确哪些代码是多线程运行代码,须写入run()方法
2、明确什么是共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。


安全问题是共享的数据通过多线程的方式来操作

代码:

java 复制代码
public class AccountTest {
    public static void main(String[] args) {
        Account acct = new Account();
        Customer customer1 = new Customer(acct, "甲");
        Customer customer2 = new Customer(acct, "乙");
        customer1.start();
        customer2.start();
    }
}
class Account { //账户
    private double balance; //余额
    public synchronized void deposit(double amt) { //this:是不是唯一的?即为acct,是唯一的
        if(amt > 0){
            balance += amt;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName() + "存钱1000块,余额为:" + 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++) {
            account.deposit(1000);
        }
    }
}

同步机制中的锁

单例设计模式之懒汉式**(线程安全)**

总结:

复制代码
解决单例模式中的懒汉式的线程安全问题
> 饿汉式:不存在线程安全问题。
> 懒汉式:存在线程安全问题,((需要使用同步机制来处理)

代码:

java 复制代码
/**
 * ClassName: BankTest
 * Package: com.atguigu04.threadsafemore.singleton
 * Description:
 * 实现线程安全的懒汉式
 * @Author: lwfstart
 * @Create 2025-03-28 11:36
 * @Version: 1.0
 */
public class BankTest {
    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();
        //保证这两个线程都进来参与完成之后,才开始执行后面主线程的代码,防止出现了b1与b2为空null的情况
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1 == b2);
    }
}
class Bank {
    private Bank(){

    }
    private static volatile Bank instance = null;
    //实现线程安全的方式1
//    public static synchronized Bank getInstance(){ //同步监视器,默认为Bank.class
//        if(instance == null){
//            try {
//                Thread.sleep(1000);
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
//            instance = new Bank();
//        }
//        return instance;
//    }
    //实现线程安全的方式2
//    public static  Bank getInstance(){ //同步监视器,默认为Bank.class
//        synchronized (Bank.class){
//            if(instance == null){
//                try {
//                    Thread.sleep(1000);
//                } catch (InterruptedException e) {
//                    throw new RuntimeException(e);
//                }
//                instance = new Bank();
//            }
//            return instance;
//        }
//    }
    //实现线程安全的方式3:相较于方式1和方式2来讲,效率更高,为了避免出现指令重排,需要将instance声明为volatile
    //假如有三个线程,第一个和第二个线程进入时instance都为空,进入synchronized只能是第一个线程能进入,此时instance已经被new好了,
    //第二个线程就不会进入到synchronized中,第三个线程也就不会通过第一个if语句判断
    public static Bank getInstance(){
        if(instance == null){
            synchronized (Bank.class){
                if(instance == null){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

线程的死锁问题

总结:

复制代码
线程的同步机制带来的问题:死锁
1.如何看待死锁?
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
我们编写程序时,要避免出现死锁。
2.诱发死锁的原因?
- 互斥条件
- 占用且等待
- 不可抢夺(或不可抢占)
- 循环等待
3.如何避免死锁?
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源.。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

代码:

java 复制代码
public class DeadLoackTest {
    public static void main(String[] args) {
        /**
         * ab
         * 12
         * abcd
         * 1234
         */
        StringBuilder s1 = new StringBuilder();
        StringBuilder s2 = new StringBuilder();
        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
        //死锁:上面的线程第一个同步代码块中,s1被拿到了,然后sleep了一分钟
        //下面的线程第一个同步代码块中,s2被拿到了,然后sleep
        //上面的想成第二个同步代码块中要拿s2,下面的线程第二个同步代码块中要拿s1这个锁
        //两个线程都在等待拿到对方占有的同步资源
        new Thread(){
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
    }
}

代码2:

java 复制代码
class A {
    public synchronized void foo(B b){
        System.out.println("当前线程名:" + Thread.currentThread().getName()
                + " 进入了A实例的foo方法"); //②
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("当前线程名:" + Thread.currentThread().getName()
                + " 企图调用B实例的last方法"); //②
        b.last();
    }
    public synchronized void last(){
        System.out.println("进入了A类的last方法内部");
    }
}
class B {
    public synchronized void bar(A a){
        System.out.println("当前线程名:" + Thread.currentThread().getName() + " 进入了B实例的bar方法"); //②
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("当前线程名:" + Thread.currentThread().getName()
        + " 企图调用A实例的last方法"); //②
        a.last();
    }
    public synchronized void last(){
        System.out.println("进入了B类的last方法内部");
    }
}
public class DeadLock implements Runnable {
    A a = new A();
    B b = new B();
    public void init(){
        Thread.currentThread().setName("主线程");
        //调用a对象的foo方法
        a.foo(b);
        System.out.println("进入了主线程之后");
    }
    public void run(){
        Thread.currentThread().setName("副线程");
        //调用b对象的bar方法
        b.bar(a);
        System.out.println("进入了副线程之后");
    }
    public static void main(String[] args) {
        DeadLock dl = new DeadLock();
        new Thread(dl).start();
        dl.init();
    }
}

Lock()

总结:

复制代码
除了使用synchronized同步机制处理线程安全问题之外,还可以使用jdk5.0提供的Lock锁的方式
1.步骤:
1、创建lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将次对象声明为static final
2.执行lock方法,锁定对共享资源的调用
3.unlock()的调用,释放对共享数据的锁定

2.面试题:
synchronized同步的方式与Lock的对比?
synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监视器的调用。
Lock是通过两个方法控制需要被同步的代码,更灵活一些。
Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。

代码:

java 复制代码
import java.util.concurrent.locks.ReentrantLock;

/**
 * ClassName: WindowTest1
 * Package: com.atguigu03.threadsafe.notsafe
 * Description:
 * 实现继承Thread类的方式,实现卖票
 * 使用同步代码块的方式解决线程安全问题
 *
 * @Author: lwfstart
 * @Create 2025-03-28 08:18
 * @Version: 1.0
 */
class Window extends Thread {
    static int ticket = 100;
    //1、创建lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将次对象声明为static final
    private static final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            try {
                //2.执行lock方法,锁定对共享资源的调用
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }finally {
                //3.unlock()的调用,释放对共享数据的锁定
                lock.unlock();
            }
        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();
        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");
        w1.start();
        w2.start();
        w3.start();
    }
}

线程的通信

总结:

复制代码
1.线程间通信的理解
当我们`需要多个线程`来共同完成一件任务,并且我们希望他们有规律的执行」 那么多线程之间需要一些通信机制,
可以协调它们的工作,以此实现多线程共同操作一份数据。
2.涉及到三个方法的使用:
wait():线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用
notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。(如果被wait()的多个线程的优先级相同,
        随机唤醒一个)。
notifyALL(): 一旦执行此方法,就会唤醒所有被wait的线程,
3.注意点:
> 此三个方法的使用,必须是在同步代码块或同步方法中。
    (超纲:Lock需要配合Condition实现线程间的通信)
> 此三个方法的调用者,必须是同步监视器。否则,会报IllegalMonitorStateException异常
> 此三个方法声明在0bject类中。

4.案例:
案例1:使用两个线程打印 1-100。线程1,线程2 交替打印
案例2:生产者&消费者
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有
固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品
了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来
取走产品。

5.wait()和 sleep()的区别?
相同点:一旦执行,当前线程都会进入阻塞状态

不同点:
> 声明的位置:wait():声明在0bject类中
             sleep():声明在Thread类中,静态的
> 使用的场景不同:wait():只能使用在同步代码块或同步方法中
                sLeep():可以在任何需要使用的场景
> 使用在同步代码块或同步方法中:wait():一旦执行,会释放同步监视器
                            sleep():一旦执行,不会释放同步监视器
> 结束阻塞的方式:wait():到达指定时间自动结束阻塞 或 通过被notify唤醒,结束阻塞
                sleep():到达指定时间自动结束阻塞

代码:

java 复制代码
class PrintNmber implements Runnable {
    private int number = 1;
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
//            synchronized (this) {
            synchronized (obj) { //运行报错IllegalMonitorStateException
                obj.notify();
//                notify(); //省略this
                if (number <= 100) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    try {
//                        wait();
                        obj.wait(); //线程一旦 执行此方法就进入等到状态,会释放对同步监视器的调用
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                } else {
                    break;
                }
            }

        }
    }
}

public class PrintNumberTest {
    public static void main(String[] args) {
        PrintNmber p = new PrintNmber();
        Thread t1 = new Thread(p, "线程1");
        Thread t2 = new Thread(p, "线程2");
        t1.start();
        t2.start();
    }
}

面包消费问题:

java 复制代码
/**
 * ClassName: ProducerConsumerTest
 * Package: com.atguigu05.communication
 * Description:
 * 案例2:生产者&消费者生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有
 * 固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生
 * 产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
 *
 * 分析:
 * 1.是否是多线程问题? 是,生产者、消费者
 * 2.是否有共享数据?有!共享数据是:产品
 * 3.是否有线程安全问题? 有!因为有共享数据
 * 4.是否需要处理线程安全问题?是!如何处理?使用同步机制
 * 5.是否存在线程间的通信? 存在。
 *
 * @Author: lwfstart
 * @Create 2025-03-28 16:12
 * @Version: 1.0
 */
class Clerk { //店员
    private int productNum = 0; //产品的数量
    //增加产品数量的方法
    public synchronized void addProduct(){
        if(productNum >= 20){
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }else {
            productNum++;
            System.out.println(Thread.currentThread().getName() + "生产了第" + productNum + "个产品");
            //唤醒
            notifyAll();
        }
    }
    //减少产品数量的方法
    public synchronized void minusProduct(){
        if(productNum <= 0){
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }else {
            System.out.println(Thread.currentThread().getName() + "消费了第" + productNum + "个产品");
            productNum--;
            //唤醒
            notifyAll();
        }
    }
}
class Producer extends Thread { //生产者
    private Clerk clerk;
    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while (true) {
            System.out.println("生产者开始生产产品...");
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            clerk.addProduct();
        }
    }
}
class Consumer extends Thread { //消费者
    private Clerk clerk;
    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while (true) {
            System.out.println("消费者开始消费产品...");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            clerk.minusProduct();
        }
    }
}
public class ProducerConsumerTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer pro1 = new Producer(clerk);
        Consumer con1 = new Consumer(clerk);
        Consumer con2 = new Consumer(clerk);
        pro1.setName("生产者1");
        con1.setName("消费者1");
        con2.setName("消费者2");
        pro1.start();
        con1.start();
        con2.start();
    }
}

JDK5.0****新增线程创建方式

新增方式一:实现Callable接口

新增方式二:使用线程池

总结:

复制代码
1.创建多线程的方式三:实现Callable(jdk5.0新增的)
与之前的方式的对比:与Runnable方式的对比的好处
> call()可以有返回值,更灵活
> call()可以使用throws的方式处理异常,更灵活
> Callable使用了泛型参数,可以指明具体的call()的返回值类型,更灵活

有缺点吗?如果在主线程中需要获取分线程ca11()的返回值,则此时的主线程是阻塞状态的。

2.创建多线程的方式四:使用线程池

此方式的好处:
> 提高了程序执行的效率。(因为线程已经提前创建好了)
> 提高了资源的复用率。(因为执行完的线程并未销毁,而是可以继续执行其他的任务)
> 可以设置相关的参数,对线程池中的线程的使用进行管理

代码:

java 复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/**
 * ClassName: Callable
 * Package: com.atguigu06.createmore
 * Description:
 * 创建多线程的方式三:实现Callable(jdk5.0新增的)
 * @Author: lwfstart
 * @Create 2025-03-28 17:41
 * @Version: 1.0
 */
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for(int i = 1; i <= 100; i++) {
            if(i%2==0){
                System.out.println(i);
                sum += i;
            }
            Thread.sleep(100);
        }
        return sum;
    }
}
public class CallableTest {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        Thread t1 = new Thread(futureTask);
        t1.start();
        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

代码2:

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * ClassName: ThreadPool
 * Package: com.atguigu06.createmore.pool
 * Description:
 * 创建并使用多线程的第四种方法:使用线程池
 * @Author: lwfstart
 * @Create 2025-03-28 19:16
 * @Version: 1.0
 */
class NumberThread implements Runnable {
    public void run(){
        for(int i = 0;i <= 100; i++){
            if(i%2==0){
                System.out.println(Thread.currentThread().getName() + ":"+i);
            }
        }
    }
}
class NumberThread1 implements Runnable {
    public void run(){
        for (int i = 1; i <= 100; i++){
            if(i%2 != 0){
                System.out.println(Thread.currentThread().getName() + ":"+i);
            }
        }
    }
}
public class ThreadPool {
    public static void main(String[] args) {
        //1.提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        System.out.println(service.getClass()); //设置线程池中线程数的上线
        service1.setMaximumPoolSize(50); //设置线程池中线程数的上限
        //2.执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread()); //适合适用于Runnable
        service.execute(new NumberThread1()); //适合适用于Runnable

        //service.submit(Callable callable);//适合适用于Callable
        //3.关闭连接池
        service.shutdown();

    }
}
相关推荐
珹洺1 小时前
C++从入门到实战(十)类和对象(最终部分)static成员,内部类,匿名对象与对象拷贝时的编译器优化详解
java·数据结构·c++·redis·后端·算法·链表
一 乐1 小时前
网红酒店|基于java+vue的网红酒店预定系统(源码+数据库+文档)
java·开发语言·数据库·毕业设计·论文·springboot·网红酒店预定系统
xyliiiiiL3 小时前
从责任链模式聊到aware接口
java·开发语言
码农老起6 小时前
与Aspose.pdf类似的jar库分享
java·pdf·jar
程序猿小D6 小时前
第三百八十九节 JavaFX教程 - JavaFX WebEngine
java·eclipse·intellij-idea·vr·javafx
lwewan7 小时前
26考研——栈、队列和数组_数组和特殊矩阵(3)
数据结构·笔记·考研·算法
self-discipline6348 小时前
【Java】Java核心知识点与相应面试技巧(七)——类与对象(二)
java·开发语言·面试
wei3872452328 小时前
java笔记02
java·开发语言·笔记
nuc-1278 小时前
sqli-labs学习记录8
数据库·学习·sqli-labs