Java学习周记录——多线程

本文详细介绍了Java中的多线程技术,包括线程与进程的区别,同步与异步,并发与并行的概念。探讨了通过继承Thread类和实现Runnable接口创建线程的方式及其优缺点,并展示了Thread类的常用方法。此外,还讨论了线程安全问题,如线程阻塞、中断和守护线程,以及如何处理线程死锁。最后,文章提到了线程通信、线程池和Lambda表达式的使用,以及线程的六种状态。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多线程技术概述

线程与进程

分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
Java使用的为抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使用率更高。

同步与异步

同步:排队执行 , 效率低但是安全.
异步:同时执行 , 效率高但是数据不安全.

并发与并行

并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)

多线程技术实现方式

继承Thread类

子类继承了Thread类就实现了一个线程,需要重写run方法,所谓run方法,其实就是线程要执行的任务方法。子类实现的run方法就是一条线程的执行路径,而这个线程的触发方式,并不是通过调用run方法,而是通过调用start方法来启动任务。

public class test {
    public static void main(String[] args) {
        //实例化对象
        MyThread myThread = new MyThread();
        //开启子线程
        myThread.start();
        //主线程
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程:输出" + i);
        }
    }
}
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程:输出"+i);
        }
    }
}

实现Runnable

实现Runnable接口,需要重写里面的抽象方法run方法,相当于写好了一个线程执行的任务,可以简单理解为是创建一个任务,然后通过创建一个线程执行任务来创建线程任务。

public class test {
    public static void main(String[] args) {
        //创建一个线程任务
        MyRunnable r =new MyRunnable();
        //创建一个线程,并分配一个线程任务
        Thread thread=new Thread(r);
        //主线程
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程:输出" + i);
        }
    }
}
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程:输出"+i);
        }
    }
}

实现Runnable与继承Thread类的优势

1.通过创建任务的方式给线程分配的方式,更适合给多个线程同时执行相同任务。
2.可以避免单继承带来的局限性。Java中只有单继承,MyThread已经继承Thread类,不能再继承其他类。Java中允许多实现,实现Runnable是实现一个接口,实现Runnable的类还可以去实现其他接口,而切他还可以去继承其他的类,少了一些局限性。
3.线程和任务是分离的,提高了程序的健壮性。
4.线程池技术,只接受Runnable类型的任务,不接受Tread类型的技术。
那么为什么用继承Thread类的方式?因为它有一个很简单的实现方式,利用匿名内部类的方式实现。

public class test {
    public static void main(String[] args) {
        //创建一个线程任务
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程:输出" + i);
                }
            }
        }.start();
        //主线程
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程:输出" + i);
        }
    }
}

Thread类的常用方法

构造器描述
Thread()无参构造器
Thread​(Runnable target)设置任务
Thread​(Runnable target, String name)设置任务和名字
方法变量和类型描述
start()导致此线程开始执行; Java虚拟机调用此线程的run方法void
getName()返回此线程的名称String
getId()返回此线程的名称long
getName()返回此Thread的标识符String
getPriority()返回此线程的优先级int
setPriority​(int newPriority)更改此线程的优先级void
currentThread()返回对当前正在执行的线程对象的引用static Thread
sleep​(long millis)导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性static void
setDaemon​(boolean on)将此线程标记为 daemon线程或用户线程void
stop()已过时, 这种方法本质上是不安全的(如:突然的终止可能让线程正在使用的资源没来的及释放)void

线程安全问题

线程阻塞

可以理解为在线程的执行路径中,所有比较消耗时间的操作都可称为线程阻塞,比如常见的文件读取文件还没有读取完,接收用户输入用户还没有输入完成等,也可称其为耗时操作。

线程中断

一个线程是一个完整的执行路径,她是否应该结束应该是由其自身决定。就像一个生命的结束理应由其本身决定。早期版本的stop就像是掐死线程,可能会导致线程占用的资源得不到释放。版本更新后通过给线程打中断标记通过try-catch捕捉异常方式来决定是否"自我结束"。
我们这里添加一个子线程,让子线程运行起来,同时主线程做出操作,在主线程运行结束时,给子线程打上中断标记,让还没结束的子线程自我结束进程。

public class test {
    public static void main(String[] args) {
        //创建一个线程任务
        Thread thread =new Thread(new MyRunnable(),"t1");
        thread.start();
        //主线程
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("主线程:输出" + i);
        }
        //给子线程打上中断标记
        thread.interrupt();
    }
}

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                //e.printStackTrace();
                //自我结束进程
                return;
            }
            System.out.println("子线程:输出"+i);
        }
    }
}

守护线程

线程分为用户线程和守护线程
用户线程:当进程不包含任何的存活的线程,进行结束
守护线程:用于守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡
在线程创建时 线程.setDaemon(true);//设置为守护线程

多线程抢票线程安全机制

我们先来模拟一个买票的过程,创建一个包含有十张票数,当票数大于零时设置线程休眠一秒,再将票数减一来模拟实际抢票的线程任务, 并让多个线程同时执行次任务,此时线程的执行是不安全的

public class Demo3 {
    public static void main(String[] args) {
        //任务
        Runnable run = new Ticket();
        //创建线程
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable {
        //票数
        private int count = 10;

        @Override
        public void run() {
            while (count > 0) {
                //买票
                System.out.println(Thread.currentThread().getName() + "正在准备买票!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                }
                count--;
                System.out.println(Thread.currentThread().getName() + "出票成功!余票: " + count);
            }
        }
    }
}

隐式锁

1.同步代码块某一线程执行一段代码时,其他线程不能插足需要排队的效果。
格式:
synchronized(锁对象){
}
2.同步方法
用synchronized修饰方法,如:public synchronized void …

显式锁

Lock * =new ReentrantLock
*.lock()上锁
*.unlock()开锁

线程死锁

如下模拟警察和罪犯,理应来说应该是 犯说:你放了我,我放了人质。警察回应:罪犯被放走了,罪犯也放了人质,但事实上在主线程调用犯罪方法时,子线程调用警察方法,在有锁的情况下,罪犯方法有调用了警察方法,而警察方法也调用了罪犯方法,因而会处于活互相等待对象结束的情况而产生死锁。


package Demo;

public class Demo5 {
    /**
     * 模拟警察和罪犯,理应来说应该是 犯说:你放了我,我放了人质。警察回应:罪犯被放走了,罪犯也放了人质
     * 但事实上在主线程调用犯罪方法时,子线程调用警察方法,在有锁的情况下,罪犯方法有调用了警察方法,而警察方法也调用了
     * 罪犯方法,因而会处于活互相等待对象结束的情况而产生死锁
     *
     * @param args
     */
    public static void main(String[] args) {
        Police p = new Police();
        Culprit c = new Culprit();
        new MyThread(c, p).start();
        c.say(p);
    }

    public static class MyThread extends Thread {
        private Culprit c;
        private Police p;

        public MyThread(Culprit c, Police p) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.c = c;
            this.p = p;
        }

        @Override
        public void run() {
            this.p.say(c);
        }
    }

    //罪犯
    static class Culprit {
        public synchronized void say(Police p) {
            System.out.println("罪犯:你放了我,我放了人质");
            p.Back();
        }

        public synchronized void Back() {
            System.out.println("罪犯被放走了,罪犯也放了人质");
        }
    }

    //警察
    static class Police {
        public synchronized void say(Culprit c) {
            System.out.println("警察:你放了人质,我放了你");
            c.Back();
        }

        public synchronized void Back() {
            System.out.println("警察救到了人质,但是罪犯也跑了");
        }
    }
}


多线程通信

Object类

方法描述变量和类型
notify()唤醒正在此对象监视器上等待的单个线程。void
notifyAll()唤醒等待此对象监视器的所有线程。void
wait()导致当前线程等待它被唤醒,通常是 通知或 中断 。void

生产者消费者问题

如下场景,厨师做菜,服务端端菜,厨师做菜,服务端端菜…是合乎常理的逻辑,但是不考虑线程安全,可能会出现服务端菜,但是厨师还没做,厨师做菜,服务员不端菜…等等这样不安全的情况。在多线程使用情况下,我们可以通过notifyAll()、wait(),等方法进行线程的中断和唤醒。

public class Demo4 {
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }
    //厨师
    static class Cook extends Thread {
        private Food f;

        public Cook(Food f) {
            this.f = f;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                if (i % 2 == 0) {
                    f.setFood("麻辣香锅", "魔鬼辣");
                } else {
                    f.setFood("香酥鸡", "孜然味");
                }
            }
        }
    }

    //服务生
    static class Waiter extends Thread {
        private Food f;

        private Waiter(Food f) {
            this.f = f;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }
    //食物
    static class Food {
        private String name;
        private String taste;
        private boolean flag = true;

        public synchronized void setFood(String name, String taste) {
            this.name = name;
            if (flag) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag = false;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public synchronized void get() {
            if (!flag) {
                System.out.println("服务员端走的菜名是:" + name + ",味道是" + taste);
                flag = true;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

线程的六种状态

线程状态。 线程可以处于以下状态之一:
·NEW
尚未启动的线程处于此状态。
·RUNNABLE
在Java虚拟机中执行的线程处于此状态。
·BLOCKED
被阻塞等待监视器锁定的线程处于此状态。
·WAITING
无限期等待另一个线程执行特定操作的线程处于此状态。
·TIMED_WAITING
正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。
·TERMINATED
已退出的线程处于此状态。

第三种线程使用方式——Callable

Callable是一个功能接口,类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类而设计的。 但是, Runnable不会返回结果,也不能抛出已检查的异常。
FutureTask​(Callable callable)会创建一个 FutureTask ,在运行时将执行给定的 Callable 。

线程池

1.缓存线程池
长度无限制,任务加入后的流程:判断线程池是否有空闲线程,存在则使用,不存在则创建线程放入线程池再使用
2.定长线程池
长度是指定数值,任务加入后的流程:判断线程池是否存在空闲,存在则使用,不存在空闲线程且线程池未满的情况下,创建线程并放入线程池并使用,不存在空闲线程,且在线程池已满的情况下,则等待线程池存在空闲线程
3. 单线程线程池
判断线程池的哪个线程是否空闲,空闲则使用,不空闲则等待池中的某个线程空闲后使用
4. 周期任务的定长线程池
判断线程是否空闲,存在则使用,不存在空闲线程且线程池未满的情况下,创建线程并放入线程池并使用,不存在空闲线程,且在线程池已满的情况下,则等待线程池存在空闲线程
周期型执行任务时,定时执行,当某个时机触发时,自动执行某任务

Lambda表达式

函数式编程思想
打印“锄禾日当午”
第一种写法实现Runnable方法,实例化对象,创建线程,添加任务

public class Demo2 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.start();
    }
    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("锄禾日当午");
        }
    }
}


第二种写法直接用匿名内部类写法

public class Demo2 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("锄禾日当午");
            }
        }).start();
    }
}

第三种Lambda表达式

public class Demo2 {
    //Lambda表达式
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("锄禾日当午"));
        thread.start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值