【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)

项目代码

https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter17/src/com/yinhai

线程

一、线程相关概念

1.程序

是为完成特定任务、用某种语言编写的一组指令的集合。简单的说:就是我们写的代码
 

2.进程

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第1张图片

        1)进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。

        2)进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程

3.线程

        1)线程由进程创建的,是进程的一个实体

        2)一个进程可以拥有多个线程, 如下图

        3)并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单的说,单核cpu实现的多任务就是并发。

        4)并行:同个时刻,多个任务同时执行。多核cpu可以实现并行。并发和并行可能都会同时存在

java查看cpu核数

public class CpuNum {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        int cpuNum = runtime.availableProcessors();
        System.out.println(cpuNum);
    }
}

二、线程基本使用

1.创建线程的两个方式

在java中线程来使用有两种方法。

        1)继承Thread类,重写run方法

        2)实现Runnable接口,重写run方法

        【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第2张图片

2.Thread类使用案例

1)请编写程序,开启一个线程,该线程每隔1秒。在控制台输出"喵喵, 我是小猫咪".

2)对上题改进:当输出8次喵喵,我是小猫咪,结束该线程

3)使用JConsole监控线程执行情况,并画出程序示意图

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第3张图片

        【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第4张图片

当主线程结束了,可能还有其他的子线程,进程可能并没有结束,会跑完子线程,直到子线程也挂掉了,进程就挂掉了

public class Thread01 {
    public static void main(String[] args) {
        //创建一个Cat对象当做线程使用
        Cat cat = new Cat();
        cat.start();
        //当main线程启动一个子线程 Thread-0主线程不会阻塞,会继续执行
        //这时我们的主线程和Thread 0线程都在执行
        for (int i = 1; i <= 10; i++) {
            System.out.println("主线程在执行中" + i + " " + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//当一个类继承了Thread类,该类就可以当做线程使用
class Cat extends Thread{
    int time = 0;
    @Override
    public void run() {//往往要重写run方法,Thread类实现了Runnable的接口的run方法
        while (true){
            System.out.println("喵喵" + ++time + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(time == 80){
                break;//当time到8次,线程就会退出
            }
        }
    }
}

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第5张图片
补充,为什么使用cat.start,再由该方法调用run呢,而不是使用cat.run呢,很好理解,因为如果直接调用run方法,主线程并不会并列执行该方法,而是由主线程main去调用run方法,会导致没有实现多线程,也就是阻塞下面的语句,这种调用不叫多线程

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第6张图片

3.Thread类源码

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第7张图片
 【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第8张图片

可以看到 这里的start0是本地方法由JVM调用的,而我们重写了该方法的内容,所以准确来说是JVM实现了多线程而不是run方法实现多线程

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第9张图片

4.Runnable接口

1. java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类方法创建线程显然不可能了。

2. java设计者们提供了另外一个方式创建线程,就是通过实现Runnable接口来创建线程,实际上底层使用了设计模式[代理模式] => 代码模拟

public class Thread02 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        //dog.start(); 不能直接调用start方法,该接口只有run方法
        Thread thread = new Thread(dog);//创建一个Thread对象,把实现了dog对象放入Thread
        thread.start();
       // Tiger tiger = new Tiger();//实现了 Runnable
       // ThreadProxy threadProxy = new ThreadProxy(tiger);
       // threadProxy.start();
    }
}

class Dog implements Runnable{//实现Runnable接口开发线程
    int time = 0;
    @Override
    public void run() {
        while (true) {
            System.out.println("汪汪" + ++time + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (time == 10){
                break;
            }
        }
    }
}

3.模拟设计代理模式

        可以简单理解为,两个类一个ThreadProxy和Tiger都实现了Runnable类,在构造器的时候传入tiger到代理对象内,此时使用代理对象的start方法就可以动态绑定到我们传入的targer的run方法

public class Thread02 {
    public static void main(String[] args) {
        Tiger tiger = new Tiger();//实现了 Runnable
        ThreadProxy threadProxy = new ThreadProxy(tiger);
        threadProxy.start();
    }
}
//线程代理类 , 模拟了一个极简的Thread类
class ThreadProxy implements Runnable {//你可以把Proxy类当做 ThreadProxy

    private Runnable target = null;//属性,类型是 Runnable

    @Override
    public void run() {
        if (target != null) {
            target.run();//动态绑定(运行类型Tiger)
        }
    }

    public ThreadProxy(Runnable target) {
        this.target = target;
    }

    public void start() {
        start0();//这个方法时真正实现多线程方法
    }

    public void start0() {
        run();
    }
}
class Animal {
}

class Tiger extends Animal implements Runnable {

    @Override
    public void run() {
        System.out.println("老虎嗷嗷叫....");
    }
}

5.线程使用的基本案例

请编写一个程序,创建两个线程,一个线程每隔1秒输出"hello,world" ,输出10次,退出,一个线程每隔1秒输出"hi" ,输出5次退出,实现Runnable的方式来完成

public class Thread03 {
    public static void main(String[] args) {
        T1 t1 = new T1();
        T2 t2 = new T2();
        Thread thread = new Thread(t1);
        Thread thread1 = new Thread(t2);
        thread.start();
        thread1.start();
    }
}

class T1 implements Runnable {

    @Override
    public void run() {
        int i = 0;
        while (true) {
            System.out.println("hello,world" + ++i + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i == 10) {
                break;
            }
        }
    }
}

class T2 implements Runnable {
    int i = 0;

    @Override
    public void run() {
        while (true) {
            System.out.println("hi" + ++i + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (i == 5) {
                break;
            }
        }
    }
}

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第10张图片

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第11张图片

6.继承Thread 和 实现Runnable的区别

        1) 从java的设计来看,通过继承Thread或者实现Runnable接口来创建线程本质上没有区别,从jdk帮助文档我们可以看到Thread类本身就实现了Runnable接口

        2)实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制

三、模拟三个售票窗口

        继承Thread类创建三个对象去调用strat方法会出现一个很严重的问题,多线程冲突,都跑去--ticketNum去了,会出现来不及减完就会超卖的现象

public class SellTicket {
    public static void main(String[] args) {
        SellTicket01 sellTicket01 = new SellTicket01();
        SellTicket01 sellTicket02 = new SellTicket01();
        SellTicket01 sellTicket03 = new SellTicket01();
        sellTicket01.start();
        sellTicket02.start();
        sellTicket03.start();
    }
}
class SellTicket01 extends Thread{
    private static int ticketNum = 100;
    @Override
    public void run() {
        while (true){
            if(ticketNum <= 0){
                System.out.println("售票结束");
                break;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("该窗口" + Thread.currentThread().getName() + "卖出了一张票,剩余 " + --ticketNum + "张");
        }
    }
}

        【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第12张图片        

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第13张图片

        使用Runnable类也会出现上面的情况,实际上两个实现的方式本质都一样

public class SellTicket {
    public static void main(String[] args) {
        //使用实现Runnable接口方式
        SellTicket02 sellTicket002 = new SellTicket02();
        new Thread(sellTicket002).start();
        new Thread(sellTicket002).start();
        new Thread(sellTicket002).start();
    }
}

class SellTicket02 implements Runnable{
    private int ticketNum = 100;
    @Override
    public void run() {
        while (true){
            if(ticketNum <= 0){
                System.out.println("售票结束");
                break;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("该窗口" + Thread.currentThread().getName() + "卖出了一张票,剩余 " + --ticketNum + "张");
        }
    }
}

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第14张图片

留下一个问题,怎么去解决超卖现象呢?看线程同步机制的具体方法内的改善

四、线程终止(通知方式

        1.当线程完成任务后,会自动退出

        2.还可以通过使用变量来控制run方法退出的方式停止线程,即通知方式

public class ThreadExit_ {
    public static void main(String[] args) {
        T t = new T();
        t.start();
        System.out.println("主线程休眠5秒");
        //如果希望主线程去控制t1的线程终止,就需要修改loop变量
        //让t1退出run方法,从而终止t1线程 - > 通知方式
        try {//让主线程休眠10秒再退出Thread线程
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.setLoop(false);
    }
}
class T extends Thread{
    private int count = 0;

    //设置一个控制变量
    private boolean loop = true;
    @Override
    public void run() {
        while (loop) {

            try {
                Thread.sleep(50);// 让当前线程休眠50ms
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("T 运行中...." + (++count));
        }

    }

    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

五、线程的常用方法

第一组(常用方法)

1. setName //设置线程名称,使之与参数name 相同

2. getName //返回该线程的名称

3. start //使该线程开始执行; Java 虚拟机底层调用该线程的startO方法

4. run //调用线程对象 run方法;

5. setPriority //更改线程的优先级

6. getPriority /获取线程的优先级

7. sleep//在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

8. interrupt //中断线程

1.方法注意事项

1. start底层会创建新的线程,调用run, run 就是一个简单的方法调用, 不会启动新线程

2.线程优先级的范围为1-10

/** * The minimum priority that a thread can have. */ 
public final static int MIN_PRIORITY = 1; 

/** * The default priority that is assigned to a thread. */ 
public final static int NORM_PRIORITY = 5; 

/** * The maximum priority that a thread can have. */ 
public final static int MAX_PRIORITY = 10;

3. interrupt:中断线程,但并没有真正的结束线程。所以一般用于中断正在休眠线程

        Thread 线程在执行任务时会周期性地检查中断状态。当 Thread.interrupt() 被调用时,线程会在适当的时机检测到中断状态并终止执行。

        Thread.currentThread().isInterrupted();获取当前是否Interrupted

        需要注意的是,interrupt() 方法并不会强制终止线程,而是提供了一种协作的机制,线程可以根据中断状态自行决定是否终止。

        如果线程不检查中断状态,或者没有处理 InterruptedException 异常,它可能会继续执行。

        所以在下列代码中,我们catch了InterruptedException 异常,但并没有退出线程的操作导致程序继续执行,也就是中断了线程的休眠状态

4. sleep:线程的静态方法,使当前线程休眠

public class ThreadMethod01 {
    public static void main(String[] args) throws InterruptedException {
        //测试相关的方法
        T t = new T();
        t.setName("yinhai");
        t.setPriority(Thread.MIN_PRIORITY);//1
        t.start();//启动子线程


        //主线程打印5 hi ,然后就中断 子线程的休眠
        for(int i = 1; i <= 5; i++) {
            Thread.sleep(1000);
            System.out.println("hi " + i);
        }

        System.out.println(t.getName() + " 线程的优先级 =" + t.getPriority());//1

        t.interrupt();//当执行到这里,就会中断 t线程的休眠.



    }
}

class T extends Thread { //自定义的线程类
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 100; i++) {
                //Thread.currentThread().getName() 获取当前线程的名称
                System.out.println(Thread.currentThread().getName() + "  吃包子~~~~" + i);
            }
            try {
                System.out.println(Thread.currentThread().getName() + " 休眠中~~~");
                Thread.sleep(20000);//20秒
            } catch (InterruptedException e) {
                //当该线程执行到一个interrupt 方法时,就会catch 一个 异常, 可以加入自己的业务代码
                //InterruptedException 是捕获到一个中断异常.
                System.out.println(Thread.currentThread().getName() + "被 interrupt了");
            }
        }
    }
}

                【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第15张图片

第二组(yield、join)

1. yield:线程的礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功。本意上也是让出cpu给其他线程插队

        【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第16张图片

2. join:线程的插队。插队的线程一旦插队成功(一定会成功),则肯定先执行完插入的线程所有的任务。

        【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第17张图片

案例:创建一个子线程,每隔1s输出hello,输出10 次,主线程每隔1秒,输出hi,输出10次.要求:两个线程同时执行,当主线程输出5次后,就让子线程运行完毕,主线程再继续。

public class ThreadMethod02 {
    public static void main(String[] args) throws InterruptedException {

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

        for(int i = 1; i <= 10; i++) {
            Thread.sleep(1000);
            System.out.println("主线程(小弟) 吃了 " + i  + " 包子");
            if(i == 5) {
                System.out.println("主线程(小弟) 让 子线程(老大) 先吃");
                //join, 线程插队
                t2.join();// 这里相当于让t2 线程先执行完毕
                // Thread.yield();//礼让,不一定成功..
                System.out.println("线程(老大) 吃完了 主线程(小弟) 接着吃..");
            }
        }
    }
}

class T2 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            try {
                Thread.sleep(1000);//休眠1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子线程(老大) 吃了 " + i +  " 包子");
        }
    }
}

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第18张图片

如果是礼让yield不一定成功

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第19张图片

课堂练习

public class ThreadMethodExercise {
    public static void main(String[] args) throws InterruptedException {
        Thread t3 = new Thread(new T3());//创建子线程
        for (int i = 1; i <= 10; i++) {
            System.out.println("hi " + i);
            if(i == 5) {//说明主线程输出了5次 hi
                t3.start();//启动子线程 输出 hello...
                t3.join();//立即将t3子线程,插入到main线程,让t3先执行
            }
            Thread.sleep(1000);//输出一次 hi, 让main线程也休眠1s
        }
        System.out.println("主线程结束");
    }
}

class T3 implements Runnable {
    private int count = 0;

    @Override
    public void run() {
        while (true) {
            System.out.println("hello " + (++count));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 10) {
                System.out.println("子线程结束");
                break;
            }
        }
    }
}

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第20张图片

第三组(用户线程和守护线程)

1.用户线程:也叫工作线程,以当线程的任务执行完成通知方式来结束

2.守护线程:一般是为工作线程服务的,当所有的用户线程结束守护线程自动结束

3.常见的守护线程:垃圾回收机制

public class ThreadMethod03 {
    public static void main(String[] args) throws InterruptedException {
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        //如果我们希望当main线程结束后,子线程自动结束,只需将子线程设为守护线程即可
        myDaemonThread.setDaemon(true);//该线程设置为守护线程
        myDaemonThread.start();

        for( int i = 1; i <= 10; i++) {//main线程
            System.out.println("宝强在辛苦的工作...");
            Thread.sleep(1000);
        }
    }
}

class MyDaemonThread extends Thread {
    public void run() {
        for (; ; ) {//无限循环
            try {
                Thread.sleep(1000);//休眠1000毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("马蓉和宋喆快乐聊天,哈哈哈~~~");
        }
    }
}

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第21张图片

六、线程的生命周期

        官方API文档指出有6种状态

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第22张图片

七个则是Runnable里的可执行的里的细分状态,Ready和Running状态,取决于调度器选中执行,不是start()后就立马执行,要取决底层调度器是否执行该线程

线程状态图

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第23张图片

public class ThreadState_ {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        System.out.println(t.getName() + " 状态 " + t.getState());
        t.start();

        while (Thread.State.TERMINATED != t.getState()) {
            System.out.println(t.getName() + " 状态 " + t.getState());
            Thread.sleep(500);
        }

        System.out.println(t.getName() + " 状态 " + t.getState());

    }
}
class T extends Thread {
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 10; i++) {
                System.out.println("hi " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            break;
        }
    }
}

线程同步机制

一、线程同步机制

1.在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

        【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第24张图片

2.也可以这里理解:线程同步,即当有一个线程在对内存进行操作时其他线程都不可以对这个内存地址进行操作直到该线程完成操作其他线程才能对该内存地址进行操作。
 

二、线程同步具体方法

1.Synchronized

        1)同步代码块synchronized (对象) { //得到对象的锁,才能操作同步代码

                //需要被同步代码;

        }

        2) synchronized还可以放在方法声明中, 表示整个方法为同步方法

                public synchronized void m (String name){

                        //需要被同步的代码

                }

        3)如何理解:就好像某小伙伴上厕所前先把门关上(上锁),完事后再出来(解锁),那么其它小伙伴就可在使用厕所了

        4)使用synchronized解决售票问题

public class SellTicket {
    public static void main(String[] args) {
        SellTicket02 sellTicket002 = new SellTicket02();
        new Thread(sellTicket002).start();
        new Thread(sellTicket002).start();
        new Thread(sellTicket002).start();
    }
}
//实现接口方法,使用synchronized实现线程同步
class SellTicket02 implements Runnable{
    private int ticketNum = 100;

    public synchronized boolean sellTick(){//同步方法,同一时间只有一个线程来执行sellTick方法

            if(ticketNum <= 0){
                System.out.println("售票结束");
                return false;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("该窗口" + Thread.currentThread().getName() + "卖出了一张票,剩余 " + --ticketNum + "张");
            return true;
    }
    @Override
    public void run() {
        while (sellTick()){
        }
    }
}

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第25张图片

注意 这时候运行状态不一定都会平均分配到到每一个线程上,因为是不公平锁,三个都在抢这个锁,谁抢到就是谁的,分析同步原理看下

三、分析同步原理

三个进程去抢这把锁,当有一个抢到之后,进入run方法输出打印车票,然后归还锁到对象上,随后加入Blocked进程状态三个一起抢这把锁,一直重复执行到结束,没抢到的进入Blocked状态,抢到的进入Runnable状态执行后退出

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第26张图片

四、互斥锁

1)Java在Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。

2)每个对象都对应于一个可称为"互斥锁”的标记,这个标记用来保证在任一一时刻,只能有一个线程访问该对象。

3)关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问

4)同步的局限性:导致程序的执行效率要降低

5)同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)

6)同步方法(静态的)的锁为当前类本身。

public class SellTicket {
    public static void main(String[] args) {
        SellTicket02 sellTicket002 = new SellTicket02();
        Thread thread1 = new Thread(sellTicket002);
        Thread thread2 = new Thread(sellTicket002);
        Thread thread3 = new Thread(sellTicket002);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

//实现接口方法,使用synchronized实现线程同步
class SellTicket02 implements Runnable {
    private int ticketNum = 10;
    Object object = new Object();
    //6)同步方法(静态的)的锁为当前类本身。
    //1. public synchronized static void m1() {} 锁是加在 SellTicket03.class

    public synchronized static void m1() {

    }
    //2. 如果在静态方法中,要实现一个同步代码块,需要像以下方式处理
    public static void m2() {
        synchronized (SellTicket02.class) {
            System.out.println("m2");
        }
    }
    //5)同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
    //同步方法,同一时间只有一个线程来执行sellTick方法
    public /*synchronized*/ boolean sellTick() {
        //2.这时锁在this对象,也可以在代码块上写synchronize,同步代码块
        synchronized (/*this*/ object) {
            if (ticketNum <= 0) {
                System.out.println("售票结束");
                return false;
            }
            System.out.println("该窗口" + Thread.currentThread().getName() + "卖出了一张票,剩余 " + --ticketNum + "张");

            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return true;
        }
    }

    @Override
    public void run() {

        while (sellTick()) {
        }
    }

    public int getTicketNum() {
        return ticketNum;
    }
}

1.注意事项

1.同步方法如果没有使用static修饰:默认锁对象为this

2.如果方法使用static修饰,默认锁对象:当前类.class

3.实现的落地步骤:

        需要先分析上锁的代码

        选择同步代码块或同步方法(如果可以最好用代码块,作用域小)

        要求多个线程的锁对象为同一个即可!

//使用Thread方式
// new SellTicket01().start();
// new SellTicket01().start();
class SellTicket01 extends Thread {

    private static int ticketNum = 100;//让多个线程共享 ticketNum

    //这样锁不住,无法保证锁的对象是同一个
    public void m1() {
        synchronized (this) {
            System.out.println("hello");
        }
    }
}

五、线程的死锁

1.基本介绍

        多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程是一定要避免死锁的发生.

2.模拟线程死锁

public class DeadLock_ {
    public static void main(String[] args) {
        //模拟死锁现象
        DeadLockDemo A = new DeadLockDemo(true);
        A.setName("A线程");
        DeadLockDemo B = new DeadLockDemo(false);
        B.setName("B线程");
        A.start();
        B.start();
    }
}


//线程
class DeadLockDemo extends Thread {
    static Object o1 = new Object();// 保证多线程,共享一个对象,这里使用static
    static Object o2 = new Object();
    boolean flag;

    public DeadLockDemo(boolean flag) {//构造器
        this.flag = flag;
    }

    @Override
    public void run() {

        //下面业务逻辑的分析
        //1. 如果flag 为 T, 线程A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁
        //2. 如果线程A 得不到 o2 对象锁,就会Blocked
        //3. 如果flag 为 F, 线程B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁
        //4. 如果线程B 得不到 o1 对象锁,就会Blocked
        if (flag) {
            synchronized (o1) {//对象互斥锁, 下面就是同步代码
                System.out.println(Thread.currentThread().getName() + " 进入1");
                synchronized (o2) { // 这里获得li对象的监视权
                    System.out.println(Thread.currentThread().getName() + " 进入2");
                }

            }
        } else {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + " 进入3");
                synchronized (o1) { // 这里获得li对象的监视权
                    System.out.println(Thread.currentThread().getName() + " 进入4");
                }
            }
        }
    }
}

        【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第27张图片

线程不是并行的 有可能出现A线程拿完锁走完了才到B线程,也很好的反应了锁的单独拿的性质

        【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第28张图片

六、释放锁

1.释放锁的条件

1)当前线程的同步方法、同步代码块执行结束

        案例:上厕所, 完事出来

2)当前线程在同步代码块、同步方法中遇到break、 return。 

        案例:没有正常的完事,经理叫他修改bug,不得已出来

3)当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束

        案例:没有正常的完事,发现忘带纸,不得已出来

4)当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

        案例:没有正常完事,觉得需要酝酿下,所以出来等会再进去

2.下面的操作不会释放锁

1)线程执行同步代码块或同步方法时,程序调用Thread.sleep()、 Thread.yield()方法暂停当前线程的执行,不会释放锁

        案例:上厕所, 太困了,在坑位上眯了一会

2)线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁。

        提示:应尽量避免使用suspend()和resume()来控制线程,这两个方法不再推荐使用

        【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第29张图片

本章作业

1.通知方式完成线程终止

(1)在main方法中启动两个线程

(2)第1个线程循环随机打印100以内的整数

(3)直到第2个线程从键盘读取了"Q" 命令。

为什么不用Listener,因为KeyListener 主要是用于监听和处理用户在图形用户界面 (GUI) 中的键盘输入事件。在 GUI 应用程序中,事件驱动编程模型允许程序在用户与应用程序界面交互时响应输入事件。

在控制台应用程序中,使用 KeyListener 不是常见的做法,因为控制台通常不提供直接的键盘事件处理功能,也没有 GUI 元素来接收键盘输入事件。控制台应用程序通常是基于命令行的,它们等待用户输入,然后以文本形式处理用户的命令或数据。

public class Homework01 {
    public static void main(String[] args) {
        PrintNum printNum = new PrintNum();
        new Thread(printNum).start();
        new Thread(new MyListener(printNum)).start();
    }
}

class PrintNum implements Runnable{
    private boolean loop = true;//通知方式,需要定义一个变量接受退出指令

    public void setLoop(boolean loop) {
        this.loop = loop;
    }

    @Override
    public void run() {
        while (loop) {
            synchronized (this) {
                try {
                    System.out.println((int)(Math.random()*100));//随机数
                    Thread.sleep(2000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("PrintNum进程退出");
    }
}
class MyListener implements Runnable{
    private PrintNum printNum;//需要定义一个printNum,面向对象保证我们改的和运行的是同一个对象
    Scanner myScanner = new Scanner(System.in);

    public MyListener(PrintNum printNum) {
        this.printNum = printNum;//构造器传入Print的对象
    }

    @Override
    public void run() {
        synchronized (this){
            while (true) {
                System.out.println("Q退出,等待用户输入...");
                char a = myScanner.next().charAt(0);
                if (a == 'Q') {
                    printNum.setLoop(false);
                    System.out.println("Listener进程退出");
                    break;
                }else{
                    System.out.println("退出失败 输入Q退出");
                }
            }
        }
    }
}

        【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第30张图片

2.线程同步

(1)有2个用户分别从同一个卡上取钱(总额: 10000)

(2)每次都取1000,当余额不足时,就不能取款了

(3)不能出现超取现象 -> 线程同步问题

        

public class Homework02 {
    public static void main(String[] args){
        Bank bank = new Bank();
        Thread thread1 = new Thread(bank);
        Thread thread2 = new Thread(bank);
        thread1.start();
        thread2.start();
    }
}

class Bank implements Runnable {
    private int bakAccount = 10000;

    public int getBakAccount() {
        return bakAccount;
    }

    @Override
    public void run() {
        //1. 这里使用 synchronized 实现了线程同步
        //2. 当多个线程执行到这里时,就会去争夺 this对象锁
        //3. 哪个线程争夺到(获取)this对象锁,就执行 synchronized 代码块, 执行完后,会释放this对象锁
        //4. 争夺不到this对象锁,就blocked ,准备继续争夺
        //5. this对象锁是非公平锁.
        synchronized (this) {
            while (true) {
                if (bakAccount <= 0) {
                    System.out.println("取完");
                    break;
                }

                System.out.println(Thread.currentThread().getName() + "取出1000余额剩余" + (bakAccount -= 1000));
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第31张图片

如果想要让两个线程近似交替运行,取完--bankAccount后休眠不要使用synchronized包起来即可。锁如果包括休眠的话,会等另一个线程休眠完后一起抢。不包起来就会一个线程开始休眠,另一个线程直接抢锁

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第32张图片

总结一下:执行过快,两个thread基本上是同时启动的,所以会出现两个争抢的情况,如果想两个线程不抢,在主线程内加一个sleep即可

thread1.start();

Thread.sleep(200);

thread2.start();

【JAVA学习笔记】61 - 线程入门、常用方法、同步机制,以及本章作业(难点)_第33张图片

你可能感兴趣的:(java,学习,笔记)