(十七)多线程

多线程


程序和进程的概念

程序:硬盘上存储的,静止的代码

进程:程序的一次执行的产生进程,每个进程对应一定的内存空间,并且 只能使用自己的内存空间,各个进程之间互不干扰

进程称为操作系统资源分配的基本单位


并发和并行

并发:在一段时间内多个进程轮流使用同一个CPU,多个进程形成并发

并行:在同一时刻多个进程使用各自的CPU,多个进程形成并行,并行需要多个CPU支持

并发是能够让操作系统从宏观看起来同一时间段执行多个任务,操作系统一般通过CPU时间片轮转实现并发


线程

线程的出现为了解决实时性问题

线程:进程中的一个执行任务(控制单元),一个进程中可以运行多个线程,多个线程可共享数据,线程是CPU调度的基本单位

多线程:在同一进程中同时运行的多个程序

主线程main:在运行一个简单的Java程序的时候,就已经存在了两个线程,一个是主线程,一个是后台线程(维护) 的垃圾回收

进程与线程间的区别

  1. 根本区别:进程是操作系统资源分配的基本单位,而线程是CPU调度和执行的基本单位
  2. 开销方面:每个进程都有独立的代码和数据空间,程序之间切换会有较大的开销,线程可以看作轻量级的进程,同一类线程共享代码和数据空间,线程之间切换的开销小
  3. 所处环境:在操作系统中能同时运行多个进程,同一个进程中有多个线程同时执行
  4. 内存分配方面:进程占用内存空间比线程大,线程除CPU外,系统不会为线程分配内存(线程所使用的资源来自所属进程的资源)
  5. 包含关系:没有线程的进程可以看成是单线程,线程是进程的一部分


实现线程的方式

  • 继承Thread
  • 实现Runnable接口

继承Thread
  • 自定义继承Thread实现多线程
  • 必须重写run方法
  • 创建自定义类对象
  • 自定义类对象调用start方法

自定义类

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

测试类

public class Test01 {
    public static void main(String[] args) {
        //创建一个线程对象
        BThread bThread = new BThread();
        //启动线程
        bThread.start();//调用Thread的start方法,JVM会自动调用run方法
        
        //在main线程中执行
        for (int i = 0; i < 5; i++) {
          System.out.println("main :" + i);
        }
    }
}

多个线程轮流使用CPU,先抢到的先使用,执行结果不固定

实现Runnable接口
  • 自定义类实现Runnable接口
  • 重写run方法
  • 创建自定义类对象
  • 创建Thread类,把自定义对象传进其构造器
  • 调用Thread对象的start方法

自定义类

//CThread不是线程,但是具备在线程中执行的能力
public class CThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("CThread==>"+i);
        }
    }
}

测试类

public class Test02 {
    public static void main(String[] args) {
        //创建CThread对象
        CThread c = new CThread();
        //创建线程,把c放入线程中执行
        Thread thread = new Thread(c);
        thread.start();
    }
}

总结

  1. 继承Thread类后,不能继承其他类;实现Runnable接口还可以继承其他类
  2. 实现Runnable接口更方便共享资源,同一份资源,多个线程并发访问

例子

模拟买票过程

  • 使用继承Thread
public class TicketThread extends Thread{
        private static int num =5;

    public TicketThread(String name) {
        super(name);
    }

    public TicketThread() {
    }

    @Override
    public void run() {
        //模拟每个窗口有10个人买票
        for (int i = 0; i < 10; i++) {
            if (num>0){
                num--;
                System.out.println(super.getName()+"  剩余"+num+"张票");
            }
        }
    }
}

测试类
    public class Test01 {
    public static void main(String[] args) {
        //模拟买票
        TicketThread ta = new TicketThread("窗口A");
        TicketThread tb = new TicketThread("窗口B");
        TicketThread tc = new TicketThread("窗口C");
        TicketThread td = new TicketThread("窗口D");
        TicketThread te = new TicketThread("窗口E");

        ta.start();
        tb.start();
        tc.start();
        td.start();
        te.start();


    }
}
  • 使用实现
public class TicketThread01 implements Runnable{
    private int count=5;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (count>0){
                count--;
                System.out.println(Thread.currentThread().getName()+"卖出了一张票,剩余"+count);
            }
        }
    }
}

测试类
    public class Test01 {
    public static void main(String[] args) {
        TicketThread01 t = new TicketThread01();
        Thread thread = new Thread(t,"窗口1");
        Thread thread2 = new Thread(t,"窗口2");
        Thread thread3 = new Thread(t,"窗口3");

        thread.start();
        thread2.start();
        thread3.start();
    }
}

多线程访问共享资源时,存在严重问题,那就是导致访问共享资源数据错误问题

原因

1.线程通过抢占CPU的方式工作,执行过程中,随时可能CPU时间片被挂起,在程序的任何地方可能被随时切换出去
2.由于被随时切换出去或者被挂起,所以导致访问共享资源数据错乱    


线程生命周期和状态

  • 新建:程序使用new创建一个线程后,该线程处于新建状态
  • 可运行状态:Runnable状态可细分为两个状态.
    • 就绪状态(Ready):当线程调用start(),该线程处于就绪状态,进入线程队列排队(此时并未开始执行,具体运行取决于CPU调度)
    • 运行状态(Running):表示某线程对象被CPU调度器的调度,执行线程体.在运行状态的线程执行自己的 run 方法中代码,直到等待某资源而阻塞或完成任 何而死亡。
  • 阻塞状态(blocked):正在运行的线程遇到某个特殊情况如,执行sleep(), 同步、等待I/O操作完成等。 进入阻塞状态的线 程让出CPU资源,并暂时停止自己的执行。
  • 死亡状态(terminated):表示线程终止,当线程执行完成或者线程抛出未捕获的异常或者被强制终止


线程的常用方法

  • Join方法:线程的强制执行 t1.join(),t1强制执行,导致其他线程(mainThread)阻塞,当t1执行完毕后,其他线程阻塞原因消除,进入就绪状态

  • 线程休眠sleep():线程调用sleep(毫秒)方法,当前线程进入阻塞状态,阻塞时间到后进入就绪状态

    • 休眠状态过程可以被中断,所以存在一个检查时异常 InterruptedException异常,当外界程序中断该线程时,休眠提前结束,进入就绪状态,等待CPU调度
         try {
              Thread.sleep(5000);
            } catch (InterruptedException e) {
                 e.printStackTrace();
         }
        // 中断线程
        t1.interrupt();
    
  • 线程优先级:优先级越高,被CPU调动的可能性越大,但不一定是优先级越高就一定先执行

三种优先级
System.out.println(Thread.MAX_PRIORITY);
System.out.println(Thread.MIN_PRIORITY);
System.out.println(Thread.NORM_PRIORITY); 
  • 线程礼让yield : 线程礼让后,进入就绪状态,也不一定能够成功礼让,只是给了一种暗示

  • 线程结束:强制停止一个线程,风险较大(不建议),建议通过interrupt方法引导(异常处理机制)线程正常结束


线程同步

通过上面买票的例子,我们可以发现线程存在安全性问题,即当多线程并发访问同一个资源对象的时候,可能出现线程不安全的问题。


解决方案

在多线程环境下,如果对共享资源进行破坏性操作时,需要同步操作

同步操作:如果需要一系列操作,要么都执行,要么都不执行,称为原子性操作(也认为业务上的不可分割)


同步代码块
synchronized(同步锁){
//需要同步操作的代码
}

同步锁,也称为同步监听对象/同步监听器/互斥锁
在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就执行,其他的线程只能在代码块外等
着。

例子

要想实现原子性操作,就必须对共享资源加锁,已经加锁的线程,如果CPU时间片到了,会进入就绪状态,其他线程争夺CPU调度,但由于已经加锁,所以会进入到阻塞状态,知道原本加锁的线程运行结束

public class TicketThread01 implements Runnable{
    private int count=100;

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //同步锁this表示count对象,程序中count对象只有一份,所以被作为同步锁
            synchronized (this){
                 if (count>0){
                        try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
               
                    count--;
                    System.out.println(Thread.currentThread().getName()+"卖出了一张票,剩余"+count);
                }
            }

        }
    }
}

判断同步锁是谁?

  • 对于非static方法,当前对象this就是同步锁
  • 对于static方法,同步锁就是当前方法所在类的字节码对象(除去自己本身的静态空间)类名.class

同步方法
[修饰符] synchronized 返回值类型 方法名称(、、、){
// 原子性操作
}

例子

public class TicketThread01 implements Runnable{
    private int count=100;

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            
            }

        }
    public synchronized void STicket(){
    //同步锁this表示count对象,程序中count对象只有一份,所以被作为同步锁
            if(count>0){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                    count--;
                    System.out.println(Thread.currentThread().getName()+"卖出了一张票,剩余"+count);
                }
            }     
    }

synchronized的优劣

优点:保证了多线程并发访问时的同步操作,避免线程的安全性问题。

缺点:使用synchronized的方法/代码块的性能要低一些

同理,StringBuilder和StringBuffer的区别 
就是方法中有没有使用synchronized的区别

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值