JAVA:多线程:创建和启动、休眠和中断、优先级、礼让和加入、线程锁和线程同步、线程锁、死锁

JAVA多线程

首先回顾以下操作系统中的进程。
进程是程序执行的实体,每一个进程都是一个应用程序,都有自己的内存空间(互不干扰),CPU一个核心同时只能处理一件事情,当出现多个进程需要同时运行时,CPU一般通过时间片轮调度算法,来实现多个进程的同时运行。
在这里插入图片描述
早期计算机,进程之间相互通信很麻烦,因此线程横空出世。
一个进程可以有多个线程,线程是程序执行中一个单一的顺序控制流程,现在线程才是程序执行流的最小单元,各个线程之间共享程序的内存空间,上下文切换速度也高于进程。

线程的创建和启动

通过创建Thread对象来创建一个新的线程,Thread构造方法中需要传入一个Runnable接口的实现(其实就是编写要在另一个线程执行的内容逻辑)

    /**
     * Allocates a new {@code Thread} object. This constructor has the same
     * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
     * {@code (null, target, gname)}, where {@code gname} is a newly generated
     * name. Automatically generated names are of the form
     * {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.
     *
     * @param  target
     *         the object whose {@code run} method is invoked when this thread
     *         is started. If {@code null}, this classes {@code run} method does
     *         nothing.
     */
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

同时Runnable只有一个未实现的方法,因此可以直接使用lambda表达式
原来的表达式

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("aaa");
            }
        });        //把执行内容写进去
        t.start();
    }
}

lambda表达式

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("aaa"));        //把执行内容写进去
        t.start();
    }
}

以下代码来展现线程魅力
Case1:

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("我是线程:"+ Thread.currentThread().getName());
            System.out.println("计算0-10000之间所有的数");
            int sum = 0;
            for (int i = 0; i<= 10000; i++){
                sum += i;
            }
            System.out.println("结果:" + sum);
        });        //把执行内容写进去
        t.start();
        System.out.println("我是主线程");
    }
}

结果:
在这里插入图片描述

Case2:

public class Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i<500; i++){
                System.out.println("我是一号线程:"+i);
            }
        });        //把执行内容写进去

        Thread t2 = new Thread(() -> {
            for (int i = 0; i<500; i++){
                System.out.println("我是二号线程:"+i);
            }
        });
        t1.start();
        t2.start();
        System.out.println("我是主线程");
    }
}

以上是交替打印,说明是在同时进行的。
除了start也有run方法,但是run不是创建一个线程,而是直接在当前线程执行。
在这里插入图片描述
实际上,线程和进程差不多,也会等待CPU资源,当需要等待外部IO操作,比如Scanner,也会暂时处于休眠状态,或者调用sleep()方法让当前线程休眠一段时间。

public class Main {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("s");
        Thread.sleep(1000); //休眠时间1000ms,需要相应中断
        System.out.println("j");
    }
}

使用stop()方法强行终止此线程。

疑问:

public class Main {
    private static int value = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i<10000; i++){
                value ++;
            }
            System.out.println("线程1完成");
        });        //把执行内容写进去

        Thread t2 = new Thread(() -> {
            for (int i = 0; i<10000; i++){
                value ++;
            }
            System.out.println("线程2完成");
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);     //主线程停止1秒
        System.out.println(value);
    }
}

为什么上述代码执行后value值小于20000?

线程的休眠和中断

前面提到,一个线程处于运行状态下,线程的下一个状态会出现以下情况:
a、当CPU给予的运行时间结束时,会从运行状态回到就绪(可运行状态),等待下一次获取CPU资源

b、当线程进入休眠/阻塞(如等待IO请求)/手动调用wait()方法时,会使得线程处于等待状态,当等待状态结束后会回到就绪状态。

c、当线程出现异常或错误/被stop()方法强行停止/所有代码执行结束时,会使得线程的运行终止。

注:
不推荐使用stop()直接杀死线程,推荐使用interrupt()。因为stop可能会导致资源无法完全释放。
实例:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("程序开始运行");
            while(true){
                if(Thread.currentThread().isInterrupted()){     //判断是否存在中断标志
                    break;  //相应中断
                }
            }
            System.out.println("线程中断咯");
        });        //把执行内容写进去
        t1.start();
        try{
            Thread.sleep(3000);//休眠3秒
            t1.interrupt();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
    }
}


复位中断:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("程序开始运行");
            while(true){
                if(Thread.currentThread().isInterrupted()){     //判断是否存在中断标志
                    System.out.println("发现中断信号,复位,继续运行...");
                    Thread.interrupted();  //复位中断标记
                }
            }
        });        //把执行内容写进去
        t1.start();
        try{
            Thread.sleep(3000);//休眠3秒
            t1.interrupt();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
    }
}

如果我们想暂停线程,比如等待其他线程执行完成后,在继续运行,那这样的操作怎么实现呢?
有一个suspend()和resume()方法,但是它很容易导致思索,因此此方法已经被抛弃,具体如何实现会放到线程锁中继续探讨。

线程的优先级

实际上,Java程序中的每个线程并不是平均分配CPU时间的,为了使得线程资源分配更加合理,Java采用的是抢占式调度方式,优先级越高的线程,优先使用CPU资源。希望CPU花费更多的时间去处理更重要的任务,而不太重要的任务可以先让出一部分资源。线程的优先级一般分为以下三种:
MIN_PRIORITY 最低优先级
MAX_PRIORITY 最高优先级
NOM_PRIORITY 常规优先级

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() ->{
            System.out.println("线程开始运行");
        });
        t.start();
        t.setPriority(Thread.MIN_PRIORITY);     //通过setPriority方法来设定优先级
    }
}

线程的礼让和加入

礼让

我们还可以当线程的工作不重要时,将CPU资源让位给其他线程,通过使用yield()方法来将当前资源让位给其他同优先级线程。

public class Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("线程1开始");
            for(int i = 0; i < 50; i++){
                if (i % 5 == 0){
                    System.out.println("让位");
                    Thread.yield();
                }
                System.out.println("1打印:" + i);
            }
            System.out.println("线程1结束");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("线程2开始");
            for (int i = 0; i < 50; i++){
                System.out.println("2打印:" + i);
            }
        });
        t1.start();
        t2.start();
}
}

加入

当我们希望一个线程等待另一个线程执行完成后再继续进行,我们可以使用join()方法来实现线程的加入。

public class Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("线程1开始");
            for(int i = 0; i < 50; i++){
                System.out.println("1打印:" + i);
            }
            System.out.println("线程1结束");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("线程2开始");
            for (int i = 0; i < 50; i++){
                System.out.println("2打印:" + i);
                if (i == 10){
                    try{
                        System.out.println("线程1加入到此线程");
                        t1.join();      //i == 10时,让线程1加入,先完成线程1的内容,再继续当前内容
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.start();
        t2.start();
}
}

礼让和加入的区别在于,礼让是把这一次的机会让给别人,如果下次CPU还有机会,就会再继续执行。而加入是等目标线程完成后才继续。

线程锁和线程同步

首先先了解一下多线程的Java内存管理情况
在这里插入图片描述
线程之间的共享变量(例如之前的value值)存储在主内存中,每个线程都有一个私有的工作内存,每个线程都有一个私有的工作内存,工作内存中存储了该线程已读/写共享变量的部分。类似于计算机组成原理中的多处理器高速缓存机制。
在这里插入图片描述
高速缓冲通过保存内存中数据的部分来提供更加快速的数据访问,但是如果多个处理器的运算任务都设计同一块内存区域,就可能导致各自的高速缓存数据不一致,在写回主内存时就会发生冲突,这就是引入告诉缓存引发的新问题:缓存一致性。

实际上,Java的内存模型也是这样类似设计的,当我们同时去操作一个共享变量时,如果仅仅是读取还好,但是如果同时写入内容,就会出现问题。

因此回到上面那个疑问:

public class Main {
    private static int value = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i<10000; i++){
                value ++;
            }
            System.out.println("线程1完成");
        });        //把执行内容写进去

        Thread t2 = new Thread(() -> {
            for (int i = 0; i<10000; i++){
                value ++;
            }
            System.out.println("线程2完成");
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);     //主线程停止1秒
        System.out.println(value);
    }
}

实际上,当两个线程同时读取value的时候,可能会拿到同样的值,然后进行自增后也是同样的值,再协会主内存后,本来应该进行2次自增操作,实际上只执行了1次。

如果要去解决这样得问题,就必须采用某种同步机制,来限制不同线程对于共享变量的访问。我们希望的是保证共享变量value自增操作的原子性(原子性是指一个操作或多个操作要么全部执行,且执行的过程不会被任何因素打断,包括其他线程,要么就不执行。)

线程锁

通过synchronized关键字来创造一个线程锁,首先来认识一下synchronized代码块,它需要在括号中填入一个内容,必须是一个对象或是一个类,我们在value自增操作外套上同步代码块:

public class Main {
    private static int value = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i<10000; i++){
                synchronized (Main.class){                  //套入代码块
                    value ++;
                }
            }
            System.out.println("线程1完成");
        });        //把执行内容写进去

        Thread t2 = new Thread(() -> {
            for (int i = 0; i<10000; i++){
                synchronized (Main.class){                  //套入代码块
                    value ++;
                }
            }
            System.out.println("线程2完成");
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);     //主线程停止1秒
        System.out.println(value);
    }
}

我们发现,此时value值就是20000了,因为在同步代码执行过程中,拿到了我们传入对象或类的锁(传入的如果是对象,就是对象锁,不同对象代表不同的对象锁,如果是类, 就是类锁,类锁只有一个,实际上类锁也是对象锁,是Class类实例,但是Class类实例同样的类无论怎么获取都是同一个),但是注意两个线程必须使用同一把锁。
例如:

public class Main {
    private static int value = 0;

    public static void main(String[] args) throws InterruptedException {
        Main main1 = new Main();
        Main main2 = new Main();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i<10000; i++){
                synchronized (main1){                  //套入代码块
                    value ++;
                }
            }
            System.out.println("线程1完成");
        });        //把执行内容写进去

        Thread t2 = new Thread(() -> {
            for (int i = 0; i<10000; i++){
                synchronized (main2){                  //套入代码块
                    value ++;
                }
            }
            System.out.println("线程2完成");
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);     //主线程停止1秒
        System.out.println(value);
    }
}

当一个线程进入到同步代码块时,会获取到当前的锁,而这时如果其他使用同样的锁的同步代码块也想执行内容,就必须等待当前同步代码块的内容执行完毕,在执行完毕后会自动释放这把锁,而其他线程才能拿到这把锁并开始执行同步代码块里面的内容。(实际上synchronized时一种悲观锁,随时都认为有其他线程对数据进行修改,后面有机会还会介绍乐观锁,如CAS算法)

synchronized关键字也可以作用于方法上,调用此方法时也会获取锁

public class Main {
    private static int value = 0;

    private static synchronized void add(){
        value++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i<10000; i++){
                add();
            }
            System.out.println("线程1完成");
        });        //把执行内容写进去

        Thread t2 = new Thread(() -> {
            for (int i = 0; i<10000; i++){
                add();
            }
            System.out.println("线程2完成");
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);     //主线程停止1秒
        System.out.println(value);
    }
}

死锁

死锁在操作系统中也有提及,它是指两个线程互相持有对方需要的锁,但是又迟迟不释放,导致程序卡住。

在这里插入图片描述

实例:

public class Main {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (o1){
                try{
                    Thread.sleep(1000);
                    synchronized (o2){
                        System.out.println("线程1");
                    }
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (o2){
                try{
                    Thread.sleep(1000);
                    synchronized (o1){
                        System.out.println("线程2");
                    }
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
    }
}

那么如何去检测思索呢?我们可以利用jstack命令来检测死锁,首先利用jps找到我们的java进程:
在这里插入图片描述
输入jstack 2816(就是Main前面的序列,我这里是2816)
在这里插入图片描述
这里看到有发现一个死锁,而且位置也标注出来了。
![在这里插入图片描述](https://img-blog.csdnimg.cn/55f28e6e16d84b3290df7e16ee24f84b.png
)

或者使用jconsole.exe
在这里插入图片描述
在这里插入图片描述

填坑:为什么不适用suspend()去挂起线程的原因,是因为suspend()在使线程暂停的同时,并不会去释放任何锁资源。其他线程都无法访问被它占用的锁。最终会导致死锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值