Java多线程第二篇-线程的常用方法和线程安全

一.线程的常用方法

1.线程终止

1.1通过成员对线程进行终止

变量创建需要以static修饰并且变量成员需要以final修饰或者非final修饰(不变的常量)才能进入到run方法中,但是如果你想终止的话,你一定要修改它的值,所以我们必须要定义一个外部成员。

public class Demo7 {
    //这里的成员默认false
    private static boolean isQuit;
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            //不为true进入循环
           while(!isQuit){
               System.out.println("Thread is Running");
               try {
                   //thread线程睡眠1000
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
           //这里跳出循环后打印
            System.out.println("Thread closing");
        });
        thread.start();
        //这里进入start后线程和主线程同时运行,程序继续往下走
        //这里sleep睡眠5000ms为条件,达成条件后继续往下执行
        Thread.sleep(5000);
        isQuit=true;
        //这里睡眠2000在执行下面代码,这时候打印为TERMINATED
            Thread.sleep(2000);
        System.out.println(thread.getState());
    }
}

1.2通过Thread本地方法终止

通过上述我们明白如果我们创建,可以看到缺点。
1.需要手动创建变量。
2.当线程内部在sleep的时候,主线程修改为ture,这时候结束了循环,但是新的线程内部无法及时的做出响应。
这时候我们就要通过Java中的本地方法来进行线程的中断。

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
          //这里的currentThread方法可以获得当前线程的实例,但是无法直接写成thread,这时候我们的thread还没有构造完成。
            //isInterrupted是标志位置,判断线程是否结束。
            //isInterrupted 默认为false
           while(!Thread.currentThread().isInterrupted()){
               System.out.println("Thread is running");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
            System.out.println("结束线程");
        });
        thread.start();
        Thread.sleep(3000);
        //这里修改为true,循环会结束?
        thread.interrupt();
        Thread.sleep(2000);
        System.out.println("Thread exit");
    }
}

在这里插入图片描述

这里我们可以看到线程还是在执行,为什么呢?
正常来说sleep会进入休眠,此处给到interrupt改为true之后就可以使用sleep内部触发一个异常,提前被唤醒,清除该实例的标志位。
这里清除该实例标志位后如果不使用break或者throw出异常则会继续执行,可以将选择权限交给开发者进行选择,我们可以通过break来跳出或者直接抛出。

  • 以下修改的部分
 while (!Thread.currentThread().isInterrupted()) {
                System.out.println("Thread is working");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                    //或者直接通过上述代码的打印异常增加一个break,break前可以增加一些想要添加的代码。
                    //break;
                }
            }
        });

2.线程的等待join

join 是实现线程等待的效果,在主线程调用thread.join(),此时是主线程等待thread线程先结束。
让一个线程,等待另一个线程执行结束,在继续执行,本质上就是控制线程结束的顺序的方式。

2.1不增加参数

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread =new Thread(()->{
           while(!Thread.currentThread().isInterrupted()){
               System.out.println("Thread is running:1");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   break;
               }
           }
        });
        thread.start();
            Thread.sleep(3000);
        thread.interrupt();
        //这里的join就是阻断,让其执行完成后在继续执行其他线程
            thread.join();
        for(int i=0;i<3;i++){
            System.out.println("Thread is running:2");
            Thread.sleep(1000);
        }
    }
}


2.2增加参数

当增加参数后我们会限定join的等待时长,如果超过该等待时长则继续执行

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
          for (int i=0;i<10;i++) {
                System.out.println("Thread is running:1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
            }
        });
        thread.start();
//           这里等待thread执行完成则主线程开始执行
//            如果join带参数后,则是需要等待的时间,如果等待时间过长我们就需要将主线程继续进行
        thread.join(3000);
        for(int i=0;i<3;i++){
            System.out.println("Thread is running:2");
        }
    }
}


2.3线程的休眠时间

线程的sleep是有误差的,因为线程的调度开销也是需要时间,所以可能不是很准确,比较随机。

  public static void main(String[] args) throws InterruptedException {
       
        long start = System.currentTimeMillis();
        Thread.sleep(1000);
        long end = System.currentTimeMillis();
        System.out.println("开始和结束的时差(以ms为单位):"+(end-start));
    }

在这里插入图片描述


二.线程的其他状态

线程状态说明
NEWTHREAD的对象已经存在,start方法还没调用,没有创建新的线程
TERMINATEDTHREAD的对象仍然存在,内核中的线程已经销毁
RUNNABLE就绪状态(线程已经在cpu上执行了/线程正在排队等待在cpu执行)
TIMED_WAITING阻塞状态,由于sleep这种固定时间的方式产生的阻塞
WAITING阻塞状态,由于wait这种不固定时间的方式产生的阻塞
BLOCKED阻塞状态,由于锁竞争导致的阻塞

  • 1.NEW状态
   public static void main(String[] args)throws InterruptedException {
            Thread thread=new Thread(()->{
            });
            //在调用start之前获取的状态,此时是NEW状态
            System.out.println(thread.getState());
        }

  • 2.TERMINATED状态
 public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
        });
        thread.start();
        thread.join();
        //这里start开始创建运行线程,当线程结束后(因为join对其阻塞)这时候线程销毁状态为TERMINATED
        System.out.println(thread.getState());
    }

  • 3. RUNNABLE状态
public static void main(String[] args) {
        Thread thread=new Thread(()->{
        });
        thread.start();
        //这里start已经创建好线程正在就绪,是RUNNABLE状态
        System.out.println(thread.getState());
    }

  • 4.TIMED_WAITING状态
   public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
           while (true){
               try {
                   Thread.sleep(800);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        thread.start();
        while(true){
            //通过thread线程的进行sleep阻塞的状态sleep是TIMED_WAITING状态
            Thread.sleep(1000);
            System.out.println(thread.getState());
        }
    }

三.线程安全

线程安全是什么?
线程安全是当一段代码在单个线程中跑的时候,不会出现问题。
但是如果放到了多个线程中则出现问题(bug),我们把这种叫做“线程的安全问题”或者是“线程不安全”。


3.1多线程执行

假设定义一个成员变量count默认为0,通过两个线程将count存储至十万数值。

 private static int count;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            for(int i=0;i<50000;i++){
                count++;
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<50000;i++){
                count++;
            }
        });
        t1.start();
        //如果t1和t2start同时执行,没有join的约束则会出现bug
        t1.join();
        //线程两个如果并发开始进行则出现bug,我们的实际结果输出十万
        t2.start();
        t2.join();
        System.out.println(count);
    }

这里的count每次自增的操作本质是分成三步进行(站在cpu的角度通过三个指令进行实现)⬇️
1.load: 将数据从内存中,读到cpu寄存器中。
2.add: 将寄存器的数据进行+1自增。
3.save: 把寄存器中的数据,保存到内存中。

在这里插入图片描述

如果多个线程执行上述的代码,因为线程的调度顺序是“随机”的,因在count++中会执行N次而且三个指令的cpu指令顺序不同,就会导致在有些调度顺序下,并发程度高,上述逻辑就会出现问题。

通过上述可以意识到,在多线程中,在随机调度的时候,多个线程之间存在诸多可能的先后顺序,我们必须要在保证所有可能的情况下,代码是正确的。


3.2线程加锁

使用线程加锁来避免解决线程的冲突。
synchronized 在使用的时候,要搭配一个代码块{} 进入{就会加锁 出了}就会解锁,这里synchronized()中需要表示一个用来加锁的对象,这个对象是谁不重要,重要的是通过这个对象来区分两个线程是否在竞争同一块锁。
在已经加锁的状态中,另一个线程尝试同样的加锁,就会产生“锁冲突/锁竞争”,后一个线程就会阻塞等待,一直到前一个线程解锁为止。

public class Test {
    private static int count;
    public static void main(String[] args) throws InterruptedException {
    //这里是上锁,前提是线程公用一个锁!!
        Object locker =new Object();
        Thread t1=new Thread(()->{
            for(int i=0;i<50000;i++){
                synchronized (locker){
                    count++;
                }
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<50000;i++){
                synchronized(locker){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(count);
    }
}


synchronized重要的特性,在Java中针对一个对象加多个锁是可重入的。
所谓的可重入锁,指的是一个线程,一把锁,加锁两次,会出现死锁,就是“不可重入“,反之,就是”可重入“。

3.2.1死锁

在多个线程中,如果是有N把锁(嵌套锁),无论是否可重入,都会进入死锁状态。


public class Test {
   private static final Object locker1=new Object();
   private static final Object locker2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized(locker1){
                System.out.println("hello t1:1");
                synchronized (locker2){
                    System.out.println("hello t1:2");
                }
            }
        });

        Thread t2=new Thread(()->{
            synchronized(locker2){
                System.out.println("hello t2:2");
                synchronized (locker1){
                    System.out.println("hello t2:1");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

3.2.2哲学家进餐

这里的锁可以以下列来举例,哲学家进餐。
注意:这里的叉子相当于筷子。
这里每个哲学家都有一份面并且左右两边都有一副叉子,每一个哲学家必须使用两副叉子才能进行吃饭,叉子在同一时间只能被同一个人使用
,如果哲学家没有拿到两副叉子,则无法吃面。
这里哲学家的状态只有两种:一种是吃面,一种是思考(等待)。

在这里插入图片描述

3.2.3并行算法

这里如果哲学家在吃饭和思考的状态下切换,这里如果哲学家吃饭和思考都是任意时间段,如果每个哲学家饿的时间和思考的时间不固定,则即可交替执行。

这里如果一个哲学家想要拿这个叉子,而另一个哲学家也想拿,这时候则为死锁,两个哲学家都吃不到面。

3.2.4死锁的达成条件

1.互斥使用:(锁的基本特性)当一个线程中持有着一把锁,另一个线程也想要获取锁,这时候就要阻塞等待。
2.不可抢占:(锁的基本特性)当锁已经被线程1拿到之后,线程2只能等线程1主动释放掉,不能强占。
3.请求保持:(代码的结构)一个线程中如果获取多把锁(当获取第一把锁之后在获取第二把锁,在获取的过程中,锁1不会被释放。)
4.循环等待/环路等待:等待的依赖关系,形成循环。
如果有两把锁设a、b,线程1获取到锁a,然后线程2也获取到锁b,这时候线程1嵌套获取b锁,线程2嵌套获取a锁,则循环等待锁释放,进不去出不来。
例如:你此时在车库,打不开车库门,这时候车钥匙在家,你这时候给你你老婆打电话,让他回家帮你拿钥匙,而你老婆这时候说家的钥匙我落到车库啦~ 。这时候就陷入一种嵌套循环,形成死锁。
⬆️以上为死锁的四个条件,尽量避免锁嵌套结构!


3.3volatile关键字

  • 保证内存的可见性。
  • 禁止指令重排序。

计算机在运行过程中需要访问数据,这些数据会放在内存中(定义一个变量时,变量就是在内存中)。
cpu在使用这个变量时,就会把这些内存中的数据读取出来,放到cpu的寄存器中参与运算(load读取内存值放到寄存器中)。
寄存器读取速度>内存读取速度>硬盘读取运算。
为了解决上述的问题,这时候编译器可能对代码进行优化,把一些本来要读取内存的操作,优化成读取寄存器,减少读取内存的次数来提高代码的整体效率

public class Test {
    private  static int isQuit=0;
    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
                //循环没有内容,但是也会循环许多次
            while(isQuit==0){
            }
            System.out.println("isQuit");
        });
        thread1.start();

        Thread thread2=new Thread(()->{
            //用户通过第二个线程输入值不为0,则循环会结束?
            Scanner scanner=new Scanner(System.in);
            isQuit=scanner.nextInt();
        });
        thread2.start();
    }
}
  

这时候一个线程读和一个线程写,仍然在执行,此时出现bug(引起线程安全问题),此时就是因为内存可见性引起。
1.load读取内存中的isQuit值放入寄存器中。
2.通过cmp指令比较寄存器中的值是否为0,绝对是否要继续循环。
此时会发生大量的循环,大量load和cmp操作,这时候编译器/JVM发现load出来的结果isQuit都是相同的,并且load读取内存到寄存器中很花费时间(1次load的时间相当于上万次的cmp)。
此时编译器则为了更方便的操作,只有第一次循环时读取内存器,后续不在读取内存,而是直接从寄存器中取isQuit的值,此时内存中无法读取数据编译器却没有进行读取,而是一直在寄存器中执行,此时就出现了bug。

在这里插入图片描述

在这里插入图片描述


  • 方案1

上述方案则因在寄存器中读取快,但是算的不准。
而volatile关键词来解决此方案,而volatile则是让JVM继续读取内存中的数据然后通过寄存器在逐个比较,解决此方案。

public class Test {
    private volatile static int isQuit=0;
    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            while(isQuit==0){
                //循环没有内容,但是也会循环许多次
            }
            System.out.println("isQuit");
        });
        thread1.start();
        Thread thread2=new Thread(()->{
            //用户通过第二个线程输入值不为0,则循环会结束?
            Scanner scanner=new Scanner(System.in);
            isQuit=scanner.nextInt();
        });
        thread2.start();
    }
}

在这里插入图片描述


  • 方案2

我们可以减少读取的次数,通过sleep睡眠来限定load的读取次数,此时就不会出发内存可见性问题,但是什么时候代码会被JVM优化,我们人是不清楚的,只有机器清除,所以我们尽量使用volatile更保险一些,此方法知道即可。

public class Test {
    private  static int isQuit=0;
    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            while(isQuit==0){
                //循环没有内容,但是也会循环许多次
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("isQuit");
        });
        thread1.start();

        Thread thread2=new Thread(()->{
            //用户通过第二个线程输入值不为0,则循环会结束?
            Scanner scanner=new Scanner(System.in);
            isQuit=scanner.nextInt();
        });
        thread2.start();
    }

3.4 wait和notify

wait和notify是用来协调多个线程中的执行顺序,本身多个线程的执行顺序是随机的,很多的时候,需要通过一些手段来对线程进行干预,前面说过join也是一种干预,但只是影响了线程的结束的先后顺序,很多时候,我们希望线程不要结束,也要进行干预形成先后顺序。

wait(不带参数): 让指定的线程进入阻塞状态,且无notify则处于一直阻塞状态,代码无法向下执行。
wait(带参数):指定线程的时间,超出时间则向下继续执行代码。
notify: 唤醒对应阻塞状态的线程。
notifyAll:唤醒所有处于阻塞状态的线程。

public class Test {
    public static void main(String[] args) {
        Object obj=new Object();
        Thread t1=new Thread(()->{
            synchronized(obj){
                try {
                    System.out.println("wait等待1");
                    obj.wait();
                    System.out.println("wait1结束");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread t3=new Thread(()->{
            synchronized(obj){
                try {
                    System.out.println("wait等待2");
                    obj.wait();
                    System.out.println("wait2结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t2=new Thread(()->{
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (obj){
                obj.notifyAll();
                System.out.println("通知");
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

评论 41
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值