2021-09-23 API之线程详解(下)

一、线程池:

线程池是一个管理线程的机制,主要解决两个问题:1、控制线程数量;2、重用线程。

1、线程池开启使用

        //创建固定大小的线程池(这里容量为2)
        ExecutorService threadpool = Executors.newFixedThreadPool(3);
        for (int i = 0; i <7 ; i++) {
            Runnable r = () -> {
                try {
                    Thread t = Thread.currentThread();
                    System.out.println(t+"正在执行任务……");
                    Thread.sleep(3000);
                    System.out.println(t+"任务执行完毕!!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            };
            System.out.println("交给线程池一个任务……");
            threadpool.execute(r);
        }

2、如何关闭线程池?

 ThreadPoolExecutor提供了两个方法用于线程池的关闭,分别是shutdown()和shutdownNow()

① shutdown()不会立即终止线程池,而是等所有任务缓冲队列中的任务都执行完后才终止,但也不会再接收新任务。

② shutdownNow()会立即终止线程池,并尝试打断正在执行的任务,并且i清空任务缓冲队列,返回尚未执行的任务。

二、守护线程

1、守护线程的概念

守护线程也称为后台线程,是通过普通线程调用 setDaemon(boolean on) 方法设置而来的,因此在创建上与普通线程无异,但是守护线程的结束实际有一点与普通线程不同,即进程的结束。

非守护线程也就是前台线程,运行结束就死亡。

虚拟机中所有存活的线程都是守护线程。只要还有存活的非守护线程虚拟机就不会退出,而是等待非守护线程执行完毕;反之,如果虚拟机中的线程都是守护线程,那么不管这些线程的死活java虚拟机都会退出。 

2、进程结束:

一个进程中的所有线程都结束时,进程就会结束。此时会杀掉所有正在运行的守护线程。

3、适合守护线程的:

通常但我们不关心某个线程的任务什么时候停下来时,它可以一直运行,但是程序重要的工作都结束时,它应当跟着结束时,这样的任务就适合放到守护线程上执行,比如GC就是在守护线程上执行的。

4、模拟泰坦尼克号:

    Thread Rose = new Thread(){
            @Override
            public void run() {
                for (int i = 0; i <3 ; i++) {
                    System.out.println("Rose:let me go!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("Rose:AaAaAaAaAaAaAa!");
                System.out.println("噗通一声跳下水!");
            }
        };
        Thread Jack = new Thread(){
            @Override
            public void run() {
                while (true){
                    for (int i = 0; i <3 ; i++) {
                        System.out.println("Jack:You jump!I jump!");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };
        Rose.start();
        Jack.setDaemon(true);//将Jack设置为守护线程;
        Jack.start();

可见:Jack是守护线程,当Rose线程结束后他才结束。如果在后面加入一个死循环:

        while (true);

这样main方法在Rose死后还在执行,Jack的守护线程就继续执行,会开启人类本质复读机。

主线程在这里死循环,只要有普通线程活着,进程就不会结束,Jack就不会被杀掉。

三、总结方法:

1、获取运行当前程序的线程 Thread t = Thread.currentThread();

2、获取线程名字 String name = t.getName();

3、获取线程的唯一标识:ID long id = t.getId();

4、获取线程优先级 int priority = t.getPriority();

5、判断线程是否活着 boolean isAlive = t.isAlive();

6、判断线程是否为守护线程 boolean isDaemon = t.isDaemon();

7、判断线程是否被中断 boolean isInterrupted = t.isInterrupted();

四、线程锁

1、多线程并发安全问题 :

当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现混乱,严重时可能导致系统瘫痪。 

临界资源:操作该资源的全过程同时只能被单个线程完成。 

相当于现实生活中多个人抢同一个东西导致的混乱。比如前些年的12315,当票没了时候大家还能继续抢,线程不安全,票直接负数。

 2、使用关键字:synchronized

当一个方法使用synchronized修饰后,这个方法称为”同步方法“。 即:多个线程不能同时在方法内部执行,只能有先后顺序的一个一个执行。 将并发操作同一临界资源的过程改为同步执行就可以有效地解决并发安全问题。

3、抢票问题

12315案例:可以设置线程锁,锁住抢票渠道(运行方法),当票数不合法时抛出异常。

class Ticket {
    private int ticket = 20;//有20张票
    public int getTicket(){
        if (ticket <= 0) {
            throw new RuntimeException("没票啦!");//当票没有了,就抛出异常
        }
        //让程序主动放弃时间片,模拟执行到这里没有事件发生程序切换。
        Thread.yield();
        return ticket--;//每次递减,把票发出去。
    }
}

 先定义一个票类,再在main方法中调用:

        Thread t1 = new Thread(){
            @Override
            public synchronized void run() {
                while (true){
                    int t = ticket.getTicket();
                    Thread.yield();
                    System.out.println(getName()+":"+t);
                }
            }
        };

这里的yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;就是获取到票之后让出自己的资源,回到就绪状态,把资源让给别的线程,让别的线程能抢到票,并且等待下一次CPU分配时间给他抢票。

如果不用线程锁,那么面临的情况就是在第一个线程抢票的同时第二个线程也在抢票,大家的数据是不互通的,不能及时更新数据,就可能出现俩人抢同一张的问题。但因为这俩人都在走这个方法,票数足够还好说,如果不够可能一顿操作下来票就是负数了。

使用线程锁后,就把这个抢票和数据更新绑定在一起,我在抢票的时候这张票就被锁住了,别人不能再获得这张票,需要等锁结束后再去获得下一张票的信息。这样虽然慢了点,但是很安全。

简单理解:锁就是捆绑,这段代码锁死了!以前是一行一行一行,现在是一行一锁一行。

五、同步块

同步块分为静态和非静态两种:

1、关键字:synchronized

2、非静态

有效地缩小同步范围可以在保证并发期安全的前提下尽可能提高并发起的效率。 同步块可以更准确的控制需要排队执行的代码片段。

语法: synchronized(同步监视对象) {需要多线程同步执行的代码片段}

使用同步块时,要制定同步监视器对象,即:上锁的对象。 这个对象可以是java中任何引用类型的实例,但是需要注意的是多个需要排队执行的线程看到的对象必须是同一个对象,否则没有效果! 在非静态方法上使用synchronized时,锁对象就是当前方法所属对象this。

 同步块和线程锁其实理解起来差不多,线程锁是把线程锁住,在方法声明时加一个修饰符synchronized。同步块就是synchronized去修饰一个代码块,只不过要声明同步监视对象。

3、非静态案例: 买衣服

买衣服试衣服需要保证试衣间一人用,即人进去了就得把试衣间锁起来。我们是正直网站,不允许出现一个人进去试衣服另一个人还能去试这种情况。

写一个Shop商店类:

class Shop{
//    Object obj = new Object();
    public void buy(){
        //获取运行buy方法的线程:
        try {
            Thread t = Thread.currentThread();
            System.out.println(t.getName()+":开始挑衣服……");
            Thread.sleep(1000);
//            synchronized (new Object()){//无效!多个线程看到的不是同一个锁
//            synchronized (obj){//有效!看到的是成员变量,一把锁
            synchronized (this){//有效!this指代当前类的对象,一把锁
                System.out.println(t.getName() + ":正在试衣服……");
                Thread.sleep(5000);
                System.out.println(t.getName() + ":试完了");
            }
            System.out.println(t.getName()+":结账离开!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

代码运行:

        Shop s = new Shop();
        Thread t1 = new Thread(){
            @Override
            public void run() {
                s.buy();
            }
        };
        Thread t2 = new Thread(){
            @Override
            public void run() {
                s.buy();
            }
        };
        t1.start();
        t2.start();

在写类时用了同步块,也就是把正在试衣服和试完了这部分代码锁起来,要么不走这块儿,要么就一走到底,中间不允许别人运行。同步监视对象是试衣间,需要找一个每次走这个类都不变的参照物去指代他,比如创建的成员变量obj,比如在本类中的this。

3、静态

静态的与对象无关,只要静态方法加synchronized就一定有同步效果。

 静态方法上使用synchronized,同步监视器对象为当前类的类对象,(Class类实例,即类名.class) JVM中每个被加载的类都有且仅有一个Class的实例与之对应,每个Class的实例都可以描述其表示的类的信息(后期反射知识点会介绍Class)

其实静态和非静态理解起来是一样的,只不过静态的是类的资源,全员共享只有一份,无论谁走它都能保证走的是这个东西,不是new的,是类的固定的唯一的。对比非静态需要使用synchronized(监视对象){ },静态的监视对象是静态的,只需方法加synchronized就可以了。

下面doSome方法的监视器的对象就是A的类对象。 使用同步块时,通常指定的也是当前类的类对象,获取方式:类名.class 

class A{
    public synchronized static void doSome(){
        try {
            Thread t = Thread.currentThread();
            System.out.println(t.getName()+"开始执行!");
            Thread.sleep(2000);
            System.out.println(t.getName()+":正在执行doSome()方法……");
            Thread.sleep(2000);
            System.out.println(t.getName()+"执行完毕!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

如果非要按照非静态那么写,静态方法是类的资源,没有this,用的是类对象,等价于synchronized(A.class)

六、互斥锁 

互斥:当多个线程执行不同的代码片段时,但是这些代码片段之间不能同时运行时就要设为互斥。

当synchronized锁定多个代码片段,并且这些同步块的指定监视器是同一个时,这些代码片段之间就是互斥的,不能同时执行它们。

简单来说,两个人玩跷跷板。默认一男一女,男比女重,两人都要使劲,才能玩的开心。

男的线程调用跷跷板的一边,女的调用另一边。不能男的下去女的也下去,这两头跷跷板是对立的,是互斥的,不能同时执行。男的只能往自己这边使劲,走自己的线程,调用自己的方法,女的也是。每个方法都通过通过synchronized把自己锁起来,一个线程只能调用一个方法,另一个线程只能调用另一个方法。

class B{
    public synchronized void m1(){
        Thread t = Thread.currentThread();
        System.out.println(t.getName()+"正在执行m1……");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m1执行完毕!");
    }
    public synchronized void m2(){
        Thread t = Thread.currentThread();
        System.out.println(t.getName()+"正在执行m2……");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2执行完毕!");
    }
}

七、死锁

 线程持有一个锁并等待另一个线程释放他的锁,而如果两个线程同时等待对方释放锁时就形成了死锁。

1、烟火案例:

一个人拿到了烟,想找火。一个人拿到了火,想找烟。

如果不使用锁的思想,单纯写两个线程。俩人烟火乱找乱窜,程序分配时间片不是固定模式的,给谁时间谁就走一下,那就乱了套了。

如果使用锁的思想,拿到烟的锁了烟还想找火锁火,拿到火的锁了火还想找烟锁烟,那么他俩都卡在这里不能进行下一步,谁也不让着谁。

这时候需要释放资源。拿到烟的锁了烟,拿完就释放资源,烟变成大家都能拿到的。拿到火的锁了火,拿完也释放资源,火也变成大家都能拿到的。锁住资源后抛出,大家互利互惠,再去找自己想要的有用的,这样问题就迎刃而解了。

    public static Object cigarette = new Object();
    public static Object lighter = new Object();

    public static void main(String[] args) {
        Thread p1 = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println("person1:叼起一根烟~");
                    synchronized (cigarette) {
                        System.out.println("person1:我的火机在哪里?");
                        Thread.sleep(3000);
                    }
                    System.out.println("person1:放回去烟~");
                    synchronized (lighter) {
                        System.out.println("person1:哇!火机欸!还有气儿~");
                        System.out.println("person1:一点火,一嘬烟,一顿快活似神仙~");
                        Thread.sleep(2000);
                    }
                    System.out.println("person1:老哥,谢谢火儿~");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread p2 = new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println("person2:掏出打火机~");
                    synchronized (lighter) {
                        System.out.println("person2:我的香烟在哪里?");
                        Thread.sleep(3000);
                    }
                    System.out.println("person2:放回去火~");
                    synchronized (cigarette) {
                        System.out.println("person2:哇!香烟欸!还有渣儿~");
                        System.out.println("person2:一杯酒,一根烟,一行代码敲一天~");
                        Thread.sleep(2000);
                    }
                    System.out.println("person2:老弟,消消烟儿~");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        p1.start();
        p2.start();
    }

线程锁就是锁住一个方法,让他这部分代码成为一个整体,不被别人干扰能独自完整。

同步块就是锁住一块儿,让这部分代码称为一个整体,运行时也不被干扰

互斥锁就是锁住一对互斥的东西,让调用的时候桥归桥路归路,大路朝天各走一边谁也别耽误谁。

死锁就是锁的时候互相包含了,自己的运行条件锁住了别人的运行条件,俩人互相耽误。为了避免死锁,就最好别一个锁套着另一个。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值