您的当前位置:首页>关注 > 正文

世界信息:程序、进程和线程——多线程的创建方法

来源:CSDN 时间:2023-03-07 08:14:43

目录


(资料图片)

程序、进程和线程的概念

多线程的优点

Thread类关于多线程的创建

Thread类的相关方法

线程的调度

线程的五种状态

线程的同步

总结同步方法

衍生内容————单例设计模式

死锁问题

锁的概念

sleep()和wait()的异同

首先要明确几个概念

程序、进程和线程的概念

程序:完成特定任务,用某种特殊的语言编写的一组指令的集合

进程:是执行路径,一个进程同一时间并行或者正在运行的程序

线程:是执行路径,一个进程同一时间并行或者执行多个进程,就是多线程

注:进程中也有可能有多个线程

CPU也分为多核CPU和单核CPU

单核CPU:实际上进行的是某种意义上的假CPU,一个CPU同时做好多事,如果一个没有准备好,就先将该事件挂起,去进行别的,可以用一张图来表示

多核CPU(取决与主频来利用哪个):多核CPU就相当于多个单核CPU工作

同时也要解释两个词的含义

并行:多个CPU任务一起进行

并发:一个CPU做多个任务

注:并发只是看上去“同时”,但是实际上只是在CPU上进行高速的切换任务,以至于仅仅是看上去是同时,并行才是真正意义上的同时

多线程的优点

1、提高应用程序的响应

2、提高CPU的利用率

3、改善程序结构,每个线程独立运行,互不干扰,便于修改

提到多线程,就不得不提一个特殊的类

Thread类关于多线程的创建

方法一:

1、创建一个继承于Thread类的子类

2、重写Thread类中的run()

3、创建Thread类子类的对象(要在主线程上创建)、

4、通过对象去调用start()

想要创建一个多线程的代码如下

//主函数中的体现为//1、创建了继承Thread的子类//在继承Thread中的表现为public class ExtendsThread extends Thread {    @Override    //2、此处为标准的对于run()函数重写    //对run()函数的重写就相当于对于这一条线程中你想做的所有任务    public void run() {        super.run();        for(int i=0;i<=20;i++)        {            System.out.println(i);        }    }}public class ThreadTest {    public static void main(String[]args) {        //3、创建了继承Thread子类的对象        Thread et=new ExtendsThread();        //4、通过对象调用了start()        et.start();        //调用start()之后就开启多线程    }}

此处需要注意的是

1、run方法的重写:将这个线程要执行的所有操作全部都声明在run方法中

2、et.run()也能在主函数中直接调用,也能完整的执行在run方法中的指令,但是不能体现多线程,就仅仅是将指令完成,et.run()就仅仅只是调用方法看

3、不能够让已经start()的线程再去重启线程

4、可以创建多个对于ExtendsThread的对象,此时这个对象可以再次开始start(),相当于多开了一个线程,只不过执行的是相同内容

5、匿名子类与匿名对象同样适用

public class ThreadTest {    public static void main(String[]args) {        Thread et=new ExtendsThread();        //此处为体现多线程,同时开启两个线程        et.start();        //以下即为匿名子类        //直接开启多线程        new Thread(){            public void run()            {                super.run();                for(int i=0;i<=10;i++)                {                    System.out.println(i+"#"+i);                }            }        }.start();    }}public class ExtendsThread extends Thread {    @Override    public void run() {        super.run();        for(int i=0;i<=10;i++)        {            System.out.println(i+"*"+i);        }    }}

第一次的执行结果

方法二:

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

2、实现Runnable接口中的抽象方法

3、创建实现类对象

4、将此对象作为参数传至Thread类的构造器,创造Thread类的对象

5、利用Thread()类的对象调用start()

public class RunnalbeThread implements Runnable//1、创建一个实现Runnable的类{    @Override//2、类中重写Runnable的方法,也就是run方法    public void run() {        for(int i=1;i<=10;i++)        {            System.out.println(Thread.currentThread().getName()+":"+i);        }    }}public class ThreadTest {    public static void main(String[] args) {        Thread rt=new Thread(new RunnalbeThread());        //3、创建一个对应类的对象        //4、将这个对象传入到Thread的构造器        rt.start();        //5、用这个对应的Thread对象来继续调用start()        rt.setName("线程3");        for(int i=1;i<=10;i++)        {            System.out.println(Thread.currentThread().getName()+":"+i+"-"+Thread.currentThread().isAlive());        }    }}

在这个地方,如果没有创建匿名对象(对于实现Runnable的实现类),一个实现类的对象,可以多次传入到Thread的构造器里面,创造更多的线程

两种方法的比较

继承法(方法一)由于Java的单继承性,导致如果需要继承Thread类的类由原本的一套体系,可能会影响该代码的实现,由此看来,实现接口的方式是更加活泛的,更自由。

实操中优先选择Runnable接口的方式

1、实现的方式没有单继承性的限制

2、实现的方式更适合多个线程共享数据的情况

注:Thread类也实现了Runnable接口

Thread类的相关方法

1、String getName();

返回线程名称

2、void setName(String name);

设置线程名称

public static void main(String[] args) {        Thread et = new ExtendsThread();        et.setName("线程--1");        System.out.printf(et.getName());}

运行结果

此处需要注意的是,主线程也是可以命名的,如以下代码

public class ThreadTest {    public static void main(String[] args) {        Thread et = new ExtendsThread();        Thread.currentThread().setName("主线程");        System.out.printf(Thread.currentThread().getName());    }}

运行结果如下

3、currentThread()方法

静态方法,返回当前执行此代码的线程(对象)

4、yield()方法

释放当前CPU的执行权

也存在当我们释放完执行权之后,CPU再次将执行权分配给目前线程的情况

5、join()方法

相当于在原本的线程1上,让另一个线程2截断,知道这个线程2执行结束,否则不再进行线程1(在线程1之中调用线程2的join方法)

代码测试如下

public class ExtendsThread extends Thread{    @Override    public void run() {        super.run();        for(int i=0;i<=10;i++)        {            System.out.println(Thread.currentThread().getName()+":"+i+"*"+i);        }    }}public class ExtendsThread2 extends Thread{    public void run() {        super.run();        for(int i=0;i<=10;i++)        {            System.out.println(Thread.currentThread().getName()+":"+i+"#"+i);        }    }}public class ThreadTest {    public static void main(String[] args) {        Thread et = new ExtendsThread();        Thread et2=new ExtendsThread2();        et.start();        et2.start();        et.setName("线程1");        et2.setName("线程2");        Thread.currentThread().setName("主线程");        for(int i=0;i<=20;i++)        {            System.out.println(Thread.currentThread().getName()+":"+i);            if(i%5==0)            {                try {                    et2.join();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }}

测试结果如下

当主线程的i跑到5的时候,此时调用了et.join()和et2.join()此时的主线程已经被挂起了,直到线程1和线程2运行完之后,才会继续主线程的进行。

6、stop()

强制结束线程,可以提前结束线程的生命周期。(不推荐使用stop()结束进程)

public class ExtendsThread extends Thread{    @Override    public void run() {        super.run();        for(int i=0;i<=10;i++)        {            System.out.println(Thread.currentThread().getName()+":"+i+"*"+i);            if(i==5)            {                Thread.currentThread().stop();                //此处用stop强制停止了                //当i=5的时候强制停止线程            }        }    }}public class ThreadTest {    public static void main(String[] args) {        Thread et = new ExtendsThread();        Thread et2=new ExtendsThread2();        et.start();        et.setName("线程1");        Thread.currentThread().setName("主线程");        for(int i=1;i<=10;i++)        {            System.out.println(Thread.currentThread().getName()+":"+i);        }    }}

测试结果

如图所示,线程1确实只进行到i=5的时候

7、sleep(long millitime)

强制线程进入休眠,单位是毫秒

在指定时间内强制休眠

需要注意的是,对某个线程使用sleep的话,该线程就会进入到挂起状态,在指定时间挂起。相当于主动让出了CPU的执行权。

8、isAlive()

判断当前线程是否存活

举例如下

public class ExtendsThread extends Thread{    @Override    public void run() {        super.run();        for(int i=0;i<=10;i++)        {            System.out.println(Thread.currentThread().getName()+":"+i+"-"+Thread.currentThread().isAlive());        }    }}public class ThreadTest {    public static void main(String[] args) {        Thread et = new ExtendsThread();        Thread et2=new ExtendsThread2();        et.start();        et.setName("线程1");        Thread.currentThread().setName("主线程");        for(int i=1;i<=10;i++)        {            System.out.println(Thread.currentThread().getName()+":"+i+"-"+Thread.currentThread().isAlive());        }        System.out.println(et.isAlive());    }}

结果如下

如图所示,在代码的最后,et所开启的线程已经结束,所以此时打印出来的false

线程的调度

线程的进行主要是看时间片,一般情况下,多个线程都是并发,所以对于CPU的执行权一般是进行抢夺,高优先级的线程优先抢夺CPU的执行权。

说到这里就不得不提到线程的优先等级(这里的优先级都是在线程诞生的时候就是设置好的,默认为5)

>MAX_PRIORITY:10

>MIN_PRIORITY:1

>NORM_PRIORITY:5

也有两个方法是关于线程的优先级

1、getPriority():返回线程优先级

2、setPriority(int newPriority):改变线程的优先级

高优先级抢占低优先级的线程的CPU执行权,但是是从概率上而言,高优先级的线程有更大的概率去执行CPU

线程的五种状态

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

2、就绪:当线程被start()之后,就会进入队列等待CPU的时间片

3、运行:获得CPU资源,进入运行状态,run定义了线程操作和功能

4、阻塞:在某种情况下,被人为挂起或执行输入输出,让出CPU的执行权

5、死亡:线程完成了全部工作或被提前强制性中止(stop),或者出现异常导致结束,比如join()会使线程被挂起,造成线程阻塞

线程的同步

线程的安全问题(不一定出现线程安全问题)

没有sleep()出现时,错误的概率小,但是安全问题总是要解决的

有可能会出现极端情况

此时带入一个场景,比如说一个线程代表一个窗口,一个售票窗口,线程每进行一次就挂起一次,会打印票号,但是如果正常进行,票号应该是连号,但是会出现如下情况

代码如下

public class RunnalbeThread implements Runnable{    public static int num=30;    public static int tnum=1;    @Override    public void run() {        while(num!=0)        {            if(num>0)            {                num--;                tnum++;                System.out.println(Thread.currentThread().getName()+":"+tnum);                try {                    Thread.currentThread().sleep(1000);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }    }}public class ThreadTest {    public static void main(String[] args) {        Thread rt3 = new Thread(new RunnalbeThread());        Thread rt2 = new Thread(new RunnalbeThread());        Thread rt = new Thread(new RunnalbeThread());        rt2.start();        rt3.start();        rt.start();        rt3.setName("线程3");        rt.setName("线程1");        rt2.setName("线程2");    }}

代码测试结果如下

很明显的,会出现重号的现象

原因:当某个线程操作票的过程中,尚未完成操作,另一个线程参与进来,也对车票进行操作(相当于是共享数据)

如何解决

加锁

当一个线程在操作共享数据的时候,其他线程不能参与,直到线程a操作结束,其他线程才能开始操作。即使a处于阻塞状态,也不能被改变

方法一:同步代码块

synchronized(同步监视器){

需要被同步的代码}

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

同步监视器,俗称锁,可以随意扔一个对象进去

要求:多个线程要共用同一把锁,不能设置多个锁,此时不能使用匿名

缺点:操作同步代码时,仅能有一个线程操作,其他的都在等待,相当于是一个单线程操作过程,相对而言效率会很低

此时会出现一个锁不唯一的问题,由于锁的创建在Thread的子类中,但是使用此方法创造进程需要newThread的子类的对象,此时会new出很多锁,此时最好的解决方案就是把锁进行static

方法展示

public class RunnalbeThread implements Runnable{    public static int num=30;    @Override    public void run() {        while(num!=0)        {            try {                Thread.currentThread().sleep(100);            } catch (InterruptedException e) {                e.printStackTrace();            }            synchronized (RunnalbeThread.class) {                if(num>0)                {                    num--;                    System.out.println(Thread.currentThread().getName()+":"+num);                }            }        }    }}public class ThreadTest {    public static void main(String[] args) {        Thread rt3 = new Thread(new RunnalbeThread());        Thread rt2 = new Thread(new RunnalbeThread());        Thread rt = new Thread(new RunnalbeThread());        rt2.start();        rt3.start();        rt.start();        rt3.setName("线程3");        rt.setName("线程1");        rt2.setName("线程2");    }}

代码中,把对于所有共享数据的操作全部都包起来了,达到监视的作用

结果如下

还有一个需要注意的点就是如果是用接口实现的方法创建的线程,可以考虑使用this的,之所以继承法不能使用,是因为其依靠创造他本身的对象来创造线程,但是实现类只创造一个对象,其他对象都是利用Thread进行创造的。

但是我的代码中,监视器之后的锁就不能使用this,因为在主函数中,我用的创建方法并不是一个对象传入到Thread的构造器中,我使用了匿名对象,如果使用this,每一次的锁都是不一样的锁,无法起到监视作用了

同时,在我的代码中,使用了synchronized (类名.class)这种方式,在这里需要注意的是,类本身也是一个对象,类仅加载一次,与每次new完之后出现的新对象不同。所以在我看来,类是一个完美的锁,不会出现重复的现象。

也需要注意对于同步代码的包装。要注意包装的范围,少包不能解决安全问题,包多了会影响效率,而且也容易出现新的问题。

方式二:

1、同步方法实现Runnable接口

synchronized可以修饰方法,但是需要符合题意,一般情况下不建议使用

在同步方法的内部,就和使用synchronized包起来是一个效果

使用同步方法时,同步监视器就是this

2、同步方法继承Thread类的方法

对于继承法而言,很明显不能直接加synchronized,加了synchronized之后,会自动使用this作为监视器,很显然不行,此时应该将方法改成静态

总结同步方法

1、仍涉及同步监视器,只是不需要显式声明

2、非静态的同步方法是this,静态方法的监视器视为当前类本身

衍生内容————单例设计模式

1、懒汉式(线程安全)

先来分析一下,在原本对于懒汉式的代码中,线程安全可能会出现的部位

public class Bank {    private Bank(){}    private static Bank instance=null;    public static Bank getInstance()    {        if(instance==null)        {            instance=new Bank();        }        //在此段就容易出现堵塞或者就绪,当多线程在此处参与时,设线程a、线程b        //a判断了instance==null,已经进入了语句,此时CPU将执行权切换给了b或        //a由于某种原因阻塞了,那么此时可能就不仅仅创建了一个对象        return instance;    }}//而在关于单例式操作,同时满足有多个线程,有共享数据这两个条件,可以实现线程安全

本质上就是线程a、b抢锁,谁先抢到就谁先造

如果想用同步方法,在本例中就可以直接将getInstance这个方法直接使用synchronized直接修饰,就可以解决线程安全问题

如果想使用同步代码块,就可以使用synchronized将getInstance这个方法中的内容直接包裹,并且利用Bank.class对代码进行监视(效率差)

同步代码块——方法一

public class Bank {    private Bank(){}    private static Bank instance=null;    public static Bank getInstance()    {        if(instance==null)        {            synchronized(Bank.class)            {                instance=new Bank();            }        }        return instance;    }}

同步代码块——方法二

public class Bank {    private Bank(){}    private static Bank instance=null;    public static Bank getInstance()    {        synchronized(Bank.class)        {            if(instance==null)            {                instance=new Bank();            }        }        return instance;    }}

两个方法在使用上的区别不大,都可以正常使用,但是实际上方法一的效率更高

假设现在有线程1和线程2,当线程1率先抢到CPU控制权,先制造了对象,线程2在方法二中仍停留在synchronized语句上等待,一直到线程1制造完对象,线程2才能够进入if,判断失败之后离开该方法,但是在方法一中,线程2先进入判断,如果1已经造完对象了,那么线程2就会直接离开。线程2就不会再进入等待区。

死锁问题

不同的线程分别占用了对象所需资源不放,都在等对方放弃,形成死锁

>不出现异常,不出现提示,所有的线程阻塞,不再进行

使用同步的时候,一定要避免死锁问题出现

锁的概念

Lock实际上就是一个接口,需要有实现类

Lock接口的具体使用,主要是对其实现类:Reentrantlock的使用

Reentrantlock

这个类有两个构造器,有一个形参fair

如果fair是true,就遵循先入先出,按照abc顺序开锁

如果fair是false或者没有参数,那么就是abc抢锁,谁先抢到谁先开

1、实例化Reentrantlock

2、将同步代码放到try中,在try首行调用Reentrantlock的对象调用Lock(),也可以调用解锁,try-finally,其中不使用catch,只是想让finally无论如果先给Lock解锁,即使try过程有异常,也会给Lock解锁

(其实本质上也就是上锁,只不过Lock需要手动开锁,但是synchronized不需要,synchronized自动就会开锁)

synchronized和Lock的异同

synchronized机制在执行完同步代码块后自动释放同步监视器

Lock需要手动开锁,不然会一直锁定一个线程不放

基本都会使用synchronized,但是实际上更建议使用Lock

sleep()和wait()的异同

相同:都可以使当前线程进入阻塞

不同:

1、两个方法声明位置不同,Thread类中声明sleep(),Object类中声明wait()

2、调用范围不同,sleep()在任何场景都能调用,wait()必须使用在同步方法或者同步代码块中

3、关于是否释放同步监视器,如果二者都在同步中,sleep()不释放锁,但是wait()会释放锁

标签:

最新新闻:

新闻放送
Top