Java多线程基础

目录

一、线程的基本使用

(一)创建线程的两种方式

(二)线程简单案例(Thread)

问题:main函数与开启的线程是否是阻塞的,即线程运行时,main函数等待线程运行结束?

问题:为什么不直接调用cat的run方法,而是通过start来使用?

start解析

 (三)线程简单案例(Runnable)

(四)线程简单案例(多线程)

二、线程终止

三、线程的常用方法

(一)常用方法一

(二)常用方法二

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

四、线程生命周期

五、线程的同步

六、互斥锁

七、线程死锁

八、释放锁

(一)下面操作会释放锁

(二)下面操作不会释放锁


一、线程的基本使用

(一)创建线程的两种方式

在Java中线程使用有两种方式:

① 继承Thread类,重写run方法

② 实现Runnable接口,重写run方法

(二)线程简单案例(Thread)

由主函数开启一个线程,每秒钟输出一句话

public class Thread01 {
    public static void main(String[] args) {
        Cat cat = new Cat(); // 创建对象当成线程使用
        cat.start(); // 调用start方法开启线程
    }
}
// 1.继承Thread类后就可以当作线程使用
// 2.重写run方法,实现自己的业务
// 3.Thread类实现了Runnable的run方法
class Cat extends Thread {
    static int count = 0;
    @Override
    public void run() {  // 重写run方法,实现业务
        while(true) {
            count ++;
            // 每隔一秒,输出一句话
            System.out.println("我是帅哥" + count);
            // 让该线程休眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count == 8) break;
        }
    }
}

问题:main函数与开启的线程是否是阻塞的,即线程运行时,main函数等待线程运行结束?

验证

(1)方式一

首先改进main函数,运行

    public static void main(String[] args) throws InterruptedException {
        Cat cat = new Cat(); // 创建对象当成线程使用
        cat.start(); // 调用start方法开启线程
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程运行" + i);
            Thread.sleep(1000);
        }
    }

查看两个线程的名字

System.out.println("主线程运行" + i + "线程名字" + Thread.currentThread().getName());
System.out.println("我是帅哥" + count + "线程名字" + Thread.currentThread().getName());

说明主线程与开启的线程是并行的,即多线程

 (2)方式二

使用Jconsole(jdk自带的线程监控工具)查看线程,首先扩大输出语句的次数方便我们验证

public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
        Cat cat = new Cat(); // 创建对象当成线程使用
        cat.start(); // 调用start方法开启线程
        for (int i = 0; i < 60; i++) {
            System.out.println("主线程运行" + i);
            Thread.sleep(1000);
        }
    }
}
// 1.继承Thread类后就可以当作线程使用
// 2.重写run方法,实现自己的业务
// 3.Thread类实现了Runnable的run方法
class Cat extends Thread {
    static int count = 0;
    @Override
    public void run() {  // 重写run方法,实现业务
        while(true) {
            count ++;
            // 每隔一秒,输出一句话
            System.out.println("我是帅哥" + count);
            // 让该线程休眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count == 80) break;
        }
    }
}

运行main函数后,在Terminal输入Jconsole

 main线程和Thread-0线程都在运行

main线程运行结束后,Thread-0线程还在继续运行 

 Thread-0线程运行结束,此时连接中断

总结:当main函数运行结束时,不意味着main函数开启的线程也结束。

问题:为什么不直接调用cat的run方法,而是通过start来使用?

解释:如果调用cat,run()来执行,此时run方法里输出的线程名字将会是main,意味着并没有开启线程,而且此时是串行化的执行,需要run方法运行结束才会继续执行main下面的内容

验证

    public static void main(String[] args) throws InterruptedException {
        Cat cat = new Cat(); // 创建对象当成线程使用
        //cat.start(); // 调用start方法开启线程
        cat.run(); // 直接调用run方法
        for (int i = 0; i < 60; i++) {
            System.out.println("主线程运行" + i + "线程名字" + Thread.currentThread().getName());
            Thread.sleep(1000);
        }
    }

start解析

查看start方法中,我们可以发现调用了start0方法,该方法是一个native(本地)方法,由JVM调用,底层是c/c++实现,真正实现多线程的效果, 是 start0(), 而不是 run

public synchronized void start() {
    。。。
    start0();
    。。。
}

private native void start0();

 (三)线程简单案例(Runnable)

java是单继承的,在某些情况下一个类可能已经继承了父类,此时无法通过继承Thread类创建线程,因此可以通过实现Runnable接口来创建线程 ,这里使用了代理模式,通过Thread类进行代理,最终调用的是实现Runnable接口的dog的run方法。       

public class Thread02 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        // 此时dog没有start方法
        // 创建Thread对象,将实现Runnable方法的dog放入,调用start方法
        Thread thread = new Thread(dog);
        thread.start();
    }
}
class Dog implements Runnable {

    static int count = 0;
    @Override
    public void run() {
        while(true) {
            System.out.println("狗叫" + (++ count) + " 线程名称" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

(四)线程简单案例(多线程)

public class Thread03 {
    public static void main(String[] args) {
        T1 t1 = new T1();
        T2 t2 = new T2();
        Thread thread1 = new Thread(t1);
        thread1.start(); // 启动线程1
        Thread thread2 = new Thread(t2);
        thread2.start(); // 启动线程2
    }
}
class T1 implements Runnable {

    static int count = 0;
    @Override
    public void run() {
        while(true) {
            System.out.println("t1..." + (++ count) + " 线程名称" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count == 10) {
                break;
            }
        }
    }
}
class T2 implements Runnable {

    static int count = 0;
    @Override
    public void run() {
        while(true) {
            System.out.println("t2..." + (++ count) + " 线程名称" + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count == 10) {
                break;
            }
        }
    }
}

总结继承Thread   VS    接口Runnable

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

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

使用继承方式的好处是,在 run() 方法内获取当前线程直接使用 this 就可以了,无须使用Thread.currentThread0方法;不好的地方是 Java 不支持多继承,如果继承了Thread类那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而 Runable 则没有这个限制。 

二、线程终止

当线程完成任务后会自动退出,也可以通过使用变量控制run方法退出的方式停止线程,即通知方式

public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
        ThreadExit threadExit = new ThreadExit();
        threadExit.start();

        // 休眠5秒再通知threadExit线程
        Thread.sleep(5 * 1000);
        // 将flag置为false,停止threadExit线程
        threadExit.setFlag(false);
    }
}
class ThreadExit extends Thread {
    static int count = 0;
    private Boolean flag = true;
    @Override
    public void run() {
        while(flag) {
            System.out.println("线程运行" + (++count) + " 线程名字" + this.getName());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void setFlag(Boolean flag) {
        this.flag = flag;
    }
}

三、线程的常用方法

(一)常用方法一

  1. setName  //设置线程名称,使之与参数 name 相同
  2. getName  //返回该线程的名称
  3. start  //使该线程开始执行; Java 虚拟机底层调用该线程的 start0 方法
  4. run  //调用线程对象 run 方法,直接调用不会创建新的线程
  5. setPriority  //更改线程的优先级
  6. getPriority  //获取线程的优先级
  7. sleep //线程的静态方法,在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
  8. interrupt  //中断线程,但没有真正终止线程,一般用于中断正在休眠的线程

线程优先级

案例:

public class ThreadMethod01 {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        t.setPriority(1);
        t.start();
        // 休眠5秒叫醒线程t
        Thread.sleep(5000);
        System.out.println("线程" + t.getName() + "的优先级为:" + t.getPriority());
        t.interrupt();
    }
}
class T extends Thread {
    @Override
    public void run() {
        while(true) {
            System.out.println("线程" + this.getName() + "吃一百个包子");
            try {
                System.out.println("睡个觉继续吃...");
                sleep(20 * 1000);
            } catch (InterruptedException e) {
                System.out.println("被叫醒了,继续吃");
            }
        }
    }
}

(二)常用方法二

  1. yield:线程的礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功
  2. join: 线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务

join解释图

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

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

        for (int i = 1; i <= 10; i++) {
            System.out.println("main线程吃 " + "吃第" + i + "个包子");
            Thread.sleep(1000);
            if(i == 5) {
                System.out.println("老大" + t1.getName() + "先吃");
                t1.join();   // 让t1先运行完
                System.out.println("老大" + t1.getName() + "吃完了");
            }
        }
    }
}
class T1 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(this.getName() + "吃第" + i + "个包子");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 yield

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

        for (int i = 1; i <= 10; i++) {
            System.out.println("main线程吃 " + "吃第" + i + "个包子");
            Thread.sleep(1000);
            if(i == 5) {
                System.out.println("老大" + t1.getName() + "先吃");
//                t1.join();
                Thread.yield();  // 线程礼让
                System.out.println("老大" + t1.getName() + "吃完了");
            }
        }
    }

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

  • 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
  • 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
  • 常见的守护线程:垃圾回收机制 

案例:子线程循环次数大于main线程,当main线程结束后,子线程也需要结束

public class ProtectThread {
    public static void main(String[] args) throws InterruptedException {
        T4 t4 = new T4();
        t4.setDaemon(true);
        t4.start();
        for(int i = 0; i < 5; i ++) {
            System.out.println("我是用户线程(工作线程)");
            Thread.sleep(1000);
        }
    }
}

class T4 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("我是守护线程...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

四、线程生命周期

JDK 中用 Thread.State 枚举表示了线程的几种状态

有的书籍会说7种状态,是将Runnable状态分为了Ready(就绪态)和Running(运行态) 

 查看线程状态

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 < 2; i++) {
                System.out.println("hi " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            break;
        }
    }
}

五、线程的同步

在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
也可以这里理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作.

可以 通过加锁实现线程的同步,比如Synchronized关键字

1、同步代码块

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

2、同步方法

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

public synchronized void m() {
    // 同步代码段    
}

六、互斥锁

  1. Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。
  2. 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
  3. 关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized修饰时, 表明该对象在任一时刻只能由一个线程访问
  4. 同步的局限性:导致程序的执行效率要降低
  5. 同步方法(非静态的)的锁可以是this,也可以是其他对象(要求是同一个对象)
  6. 同步方法(静态的)的锁为当前类本身

注意事项和细节:

  1. 同步方法如果没有使用static修饰:默认锁对象为this
  2. 如果方法使用static修饰,默认锁对象:当前类.class
  3. 实现的落地步骤:
  • 需要先分析上锁的代码
  • 选择同步代码块或同步方法
  • 要求多个线程的锁对象为同一个即可! 

七、线程死锁

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

案例:

妈妈:你先完成作业,才让你玩手机

小明:你先让我玩手机,我才完成作业

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();
    private boolean flag;
    public DeadLockDemo(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {

        if(flag) {
            synchronized (o1) {
                System.out.println(this.getName() + "进入了1" + " 请求2");
                synchronized (o2) {
                    System.out.println(this.getName() +" 请求2");
                }
            }

        } else {
            synchronized (o2) {
                System.out.println(this.getName() + "进入了3" + " 请求4");
                synchronized (o1) {
                    System.out.println(this.getName() +" 请求4");
                }
            }
        }
    }
}

八、释放锁

(一)下面操作会释放锁

(二)下面操作不会释放锁

  • 28
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 27
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zoeil

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值