Java多线程:让 cpu 从摸鱼到加班

在学习多线程以前,我们所写的大部分程序只开发了电脑强大的cpu一部分的算力,在运行迭代或者循环体量大的代码时会显得效率低下,多线程就像通过追加同时操作的通道来达到深度开发cpu算力的效果,让cpu从摸鱼到加班。(黑心老板竟是我自己)

1. 多线程的有关概念

1.线程与进程

线程被称为轻型进程,其被包含在进程中,一条进程必须有一条以上的线程

 

堆:作为内存存储大量数据,包括我们新new的对象。

方法区:存储的便是类中的信息,属性,方法等


一条进程有多条线程,在该任务有大部分时间使cpu休息的情况下,多线程使cpu在不同的线程跳转运算,可以增加cpu的效率,(黑老板压榨员工剩余价值)(大雾)

2.线程调度

定义:根据Jvm虚拟机的调度方式,来给多个线程分配cpu的使用时间片

调度方式:

  1. 分时调度,让每个线程平均分配cpu的使用权,且分配时间片也相同。

     

这是某个自律的人的任务进程表,这里可以看到自律的人有多可怕,有条不紊,可惜的是没有充分利用cpu(大脑),运动双线程之类的

  1. 抢占式调度,优先让可运行的优先级高线程运行,但也只是概率提高,但使用cpu的线程会一直运行,直至完成

     

这时我们可以看到,cpu成了香饽饽,各位都蓄势待发的争抢,但有人拿到入场券后又会等待,直到有人使用出来,这时可能他还会回首掏(优先让可运行的优先级高线程运行,但也只是概率提高),大大增加再抢到cpu的概率

Java使用的是抢占式调度

3.同步与异步

同步:当(甲)某线程请求某个资源时,若(乙)另一线程正在使用该资源,甲会等到乙线程使用完毕再使用

异步:当(甲)某线程请求某个资源时,若(乙)另一线程正在使用该资源,甲也能申请到该资源,不必等待

对于多线程,同步特化安全方面,异步特化效率方面,两者并没有优劣,具体从要解决的问题分析。这里讲的同步强调数据(资源)共享要同步

异步容易造成的数据问题

 

可以看出,线程甲在对链表A遍历,而线程乙想要操作C元素,如果两线程没加安全锁,是异步模式,甲已然遍历该链表,后乙修改C元素,那甲所储存的数据与实际数据就发生了错误,如果集合还未不安全的结构,进程直接报错,程序崩溃

4. 并发与并行

并发:指 2~n 个事件在某时间段发生

并行:指 2~n 个事件在同一时刻发生

这其实是最好理解的一组概念了,可以将自己的手指想象成一个cpu,一个手指打LOL时无论运行(手速)多快,都只能一次执行一个事件,但又因为手速很快,看起来像同时执行了几个事件,这就是并发,而五个手指操作qwerf等键就是完全的并行,每个手指(cpu)在团战时都有各自的任务

2.如何使用代码开启线程

1. Thread (类)

public class ThreadTest {
​
​
    public static void main(String[] args) {
        Shopper shopper = new Shopper();
        shopper.start();
        //或者
        //Thread thread = new Shopper();
        //thread.start();
    }
​
}
​
class Shopper extends Thread{
​
    String need = "汉堡";
    @Override
    public void run() {
        System.out.println("我想买个" + need);
    }
}

通过新建线程继承抽象类Thread,需要重写run()方法,且run()方法就是线程要执行的任务,run()里的代码是一条新的执行路径。

该线程的触发方式为:thread对象的start()来启动

2. 实现Runnable(接口)

public static void main(String[] args) {
    Runnable myRunnable = new Runnable() {
        String need = "油条";
        @Override
        public void run() {
            System.out.println("我想买个" + need);
        }
    };
    new Thread(myRunnable).start();
​
}

或者


public static void main(String[] args) {
    Runnable m = new myRunnable();
    new Thread(m).start();
​
}
​
static class myRunnable implements Runnable{
    String need = "油条";
    @Override
    public void run() {
        System.out.println("我想买个" + need);
    }
}

步骤:1.创建一个任务对象

2.创建一个线程,并分配一个任务

3.执行这个线程

3.两种线程创建并使用的方法总结

Runnable 与 Thread 相比

优势:

  1. 通过创建任务给线程分配来创建线程,更适合多个线程执行相同任务的情况(一核遇难,八核救援)

  2. 可以避免单继承带来的局限性(可以使一个线程完成更多任务(负载提升))

  3. 任务与线程分离,提高了程序的健壮性(适于逻辑思考)

  4. 线程池技术,仅接受Runnable类型的任务。

4.具有返回值的线程创建Callable(使用少)

public static void main(String[] args) {
        Callable<String> c = new callable();
        FutureTask<String> f = new FutureTask<>(c);
        new Thread(f).start();
    }
​
}
​
class callable implements Callable<String> {
    String need = "面包";
    @Override
    public String call() throws Exception {
        System.out.println(need);
        return need;
    }

不常用,了解即可。

3. Thread类常用方法分析

1. 构造方法

Thread有8种构造方法,通过上面的线程创建将其分类:

  1. Thread继承创建

    • Thread()

    • 分配一个新的 Thread对象。

    • Thread(String name) //给线程取名

    • 分配一个新的 Thread对象。

    • Thread(ThreadGroup group, String name)

    • //给线程分组,取名

    • 分配一个新的 Thread对象。

线程组(Thread Group):表示线程的集合,以树的方式存储,每个线程都有父线程组。

  1. Runnable实现任务创建

    • Thread(Runnable target)

    • 分配一个新的 Thread对象。

    • Thread(Runnable target, String name) //取名

    • 分配一个新的 Thread对象。

    • Thread(ThreadGroup group, Runnable target) //分组

    • 分配一个新的 Thread对象。

    • Thread(ThreadGroup group, Runnable target, String name) //取名并分组

    • 分配一个新的 Thread对象,使其具有 target作为其运行对象,具有指定的 name作为其名称,属于 group引用的线程组。

    • Thread(ThreadGroup group, Runnable target, String name, long stackSize) //加了个堆栈大小

    • 分配一个新的 Thread对象,以便它具有 target作为其运行对象,将指定的 name正如其名,以及属于该线程组由称作 group ,并具有指定的 堆栈大小

2.设置和获取

观测指标:

设置于获取:标识符(Id),名称(Name),优先级(Priority),状态(State)

方法:

  1. set+设置于获取观测指标();

  2. get+设置于获取观测指标();

toString(); 方法:输出线程的名称,优先级,线程组等信息


设置与检测:线程中断,守护线程

线程中断:

interrupt();让其中断

interrupted();/isInterrupted(); 测试是否中断

守护线程:守护用户线程(默认),当最后一个用户线程结束时,自动全部死亡。

setDaemon(true); 设为守护线程

isDaemon(); 检测这个线程是否为守护线程

3. sleep()

    • sleep(long millis)

    • 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。

    • sleep(long millis, int nanos)

    • 导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。

4.线程的尊严

线程应该被得到尊重,有自己中断自己的权利! (大雾)

==》其实就是实时监测,达到要求自动掐死。(具体检测环节写在任务的run方法中)

Thread对象.interrupt();给线程添加中断标记,只是给线程一个异常,我们(黑心老板)决定后续操作

5.线程的6种状态

 


6.线程安全

当我们使用多线程来使cpu干活时,经常会因为cpu时间片的分配导致出现逻辑错误,比如下面这个程序,会出现餐厅卖空了仍继续卖的情况。

public class Test {
    public static void main(String[] args) {
        Runnable sell = new sell();
        new Thread(sell,"甲").start();
        new Thread(sell,"乙").start();
        new Thread(sell,"丙").start();
    }
}
class sell implements Runnable{
    private int hamburger = 10;
    @Override
    public void run() {
        while (true){
            if(hamburger > 0){ 
                System.out.println("准备食材中");
                hamburger--;
                System.out.println(Thread.currentThread().getName()+"售卖了一个汉堡" + "剩余汉堡为" + hamburger);
​
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
准备食材中
准备食材中
准备食材中
乙售卖了一个汉堡剩余汉堡为7
丙售卖了一个汉堡剩余汉堡为8
甲售卖了一个汉堡剩余汉堡为9
准备食材中
甲售卖了一个汉堡剩余汉堡为6
准备食材中
丙售卖了一个汉堡剩余汉堡为5
准备食材中
乙售卖了一个汉堡剩余汉堡为4
准备食材中
准备食材中
准备食材中
丙售卖了一个汉堡剩余汉堡为2
甲售卖了一个汉堡剩余汉堡为3
乙售卖了一个汉堡剩余汉堡为1
准备食材中
准备食材中
准备食材中
乙售卖了一个汉堡剩余汉堡为-2
甲售卖了一个汉堡剩余汉堡为-1
丙售卖了一个汉堡剩余汉堡为0

要解决这样的线程安全问题,有三种方法:

1.同步代码块

原理:通过synchronized(锁对象(Object))来对线程run任务种一段代码块打标记来锁住,当运行时不会被其他线程截胡。

class sell implements Runnable{
    private int hamburger = 10;
    Object o = new Object();
    @Override
    public void run() {
        while (true){
            synchronized (o) {  //锁
                if (hamburger > 0) {
                    System.out.println("准备食材中");
                    hamburger--;
                    System.out.println(Thread.currentThread().getName() + "售卖了一个汉堡" + "剩余汉堡为" + hamburger);
​
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
准备食材中
甲售卖了一个汉堡剩余汉堡为9
准备食材中
甲售卖了一个汉堡剩余汉堡为8
准备食材中
甲售卖了一个汉堡剩余汉堡为7
准备食材中
甲售卖了一个汉堡剩余汉堡为6
准备食材中
甲售卖了一个汉堡剩余汉堡为5
准备食材中
甲售卖了一个汉堡剩余汉堡为4
准备食材中
甲售卖了一个汉堡剩余汉堡为3
准备食材中
丙售卖了一个汉堡剩余汉堡为2
准备食材中
丙售卖了一个汉堡剩余汉堡为1
准备食材中
丙售卖了一个汉堡剩余汉堡为0

这种方法有很大的缺陷,大部分时间其余的线程会阻塞,浪费内存,而且同一线程会对锁对象回首掏导致看起来就是单线程,所以可以优化为在汉堡数量告急时再使用锁来保证不出现逻辑错误。

2. 同步方法

将实际的运作代码块抽成一个方法,加入synchronized关键词修饰,再放入run();中方便使用

与第一个方法没有原理上的区别,在此不做演示。

3.显示锁Lock

原理: 建立锁对象:Lock lock = new ReentrantLock();

while(true){

lock.lock();

操作;

lock.unlock();

}

public class LockTest {
​
    public static void main(String[] args) {
        Runnable sell = new sell();
        new Thread(sell,"甲").start();
        new Thread(sell,"乙").start();
        new Thread(sell,"丙").start();
    }
​
    static class sell implements Runnable{
        Lock lock = new ReentrantLock();
        private int hamburger = 10;
        Object o = new Object();
        @Override
        public void run() {
            while (true){
                    lock.lock();  //Lock显示锁
                    if (hamburger > 0) {
                        System.out.println("准备食材中");
                        hamburger--;
                        System.out.println(Thread.currentThread().getName() + "售卖了一个汉堡" + "剩余汉堡为" + hamburger);
​
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    lock.unlock();
                }
            }
        }
    }

7.锁

线程死锁

线程死锁是跟锁有关的概念,当我们给各种方法套上锁,当两个线程需要互相调用被同一个锁修饰的方法时,就会产生线程死锁

public class ThreadDeadlock {
    public static void main(String[] args) {
        Study study = new Study();
        Rest rest = new Rest();
        Runnable play = new Runnable() {
            @Override
            public void run() {
                try {
                    study.say(rest);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Runnable learn = new Runnable() {
            @Override
            public void run() {
                try {
                    rest.say(study);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(learn).start();
        new Thread(play).start();
    }
​
​
​
    static class Study {
        public synchronized void say(Rest r) throws InterruptedException {
            System.out.println("我学累了就去休息.");
            Thread.sleep(1000);
            r.go();
        }
        public synchronized void go(){
            System.out.println("我去学习了!");
        }
    }
​
    static class Rest {
        public synchronized void say(Study s) throws InterruptedException {
            System.out.println("我休息好了就去学习.");
            Thread.sleep(1000);
            s.go();
        }
        public synchronized void go(){
            System.out.println("我去休息了!");
        }
    }
​
}

结果为:

我休息好了就去学习. 我学累了就去休息.

产生死锁

8.多线程交互问题

经典的多线程交互问题为两个线程交替进行时,这时候不仅需要安全锁,同时为了防止回首掏还要进行标记来使线程交替运作,如下:

public class MultiThreadedInteractionProblem {
​
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }
​
    public static class Cook extends Thread{
        private Food f;
        public Cook(Food f){
            this.f = f;
        }
        @Override
        public void run() {
            boolean flag =true;
            for (int i = 0; i < 10; i++) {
                if(flag){
                    try {
                        f.setNameAndTaste("蒙德土豆饼","美味的");
                        flag = false;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
​
                }
                if(!flag){
                    try {
                        f.setNameAndTaste("爪爪土豆饼", "奇怪的");
                        flag = true;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
​
    public static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f){
            this.f = f;
        }
​
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    f.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
​
    static class Food{
        private String name;
        private String taste;
​
        //这里的flag是为了防止回首掏导致一个线程一直运行
        private  boolean flag = true;
​
        public synchronized void setNameAndTaste(String name,String taste) throws InterruptedException {
            this.name = name;
            if(flag){
                Thread.sleep(100);
                this.taste = taste;
                flag = false;   //条件true才会使其运行
                this.notifyAll();
                this.wait();
            }
        }
​
        public synchronized void get() throws InterruptedException {
            if(!flag){  //条件false才会使其运行
                System.out.println("服务员端上的菜为:" + taste+name);
                flag = true;
                this.notifyAll();
                this.wait();
            }
        }
    }
}
服务员端上的菜为:美味的蒙德土豆饼
服务员端上的菜为:奇怪的爪爪土豆饼
服务员端上的菜为:美味的蒙德土豆饼
服务员端上的菜为:奇怪的爪爪土豆饼
服务员端上的菜为:美味的蒙德土豆饼
服务员端上的菜为:奇怪的爪爪土豆饼
服务员端上的菜为:美味的蒙德土豆饼
服务员端上的菜为:奇怪的爪爪土豆饼
服务员端上的菜为:美味的蒙德土豆饼
服务员端上的菜为:奇怪的爪爪土豆饼
  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值