java多线程详解

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

一、基本概念?

  • 程序:是为了完成特定的任务,而用某种语言编写的一组指令的集合,即指一段静态的代码,静态的对象。
  • 进程:是指运行中的程序,比如我们使用QQ时,就启动了一个进程,操作系统就会为该进程分配内存空间,当我们使用微信时,又启动了一个进程,操作系统将再次为其分配内存空间。是程序的一次执行过程,或是正在运行的一个程序,是动态过程,有他自身的产生,存在和消亡过程。
  • 线程:
    1.线程由进程创建,是线程的一个实体
    2.一个进程可以拥有多个线程
    3.单线程,同一个时刻,只允许执行一个线程
    4.多线程,同一时刻,可以执行多个线程,比如一个微信程序,可以打开多个聊天窗口。
    5.并发:同一时刻,多个任务交替执行,造成一种貌似同时的错觉,简单的说,单核cpu实现的多任务就是并发(即:一个cpu执行多个任务)。
    6.并行:同一时刻,多个任务同时执行,多核cpu可以实现并行,或者并发和并行(即多个cpu可以同时执行不同的任务)。

单线程和多线程的示意图:
在这里插入图片描述
线程的三大优势:

  • 系统开销小:
    创建和撤销线程的系统开销,以及多个线程之间的切换,都比使用进程进行相同操作要小的多
  • 方便通信和资源共享
    如果实在进程之间通信,往往要求系统内核的参与,以提供通信机制和保护机制,而线程间通信在同一进程的地址空间内,共享内存和文件,操作简单,无需内核参与。
  • 简化程序结构
    用户在实现多任务的程序是,采用多线程机制,可以使程序结构清晰,独立性强。

二、线程的调度

时间片:线程的调度采用时间片轮转的方式。
抢占式:高优先级的线程抢占CPU。
线程调度是抢占式调度,即如果在当前线程执行过程中一个更高优先级的线程进入就绪状态,则这个线程立即被调度执行。
注意:对于同优先级的线程组成先进先出的队列,使用时间片策略,对于高优先级的线程,使用抢占式的策略。

三个常量

  • MAX_PRIORITY:10
  • MIN_PRIORITY:1
  • NORM_PRIORITY:5
  • 通过setPriority(int pri)方法设置优先级 ,getPriority()返回当前线程的优先级。

三、线程的生命周期

五种状态

  1. 新建状态:创建一个新的子线程。
  2. 就绪状态:线程已经句被运行的条件,等待调度程序分配CPU资源使其运行。
  3. 运行状态:调度程序分配CPU资源给该线程。
  4. 阻塞状态:线程正等待除了CPU资源意外的某个条件符合或某个事件发生。
  5. 死亡状态:表示线程已经操作结束。
    示例图:
    在这里插入图片描述

四、创建线程的两种方式

1.继承Thread类

public class MyThread01 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        //启动线程调用线程的start()方法,注意不能直接调用run方法
        MyThread01 myThread01 = new MyThread01();
        myThread01.start();
    }
}

2.实现Runnable接口


public class MyThread02 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        MyThread02 myThread02 = new MyThread02();
        //通过new Thread类的对象,将实现了Runnable接口的子类当做参数传递进去
        Thread thread = new Thread(myThread02);
        //调用start方法开启线程
        thread.start();
    }
}

注意:当调用线程的start方法时,run方法会自动被调用,所以说调用start方法就是开启线程。

五、常用方法

  • Thread.currentThread():获取当前线程对象,可以调用getName(),获取当前线程的名。
  • start():启动当前线程,虚拟机负责调用线程中的run()方法
  • sleep(long millis):线程休眠指定的毫秒数,使线程进入阻塞状态,当休眠时间过后,会重新进入就绪状态,等待cpu的调度
  • yield():使当前线程放弃占用CPU资源,回到就绪状态,使其它优先级不低于自己的线程有机会被执行,注意:该方法不一定会礼让成功
  • join():只有当前线程执行完,才会执行另一个线程
  • interrupt():中断线程的阻塞状态(而非中断线程),例如一个线程 sleep(1000000000) ,为了中断这个过长的阻塞过程,可以调用该线程的 interrupt() 方法,中断阻塞。需要注意的是,此时 sleep() 方法会抛出 InterruptedException 异常。
  • void isAlive():判定该线程是否处于活动状态,处于就绪、运行和阻塞状态的都属于活动状态。
  • void setPriority(int newPriority):设置当前线程的优先级。
  • int getPriority():获得当前线程的优先级。

注意::yield() 和 sleep() 的区别

  • yield() 方法和 sleep() 方法都是 Thread 类的静态方法,都会使当前处于运行状态的线程放弃 CPU 资源,把运行机会让给别的线程。
  • sleep() 方法会给其他线程运行的机会,不考虑其他线程的优先级,因此会给较低优先级线程一个运行的机会;而 yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。
  • 当线程执行了 sleep(long millis) 方法后,将转到阻塞状态,参数 millis 指定了睡眠时间,过了指定时间后会到就绪状态;而当线程执行了 yield() 方法后,将直接转到就绪状态。

六、守护线程

  1. 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
  2. 守护线程:一般视为工作线程服务的,当所有的用户线程结束,守护线程自动结束
  3. 常见的守护线程:垃圾回收机制
    示例代码:
package 线程;

public class Test5 {
    public static void main(String[] args) {
        MyDaemon myDaemon = new MyDaemon();
        //将该线程设置为守护线程
        myDaemon.setDaemon(true);//子线程中的run方法是死循环,将其设置为守护线程后,当主线程中的代码执行完毕之后,子线程也就执行完毕
        myDaemon.start();
        //主线程
        for(int i = 0;i < 5;i++){
            System.out.println("主线程开始调用!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("主线程结束");

    }
}
class MyDaemon extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("子线程");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

一个程序只有处于守护线程时该程序结束运行,所以即便程序中创建并启动了一个线程 t 且 t 的 run() 方法永久循环输出,仍会在主程序执行完毕后退出程序。

七、多线程数据的共享

当一个数据被多个线程存取的时候,通过检查这个数据的值来进行判断并执行操作是极不安全的。

因为在判断之后,有可能因为 CPU 时间切换或阻塞而挂起,挂起过程中这个数据的值很可能被其他线程修改了,判断条件也可能已经不成立了,但此时已经经过了判断,之后的操作还需要继续进行。这就会造成逻辑的混乱,导致数据不一致。
案例:卖票的问题

package com.lanqiao;

public class SaleTicket {
    public static void main(String[] args) {
        //创建两个窗口
        SaleWindow w1 = new SaleWindow();
        SaleWindow w2 = new SaleWindow();
        w1.setName("窗口1");
        w2.setName("窗口2");
        w1.start();;
        w2.start();
    }
}
class SaleWindow extends Thread{
    static int tickets = 100;
    @Override
    public void run() {
        while(tickets > 0){
            System.out.println(Thread.currentThread().getName() + ":票数" + (--tickets));
        }
    }
}

出现了重票(票被反复的卖出,ticket未被减少时就打印出了)错票。
问题出现的原因:当某个线程操作车票的过程中,尚未完成操作时,其他线程参与进来,也来操作车票
在这里插入图片描述

八、多线程同步处理方式一

  • 线程同步机制
  1. 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性
  2. 也可以这么理解:线程同步,即当有一个线程在堆内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作
  • 同步代码块
    示例代码:
    实现Runnable接口
package Test;
public class Test5 {
    public static void main(String[] args) {
        Sell sell = new Sell();
        new Thread(sell).start();
        new Thread(sell).start();
        new Thread(sell).start();
    }
}
class Sell implements Runnable{
    private  int ticket = 100;
    boolean flag = true;
    //public synchronized void s(){//当前锁对象为this,因为是操作的同一个对象
    public void s(){
        synchronized (this){ //同步代码块的方式,指的依然是当前的对象,而且是同一个对象
            if(ticket <= 0)
            {
                flag = false;
                return;
            }
            try {
                System.out.println(Thread.currentThread().getName() + "剩余票数:" + (--ticket));
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    @Override
    public  void run() {
        while(flag){
            s();
        }
    }
}

继承Thread类

package Test;


public class Test5 {
    public static void main(String[] args) {
        Sell sell = new Sell();
        Sell sell1 = new Sell();
        Sell sell2 = new Sell();
        sell.start();
        sell1.start();
        sell2.start();
    }
}
class Sell extends Thread{
    private static int ticket = 100;
    private static final Object obj = new Object();
    boolean flag = true;
    public  void s(){
      synchronized (obj){ //当前对象时Object类型的对象,三个线程共用一把同步锁
          if(ticket <= 0)
          {
              flag = false;
              return;
          }
          try {
              System.out.println(Thread.currentThread().getName() + "剩余票数:" + (--ticket));
              Thread.sleep(50);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
    }
    @Override
    public  void run() {
        while(flag){
            s();
        }
    }
}

互斥锁

  1. java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。
  2. 每个对象都对应于一个可称为”互斥锁“的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象
  3. 关键字synchronized来与对象的互斥锁联系,当某个对象用synchronized修饰符时,表名该对象在任意时刻只能有一个线程访问
  4. 同步的局限性:导致程序的执行效率要降低
  5. 同步方法(非静态的)锁,可以是this,也可以是其他对象例如:Object对象(要求是同一个对象)
  6. 同步方法(静态的)锁为当前类本身

注意:

  1. 同步方法如果没有使用static修饰:默认对象为this
  2. 如果方法使用static修饰,默认对象:当前类.class
  3. 实现的落地步骤:
    • 需要先分析上锁的代码
    • 选择同步代码块或同步方法
    • 要求多个线程的锁对象为同一个即可

死锁

如果线程 A 只有等待线程 B 的完成才能继续,而在线程 B 中又要等待线程 A 的资源,那么这两个线程相互等待对方释放锁时就会发生死锁。出现死锁后,不会出现异常,因此不会有任何提示,只是相关线程都处于阻塞状态,无法继续运行。

  • 死锁产生的原因有以下三个方面:

系统资源不足。如果系统的资源充足,所有线程的资源请求都能够得到满足,自然就不会发生死锁。
线程运行推进的顺序不合适。
资源分配不当等。
产生死锁的必要条件有以下四个:

互斥条件:一个资源每次只能被一个线程使用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

模拟死锁问题

package 线程;

public class DeadLock {
    public static void main(String[] args) {
        Dead dead = new Dead(true);
        Dead dead1 = new Dead(false);
        Thread thread = new Thread(dead);
        Thread thread1 = new Thread(dead1);
        thread.start();
        thread1.start();
    }
}
class Dead implements Runnable{
    private boolean flag;
    private static Object o1 = new Object();
    private static Object o2 = new Object();

    public Dead(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        //同一时刻:
        //如果flag为真,线程A就会先抢o1对象锁,然后去尝试获取o2对象锁,如果线程A得不到O2对象锁,就会Blocked
        //如果flag维嘉,线程B就会先得到o2对象锁,然后去尝试获取o1对象锁,如果线程B得不到O1对象锁,就会Blocked
        //此时就形成了死锁的状态
        if(flag){
            synchronized (o1){
                System.out.println("进入1");
                synchronized (o2){
                    System.out.println("进入2");
                }
            }
        }else{
            synchronized (o2){
                System.out.println("进入3");
                synchronized (o1){
                    System.out.println("进入4");
                }
            }
        }
    }
}

释放锁

  • 以下操作会释放锁

    1. 当前线程的同步方法,同步代码块执行结束
    2. 当前线程在同步代码块,同步方法中遇到break,return
    3. 当前线程在同步代码块,同步方法中出现了未处理的Error或Exception,导致异常结束
    4. 当前线程在同步代码块,同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁
  • 以下操作不会释放锁

    1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep(),Thread.yield()方法暂停当前线程的执行,不会释放锁

    2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁

      注意:应尽量避免使用suspend()和resume()来控制线程,方法不在推荐使用

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值