java线程

一、线程的相关概念

  • 程序(program)

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

  • 进程

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

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

  • 线程

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

    2、一个进程可以拥有多个线程

  • 其他相关概念

    1、单线程:同一个时刻,只允许执行一个线程

    2、多线程:同一时刻,可以执行多个线程,比如:一个qq进程,可以打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件

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

    4、并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行。并发金和并行可以同时存在

二、线程的创建

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

类图关系

代码案例:

public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
        /**
         * 1、请编写一个程序,开启一个线程,该线程每隔一秒,在控制台输出“ hello,world!”
         * 2、对上题改进:当输出80次 ”hello,world“,结束该线程
         * 3、使用JConsole 监控线程执行情况,并画出程序示意图
         */
        //创建一个Cat对象,当做线程使用
        Cat cat = new Cat();
        cat.start();
        //说明:当main线程启动一个子线程时,也就是Thread-0子线程,主线程不会阻塞,会继续执行下面的方法
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程:" + Thread.currentThread().getName() + " 正在打印: 我是主线程");
            Thread.sleep(1000);
        }
    }
}
//继承 Thread 类
//1、当一个类继承了Thread,该类就可以当做线程使用
//2、一般我们会重写run方法,写上自己的逻辑
//3、run Thread类 实现了 Runnable 接口的run方法
class Cat extends Thread{
    int time = 0;
    //重写run方法
    @Override
    public void run() {
        while (time < 10){
            //线程名称
            System.out.println("线程名称:=====>"+Thread.currentThread().getName() + " 正在打印:====> hello world!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            time++;
        }
    }
}

1、当运行该程序时,就相当于启动了一个进程,

2、启动进程过后,就会马上进入这个主方法,也就是main方法,进入main方法,就是相当于开启了一个主线程,也就是进程里面开启了一个主线程(main线程)

3、在这个main主线程里面,new了一个Cat,因为这个Cat继承了Thread,所以是一个线程类,Cat调用 start() 方法,就是相当于在main主线程中又开启了一个新的线程,Cat线程

4、当main线程启动一个子线程时,也就是Thread-0子线程,主线程不会阻塞,会继续执行下面的方法,这时的主线程和子线程都是交互执行的

5、当主线程执行完并退出时,子线程还在运行,这样并不会造成这个进程的结束(这个进程也就是应用程序)

6、主线程可以开启多个子线程,子线程也可以开启多个子线程,当最后一个子线程结束后,整个进程才会退出

2、实现Runnable接口

说明:

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

2、java设计者们提供了另一个方式创建线程,就是通过实现Runnable接口来创建线程

代码案例:

三、start() 和 run()

run() 方法只是一个普通的方法,在main主方法中调用线程的 run() 方法,只会是等待被调用的线程的 run() 方法执行完后才会再执行下一行代码,没有真正的启动一个线程,线程类调用的 run() 方法所在的线程是主线程,也就是main线程

start() 才会真正的启动线程

源码:

//首先调用start方法
public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0(); //这个方法才是真正的启动线程
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

真正实现多线程的是 start0() 这个方法

//start0()是一个 本地方法,是JVM进行调用,底层是c/c++
//真正实现多线程的是 start0() 这个方法,而不是 run() 方法
private native void start0();

原理图:

start() 方法调用 start0() 方法后,该线程并不一定会立马执行,只是将线程变成了可运行状态,具体什么时候执行,取决于CPU,由CPU统一调度。

静态代理测试代码:

public class Thread02 {
    public static void main(String[] args) {
        //编写程序,该程序每隔一秒,在控制台输出 “hi” ,当输出10次后,自动退出,请使用实现Runnable接口方式实现,这里是静态代理
        //Dog dog = new Dog();
        //创建一个 Thread 对象,把dog(实现了Runnable接口),放入new Thread(dog)

        //这里使用了一个设计模式,代理模式,静态代理
        //Thread t = new Thread(dog);
        //t.start();

        Tiger tiger = new Tiger();
        ThreadProxy threadProxy = new ThreadProxy(tiger);
        threadProxy.start();
    }
}
//测试
class Animal {}
class Tiger extends Animal implements Runnable {

    int time = 0;

    @Override
    public void run() {
        while (time < 10){
            
            System.out.println("老虎叫了" + (++time) + "声");
        }
    }
}


//模拟代理模式
//可以将ThreadProxy看做一个代理类
//模拟了一个最简单的Thread类
class ThreadProxy implements Runnable {
    //属性,类型是 Runnable
    private Runnable target = null;

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

    //创建一个构造器,将实现了Runnable对象传进来
    public ThreadProxy(Runnable target) {
        this.target = target;
    }

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

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


//通过实现 Runnable 接口实现线程类
class Dog implements Runnable{
    int time = 0;

    //重写run方法
    @Override
    public void run() {
        while (true) {
            System.out.println("hi" + (++time) + " 线程名称:" + Thread.currentThread().getName());

            try {
                //休眠1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (time == 10){
                break;
            }
        }
    }
}

四、多线程执行

要求:

请编写一个 程序,创建两个线程,一个线程每隔一秒输出 “hello,world”,输出10次,一个线程每隔1秒输出 “hi”,输出5次,退出

public class Thread03 {
    public static void main(String[] args) {
        //主线程执行后,下面的子线程不会造成阻塞所以主线程执行完成后,直接结束
        //但是子线程还在运行,所以这个主进程还在运行,主线程结束并不会影响主进程的运行
        T1 t1 = new T1();
        T2 t2 = new T2();
        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t2);

        thread1.start();
        thread2.start();
    }
}

//每隔一秒输出 “hello,world”,输出10次退出
class T1 implements Runnable{
    int time = 0;
    @Override
    public void run() {
        while (time < 10){
            System.out.println("hello,world " + (++time));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//每隔一秒输出 “hi”,输出5次退出
class T2 implements Runnable{
    int time = 0;
    @Override
    public void run() {
        while (time < 5){
            System.out.println("hi " + (++time));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

五、Thread 和 Runnable 的区别

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

底层都是通过 start()方法 去调用 start0()方法

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

理解:当创建了一个线程类,继承Runnable接口,那么可以开启两个线程进行去同时执行这个线程类,这样两个线程就可以共享一个资源

T1 t1 = new T1();
Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t1);
thread1.start();
thread2.start();

六、模拟售票

public class SellTicket {
    public static void main(String[] args) {
        //Thread 测试
//        SellTicket01 s1 = new SellTicket01();
//        SellTicket01 s2 = new SellTicket01();
//        SellTicket01 s3 = new SellTicket01();

        //启动
//        s1.start();
//        s2.start();
//        s3.start();

        //Runnable 测试
        SellTicket02 sellTicket02 = new SellTicket02();
        //启动 操作的是同一个对象
        new Thread(sellTicket02).start();
        new Thread(sellTicket02).start();
        new Thread(sellTicket02).start();

    }
}

//使用 Thread 方式   互斥同步的问题
class SellTicket01 extends Thread{
    //票数,让多个线程共享
    private static int ticketNum = 100;
    @Override
    public void run() {
        while (true){
            //如果卖完了,直接退出
            if (ticketNum <= 0){
                System.out.println("售票结束....");
                break;
            }
            //没卖完,休眠50毫秒继续卖
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("窗口 " + Thread.currentThread().getName() +
                    " 卖出了1张票,剩余票数 ===> " + (--ticketNum));
        }
    }
}

//通过 Runnable 接口实现
class SellTicket02 implements Runnable{
    //票数,让多个线程共享
    private int ticketNum = 100;

    @Override
    public void run() {
        while (true){
            //如果卖完了,直接退出
            if (ticketNum <= 0){
                System.out.println("售票结束....");
                break;
            }
            //没卖完,休眠50毫秒继续卖
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("窗口 " + Thread.currentThread().getName() +
                    " 卖出了1张票,剩余票数 ===> " + (--ticketNum));
        }
    }
}

出现的共同问题:

当出现三个线程共享一个资源(票数)
当票数只剩2张,当t1先进行判断时,发现还有2张票,但是在t1没有来得及进行售票操作时,t2进来,此时的票数在t1没有减去1张前,还有2张,就在t2和t1正在往下执行售票操作时,t3进来,判断发现票数还没有减去,还有2张,也就执行售票操作,最终导致的结果是t1和t2已经完成了售票操作,分别卖出1张,那么t3就没有票,但是也执行了售票操作,导致票数为负数,超过额定的票数

七、线程退出和中断

1、线程退出

基本说明:

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

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

示例代码:

要求:启动一个线程t,要求在main线程中去停止线程t

public class ThreadExit_ {
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        t1.start();

        //如果希望main线程去终止t1这个线程,必须修改loop,让t1退出run方法,从而终止 t1 -> 通知方法
        //让main线程休眠5秒,再去通知t1子线程
        Thread.sleep(5000);
        t1.setLoop(false);
    }
}

class T extends Thread{
    int count = 0;
    //设置一个变量
    private boolean loop = true;

    //通过set方式修改 loop 的值,从而达到将 run 方法停止的目的
    public void setLoop(boolean loop) {
        this.loop = loop;
    }

    @Override
    public void run() {
        while (loop){
            System.out.println("我是子线程 " + Thread.currentThread().getName() + (++count));

            try {
                //休眠50毫秒
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2、线程中断

线程中断就是将正在休眠的线程提前中断,打断线程休眠,将线程唤醒,打断的线程将会重新进入 while 循环,再次执行

public class ThreadInterrupt_ {
    public static void main(String[] args) throws InterruptedException {
        A a = new A();
        //开启子线程
        a.start();
        //主线程打印5次 “hi”,中断子线程的休眠
        for (int i = 0; i < 5; i++) {
            Thread.sleep(1000);
            System.out.println("hi " + i);
        }
        //中断子线程的休眠
        a.interrupt();
    }
}
//自定义线程类
class A 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(200000);
            } catch (InterruptedException e) {
                // InterruptedException 中断异常
                //当线程执行到一个interrupt方法时,就会catch一个异常,可以加入自己的业务代码
                System.out.println(Thread.currentThread().getName() + " 被interrupt了~~~ ");
            }
        }
    }
}

八、线程的常用方法

1、基本方法

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

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

3、start //使该线程开始执行;java虚拟机底层调用该线程的 start0() 方法

4、run //调用线程对象 run 方法

5、setPriority //更改线程优先级

6、getPriority //获取线程优先级

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

8、interrupt //中断线程

需要注意的细节:

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

2、线程优先级的范围

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

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

源码,优先级常量:

    /**
     * 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; //优先级最高

示例代码:

public class ThreadMethod01 {
    public static void main(String[] args) throws InterruptedException {
        //测试相关方法
        T t = new T();
        //设置线程名称 setName
        t.setName("猪八戒");
        //设置优先级
        t.setPriority(Thread.MIN_PRIORITY);
        //开启线程
        t.start();

        //输出线程名称
        System.out.println("线程名称:" + Thread.currentThread().getName());

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

    }
}

//自定义线程类
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(200000);
            } catch (InterruptedException e) {
                // InterruptedException 中断异常
                //当线程执行到一个interrupt方法时,就会catch一个异常,可以加入自己的业务代码
                System.out.println(Thread.currentThread().getName() + " 被interrupt了~~~ ");
            }
        }
    }
}

2、线程礼让和线程插队

1、yield:线程礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功

public class ThreadMethod02 {
    public static void main(String[] args) throws InterruptedException {
        B b = new B();
        b.start();
        //让主线程输出 20次 hi,每隔1秒
        for (int i = 0; i <= 20; i++) {
            Thread.sleep(1000);
            System.out.println("主线程输出 hi" + i);
            //判断主线程是否到了第5次,让子线程先执行,执行完成后,主线程再执行
            if (i == 4){
                //线程礼让,不一定成功,这是一个静态方法,让主线程先执行
               Thread.yield();
            }
        }
    }
}

//创建子线程
class B extends Thread{
    int time = 0;
    @Override
    public void run() {
        //让子线程循环输出20次 hello,每隔1秒
        while (true){
            if (time == 20){
                break;
            }
            try {
                System.out.println("子线程输出 hello " + (++time));
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

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

案例:

创建一个子线程,每隔1s输出 hello ,输出20次,主线程每隔1s ,输出 hi,输出 20 次

要求:

两个线程同时执行,当主线程输出 5 次后,就让子线程运行完毕,主线程再继续

join代码示例:

public class ThreadMethod02 {
    public static void main(String[] args) throws InterruptedException {
        B b = new B();
        b.start();
        //让主线程输出 20次 hi,每隔1秒
        for (int i = 0; i <= 20; i++) {
            Thread.sleep(1000);
            System.out.println("主线程输出 hi" + i);
            //判断主线程是否到了第5次,让子线程先执行,执行完成后,主线程再执行
            if (i == 4){
                //让子线程先执行
                //线程插队
                b.join();
            }
        }
    }
}

//创建子线程
class B extends Thread{
    int time = 0;
    @Override
    public void run() {
        //让子线程循环输出20次 hello,每隔1秒
        while (true){
            if (time == 20){
                break;
            }
            try {
                System.out.println("子线程输出 hello " + (++time));
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3、用户线程和守护线程

基本介绍:

1、用户线程:也叫做工作线程,当线程的任务执行完或者通知方式结束

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

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

制作一个守护线程:

public class ThreadMethod03 {
    public static void main(String[] args) throws InterruptedException {
        //声明守护线程
        MyDeamonThread mt = new MyDeamonThread();

        //将子线程设置为守护线程
        //一旦所有的线程结束工作,那么守护线程也将会退出工作
        mt.setDaemon(true);

        //启动守护线程
        mt.start();
        
        //主线程循环
        for (int i = 0; i <= 10; i++) {
            Thread.sleep(1000);
            System.out.println("我是主线程,我正在工作...");
        }
    }
}
//线程类
class MyDeamonThread extends Thread{
    @Override
    public void run() {
        //等价于while循环,无限循环
        for (; ;){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我是守护线程,我正在守护.....");
        }
    }
}

注意点:

需要将 mt.setDaemon(true) 在线程启动之前设置,否则就会抛出异常

九、线程的生命周期

官方文档的生命周期:

  • NEW

    尚未启动的线程处于此状态

  • RUNNABLE

    在java虚拟机中执行的线程处于此状态

  • BLOCKED

    被阻塞等待监视器锁定的线程处于此状态

  • WAITING

    在等待另一个线程执行特定动作的线程处于此状态

  • TIMED_WAITING

    正在等待另一个线程执行动作达到指定等待时间的线程处于此状态

  • TERMINATED

    已经退出的线程处于此状态

线程生命周期转换图:

说明:

1、当创建一个线程,也就是new一个线程出来过后,首先进入一个NEW状态,一旦调用了 start() 方法,就进入 Runnable状态(可运行状态),Runnable状态细化的话可分为两个状态,Ready状态(就绪状态)和 Running(运行状态)

2、线程是否被执行,还要取决于线程是否被调度器选中执行,一旦被调度器选中,那么就从Ready状态(就绪状态),那么线程就进入Running状态(运行状态),如果线程运行结束,那么就会进入Teminated状态(终止状态),相当于线程挂了

3、如果线程在 Runnable状态(可运行状态)调用了 Tread.sleep(time)、o.wait(time)、t.join(time)、LockSupport.parkNanos() 这些方法,就会进入TimeWaiting状态(超时等待状态),等待时间结束后在进入 Runnable状态(可运行状态)

4、如果线程在 Runnable状态(可运行状态)调用了 o.wait()、t.join()、LockSupport.park() 这些方法,那么就会进入 Waiting状态(等待状态),通过线程执行 o.notify()、o.notifyAll()、LockSupport.unpark() 这些方法重新进入 Runnable状态(可运行状态)

5、当线程在Runnable状态(可运行状态)要去获取一把锁或者进入一个同步代码块时,会先进入 Blocked 状态(阻塞状态),获取锁后才会重新进入Runnable状态(可运行状态),等待 Running

在官方文档中有6种状态,但是在Runnable状态(可运行状态)线程是否在运行,要取决于调度器,由内核的调度器来执行,因此还可再细分为 Ready状态(就绪状态)和 Running状态(运行状态)

实例代码:

public class ThreadState_ {
    public static void main(String[] args) throws InterruptedException {
        Td td = new Td();
        //线程启动前状态
        System.out.println(td.getName() + " 线程启动前的状态 " + td.getState());
        td.start();
        //循环线程启动后的状态
        //Thread.State.TERMINATED != td.getState() 判断当前线程状态是否为终止状态
        while (Thread.State.TERMINATED != td.getState()){
            //当线程状态不为终止状态,就打印线程的实时状态
            System.out.println(td.getName() + " 线程正在运行的状态 " + td.getState());
            Thread.sleep(500); //让主线程休眠再打印
        }
        //线程停止的状态
        System.out.println(td.getName() + " 线程停止的状态 " + td.getState());
    }
}
class Td extends Thread{
    @Override
    public void run() {
        while (true){
            for (int i = 0; i < 10; i++) {
                System.out.println("hi~ " + i);
                try {
                    //这里的休眠会让线程进入 超时等待状态 TIMED_WAITING
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //for循环结束后直接退出子线程
            break;
        }
    }
}

十、线程同步机制

说明:

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

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

同步实现具体方法 - synchronized

1、同步代码块,对象锁

synchronized(对象){ //得到对象的锁,才能操作同步代码
	//需要被同步代码
}

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

public synchronized void method(String name){
	//需要被同步的代码
}

3、理解:当一个人去上厕所前,进入厕所要先关门,上完厕所出来后,把门打开才能让下一个人进去上厕所

4、使用 synchronized 解决售票问题

public class SellTicket {
    public static void main(String[] args) {
        SellTicket01 sk = new SellTicket01();
        new Thread(sk).start();
        new Thread(sk).start();
        new Thread(sk).start();
    }
}

//通过 Runnable 接口实现
class SellTicket01 implements Runnable{
    //票数,让多个线程共享
    private int ticketNum = 100;
    //控制while循环
    private boolean loop = true;

    //public synchronized void sell(){} 通过synchronized加上一把锁,那么这个方法就是一个同步方法,这时,这个锁是在this对象上(SellTicket01)
    public synchronized void sell(){ // 同步方法,在同一时刻,只能有一个线程来操作这个卖票方法
        //如果卖完了,直接退出
        if (ticketNum <= 0){
            System.out.println("售票结束....");
            loop = false;
            return;
        }
        //没卖完,休眠50毫秒继续卖
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("窗口 " + Thread.currentThread().getName() +
                " 卖出了1张票,剩余票数 ===> " + (--ticketNum));
    }

    @Override
    public void run() {
        while (loop){
            sell(); //这个sell方法是一个同步方法
        }
    }
}

同步原理:

比如有三个线程 t1、t2、t3,刚开始会去争夺锁,锁在对象上,当 t1 抢到这把锁,进去操作相关的代码,当操作完后, t1 就会把这把锁放回去,然后再去与 t2、t3 争夺这把锁,synchronized 是一把非公平锁,每个线程都可以去争夺

十一、互斥锁

说明:

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

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

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

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

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

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

//同步方法(静态的)的锁为当前类本身
//1、public synchronized static void method(){} 锁是加在 SellTicket01.class上的,因为这个method01是一个静态方法
public synchronized static void method01(){}
//2、如果是静态方法中,实现一个同步代码块,那么synchronized中就是这个类的 .class
public static void method02(){
    synchronized (SellTicket01.class){}
}

将锁加在代码块上:

public class SellTicket {
    public static void main(String[] args) {
        SellTicket01 sk = new SellTicket01();
        new Thread(sk).start();
        new Thread(sk).start();
        new Thread(sk).start();
    }
}

//通过 Runnable 接口实现
class SellTicket01 implements Runnable{
    //票数,让多个线程共享
    private int ticketNum = 100;
    //控制while循环
    private boolean loop = true;
    //可以用这个object替代当前synchronized中的this,因为这三个线程操作的都是同一个类
    //synchronized (object)
    Object object = new Object();

    //同步方法(静态的)的锁为当前类本身
    //1、public synchronized static void method(){} 锁是加在 SellTicket01.class上的,因为这个method01是一个静态方法
    public synchronized static void method01(){}
    //2、如果是静态方法中,实现一个同步代码块,那么synchronized中就是这个类的 .class
    public static void method02(){
        synchronized (SellTicket01.class){}
    }

    public void sell(){ // 同步方法,在同一时刻,只能有一个线程来操作这个卖票方法
        //同步代码块 synchronized
        synchronized (this){
            //如果卖完了,直接退出
            if (ticketNum <= 0){
                System.out.println("售票结束....");
                loop = false;
                return;
            }
            //没卖完,休眠50毫秒继续卖
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("窗口 " + Thread.currentThread().getName() +
                    " 卖出了1张票,剩余票数 ===> " + (--ticketNum));
        }
    }

    @Override
    public void run() {
        while (loop){
            sell(); //这个sell方法是一个同步方法
        }
    }
}

注意事项和细节:

1、同步方法如果没有使用 statis 修饰,默认锁的对象为 this

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

3、实现的落地步骤:

  • 需要分析上锁的代码
  • 选择同步代码块或同步方法
  • 要求多个线程的锁对象为同一个即可
/**
 * 当调用该线程时,肯定是如下场景
 * new SellTicket02().start()
 * new SellTicket02().start()
 * 这样这个同步所就无法实现,原因是每次都是 new 一个新的 SellTicket02
 * 而不是操作同一个SellTicket02对象,所以 this 的指向都是新的对象,无法锁住
 * 最后失效
 */
class SellTicket02 extends Thread{

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

    public void method(){
        synchronized (this){
            System.out.println("hello-");
        }
    }

    @Override
    public void run() {
        super.run();
    }
}

十二、线程死锁

基本介绍:

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

发生死锁实例代码:

public class DeadLock_ {
    public static void main(String[] args) {
        //线程A启动
        DeadLockDemo A = new DeadLockDemo(true);
        A.setName("A线程 ");
        //线程B启动
        DeadLockDemo B = new DeadLockDemo(false);
        B.setName("B线程 ");

        A.start();
        B.start();
    }
}

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

    public DeadLockDemo(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        /**
         * 业务逻辑分析
         * 1、如果flag为true,线程A 就会先持有 o1 这个对象锁,然后尝试去获取 o2 对象锁
         * 2、如果 线程A 得不到 o2 对象锁,就会进入 Blocked 状态(阻塞状态)
         * 3、如果flag为false,线程B 就会先得到 o2 这个对象锁,然后尝试获取 o1 对象锁
         * 4、如果 线程B 拿不到 o1 对象锁,就会进入 Blocked 状态(阻塞状态)
         */
        if (flag){
            synchronized (o1){ //synchronized加上对象互斥锁,下面就是同步对象
                System.out.println(Thread.currentThread().getName() + " 进入o1 ");
                synchronized (o2){
                    System.out.println(Thread.currentThread().getName() + " 进入o2 ");
                }
            }
        }else{
            synchronized (o2){
                System.out.println(Thread.currentThread().getName() + " 进入o3 ");
                synchronized (o1){
                    System.out.println(Thread.currentThread().getName() + " 进入o4 ");
                }
            }
        }
    }
}

十三、释放锁

基本介绍:

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

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

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

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

下面的操作不会释放锁:

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

2、线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁,提示:应尽量避免使用 suspend() 和 resume() 来控制线程,方法不再推荐使用

posted @ 2022-08-27 20:12  花椒蛋炒饭  阅读(465)  评论(0编辑  收藏  举报