这是一个不完整的详细Java多线程,但对于初学者足够了,相信我,你会爱上她的

这是一个不完整的详细Java多线程,但对于初学者足够了,相信我,你会爱上她的,第1张

这是一个不完整的详细Java多线程,但对于初学者足够了,相信我,你会爱上她的

零基础学习之Java多线程

概述线程的创建

继承Thread类

创建线程的步骤线程的使用步骤代码示例 实现Runnable接口

创建线程的步骤线程的使用步骤代码示例使用匿名内部类创建线程

代码示例 Thread类常用的方法 线程生命周期线程安全

同步代码块

语法格式代码示例 同步方法

语法格式代码示例 锁机制

概述

要深入学习多线程,首先我们需要找到什么是线程,根据百度百科的定义:线程(thread)是 *** 作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。。我们了解到线程是进程的实际运行单位,那问题来了什么是进程呢?根据百度百科定义:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,进程是程序的实体。这个就比较好理解了,进程不就是我们运行的程序吗?那这样来看,线程就是某个程序的一次实际的运行(一个程序可以打开多次,一个进程可以有多个线程组成)。

那这里的Java多线程指的就是Java开发中,程序启动多个线程实现并发执行,提高CPU的利用率。

并发:指两个或多个事件可以在同一个时间段内发生。也就是说在同一个时刻只能执行一条指令,但多个指令可以被快速轮换执行,使得在宏观上具有多个进程同时执行的效果

线程的创建

创建线程是使用多线程的第一步。首先先介绍了Java创建线程的方式,创建了线程才会有多个线程的情况。Java创建多线程有四种方式:

继承Thread类实现Runnable接口实现实现Callable(本文未涉及)使用线程池方式(本文未涉及)

本篇文章主要介绍通过继承Thread类和实现Runnable接口这两种方式来创建线程。 具体如下:

继承Thread类

在Java中,使用Thread类来表示线程,所有的线程都是Thread类的对象或者其子类的实例。(Thread类Java.lang包下的类)

通过继承方式实现的线程,会有Java单继承的缺点

创建线程的步骤

定义一个类,让其继承Thread类在该类中重写Thread类的run()方法、创建该类的实例,即创建了线程对象

其中:run()方法的方法体就是线程的执行逻辑,也称为线程执行体。

线程的使用步骤

创建线程通过线程对象调用start()方法。

注:线程不用手动结束,执行完线程其自动结束

代码示例

public class Demo1 {
    public static void main(String[] args) {

        //创建自定义线程对象
        MyThread myThread = new MyThread();

        //调用start方法,开启线程
        myThread.start();

    }
}

//继承Thread方法,自定义线程
class MyThread extends Thread{
    
    
    @Override
    public void run() {
        System.out.println("继承Thread方式创建线程");
    }
}

实现Runnable接口

上面说到通过继承Thread类的方式实现线程有Java单继承的缺点,针对单继承这样一个问题,Java类是通过实现接口来解决的。那么创建线程是不是也可以通过接口呢?答案是肯定的。在Java中,可以通过实现Runnable接口的方式来创建线程。

注:在Thread类的源码中可以看出,其也是实现了Runnable接口

创建线程的步骤

定义一个类,让其实现Runnable接口在该类中重写Runnable接口中的run()方法创建该类的对象(线程的任务对象)通过Thread类的有参构造方法创建线程对象(真正的线程对象) 线程的使用步骤

创建线程通过线程对象调用start()方法。 代码示例



public class Demo2 {
    public static void main(String[] args) {
        //创建自定义类对象  线程任务对象
        MyThread1 runnable = new MyThread1();

        //创建线程对象
        Thread thread = new Thread(runnable,"xiaocheng");

        //调用start方法,开启线程
        thread.start();

    }
}

//实现Runable类,创建线程
class MyThread1 implements Runnable{

    
    @Override
    public void run() {
        System.out.println("实现Runable类,创建线程");
    }
}
使用匿名内部类创建线程

既然创建多线程是通过继承Thread类,然后创建类的实例的方式实现的。那如果是本身这个线程只用一次,在其他地方没有调用的情况,是不是可以通过匿名内部类来创建线程呢? Why not?
下面用一个例子来说明使用匿名内部类如何来创建线程:

代码示例


public class Demo3 {
    public static void main(String[] args) {

        //匿名内部类,一个参数
        new Thread(new Runnable() {
			
			//依然需要重写run方法,实现线程的执行逻辑
            @Override
            public void run() {
                System.out.println("匿名内部类创建线程1");
            }
        }){
        }.start();

    //匿名内部类,两个参数
        new Thread(new Runnable() {

			//依然需要重写run方法,实现线程的执行逻辑
            @Override
            public void run() {
            	//调用currentThread().getName()方法,获取当前线程的名字
                System.out.println(Thread.currentThread().getName());
            }
        },"匿名内部类创建线程2 ").start();
    }
}

匿名内部类不熟的话,可以点击上面的链接看下我写过的博客

Thread类常用的方法

从上面的例子我们可以看出来,线程对象实际都是Thread实例。那这样来说,如果我们使用线程的话,除了我们自定以类中的方法外,Thread类中的方法也是必须要知道的。那下面就介绍下Thread类中常用的方法:

public void run():实现多线程的执行逻辑(必不可少的方法)public void start() :线程开始执行的方法 (线程启动方法)public static void sleep(long millis) :暂停当前线程的执行(参数为毫秒数)public String getName() :获取当前线程名字public static Thread currentThread() :返回对当前正在执行的线程对象的引用(可以和其他的一起用)public final boolean isAlive():判断线程的状态,是否为活动状态(非终止)public final int getPriority() :获取线程的优先级public final void setPriority(int newPriority) :改变线程的优先级

public final void stop():使线程停止执行 public final void setDaemon(boolean on):指定线程设置为守护线程。(必须在线程启动之前设置,否则会报IllegalThreadStateException异常)public final boolean isDaemon():判断当前线程是否为守护线程 线程生命周期

前面说到创建线程和启动线程,还有常用方法里说到的线程的停止,这些都是线程在整个执行过程中可能出现的状态。总的来说,线程在其生命周期内(执行过程中)一共有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。
线程生命周期中的状态转换图如下:

其中,各个状态说明如下:

新建(New)

通过上述方法创建线程,不调用任何方法即为新建一个线程,此时该线程只有JVM为其分配的内存,并初始化了实例变量的值,没有执行线程任何 *** 作。

就绪(Runnable)

线程的就绪状态是在其调用start()方法之后,此时线程启动,即变成了就绪状态,JVM此时会为其创建方法调用栈和程序计数器。(注意这个时候只是启动线程,没有线程的执行行为)。

注意:程序只能对新建状态的线程调用start(),并且只能调用一次,如果对非新建状态的线程,如已启动的线程或已死亡的线程调用start()都会报错IllegalThreadStateException异常。

运行(Running)

处于就绪状态的线程获得CPU资源,那该线程就会调用run()方法,开启线程的执行逻辑,此时线程处于运行状态。(所谓多线程,指的是系统中有多个CPU资源,多个线程获得CPU资源后处于运行状态)。

阻塞(Blocked)

系统中的线程由于实际情况并不总是能够顺利执行完毕,但如果稍微有情况不能执行就杀死线程会造成很大的浪费,这时候就会出现线程阻塞的概念,阻塞的线程还可以通过一定的条件转化为就绪状态,继续等待下次的运行。

线程遇到如下情况,会进入阻塞状态:

线程调用了sleep()方法线程试图获取一个同步监视器,但该同步监视器正被其他线程持有;线程执行过程中,同步监视器调用了wait(),让它等待某个通知(notify / notifyAll);线程执行过程中,同步监视器调用了wait(time)线程执行过程中,遇到了其他线程对象的加塞(join);线程被调用suspend方法被挂起(已过时,因为容易发生死锁);

阻塞状态的线程,可以通过下面的形式重新转换到就绪状态,等待下次的运行:

线程的sleep()时间到;线程成功获得了同步监视器;线程等到了通知(notify / notifyAll);线程wait的时间到了加塞的线程结束了;被挂起的线程又被调用了resume恢复方法(已过时,因为容易发生死锁);

死亡(Dead)

线程总归要结束的,不管是被动的还是主动的(执行完了),

run()方法执行完成;线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)直接调用该线程的stop()来结束该线程(已过时,因为容易发生死锁) 线程安全

这里说的线程安全指的是多线程才会有的安全性问题。这是因为当系统同时运行多个线程的时候,会出现多个线程同时访问同一个资源的情况,如果仅仅是涉及到读 *** 作的话,不会出现问题。但是涉及到写(即修改)就会出安全问题。

举个例子解释一下安全问题:

比如在我们在线上购票的过程中,是很多人可以同时购买的(多线程),我们首先要看看有没有票(涉及到读 *** 作),然后如果有票的话,就可以买票了(涉及到写 *** 作)。理论上,每次有人购票的时候,系统就会让票的数量减少一个,但是如果多人同时购票的话(多个线程访问同一个资源),会出现多人同时购票,而系统只让票的数量减一,这些人就会是重票。

那出现上述的肯定是不行的,下面介绍三种方法来解决线程安全问题

同步代码块同步方法锁机制

下面就对这三种方法进行分别介绍

同步代码块

Java中同步代码块是通过添加 synchronized 关键字实现的,将synchronized 关键字用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问(同时只能由一个线程访问该该区块中的资源)。

语法格式
synchronized(同步锁){
     //需要互斥的代码
}

这里的同步锁只是一个概念,指的是为你想实现互斥访问资源的对象上标记了一个锁。

想对谁互斥访问,同步锁里面就写谁,锁对象可以是任意类型

代码示例

public class Demo7 {
    public static void main(String[] args) {

        Tacket1 tacket1 = new Tacket1();


        //通过匿名内部类创建线程
        new Thread(new Runnable() {
			//重写run方法实现线程执行逻辑
            @Override
            public void run() {
            	//售票员1卖票(线程1)
                for (int i = 0; i < 40 ; i++) {
                    tacket1.sellTacket();
                }
            }
        },"售票员1").start();

        new Thread(new Runnable() {
       	 //重写run方法实现线程执行逻辑
            @Override
            public void run() {
                for (int i = 0; i < 40 ; i++) {
                	//售票员2卖票(线程2)
                    tacket1.sellTacket();
                }

            }
        },"售票员2").start();

    }
}

class  Tacket1{
    private int number =30; //票有30张

    public void sellTacket(){ //票数大于0才能卖

        synchronized (this){ //this作为锁,是因为对于这几个线程,Ticket的this是同一个
            if (number > 0){
                try {
                    Thread.sleep(10); //等10毫秒,效果好点
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票数-1
                System.out.println(Thread.currentThread().getName()+"卖票,剩余票:"+number--);
            }
        }
    }

}
同步方法

在Java中,同步方法使用的也是synchronized 关键字,不同于同步代码块的是,同步方法直接将synchronized 关键字修饰方法。这使得当线程执行该方法时候,其他线程就不能执行该方法,也实现了互斥访问资源(方法里有访问资源的 *** 作)。

语法格式
public synchronized void methodname(){
    //可能会产生线程安全问题的代码
}

同步方法的锁对象:

静态方法:当前类的Class对象非静态方法:this 代码示例


public class Demo8 {
    public static void main(String[] args) {

        Tacket2 tacket2 = new Tacket2();

        //通过匿名内部类创建线程
        new Thread(new Runnable() {
         //重写run方法实现线程执行逻辑
            @Override
            public void run() {
                for (int i = 0; i < 40 ; i++) {
                //售票员1卖票(线程2)
                    tacket2.sellTacket();
                }
            }
        },"售票员1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40 ; i++) {
               	 //售票员2卖票(线程2)
                    tacket2.sellTacket();
                }

            }
        },"售票员2").start();
    }
}

class  Tacket2{
    private int number =30; //票有30张

    public synchronized void sellTacket(){ //同步方法

            if (number > 0){//票数大于0才能卖
                try {
                    Thread.sleep(10); //等10毫秒,效果好点
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票数-1
                System.out.println(Thread.currentThread().getName()+"卖票,剩余票:"+number--);
            }
    }

}
锁机制

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,这种锁定保证了线程安全。

释放锁的 *** 作

可以锁定,但又不能一直锁定,所以什么时候释放锁也是一个关键点。下面介绍释放锁的 *** 作:

当前线程的同步方法、同步代码块执行结束。当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁。

不会释放锁的 *** 作

有的方法执行了,当前线程会暂停,但此时该线程并不会释放锁,仍然保持锁定状态。具体方法如下:

线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,

死锁

就像不是所有的线程都能完成一样,有些锁可能就不能释放了,变成了死锁。当不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5703183.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存