Java进阶学习6:【多线程、线程安全、线程状态、唤醒机制】

  • A+
所属分类:Java Java进阶

01. 多线程实现代码的回顾

/*
    多线程的实现方式
        1. 定义一个类,然后这个类继承Thread
        2. 在这个类中重写Thread类的run方法,并在run方法中定义线程要执行的任务。
        3. 创建这个Thread子类对象。
        4. 通过Thread子类对象调用start方法,启动线程,线程会执行run方法。
 */
public class Demo01Thread {
    public static void main(String[] args) {
        System.out.println("main...start");
        //创建这个Thread子类对象。
        MyThread mt = new MyThread();
        //通过Thread子类对象调用start方法
        mt.start();
        //输出100行HelloWorld
        for(int i = 0; i < 100; i++) {
            System.out.println("HelloWorld:" + i);
        }
    }
}

public class MyThread extends Thread{
    @Override
    public void run() {
        //输出
        for(int i = 0; i < 100; i++) {
            System.out.println("HelloJava:" + i);
        }
    }
}

02. 多线程的执行流程

参考 Java基础学习14:【线程、同步、线程状态】中的图

03. 多线程的内存图

参考 Java基础学习14:【线程、同步、线程状态】中的图

04. Thread中的常见方法

/*
    Thread中的常见方法:

    构造方法:
        Thread(): 空参数构造方法。
        Thread(String name): 参数需要一个字符串,这个字符串表示线程的名字

    其他方法:
        String getName(): 获取线程的名字。
        void setName(String name): 设置线程名字
        static Thread currentThread(): 获取当前的线程对象。
        static void sleep(long millis): 线程休眠, 参数为休眠的毫秒值。
 */
public class Demo01ThreadMethod {
    public static void main(String[] args) {
        //创建一个MyThread对象
        MyThread m = new MyThread("王叔叔");
        //void setName(String name): 设置线程名字
        //m.setName("狗蛋");
        m.start();
        //new MyThread().start();
        //new MyThread().start();
        //new MyThread().start();

        //static Thread currentThread(): 获取当前的线程对象。 该方法是通过哪个线程调用的,那么          获取到的就是哪个线程对象。
        Thread t = Thread.currentThread();
        System.out.println("main线程的名字是:" + t.getName());
    }
}
public class MyThread extends Thread{

    //提供一个构造方法,在这个构造方法中调用Thread一个参数是字符串的构造方法
    public MyThread(String name) {
        //Thread(String name): 参数需要一个字符串,这个字符串表示线程的名字
        super(name);
    }

    @Override
    public void run() {
        ////String getName(): 获取线程的名字。
        System.out.println(getName() + "执行了");
    }
}
/*
     static void sleep(long millis): 线程休眠, 参数为休眠的毫秒值。
 */
public class Demo02Sleep {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("开始");
        //让线程休眠5秒钟
        Thread.sleep(2000);
        System.out.println("结束");
    }
}

05. 多线程的第二种实现方式

/*
    多线程的第二种实现方式
        1. 定义一个类,然后实现Runnable接口。
        2. 重写Runnable接口中的run方法,要在run方法中定义线程要执行的任务。
        3. 创建Runnable实现类对象。
        4. 创建Thread线程对象, 并且将Runnable实现类对象作为参数传递。
        5. 调用线程对象的start方法,启动线程。
 */
public class Demo01RunnableTest {
    public static void main(String[] args) {
        //创建Runnable实现类对象。
        //MyTask并不是线程类,所以创建的并不是线程对象,MyTask表示的是线程要执行的任务。
        MyTask mt = new MyTask();
        //创建Thread线程对象, 并且将Runnable实现类对象作为参数传递。
        Thread t = new Thread(mt);//传递mt表示将来线程执行的是mt中的run方法
        //调用线程对象的start方法,启动线程。
        t.start();
        //在main线程中输出HelloWorld
        for(int i = 0; i < 100; i++) {
            System.out.println("main线程在输出HelloWorld:" + i);
        }
    }
}

/*
    MyTask继承了Runnable,但是MyTask他不是一个线程类。
    在Java中,Thread才表示线程类, 而这个MyTask和Thread没有关系,所以不是线程类。

    这个MyTask可以看成线程任务类, 因为实现了Runnable接口,Runnable接口中只有一个run方法,表示线程    要执行的任务。
 */
public class MyTask implements Runnable{
    //重写run方法,在run方法中定义线程要执行的任务。
    @Override
    public void run() {
        //输出100行HelloJava
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"输出HelloJava:" + i);
        }
    }
}

06. 多线程第二种实现方式的好处

第二种方式更好:
        1. 解决了Java中类与类之间只能单继承的局限性。
        2. 降低了耦合性(关联性, 降低了类与类之间的关联)
        3. Runnable接口中只有一个run方法,没有start,getName,sleep这些方法, Runnable接口的功            能更加纯粹,我们只需要在里面关注线程要执行的任务就可以了, 在设计模式中有一个原则是单一职责           原则, 实现接口的方式更加符合这个单一职责原则。
        4. 更加有利于实现资源的共享。

07. 匿名内部类实现多线程

/*
    匿名内部类: 临时定义某个子类,并创建该子类的对象。

    格式:
        new 父类或接口() {
            //要重写的方法
        }

    举例:
        new Person() {

        }
        这个代码创建的并不是Person对象,创建的是Person子类的对象, 但是这个子类到底是谁,我们不知         道,因为他是匿名的。
 */
public class Demo01Inner {
    public static void main(String[] args) {
        //继承Thread类 + 匿名内部类 完成多线程。
        Thread t = new Thread() {
            public void run() {
                System.out.println(Thread.currentThread().getName() + "线程执行了");
            }
        };
        //调用start方法,启动线程
        t.start();
        //继承Thread类 + 匿名内部类 + 匿名对象完成多线程
        new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "线程执行了");
            }
        }.start();

        //实现Runnable接口 + 匿名内部类实现多线程
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "线程执行了");
            }
        };

        new Thread(r).start();

        //实现Runnable接口 + 匿名内部类 + 匿名对象的方式实现多线程。
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "线程执行了");
            }
        }).start();

    }
}

08. 卖票案例引发的线程安全问题

/*
    电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个
    (本场电影只能卖100张票)。
    我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)
    需要窗口,采用线程对象来模拟;需要票,Runnable接口实现类来模拟

    我们使用三个线程一起卖票,一起卖100张票。
 */
public class Demo01TicketTest {
    public static void main(String[] args) {
        //创建Ticket对象
        Ticket t = new Ticket();
        //创建三个线程,执行卖票任务
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}
/*
    如果多个线程同时操作共享数据那么就有可能引发线程安全问题。
 */
public class Ticket implements Runnable{//因为Ticket实现了Runnable接口,所以Ticket也表示线程要执行的任务。
    //定义变量,表示票的数量
    int num = 100;
    //在run方法中定义线程要执行的任务, 线程要执行的任务是卖票。 只要有票,那么就一直卖。
    @Override
    public void run() {
        //因为线程要一直卖票,所以定义死循环
        while(true) {
            //进行判断,如果票的数量大于0,那么表示有票,那么就卖票
            if(num > 0) {
                //磨磨唧唧,掏身份证,用了20毫秒
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在卖票:" + num);
                num--;
            }
        }
    }
}

09. 线程安全问题出现的原因

参考 Java基础学习14:【线程、同步、线程状态】中的图

10. 同步代码块解决线程安全问题以及注意事项

/*
    synchronized 叫做同步,我们可以使用这个关键字来解决线程安全问题。
    synchronized可以修饰代码块,也可以修饰方法。
    如果synchronized修饰代码块,那么它就叫做同步代码块。

    格式;
        synchronized(锁对象) {
            //代码块内容
        }
        锁对象就是一个标记,没有其他作用,
        锁对象可以是任何对象, 可以是String,可以是ArrayList或者Person或者Student....

    作用:
        同步代码块可以保证只有持有锁的线程才能够进入到这个代码块中。

    同步代码块虽然可以保证线程安全,但是会牺牲效率。
 */
public class Demo02TicketTest {
    public static void main(String[] args) {
        //创建Ticket2对象
        Ticket2 t = new Ticket2();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}
public class Ticket2 implements Runnable{
    int num = 100;

    //创建一个对象,表示锁对象。 该对象仅仅起到一个标记的作用。
    //锁对象一定要是唯一的, 多个线程用的必须是同一个锁
    Object lock = new Object();

    @Override
    public void run() {
        while(true) {
            //当一个线程执行到synchronized同步代码块时,会先看一下这个同步代码块上面还有没有锁。
            //如果这个同步代码块上面还有锁, 那么这个线程就会获取到这个锁,然后进入到同步代码块。
            //如果这个同步代码块上面没有锁, 那么这个线程会一直在同步代码最开始位置等着, 什么时候有              了,什么时候才可能进去。
            synchronized(lock) {
                if(num > 0) {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖票:" + num);
                    num--;
                }
            }
            //当线程离开同步代码块, 那么线程会释放掉锁。
            //释放掉锁之后,其他线程就还可以去竞争这个锁,哪个线程抢到,那么哪个线程就可以进入到同步代            码块执行。
        }
    }
}

11. 同步代码块解决线程安全问题的分析

参考 Java基础学习14:【线程、同步、线程状态】中的图

12. 同步上厕所的原理

参考 Java基础学习14:【线程、同步、线程状态】中的图

13. 同步方法解决线程安全问题

/*
    如果synchronized修饰方法,那么这个方法就是一个同步方法,
    同步方法也可以解决线程安全问题。

    格式:
        修饰符 synchronized 返回值类型 方法名(参数列表) {
            方法体;
        }

    同步方法相当于把整个方法体都加了同步代码块。

    同步方法也是有锁的
        如果这个方法是非静态的, 那么锁对象是this
        如果这个方法是静态的,那么锁对象是类名.class(字节码对象,反射讲解)

    同步方法和同步代码块都可以解决线程安全问题:
        同步代码块的好处: 使用起来更加灵活。
        同步方法的好处:语法简洁。
 */
public class Demo03TicketTest {
    public static void main(String[] args) {
        //创建Ticket3对象
        Ticket3 t = new Ticket3();
        //创建线程,并卖票
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

public class Ticket3 implements Runnable{
    int num = 100;

    Object lock = new Object();
    @Override
    public void run() {
        while(true) {
            sell();
        }
    }

    //定义方法,用来卖票
    public void sell() {
        synchronized(this) {
            if(num > 0) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在卖票:" +num);
                num--;
            }
        }
    }

    //如果一个方法的整个方法体都加了同步代码块,那么我们就可以使用同步方法表示,
    public synchronized void sell2() {
        if (num > 0) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖票:" + num);
            num--;
        }
    }
}

14. Lock接口解决线程安全问题

/*
    在JDK5之后,多了一个Lock接口, 里面提供了一些方法可以手动的获取锁以及释放锁。
        void lock(): 获取锁。
        void unlock(): 释放锁。

    Lock接口常见的是类是 ReentrantLock
 */
public class Demo04TicketTest {
    public static void main(String[] args) {
        //创建Ticket4对象
        Ticket4 t = new Ticket4();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}
public class Ticket4 implements Runnable{
    int num = 100;
    //创建Lock对象,用来获取锁和释放锁
    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true) {
            //获取锁
            lock.lock(); //通过lock对象调用lock方法获取锁
            if(num > 0) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在卖票:" + num);
                num--;
            }
            //释放锁
            lock.unlock();//通过lock对象调用unlock手动释放锁
            ​}
        }
    }

15. 线程状态的介绍

/*
    线程的状态

    新建(NEW): 刚刚创建出来的线程对象处于此状态
    运行(RUNNABLE): 正在运行的线程处于此状态。(调用start方法后会变成运行)
    受阻塞(BLOCKED):等待获取锁的线程处于此状态。
    无限等待(WAITING): 当调用了wait()方法后,线程会变成无限等待。
    计时等待(TIMED_WAITING): 当调用sleep(毫秒值),wait(毫秒值)方法后会进入到计时等待
    退出(TERMINATED): 当线程的run方法执行完了,或者调用了线程的stop方法,线程会变成退出状态。
 */
public class Demo01ThreadState {
}

16. 线程的状态图

参考 Java基础学习14:【线程、同步、线程状态】中的图

17. wait以及notify方法的介绍

/*
    wait可以让线程等待, notify可以唤醒等待的线程,
    这两个方法一般用于线程通信, 或者称为等待唤醒机制。

    wait和notify方法是属于Object的,而不是Thread
        void wait(): 让线程等待,直到其他线程唤醒它。
        void wait(long timeout): 让线程等待, 知道其他线程唤醒它或者参数指定的时间(毫秒值)已过。
        void notify(): 唤醒一个线程。
        void notifyAll(): 唤醒所有的线程。

    上面的方法不能通过对象直接调用, 上面代码要放在同步代码中, 并且要通过锁对象进行调用。
    通过那个锁调用的notify方法,那么唤醒的就是通过哪个锁调用wait方法等待的线程。
 */

18. 等待唤醒机制案例的分析

参考 Java基础学习14:【线程、同步、线程状态】中的图

19. 等待唤醒机制代码的实现

public class Demo02Thread {
    public static void main(String[] args) throws InterruptedException {
        //创建包子对象
        BaoZi baoZi = new BaoZi();
        //创建包子铺对象
        BaoZiPu baoZiPu = new BaoZiPu(baoZi);
        //创建吃货对象
        ChiHuo chiHuo = new ChiHuo(baoZi);
        //新建线程并执行了。
        new Thread(baoZiPu).start();
        new Thread(chiHuo).start();
    }
}

public class BaoZiPu implements Runnable{
    //因为包子铺要用到包子,并且包子铺用的包子要和吃货用的包子是同一个包子,所以可以在成员位置定义          BaoZi, 并且将来再 从外界传递过来一个包子
    BaoZi baoZi;
    //定义构造方法,参数接收一个外界传递过来的BaoZi对象
    public BaoZiPu(BaoZi baoZi) {
        this.baoZi = baoZi;
    }
    //我们可以在run方法中定义包子铺要执行的任务。 包子铺要执行的任务是生产包子,并且等着吃货吃包子。
    @Override
    public void run() {
        //因为包子铺要一直生产包子,那么所以可以使用死循环
        while(true) {
            //因为包子铺和吃货操作的是同一个包子,所以要加同步,保证线程安全
            synchronized (baoZi) {//因为包子铺和吃货操作的是同一个包子,所以包子是唯一的,可以把                                    包子当成锁对象。
                //判断当前有没有包子, 如果有包子, 那么要等待吃货吃掉这个包子
                if(baoZi.flag) {
                    //条件成立表示有包子,那么包子铺就等待.
                    try {
                        baoZi.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //如果没有包子,如果没有包子,那么包子铺就生产包子
                System.out.println("包子铺生产了一个包子");
                //把标记改为true表示有包子了
                baoZi.flag = true;
                //通知吃货吃包子。
                baoZi.notify();
            }
        }
    }
}

public class ChiHuo implements Runnable{
    //吃货需要操作包子,并且操作的包子和包子铺生产的包子是同一个,所以这个包子对象要通过外界传递过来
    BaoZi baoZi;
    public ChiHuo(BaoZi baoZi) {
        this.baoZi = baoZi;
    }

    //我们要在run方法中定义吃货要执行的任务,吃货执行的任务是吃包子,并且等着包子铺生产包子。
    @Override
    public void run() {
        //因为吃货要一直吃包子,所以可以使用死循环。
        while(true) {
            //因为吃货吃的包子和包子铺操作的是同一个包子, 多个线程操作共享数据,需要加同步保证线程安              全。
            synchronized (baoZi) {
                //判断有没有包子,如果没有包子, 那么吃货就要等待
                if(!baoZi.flag) {
                    //如果条件成立,表示没有包子,那么我们就让吃货等待
                    try {
                        baoZi.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //如果有包子,那么吃货就吃包子
                System.out.println("吃货了一个包子");
                //把标记改为false,表示没有包子了.
                baoZi.flag = false;
                //调用notify方法唤醒包子铺,通知包子铺生产包子
                baoZi.notify();
            }
        }
    }
}
public class BaoZi {
    //定义变量,表示包子是否存在
    boolean flag = false;
}

20. 等待唤醒机制的执行流程分析

参考 Java基础学习14:【线程、同步、线程状态】中的图

  • 资源分享QQ群
  • weinxin
  • 官方微信公众号
  • weinxin
沙海
美女讲师教你学C语言
C语言郝斌老师教程
C语言项目源码分享
C语言速查手册

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: