<Java>java多线程的创建、使用以及如何解决线程安全问题

本文详细介绍了Java中程序、进程和线程的概念,多线程的创建方式,包括继承Thread类和实现Runnable接口,并通过实例分析了线程的生命周期。重点讨论了线程安全问题,如线程不安全的卖票程序,并展示了如何通过同步代码块和同步方法解决这类问题。文章最后总结了线程同步的优缺点和适用场景。
摘要由CSDN通过智能技术生成

一、程序、进程、线程

  • 程序是为完成特定任务的一组指令的集合,是静态的

  • 进程是正在执行的程序,是动态的,如QQ,正在播放的音乐App

  • 线程是进程的一个实体,如QQ的聊天窗口

每个线程有一套虚拟机栈程序计数器,每个进程有一套方法区(static变量在方法区)和,隶属于同一进程的线程共享堆的方法区和堆
在这里插入图片描述

二、多线程的创建

方式一:继承于Thread类

①创建一个继承于Thread类的子类

②重写Thead类run方法(将此线程执行的操作声明在run()中)

③创建Thread类的子类的对象

④通过此对象调用start()方法

求1-100的所有偶数代码如下:

package ThreadDemo;


//1、创建一个继承于Thread类的子类
class MyThread extends Thread{
    //2、重写Thead类的run方法
    public void run()
    {
        for(int i=1;i<=100;i++)
        {
            if(i%2==0)
                System.out.print(i+"\t");
            if(i%10==0)
                System.out.println();
        }
    }
}

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

        //3、创建Thread类的子类的对象
        MyThread thread=new MyThread();
        //4、通过此对象调用start()方法
        thread.start();

        for(int i=1;i<=100;i++)
        {
            if(i%2!=0)
                System.out.print(i+"?");
            if(i%10==0)
                System.out.println();
        }
    }


}

我创建了一个线程MyThread用于输出1-100之间的偶数,在主线程中又编写了一个循环用于输出1-100之间的奇数,并在后面添上

在调用了thread.start();以后,线程thread就开始执行了,同时JVM就去调用thread中的run()方法。同时主线程中的循环也开始执行,这两个线程是并发执行,执行哪个线程就看此时CPU的调度策略了

如果不是通过调用start()方法创建一个新线程后,由JVM去调用run()方法,而是直接通过创建的对象thread直接调用run()方法,那么就只是简单的“创建一个对象,调用对象的方法”,不会有线程并发执行的情况产生。

运行结果:
在这里插入图片描述

方式二:实现Runnable接口

①创建一个实现了Runnable接口的类

②实现Runnable接口中的抽象方法run()

③创建实现类的对象

④将此对象作为参数传递到Thread类的构造方法中,创建Thread类的对象

⑤调用Thread的start()方法

package ThreadDemo;

class myThread implements Runnable{
    public void run()
    {
        for(int i=0;i<20;i++)
        {
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}

public class Threada {
    public static void main(String[] args) {
        myThread t=new myThread();
        Thread res=new Thread(t);
        res.setName("线程一");

        res.start();
    }
}

两种方式比较:

优先选择:实现Runnable接口的方式

原因:
1、没有类单继承的局限性
2、更适合用来处理多个线程共享数据的情况(继承Thread类的实现方式需要添加static约束

联系:public class Thread implements RunnableThread也实现了Runnable接口

相同点:两种方式都需要重写run()方法,将需要实现的操作声明在run()方法

三、Thread中的常用方法

1、start():启动当前线程,调用当前线程的run()方法

2、run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中

3、currentThread()静态方法,返回执行当前代码的线程

4、getName():获得当前线程的名字

5、setName():设置当前线程的名字

6、yield():释放当前线程的CPU执行权

7、join():在线程1中调用线程2的join(),此时线程1就进入阻塞状态,直到线程2执行完毕,线程1才结束阻塞状态

8、sleep(long millitime):线程进入睡眠状态

–yield()举例–

yield():释放当前线程的CPU执行权

import static java.lang.Thread.yield;

//创建一个线程,完成1-1000偶数的输出
//遇到i为100的倍数则执行yield释放CPU使用权
class myYieldThread implements Runnable
{
    public void run()
    {
        for(int i=1;i<=1000;i++)
        {
            if(i%2==0)
                System.out.println(Thread.currentThread().getName()+": "+i);
            if(i%100==0)
                Thread.currentThread().yield();  //当前线程释放 CPU使用权
        }
    }
}


public class yieldEx {
    public static void main(String[] args) {
        Thread t1=new Thread(new myYieldThread());
        t1.setName("线程1");
        t1.start();

        for(int i=1;i<500;i++)
        {
            System.out.println("main: "+i);
        }
    }

}

在创建的线程myYieldThread有这么一段条件判断,使得当每次i为100的倍数时,线程myYieldThread都会释放CPU的使用权

  if(i%100==0)
      Thread.currentThread().yield();  //当前线程释放 CPU使用权

由于只要i是100的整数时就会释放CPU使用权,然后操作系统就重新分配CPU的使用权
在这里插入图片描述

在这里插入图片描述
可以看到,到达100的倍数时,线程myYieldThread释放CPU使用权,而操作系统分配CPU使用权给了主线程,因此主线程main运行

主线程main运行完后,即使线程myYieldThread到达100的倍数,释放了CPU使用权,由于没有别的线程了(这里只设置了线程myYieldThread主线程main),因此CPU的使用权只能分配给线程1(即线程myYieldThread的线程名),因此线程1表现出来就是连续执行
在这里插入图片描述

需要注意的是,线程1执行yield()方法后,虽然释放了CPU使用权,但是还是会与其它线程抢夺CPU使用权(即参与CPU使用权的分配),而不是拱手相让。而是让出CPU使用权,然后所有线程包括释放CPU使用权的线程1,共同参与CPU使用权的分配,CPU使用权分配到哪个线程,哪个线程就得以执行

下面这个运行结果就是其它线程尚未执行完,并且i为100的倍数,但是线程1仍然连续执行,就是因为 线程1释放CPU使用权后又恰好得到了操作系统分配的CPU的使用权,因此得以连续执行
在这里插入图片描述

四、线程的生命周期

5种状态:

新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态

就绪:处于新建的状态的线程调用start()方法,进入就绪态,等待CPU调度

运行:就绪态的线程获得CPU调度

阻塞:被人为挂起或执行输入输出操作时,让出CPU给其他线程,使本线程进入阻塞状态

死亡:线程完成或发生异常或被强制停止

在这里插入图片描述

例题:多窗口卖票

创建3个窗口卖票,总票数为100张

方法1 Thread的子类

Windows类 将其作为卖票的窗口

class Windows extends Thread
{
    private int tickets=100;

    public void run()
    {
        while(true)
        {
            //判断是否有余票
            if(tickets>0)
            {
                System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+(100-tickets+1));
                tickets--;
            }
            else
            {
                break;
            }
        }
    }
}

主函数,开启三个卖票的窗口线程


  public static void main(String[] args) {
      Windows t1=new Windows();
      Windows t2=new Windows();
      Windows t3=new Windows();

      t1.setName("窗口1");
      t2.setName("窗口2");
      t3.setName("窗口3");

      t1.start();
      t2.start();
      t3.start();

  }

截取部分运行结果如下
在这里插入图片描述
显然,这样的卖票结果是不合理的,同一个票号只应该出现一次。本意是让三个窗口一起卖,共卖100张票。但现在变成了3个窗口,各卖100张票,显然不符合要求

原因就是在Windows类中的成员变量

    private int tickets=100;

我们在创建线程时,用以下方法分别创建了3个对象,而每个对象各自都有一个成员变量tickets,这就是为什么每个窗口都卖100张票的原因

        Windows t1=new Windows();
        Windows t2=new Windows();
        Windows t3=new Windows();

解决办法就是将这个成员变量tickets设置为static属性,这样就是三个对象拥有共同的成员变量,即共享这个静态变量

虽然设置成静态变量解决了三个窗口各卖100张票的问题,但还是存在一个问题,那就是票号重复,此即线程安全问题,这将在后文解决^ ^

在这里插入图片描述

方法2 实现Runnable方法

runnableWindows类

class runnableWindows implements Runnable
{
    private int tickets=100;
    public void run()
    {
        while(true)
        {
            //判断是否有余票
            if(tickets>0)
            {
                System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+(100-tickets+1));
                tickets--;
            }
            else
                break;
        }
    }
}

主函数

    public static void main(String[] args) {
        runnableWindows t=new runnableWindows();

        Thread t1=new Thread(t);
        Thread t2=new Thread(t);
        Thread t3=new Thread(t);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

在这里没有像使用继承自Thread创建线程那样有三个窗口各自卖100张票的问题,因为从下面的创建线程的方式可以看出,创建3个线程都是使用同一个runnableWindows对象,因此这3个线程共享同一个tickets变量,因此在这里不用加上static修饰

        runnableWindows t=new runnableWindows();

        Thread t1=new Thread(t);
        Thread t2=new Thread(t);
        Thread t3=new Thread(t);

但是仍然存在票号重复的线程不安全问题,这个问题也将在后文解决^ ^

五、线程同步

由窗口卖票例题可以看出,可能会重票(有两张及两张以上相同票号的票)或者错票(票号不合理,如100张票,出现了票号为101的情况)的问题

出现这些问题的原因是如下代码造成的线程不安全

    public void run()
    {
        while(true)
        {
            //判断是否有余票
            if(tickets>0)
            {
                System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+(101-tickets));
                tickets--;
            }
            else
                break;
        }
    }

重票:假设此时有100张票
此时tickets=100
线程1tickets>0—售票,票号为1—由于某些原因线程1阻塞,未能执行tickets自减操作

由于线程1尚未执行成功tickets自减操作,因此tickets=100

此时线程2获得CPU使用权,线程2执行
线程2:tickets>0—售票,票号为1—…

因此出现了重票


错票:假设此时还剩余1张票
此时tickets=1

线程1tickets>0—售票,票号为100—由于某些原因线程1阻塞,未能执行tickets自减操作
由于线程1尚未执行成功tickets自减操作,因此tickets=1

此时线程2获得CPU使用权,线程2执行
线程2:tickets>0线程2进入到if执行语句块里,但是尚未执行语句,由于某些原因线程2阻塞

此时线程1解除阻塞,tickets自减tickets=0,但是由于线程2已经进入到if执行语句块里,因此当线程2继续执行时,无需再判断条件是否合理,于是线程2执行if执行语句块,此时售票,票号为101。只有100张票,票号应为1-100,因此出现了错票


线程安全问题出现的原因:某个线程在操作票的过程中,尚未完成操作时,就有其它线程参与进来,也操作票

解决方法:当一个线程a在操作tickets共享数据)时,其它线程不能参与进来,直到线程a操作完tickets,其它线程才可以开始操作tickets

具体方法:通过同步机制来解决线程安全问题

方式一:同步代码块

    synchronized(同步监视器)
    {
        //需要被同步的代码
    }

说明:
1、操作共享数据的代码,即为需要被同步的代码

2、共享数据多个线程共同操作的变量,比如例子中的票数tickets

3、同步监视器,俗称为。任何一个类的对象都可以充当锁

要求:多个线程必须共用同一把锁


1 解决实现Runnable方法的方式的线程安全问题

在售票例子中需要被同步的代码块是:

在这里插入图片描述

那么为其代码块实现同步如下

在这里插入图片描述
这里解决的是实现Runnable的线程安全问题,解决继承Thread类的线程安全问题不能这么做。因为多个线程共用同一个同步监视器来实现同步机制,而继承Thread类创建线程不满足此条件,因此不能使用上述方法解决其线程安全问题

实现Runnable创建线程

        runnableWindows t=new runnableWindows();

		//创建线程,使用的参数是同一个对象t
		//因此只会进行一次Object obj=new Object();
        Thread t1=new Thread(t);
        Thread t2=new Thread(t);
        Thread t3=new Thread(t);

继承Thread类创建线程

class Windows extends Thread
{...}
		//创建线程,3个线程各进行一次Object obj=new Object();
        Windows t1=new Windows();
        Windows t2=new Windows();
        Windows t3=new Windows();

一定要注意看是否用的是同一把锁

锁是任何一个类的对象,可以是当前调用的对象,使用this也是可以的

使用同步机制的优点和局限性:
优点:解决了线程的安全问题

局限性:操作同步代码块时,只能有一个线程参与,其余线程需要等待,相当于是一个单线程的过程,降低了效率

2 解决继承Thread类的方式的线程安全问题

同样地,线程安全问题出现在

在这里插入图片描述

那么需要考虑同步监视器应该如何设置

像解决Runnable方式的线程安全问题那样

Object obj=new Object();   //错误

这不适用于继承Thread方式,因为在创建多个线程时,每个线程都将会创建一个obj,那么就不是多线程共用同一个同步监视器了。我们可以考虑将其使用static修饰,这样就可以多线程共用同一个同步监视器了

在这里插入图片描述

在实现Runnable接口创建多线程的方式中,可以考虑使用this作为同步监视器

synchronized(this)
{...}

在继承Thread类创建多线程的方式中,慎用this充当同步监视器,可以考虑使用当前类同步监视器

synchronized(Windows.class)
{...}

方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,那么我们就可以将这个方法声明成同步的

使用同步方法解决实现Runnable接口的线程安全问题

原来的代码块

class runnableWindows implements Runnable
{
    private int tickets=100;
    public void run()
    {
        while(true)
        {
            //判断是否有余票
            if(tickets>0)
            {
                System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+(101-tickets));
                tickets--;
            }
            else
                break;
        }
    }
}

既然要使用同步方法解决,那么就是在方法定义的时候加上关键字synchronized,那么就需要一个方法,因此把售票的过程封装成一个方法sellTickets()

1 使用同步方法解决实现Runnable接口的线程安全问题
class runnableWindows implements Runnable
{
    private int tickets=100;
    public void run()
    {
        while(true)
        {
            sellTickets();
            
        }
    }

    public synchronized void sellTickets()   //默认同步监视器是this
    {
        if(tickets>0)
        {
            System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+(101-tickets));
            tickets--;
        }
    }

}
2 使用同步方法解决继承Thread类的线程安全问题

原来的代码块

class Windows extends Thread
{
    private static int tickets=100;
    public void run()
    {
        while(true)
        {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + (100 - tickets + 1));
                tickets--;
            } else {
                break;
            }
            
        }
    }
}

使用同步方法解决继承Thread类的线程安全问题

class Windows extends Thread
{
    private static int tickets=100;
    public void run()
    {
        while(true)
        {
            sellTickets();
        }
    }

    public static synchronized void sellTickets()   //同步监视器就是当前的类:Windows.class
    {
        if (tickets > 0) {
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + (100 - tickets + 1));
            tickets--;
        }
    }
}

Runnable的解决方法不同之处在于,Thread的同步方法还多了static限定

总结:

  • 同步方法仍然涉及到同步监视器
    • 非静态的同步方法,同步监视器为:this,即当前对象
    • 静态的同步方法,同步监视器为: 当前类本身
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值