Java多线程详解一

概要

1.什么是多线程
有了多线程,可以让我们的程序同时做多件事情
2.多线程的作用
提高效率
3.多线程的应用场景
只要你想让多个事情同时运行,就需要用到多线程。比如:软件中的耗时操作、所有的聊天软件、所有的服务器。

例如:
QQ中,聊天的同时可以传输文件,这就是用了多线程。

并发和并行

并发:在同一时刻,有多条指令在单个CPU上交替执行。(单核CPU)
并行:在同一时刻,有多条指令在多个CPU上同时执行。(多核CPU)

多线程的实现方式

表头优点缺点
继承Thread类编程比较简单可以直接使用,无法获取多线程的返回结果扩展性较差
实现Runna接口扩展性强,实现该接口的同时还可以继承其他类 ,无法获取多线程的返回结果编程相对复杂,不能直接使用Thread类中的方法
实现Callable接口扩展性强,实现该接口的同时还可以继承其他类。但是可以获取多线程的返回结果编程相对复杂,不能直接使用Thread类中的方法

1. 继承Thread类的方式实现

定义一个类,并且继承Thread类,重写run()方法

public class MyThread extends Thread{

    @Override
    public void run() {
        //书写线程要执行的代码
        for (int i = 0; i < 10; i++) {
            //获取线程名字
            System.out.println(getName()+"Hello world");
        }
    }
}

再定义一个启动类用来测试
1.用对象名.start()来启动线程
2.区分线程可以用**对象名.setName()**来给线程起一个名字

public class A01_ThreadDemo1 {
    public static void main(String[] args) {
        /*多线程的第一种启动方式
        1.自己定义一个类继承Thread
        2.重写run方法
        3.创建子类的对象并启动线程
         */

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        //给线程起个名字(用来区分线程)
        t1.setName("线程一:t1 ");
        t2.setName("线程二:t2 ");
        //开启线程 不能直接调用run方法
        t1.start();
        t2.start();
    }
}

2. 实现Runnable接口的方式进行实现

 public class ThreadDemo {
    public static void main(String[] args) {
        /*多线程的第二种启动方式
        1.自己定义一个类实现Runnable接口
        2.重写里边的run方法
        3.创建自己的这个类的对象
        4.创建一个Thread对象 并开启多线程
         */

        //创建MyRunnable对象
        //表示多线程要执行的任务
        MyRunnable myRunnable = new MyRunnable();

        //创建线程Thread对象
        Thread t1 = new Thread(myRunnable);
        Thread t2 = new Thread(myRunnable);

        //重命名线程
        t1.setName("线程1:");
        t2.setName("线程2:");

        //开启线程
        t1.start();
        t2.start();
    }
}

自己创建一个类实现Runnable接口 并重写run()方法。
注意:这个方法不能像第一种 直接获得线程名称,因为这个类并没有继承Thread类,所以要先获得当前执行的线程对象,在获取线程名称

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            //获取当前执行的线程对象
            Thread thread = Thread.currentThread();
            //再用对象获取线程的名字
            System.out.println(thread.getName()+"Hello world");
        }
    }
}

3. 利用Callable接口和Future接口方式实现

public class ThreadDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*
        多线程的第三种方式
            特点:可以获取多线程运行的结果
            1.创建一个类MyCallable实现Callable接口
            2.重写call方法(是有返回值的,表示多线程的返回结果)
            3.创建MyCallable的对象(表示多线程要执行的任务)
            4.创建FutureTask对象(作用:管理多线程运行的结果)
            5.创建Thread对象 并启动(表示线程)
         */

        //创建MyCallable的对象(表示多线程要执行的任务)
        MyCallable myCallable = new MyCallable();
        //创建FutureTask对象(作用:管理多线程运行的结果)
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        //创建线程的对象
        Thread thread1 = new Thread(futureTask);
        //启动多线程
        thread1.start();

        //获取方法返回值的结果
        Integer result = futureTask.get();
        System.out.println(result);
    }
}

写一个类用来实现Callable接口,泛型的类型要和方法的类型保持一致

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        //求1~100之间的和
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            sum += i;
        }
        return sum;
    }
}

多线程中常用成员方法

1、设置线程名字和休眠线程

方法名称说明
String Name()返回线程的名字
void setName()设置线程的名字(构造方法也可以设置名字)
static Thread currentThread()获取当前线程对象
static void sleep(long time)让线程休眠指定时间,单位为毫秒
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
       /*
            String getName()                    返回此线程的名称
            void setName(String name)           设置线程的名字(构造方法也可以设置线程名字)
            细节:
                1、如果我们没有给线程设置名字,线程也是有默认的名字的
                        格式:Thread-X(X是序号,从0开始的)
                2、如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法设置

            static Thread currentThread()       获取当前线程的对象
            细节:
                当JVM虚拟机启动之后,会自动的启动多条线程
                其中有一条线程就叫做main线程
                他的作用就是去调用main方法,并执行里面的代码
                在以前,我们写的所有的代码,其实都是运行在main线程当中

            static void sleep(long time)        让线程休眠指定的时间,单位为毫秒
            细节:
                1、哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
                2、方法的参数:就表示睡眠的时间,单位毫秒
                    1 秒= 1000毫秒
                3、当时间到了之后,线程会自动的醒来,继续执行下面的其他代码
       */

        //1.创建线程的对象
        MyThread t1 = new MyThread("飞机");
        MyThread t2 = new MyThread("坦克");

        //2.开启线程
        t1.start();
        t2.start();

        //哪条线程执行到这个方法,此时获取的就是哪条线程的对象
       /* Thread t = Thread.currentThread();
        String name = t.getName();
        System.out.println(name);//main*/

        /*System.out.println("11111111111");
        Thread.sleep(5000);
        System.out.println("22222222222");*/
    }
}
public class MyThread extends Thread{

    public MyThread() {
    }

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

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName() + "@" + i);
        }
    }
}

2、线程优先级

方法名称说明
setPriority(int newPriority)设置线程的优先级,线程默认优先级是5,线程优先级的范围是:1-10
final int getPriority()获取线程的优先级
public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
        return "线程执行完毕了";
    }
}
public class Demo {
    public static void main(String[] args) {
        //优先级: 1 - 10 默认值:5
        MyCallable mc = new MyCallable();

        FutureTask<String> ft = new FutureTask<>(mc);

        Thread t1 = new Thread(ft);
        t1.setName("飞机");
        t1.setPriority(10);
        //System.out.println(t1.getPriority());//5
        t1.start();

        MyCallable mc2 = new MyCallable();

        FutureTask<String> ft2 = new FutureTask<>(mc2);

        Thread t2 = new Thread(ft2);
        t2.setName("坦克");
        t2.setPriority(1);
        //System.out.println(t2.getPriority());//5
        t2.start();
    }
}

3、守护线程

方法名说明
void setDaemon(boolean on)将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出

应用场景:例如QQ一边聊天一边传输文件,当你把聊天框关闭了,文件传输也会断掉,此时文件传输就是守护线程。

public class MyThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + "---" + i);
        }
    }
}
public class MyThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "---" + i);
        }
    }
}
public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

        t1.setName("女神");
        t2.setName("备胎");

        //把第二个线程设置为守护线程
        //当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了.
        t2.setDaemon(true);

        t1.start();
        t2.start();
    }
}   

线程安全问题

同步代码块和同步方法的区别

1.同步代码块 synchronized()

提示:同步代码块就是把synchronized()写在方法内,将需要上锁的代码包裹起来
注意:

  1. synchronized(锁对象):锁对象是唯一的 一般采用当前类的字节码文件。比如MyThread.class
  2. synchronized(锁对象):同步代码块不能写在循环外边,会导致第一个进去的线程就全部执行完了,例如案例的第一个线程就把票卖完了
public class MyThreadDemo {
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        MyThread myThread3 = new MyThread();
        
        myThread1.setName("窗口1:");
        myThread2.setName("窗口2:");
        myThread3.setName("窗口3:");
        
        myThread1.start();
        myThread2.start();
        myThread3.start();
    }
}

class MyThread extends Thread{
    //用static修饰 表示这个类的所有对象,都共享ticket对象
    static int ticket = 0;
    @Override
    public void run() {
        while (true){
            //同步代码块
            // synchronized (obj) 锁 锁代码默认开启 当有一个线程进去执行的时候
            //就会锁起来 其他的线程就只能在外边等待 里边的线程执行完 锁才会打开。
            synchronized (MyThread.class){
                if (ticket<100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    ticket++;
                    System.out.println(getName()+"正在卖第:"+ticket+"张票!!!");
                }else {
                    break;
                }
            }
        }
    }
}

2.同步方法 synchronized

同步方法就是把synchronized关键字加到方法上
格式:修饰符 synchronized 返回值类型 方法名(方法参数){...}
小技巧:不熟练的话 可以先把要锁住的代码 写在同步代码块内,然后再抽取出来,形成一个同步方法
特点:

  1. 同步方法会锁住方法里面的所有代码
  2. 锁对象不能自己指定 非静态:this ,静态:当前类的字节码文件对象 (如MyThread.class)
public class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread1 = new Thread(myRunnable,"窗口1:");
        Thread thread2 = new Thread(myRunnable,"窗口2:");
        Thread thread3 = new Thread(myRunnable,"窗口3:");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class MyRunnable implements Runnable {
    int ticket = 0;
    @Override
    public void run() {
        //1.循环
        //2.同步代码块(同步方法)
        //3.判断共享数据是否到了末尾,如果到了末尾
        while (true) {
            if (method()) break;
        }
    }

    //非静态的 锁对象是this
    private synchronized boolean method() {
        if (ticket < 100) {
            ticket++;
            System.out.println(Thread.currentThread().getName() + "正在卖第:" + ticket + "张票!!!");
        } else {
            return true;
        }
        return false;
    }
}

3.Lock锁【应用】

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

  • ReentrantLock构造方法

    方法名说明
    ReentrantLock()创建一个ReentrantLock的实例
  • 加锁解锁方法

    方法名说明
    void lock()获得锁
    void unlock()释放锁
  • 代码演示 这里尽量使用try catch finally 这样才能保证释放锁一定会被执行

    public class Ticket implements Runnable {
        //票的数量
        private int ticket = 100;
        private Object obj = new Object();
        private ReentrantLock lock = new ReentrantLock();
    
        @Override
        public void run() {
            while (true) {
                //synchronized (obj){//多个线程必须使用同一把锁.
                try {
                    lock.lock();
                    if (ticket <= 0) {
                        //卖完了
                        break;
                    } else {
                        Thread.sleep(100);
                        ticket--;
                        System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                // }
            }
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            Ticket ticket = new Ticket();
    
            Thread t1 = new Thread(ticket);
            Thread t2 = new Thread(ticket);
            Thread t3 = new Thread(ticket);
    
            t1.setName("窗口一");
            t2.setName("窗口二");
            t3.setName("窗口三");
    
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
    

4.死锁【应用】

  • 概述

    线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
    注意: 写锁的时候尽量不要让锁进行嵌套,这样就会尽量避免死锁

  • 什么情况下会产生死锁

    1. 资源有限
    2. 同步嵌套
  • 代码演示 :当线程一执行到了objA 锁,此时objA 关闭,但在这是执行权被线程二抢到,线程二执行到objB 锁,然后objB 锁也关闭;此时两把锁都关闭了,线程一 进不去objB 锁,线程二也进不去objA 锁。程序就会被卡住,这就是死锁

public class Demo {
    public static void main(String[] args) {
        Object objA = new Object();
        Object objB = new Object();

        new Thread(()->{
            while(true){
                synchronized (objA){
                    //线程一
                    synchronized (objB){
                        System.out.println("小康同学正在走路");
                    }
                }
            }
        }).start();

        new Thread(()->{
            while(true){
                synchronized (objB){
                    //线程二
                    synchronized (objA){
                        System.out.println("小薇同学正在走路");
                    }
                }
            }
        }).start();
    }
}

生产者消费者

  • 概述

    生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。

    所谓生产者消费者问题,实际上主要是包含了两类线程:

    ​ 一类是生产者线程用于生产数据

    ​ 一类是消费者线程用于消费数据

    为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

    生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为

    消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

  • Object类的等待和唤醒方法

方法名说明
void wait()导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
void notify()唤醒正在等待对象监视器的单个线程
void notifyAll()唤醒正在等待对象监视器的所有线程
  • 案例需求

  • 桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量

  • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

    1.判断是否有包子,决定当前线程是否执行

    2.如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子

    3.生产包子之后,更新桌子上包子状态,唤醒消费者消费包子

  • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务

    1.判断是否有包子,决定当前线程是否执行

    2.如果没有包子,就进入等待状态,如果有包子,就消费包子

    3.消费包子后,更新桌子上包子状态,唤醒生产者生产包子

  • 测试类(Demo):里面有main方法,main方法中的代码步骤如下

    创建生产者线程和消费者线程对象

    分别开启两个线程

  • 代码实现

public class Desk {

    //定义一个标记
    //true 就表示桌子上有汉堡包的,此时允许吃货执行
    //false 就表示桌子上没有汉堡包的,此时允许厨师执行
    public static boolean flag = false;

    //汉堡包的总数量
    public static int count = 10;

    //锁对象
    public static final Object lock = new Object();
}

public class Cooker extends Thread {
//    生产者步骤:
//            1,判断桌子上是否有汉堡包
//    如果有就等待,如果没有才生产。
//            2,把汉堡包放在桌子上。
//            3,叫醒等待的消费者开吃。
    @Override
    public void run() {
        while(true){
            synchronized (Desk.lock){
                if(Desk.count == 0){
                    break;
                }else{
                    if(!Desk.flag){
                        //生产
                        System.out.println("厨师正在生产汉堡包");
                        Desk.flag = true;
                        Desk.lock.notifyAll();
                    }else{
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

public class Foodie extends Thread {
    @Override
    public void run() {
//        1,判断桌子上是否有汉堡包。
//        2,如果没有就等待。
//        3,如果有就开吃
//        4,吃完之后,桌子上的汉堡包就没有了
//                叫醒等待的生产者继续生产
//        汉堡包的总数量减一

        //套路:
            //1. while(true)死循环
            //2. synchronized 锁,锁对象要唯一
            //3. 判断,共享数据是否结束. 结束
            //4. 判断,共享数据是否结束. 没有结束
        while(true){
            synchronized (Desk.lock){
                if(Desk.count == 0){
                    break;
                }else{
                    if(Desk.flag){
                        //有
                        System.out.println("吃货在吃汉堡包");
                        Desk.flag = false;
                        Desk.lock.notifyAll();
                        Desk.count--;
                    }else{
                        //没有就等待
                        //使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        /*消费者步骤:
        1,判断桌子上是否有汉堡包。
        2,如果没有就等待。
        3,如果有就开吃
        4,吃完之后,桌子上的汉堡包就没有了
                叫醒等待的生产者继续生产
        汉堡包的总数量减一*/

        /*生产者步骤:
        1,判断桌子上是否有汉堡包
        如果有就等待,如果没有才生产。
        2,把汉堡包放在桌子上。
        3,叫醒等待的消费者开吃。*/
        Foodie f = new Foodie();
        Cooker c = new Cooker();
        f.start();
        c.start();
    }
}

阻塞队列

  • 常见BlockingQueue:
    ArrayBlockingQueue: 底层是数组,有界

    LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值

  • BlockingQueue的核心方法:

方法名说明
put(anObject)将参数放入队列,如果放不进去会阻塞
take()取出第一个数据,取不到会阻塞
public class ThreadDemo {
    public static void main(String[] args) {
        /*
         *    需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
         *    细节:
         *           生产者和消费者必须使用同一个阻塞队列
         * */
        //创建阻塞队列的对象 必须是唯一的
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

        //创建线程的对象 并把阻塞对象传过去
        Cook c = new Cook(queue);
        Foodie f = new Foodie(queue);

        c.start();
        f.start();
    }
}

public class Foodie extends Thread{
    ArrayBlockingQueue<String> queue;
    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        while (true){
            //不断地把面条放到阻塞队列中
            try {
                queue.put("面条");
                System.out.println("厨师放了一晚面条");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Cook extends Thread{
    ArrayBlockingQueue<String> queue;
    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            //不断地从阻塞队列中获取面条
            try {
                String food = queue.take();
                System.out.println(food);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

鸡蛋糕生产线

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

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

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

打赏作者

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

抵扣说明:

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

余额充值