Java 进阶——多线程应用小结(一)

引言

我们今天所使用的操作系统是多任务操作系统。多线程就是是实现多任务的一种方式。一个程序可以看作为一个进程,进程是是一个正在执行中的程序,每一个进程执行都是有一个执行顺序,该顺序是一个执行路径或者叫一个控制单元。用于封装每一个程序的控制单元。比如在Windows系统中,一个运行的exe就是一个进程。而线程就是进程中的一个独立的控制单元,线程在控制着进程的执行。
一个进程中至少有一个线程。比如深究JVM,JVM启动都不是单线程,因为除了主线程之外还有负责垃圾回收GC的一个线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。总的来说,CPU真正执行是线程,在CPU某一时刻永远都是在执行一个程序,即永远只有一个控制单元在执行(多核除外),实际情况cpu总是在极短的时间内不停地切换执行路径即线程,是因为切换时间极短所以根本感受不到。

一、创建线程的方式

1.继承Thread类,然后重写run方法

  1. 继承Thread,重写run的方法,(因为父类Thread的run方法中并没有任何操作代码,而我们需要执行自己的功能就应该复写父类的run)
  2. 创建线程对象,调用线程的start方法启动线程

2.实现Runnable接口,然后实现其run方法。

  1. 实现Runnable 的接口,实现run方法
  2. 通过Thread类的构造方法Thread(Runable r)创建Thread对象(为什么要将Runnable借口的子类对象传递给Thread的构造函数,因为自定义的run方法所属对象是Runnable接口的子类对象,要想让线程去执行指定对象的run方法,就必须明确该run方法的所属对象)

    二多线程的简单应用

    例子:模拟火车站卖票窗口卖票900,多个窗口同时卖票

    2.1继承Thread方式

class SaleTicket extends Thread{
    private staic int num=900;//因为总的票数是所有对象共享的,但是并不是优秀方案,宜采用Runnable的方式实现多线程
    SaleTicket(String name){
        super(name);
    }
    /**
    因为虚拟机定义时,Thread类的run方法就是用于存储线程要运行的代码,如果不重写run方法,虚拟机加载run方法的时候,找不到执行代码。
    */
    @override
    pubic void run(){
        while(true){
            if(num>0){
                System.out.println(Thread.currebtThread().getName()+"Sale :"+num--");
            }
        }
    }
}

测试类:

//因为虚拟机定义时,而主线程的运行代码就是存在main方法里的
public class TestThread {
    public static void main(String[] args) {
    SaleTicket d=new Demo("1窗口");//创建好了一个线程,即CPU中的一个控制单元
    SaleTicket d2=new Demo("2窗口");
    SaleTicket d2=new Demo("3窗口");
    d.start();//开启线程并执行该线程的run方法
    d2.start();
    d3.start();
    //d.run();仅仅是在主线程调用了run方法,线程创建了,但并没有运行,还是单线程程序,只有在线程的run方法执行了之后才会执行下面的循环语句。
    for(int x=0;x<60;x++){
        System.out.println("Main 线程 run");
    }
}

执行情况:是main线程和SaleTicket 线程交替执行的,因为在CPU某一时刻永远都是在执行一个程序,即永远只有一个控制单元在执行(多核除外),实际情况cpu总是在极短的时间内不停地切换执行路径即线程,是因为切换时间极短所以根本感受不到。多线程执行的时间由cpu说的算。
执行情况:主线程 做两个部分的工作,一创建多线程d,d2,d3,创建了线程之后,启动d,d2,d3,二执行循环语句,而线程d,d2,d3只负责run方法内部分工作。

2.2实现Runnable接口并实现run方法(推荐)

class ThreadRunableDemo implements Runnable{

    private int num=1000;
    Object obj=new Object();
    /**
    因为虚拟机定义时,Thread类的run方法就是用于存储线程要运行的代码
    */
    @override
    pubic void run(){

        while(true){
        /**
        如果不加上,同步代码块synchronized可能会出现问题:
    会卖出0,-1,-2等错票。
        原因:当多条语句在操作同一线程共享数据时,一个线程中多条语句执行了一部分,还没有执行完的时候,另一个线程参与进来执行,导致共享数据的错误。
        解决方案:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即即使拿到了执行权也不能让他执行。 即利用同步代码块把操作共享数据的语句同步起来。
        */
        synchronized(obj){
            if(num>0){
                try{
                    Thread.sleep(20);
                }
                catch(Exception e){
                }               
System.out.println(Thread.currebtThread().getName()+"Sale :"+num--");
            }
        }
    }
} 

测试:

//因为虚拟机定义时,而主线程的运行代码就是存在main方法里的
public class TestRunnable {
    public static void main(String[] args) {
    //Runnale 的接口实现方式
    ThreadRunableDemo tr=new ThreadRunableDemo();
    Thread tr1=new Thread(tr);
    Thread tr2=new Thread(tr);
    Thread tr3=new Thread(tr);
    tr1.start();
    tr2.start();
    tr3.start();
    for(int x=0;x<60;x++){
        System.out.println("Main 线程 run");
    }
}

如果不加上同步代码块的话,可能会导致出现安全问题。重点内容现在开辟了3个线程,都在操作num,因为线程都是随机性的,是由CPU决定的,假设cpu执行1线程,1线程进入到CPU,判断num是否大于0,刚刚判断完,1线程刚刚准备执行卖票,具备执行资格,但是cpu切换到了其他线程2,2线程获得了执行资格,但是也有可能是被切换了到了3,3也具备执行资格,而1、2、3 都挂着的时候,CPU再切回了1,执行了卖票,num变成0的时候,此时3获得执行权,就不需要再判断了直接执行卖票,但是票已经没有了,再执行到3线程的时候num已经变成了-1号票,已经出现了安全问题,因为在执行线程的时候,cpu有可能切换到其他的的线程。

三、多线程并发

比如以上例子的,开辟了三个窗口(线程)同时售票,多线程并发是线程同步中比较常见的现象,java多线程为了避免多线程并发解决多线程共享数据同步问题提供了synchronized关键字

1. synchronized关键字:当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。
  1. 对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。 Java中的每个对象都有一个锁(lock)或者叫做监视器(monitor),当访问某个对象的synchronized方法时,表示将该对象上锁,此时其他任何线程都无法再去访问该synchronized方法了,直到之前的那个线程执行方法完毕后(或者是抛出了异常),那么将该对象的锁释放掉,其他线程才有可能再去访问该synchronized方法。
  2. 如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的。
  3. 如果某个synchronized方法是static的,那么当线程访问该方法时,它锁的并不是synchronized方法所在的对象,而是synchronized方法所在的对象所对应的Class对象,因为Java中无论一个类有多少个对象,这些对象会对应唯一一个Class对象,因此当线程分别访问同一个类的两个对象的两个static,synchronized方法时,他们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行完毕后另一个线程才开始执行。
  4. synchronized方法是一种粗粒度的并发控制,某一时刻,只能有一个线程执行该synchronized方法;synchronized块则是一种细粒度的并发控制,只会将块中的代码同步,位于方法内、synchronized块之外的代码是可以被多个线程同时访问到的
  5. 同步代码块:表示线程在执行的时候会对object对象上锁。
//对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
synchronized(对象){
    //需要同步的代码块
}
2. 同步代码块synchronized的通俗形象描述

synchronized就像是多线程一个”看门狗”,当初始化的时候,cpu中没有对应的线程的时候,此时任何线程都可以进入到cpu获取执行权然后运行相关代码。拿买票的说,一开始主线程创建了3个线程1 、2、3,调用start方法之后,假设1线程首先被执行,1得先进来,进来之后,做的第一件事应该是改变”看门狗”的状态(可以理解为标记位,标记位有人),改变了状态之后再去执行if判断,满足则有可能执行到sleep然后释放了执行权,此时2 、3 都有可能争抢执行权,假设2争取到了,2想进入到同步代码块,也会先经过同步代码块,这“看门狗”的标记位还是”有人“,2即使是撞得头破血流”看门狗“也不会放他进来,此时1睡醒了,继续执行接下的代码,卖票,出了同步代码块,把”看门狗“标记位置为”无人“,释放了执行权,此时假设3进来,就又可以执行了,如此反复。

3. 同步的前提
  1. 必须要有两个或者以上的线程
  2. 必须是多个线程使用同一个锁
  3. 必须保证同步中只有一个线程在运行
4. 同步代码块虽然能解决多线程的安全性问题,但是消耗资源,所以不是所有的代码都应该放到同步代码块中,只有都符合以下条件才应该用同步处理:
  1. 只有运行在多线程的代码
  2. 共享数据
  3. 操作共享数据的语句

四、关闭线程的方法

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  2. 使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果,一般要设定一个变量,在run方法中是一个循环,循环每次检查该变量,如果满足条件则继续执行,否则跳出循环,线程结束。)。
  3. 使用interrupt方法来终端线程可分为两种情况:
    (1)线程处于阻塞状态,如使用了sleep方法。
    (2)使用while(!isInterrupted()){……}来判断线程是否被中断。

五、继承Thread方式和实现Runnable接口方式的区别

两种方法均需执行线程的start方法为线程分配必须的系统资源、调度线程运行并执行线程的run方法。实现方式避免了单继承的局限性,在定义线程时宜采用。两者相比,线程执行的代码存放在Thread子类的run方法,而实现Runnable时线程数放到实现接口的子类。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CrazyMo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值