Java深入了解多线程

程序 进程 线程

  1. 程序是静态的概念,是为了完成某个任务而编写的一组指令的集合
  2. 进程是动态的概念,是程序的一次实际运行
    • 是操作系统进行资源分配的基本单位
    • 进程是重量级的任务
  3. 为提高CPU利用率,可以在一个进程中划分出多条代码执行路径,并且同时执行。每一条独立的代码执行路径称为线程
    • 线程是CPU调度和执行的基本单位
    • 线程是轻量级的任务
    • 单核CPU电脑同一时间内只能执行一个线程
  4. 操作系统是容纳多个进程的容器,进程又是容纳多个线程的容器

线程vs进程

在这里插入图片描述

方法嵌套调用 vs 多线程

  1. 无论再复杂的方法嵌套调用,其本质都只有一条代码执行路径,而多线程是在程序中有多条执行路径同时运行

  2. 以前写的Java程序都是单线程程序,即使代码中不显式创建线程,main()方法中的代码也都运行在一个叫main的主线程中

  3. 当Java程序运行时,JVM也会自动启动四个线程:Attach Listener ,Finalizer,Reference Handler和Signal Dispatcher

线程创建的俩种基本方法

  1. 方式一:继承Thread类
    1. 自定义类继承Thread类(其实Thread类实现了Runnable接口)
    2. 重写run()方法
    3. 创建自定义线程类的对象,并且调用start()方法启动线程,该方法调用run方法
      注意:run()方法是系统自动调用,手动调用无法起到多线程的效果
      一个线程对象的start方法只能调用一次,否则会抛出异常
  2. 方式二: 实现Runnable接口
    1. 自定义类实现Runnable接口,重写run()方法
    2. 创建Thread类的对象,构造方法中传入Runnable的实现类
    3. 调用start方法启动线程
  3. 俩种方法比较
    1. 接口方法不受类的单一继承的局限
    2. 接口方法将线程的生命周期管理功能和线程的执行任务代码解耦,更加符合类的单一职责
    3. 接口方法在处理多线程共享资源的情况下,更加方便。(无需使用start关键字)
    4. 接口方法配合线程池技术,线程的利用率更高(无需反复创建和销毁线程)

Thread的常用方法

构造方法
常用方法
  1. String getName() //返回线程的名称
    void setName(String name) //设置线程的名称

  2. static Thread currentThread() //静态方法,返回当前线程对象的引用

  3. int getPriority() //返回线程的优先级
    void setPriority(int newPriority) //设置线程的优先级

    优先级的范围是1到10,默认线程的优先级是5,优先级的数字越大,优先级越高

    static int MAX_PRIORITY = 10 //最高优先级
    static int MIN_PRIORITY = 1 //最低优先级
    static int NORM_PRIORITY = 5 //默认优先级

    【注意1】 优先级的高低不代表绝对的执行顺序,只是优先级高的线程执行概率相对较高
    【注意2】 该方法必须在启动线程前调用,start()之前

  4. static void sleep(long millis) throws InterruptedException

    静态方法,休眠(暂停执行)指定毫秒,用于模拟延时操作,由运行状态转换为阻塞状态

  5. void join()

    暂停当前正在执行的线程,等待调用了join()方法的线程执行完毕后,再继续执行,由运行状态转换为阻塞状态
    哪个线程调用了join()方法,就先等待哪个线程执行完

  6. static void yield()

    静态方法,暂停当前正在执行的线程,让出CPU的使用权给其它线程,由运行状态转换为就绪状态

守护线程

线程分为用户线程和守护线程两种

(1)用户线程又叫前台线程,新创建的线程默认都是用户线程,JVM必须确保用户线程执行完毕
(2)守护线程又叫后台线程,默默在后台为前台线程服务,JVM无需确保守护线程执行完毕

【特点】只要有一个前台线程存活,守护线程就不会死亡,只有所有前台线程都结束时,守护线程才会自动结束,守护线程的优先级一般比前台线程低

【使用场景】JVM的垃圾回收,内存监控,记录日志,后台杀毒和漏洞修复,播放背景音乐

【常用方法】

(1)boolean isDaemon()
判断一个线程是否为守护线程,线程默认不是守护线程

(2)void setDaemon(boolean flag)
设置线程为守护线程,只有执行了setDaemon(true)以后,前台线程才能变为守护线程

【注意】该方法必须在启动线程前调用

创建线程的其它三种扩展方式

以下三种方式都封装了创建线程的基本代码,并扩展了线程功能

方式1—TimerTask + Timer
  java.util包中的TimerTask类用于定义线程的执行任务,Timer类用于任务的定时调度
方式2—Callable + FutureTask
  java.util.concurren包中的Callable接口的实现类用于定义线程的执行任务,call()方法可以有返回值供FutureTask类获取,FutureTask类表示可取消的异步计算任务
public class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        return "新创建了线程";
    }
}


MyCallable callable = new MyCallable();
FutureTask<String> task  = new FutureTask<>(callable);
Thread t = new Thread(task);
t.start();
System.out.println(task.get());
方式3—利用线程池
java.util.concurren包中的Excecutors工具类创建线程池
 ExecutorService executor = Executors.newFixedThreadPool(5);

    for(int i=0; i<5; i++) {
        
        executor.submit(new Runnable() {

            @Override
            public void run() {
                for(int i=0; i<3; i++) {
                    System.out.println(Thread.currentThread().getName() + i);
                }

            }
        });
    }
}

【总结】创建线程的本质只有一种方法,那就是创建Thread类或其子类的实例(也可以理解为实现Runnable接口)
其它方式都是基本方式的包装而已

线程的生命周期<6> <重点>

  1. 新建状态—New
    使用new关键字创建线程
  2. 可执行状态—Runnable <包括就绪状态和运行状态>
    使用start启动后的状态
    就绪状态—start方法会调用本地方法start0(),操作系统会为线程分配除CPU以外的所有资源
    运行状态—处于就绪状态的线程被线程调度器选中获取了CPU执行权,正在执行run()方法
  3. 阻塞状态—Blocked
    未获得对象的锁定,被阻挡在synchronized同步代码块外或同步方法外时的状态,一旦获得对象的锁定,则转为可执行状态
  4. 等待状态—Waiting
    线程执行完无参的join或者wait方法后,会进入等待状态,直到其他线程执行notify或者notifyAll方法,线程会自动变为可执行状态
  5. 计时等待状态—Timed_Waiting
    Thread执行了sleep方法,或者线程执行完有参的join或者wait方法后,进入计时等待状态,直到其他线程执行notify或者notifyAll方法,或者计时时间过了,线程会自动变为可执行状态
  6. 销毁状态—Termibated
    线程run方法正常执行完毕,或执行过程中抛出未捕获异常自动退出后的状态
    在这里插入图片描述

描述线程的内部枚举类—Thread.State

public enum State {
   
    NEW,      //新建

    RUNNABLE,  //可运行,包括就绪和运行
    
    BLOCKED,   //阻塞

    WAITING,   //等待

   TIMED_WAITING,   //计时等待

   TERMINATED;  //终止
}

获取线程状态的方法

Thread.State getState() //返回线程的当前状态的枚举值

线程定时任务调度

  • 之后再看

线程同步

  1. 找出多个线程的共享数据,保证该数据是唯一的
  2. 找出对共享数据的读写过程,对其看做是一个整体,加锁
  3. 保证锁对象是唯一的

synchronized关键字有俩种使用方式

  1. 同步代码块
synchronized(锁对象){
数据读写操作
}
  1. 线程进入同步代码块前,检测锁对象是否被其它线程加锁,如果未被加锁,则允许进入执行,并将对象加锁(获取锁定),否则进入线程阻塞池中等待其它线程释放锁定后,再重试获得锁定

  2. 线程退出同步代码块时,自动释放锁对象<synchronized 关键字 加锁和解锁都是自动的>
    注意:

  3. 一定要达到锁对象唯一,才能达到线程同步的效果

  4. 在同步代码块中调用sleep()方法,join()方法,yield()方法时,不会释放锁对象,而调用wait()方法时,会释放锁对象

  5. 同步方法

    • 实例方法— 锁对象是调用该方法的当前对象—this,锁对象可能不唯一
synchronized void method(){
    	方法体代码
    }
  • 静态方法—锁对象是方法所在类的class对象— 类名.class,锁对象肯定唯一
    synchronized static void method(){
	      方法体代码
    }

同步代码块和同步方法的区别 重点

【相同点】
(1)无论使用同步块还是同步方法,都要包裹住对共享资源的读写操作
(2)无论使用同步块还是同步方法,锁定的永远是对象,一定要保证锁对象的唯一性

【不同点】

 (1)加锁对象不同

            同步块可以任意指定锁对象,如字符串常量作为锁对象肯定唯一
            同步实例方法锁定的是this,此时一定要注意锁对象是否唯一
            同步静态方法锁定的是类名.class,锁对象肯定唯一

 (2)锁定代码范围不同

            同步块可以任意指定锁定的代码范围
            同步方法的锁定范围一定是整个方法的全部代码	

JDK中的线程安全类

  1. StringBuffer线程安全,StringBuilder线程不安全
    • StringBuffer类的每一个写操作都是同步操作,每一个读操作都没交synchronized
  2. Vector是线程安全类,ArrayList是线程不安全
    • Vector每一个读写都是同步操作,效率很低
      想在多线程下使用ArrayList的三种方法
    1. 自己添加synchronized同步控制块
    2. 使用Collections工具类提供的synchronizedXXX静态方法,返回对象的线程安全类
      List synclist=Collections.synchronizedList(list);
    3. 使用并发包中的CopyOnWriteArrayList类,该类只有在增删改集合元素时,复制一份新的数组再操作
      CopyOnWriteArrayList是线程安全的,是ArrayList在多线程环境下的最佳代替者
  3. Hashtable是线程安全(同步)的老类,HashMap是线程不安全(异步)的新类
    ConcurrentHashMap是线程安全的,是HashMap在多线程环境下的最佳代替者

死锁问题

  • 各个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行。而导致俩个或多个线程都在等待对方释放资源,都停止执行的情况
  • 最常见的 一个线程拿到了筷子在等待事务,另一个线程拿到了食物在等待筷子。俩个人都不释放,则造成死锁
  • 不要在synchronized里面用synchronized 这样容易造成死锁。
    产生死锁的四个条件
  1. 互斥条件 一个资源只能被一个线程占有
  2. 请求和保持条件 一个进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件 进程未获得的资源在未使用完之前,不能被强行剥夺
  4. 循环等待条件 若干进程之间形成一种头尾相接的循环等待资源关系

Synchronized的特点

  1. 独占性
  2. 原子性
  3. 不可中断性
  4. 可重入性
  5. 内存可见性

线程间的通信

一个线程根据业务逻辑条件是否满足决定是否需要进入等待池等待,另一个线程可以唤醒该线程退出等待池,继续进行后续的操作

Object类中的线程通信方法

  1. 线程等待池是一个存放因某些条件不满足而必须等待执行的线程的Map集合
    K=锁对象 V=属于此锁对象下的所有等待的线程

  2. wait()方法—使当前线程暂停运行,并放弃锁对象,进入等待状态,直到持有相同锁定的其它线程调用notify()或notifyAll()方法才能被唤醒

  3. notify()方法—唤醒等待池中持有相同锁定的任意一个等待线程,然后释放掉锁对象, 此时被唤醒的线程由于没有锁对象,因此处于被阻塞状态,一旦该线程获得了锁对象,就变为就绪状态

  4. notifyAll()方法—唤醒等待池中持有相同锁定的所有等待线程

  • 为什么在Object类中定义这三个方法,而不是在Thread类中
    因为这三个方法都必须由线程持有的锁对象调用,而锁对象可以是任意类,所以写在Object中
  • 这三个方法都必须在同步块或同步方法中才能调用,否则会抛出IllegalMonitorStateException异常
  • 必须由锁对象调用这三个方法,而且锁对象不能随意指定,必须是共享资源对象,否则会抛出IllegalMonitorStateException异常

线程中断

线程什么时候停止

  1. run()方法正常执行完
  2. run()方法执行过程中抛出异常
  3. 外界调用中断程序的方法,强制中断

线程中断相关的三个方法

  1. void interrupt()方法

    请求中断调用此方法的线程,此方法内部将线程的中断状态置为true,同时抛出InterruptedException异常给该线程,从而结束线程的休眠,连接或等待

  2. boolean isInterrupted()

    判断调用此方法的线程是否已经中断,但不清除线程的中断状态(依然保持true—已被中断)

  3. static boolean interrupted()

    判断当前线程是否已经中断,并清除当前线程的中断状态(变为false—未被中断)

  • 哪些方法可以暂停线程的运行,有什么区别
  1. sleep()方法—Thread类中的静态方法,休眠时不释放锁定,休眠时间结束后变为就绪状态,如果休眠过程中被其它线程中断,则会收到InterruptedException异常

  2. join()方法—Thread类中的方法,连接时不释放锁定,子线程调用后会等待子线程执行完毕后,父线程再继续执行,如果连接过程中被其它线程中断,则会接收到InterruptedException异常

  3. yield()方法—Thread类中的静态方法,让步时不释放锁定,让当前线程转变为就绪状态,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行

  4. wait()方法—Object类中的方法,等待时会释放锁定,被其它线程唤醒后,必须重新获得锁定,才能变为就绪状态,如果等待过程中被其它线程中断,则会接收到InterruptedException异常

  • 哪些方法释放锁资源,哪些方法不释放锁资源
    1. 调用sleep(),join(),yield()方法暂停线程运行时,都不会释放锁定
    2. 同步块或同步方法正常执行完毕,或调用了wait()方法,或执行中抛出了异常,都会释放锁定

synchronized和Lock的区别 重点

(1)synchronized是Java语言内置的关键字,提供了最底层的线程同步功能
Lock是JDK5新增的并发包中的接口,常用的实现类是ReentrantLock,是线程同步功能的封装扩展

(2)synchronized是隐式锁,进入同步块或同步方法时自动加锁,退出时自动解锁
Lock是显式锁,必须手工获取锁定(调用lock()方法)和释放锁定(调用unlock()方法)

(3)synchronized有同步块或同步方法两种使用方式
Lock只有代码块锁,没有方法锁

(4)使用Lock,JVM将花费更少的时间进行线程同步控制,效率比synchronized更高

(5)使用的优先顺序: Lock > synchronized同步块 > synchronized同步方法

Lock lock = new ReentrantLock();  //创建锁对象

try{
    lock.lock();   //获取锁定
    共享数据的操作
}finally{
    lock.unlock();   //释放锁定
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值