[并发实现原理] 01 多线程基础

一、线程的优雅关闭

1、为什么需要优雅关闭?运行一半的线程能否强制杀死?

  1. 杀死线程的方法:stop()、destory()函数
  1. 不建议使用,强制杀死线程,则线程中所使用的资源不能正常关闭(例如文件描述符、网络连接等)。
  1. 一个线程一旦运行起来,就不要去强行打断它,合理的关闭办法是让其运行完(也就是函数执行完毕),干净地释放掉所有资源,然后退出。
  1. 如果是一个不断循环运行的线程,就需要用到线程间的通信机制,让主线程通知其退出。

2、守护线程

  1. 介绍

a: 当在一个JVM进程里面开多个线程时,这些线程被分成两类:守护线程和非守护线程。默认开的都是非守护线程。在Java中有一个规定:当所有的非守护线程退出后,整个JVM进程就会退出。意思就是守护线程“不算作数”,守护线程不影响整个JVM进程的退出。

b: 例如,垃圾回收线程就是守护线程,它们在后台默默工作,当开发者的所有前台线程(非守护线程)都退出之后,整个JVM进程就退出了。

  1. 代码
package com.hao.demo.thread;

import lombok.extern.slf4j.Slf4j;

/**
 * @date 2020-07-02
 * 在main(..)函数中开了一个线程,不断循环打印。请问:main(..)函数退出之后,
 * 该线程是否会被强制退出?整个进程是否会强制退出?
 */
@Slf4j
public class Thread01 {
    public static void main(String[] args) {
        log.info("main start ............");
        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    log.info("子线程。。。。。。。。start");
                    Thread.sleep(500);
                } catch (InterruptedException e) {

                }
            }
        });
        /**
         * 加入下面的语句 当main(..)函数退出后,线程t1就会退出,整个进程也会退出。
         */
        //thread.setDaemon(true);
        thread.start();

        log.info("main end ............");
    }
}

3、设置关闭的标志位

package com.hao.demo.thread;

import lombok.extern.slf4j.Slf4j;

/**
 * @author flame
 */
@Slf4j
public class MyThread extends Thread{
    private boolean stoped = false;

    @Override
    public void run() {
       while (!stoped) {
           log.info("start..........");
       }
    }

    public void stop1() {
        this.stoped = true;
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        myThread.stop1(); // 通知线程退出
        myThread.join();  // 等待线程退出循环,自行退出
    }
}

二、InterruptedException()函数与interrupt()函数

1、什么情况下会抛出Interrupted异常

  1. 声明了InterruptedException 的函数才会抛出异常

2、轻量级阻塞与重量级阻塞

  1. 能够被中断的阻塞称为轻量级阻塞,对应的线程状态是WAITING或者TIMED_WAITING
  1. 像synchronized 这种不能被中断的阻塞称为重量级阻塞,对应的状态是BLOCKED。

3、线程的状态迁移过程

  1. 初始线程处于NEW状态,调用start()之后开始执行,进入RUNNING或者READY状态。
  1. 如果没有调用任何的阻塞函数,线程只会在RUNNING和READY之间切换,也就是系统的时间片调度。这两种状态的切换是操作系统完成的,开发者基本没有机会介入,除了可以
  1. 调用yield()函数,放弃对CPU的占用。一旦调用了任何阻塞函数,线程就会进入WAITING(无限期阻塞)或者TIMED_WAITING(阻塞有限时间)状态。
  1. 使用了synchronized关键字或者synchronized块,则会进入BLOCKED状态。
  1. 阻塞/唤醒函数,LockSupport.park()/unpark()=>Concurrent包中Lock的实现即依赖这一对操作。还有常用的阻塞唤醒函数。
  1. t.interrupted()的精确含义是“唤醒轻量级阻塞”,而不是字面意思“中断一个线程”。

4、t.isInterrupted()与Thread.interrupted()的区别

  1. t.interrupted()相当于给线程发送了一个唤醒的信号,所以如果线程此时恰好处于WAITING或者TIMED_WAITING状态,就会抛出一个InterruptedException,并且线程被唤醒。如果线程此时并没有被阻塞,则什么都不会做。
  1. 区别:t.isInterrupted只是读取中断状态,不修改状态;Thread.interrupted不仅读取中断状态,还会重置中断标志位。

三、synchronized关键字

1、锁的对象是什么

  1. 对于非静态成员函数,锁其实是加在对象a上面的;对于静态成员函数,锁是加在A.class上面的。当然,class本身也是对象。
  1. 伪代码
class A{
    public void synchronized f1() {...}
    public static void synchronized f1() {...}
}

等价于

class A{
    public void f1() {
       synchronized(this) {...} 
    }
    public static void f1() {
        synchronized(A.class) {...}
    }
}
A a = new A();
a.f1();
a.f2();

2、锁的本质是什么

  1. 多个线程要访问同一个资源。线程就是一段段运行的代码;资源就是一个变量、一个对象或一个文件等;而锁就是要实现线程对资源的访问控制,保证同一时间只能有一个线程去访问某一个资源
  1. 图片
thread1
lock
thread2
thread3
thread4
共享资源 变量 对象 文件
  1. 锁完成的事情

(1)这个对象内部得有一个标志位(state变量),记录自己有没有被某个线程占用(也就是记录当前有没有游客已经进入了房子)。最简单的情况是这个state有0、1两个取值,0表示没有线程占用这个锁,1表示有某个线程占用了这个锁。

(2)如果这个对象被某个线程占用,它得记录这个线程的thread ID,知道自己是被哪个线程占用了(也就是记录现在是谁在房子里面)。

(3)这个对象还得维护一个thread id list,记录其他所有阻塞的、等待拿这个锁的线程(也就是记录所有在外边等待的)。在当前线程释放锁之后(也就是把state从1改回0),从这个thread id list里面取一个线程唤醒。

3、synchronized实现原理

在对象头里,有一块数据叫MarkWord。在64位机器上,Mark Word是8字节(64位)的,这64位中有2个重要字段:锁标志位和占用该锁的thread ID。

四、wait()与notify()

1、生产者-消费者模型

一个内存队列,多个生产者线程往内存队列中放数据;多个消费者线程从内存队列中取数据。

(1)内存队列本身要加锁,才能实现线程安全。

(2)阻塞。当内存队列满了,生产者放不进去时,会被阻塞;当内存队列是空的时候,消费者无事可做,会被阻塞。

(3)双向通知。消费者被阻塞之后,生产者放入新数据,要notify()消费者;反之,生产者被阻塞之后,消费者消费了数据,要notify()生产者。

2、如何阻塞?

  1. 线程自己阻塞自己,也就是生产者、消费者线程各自调用wait()和notify()。
  1. 用一个阻塞队列,当取不到或者放不进去数据的时候,入队/出队函数本身就是阻塞的=>BlockingQueue的实现

3、如何双向通知?

  1. wait()与notify()机制。
  1. Condition机制。

4、为什么必须和synchronized一起使用?

  1. 两个线程之间要通信,对于同一个对象来说,一个线程调用该对象的wait(),另一个线程调用该对象的notify(),该对象本身就需要同步!所以,在调用wait()、notify()之前,要先通过synchronized关键字同步给对象,给该对象加锁。
  1. 伪代码
class A{
    private Object obj1 = new Object();
    public void f1() {
        synchronized(obj1) {
            ...
            obj1.wait();
            ...
        }
    }
    public void f2() {
        synchronized(obj1) {
            ...
            obj1.notify()
            ...
        }
    }
    
}

等于

class A{
    private Object obj1 = new Object();
    public synchronized void f1() {
        ...
        obj1.wait();
        ...
    }
    public synchronized void f2() {
        ...
        obj1.notify()
        ...
    }
    
}

5、为什么wait()的时候必须释放锁

  1. 当线程A进入synchronized(obj1)中之后,也就是对obj1上了锁。此时,调用wait()进入阻塞状态,一直不能退出synchronized代码块;那么,线程B永远无法进入synchronized(obj1)同步块里,永远没有机会调用notify(),就会出现死锁了?
  1. 在wait()的内部,会先释放锁obj1,然后进入阻塞状态,之后,它被另外一个线程用notify()唤醒,去重新拿锁!其次,wait()调用完成后,执行后面的业务逻辑代码,然后退出synchronized同步块,再次释放锁。
  1. wait() 内部伪代码
wait() {
    // 释放锁
    // 阻塞,等待被其他线程notify
    // 重新拿锁
}

6、wait()与notify()的问题

  1. 生产者本来只想通知消费者,但它把其他的生产者也通知了;
  1. 消费者本来只想通知生产者,但它被其他的消费者通知了。
  1. 原因就是wait()和notify()所作用的对象和synchronized所作用的对象是同一个,只能有一个对象,无法区分队列空和列队满两个条件。
  1. 伪代码
public void enqueue() {
    synchronized(queue) {
        while(queue.full()) 
         queue.awit();
         ... // 入队列
         queue.notify() // 通知消费者,队列中有数据了
    }
}

public void dequeue() {
    synchronized(queue) {
        while(queue.empty())
         queue.awit();
         ... //出队列
         queue.notify() // 通知生产者,队列中有空位了,可以继续放数据 
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值