多线程基础总结

Posted cxyyh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程基础总结相关的知识,希望对你有一定的参考价值。

第一、多线程介绍

通过任务管理器可以看到进程的存在,进程:进程指正在运行的程序。当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中程序,并且具有独立功能的程序。

技术图片

 

 线程:在同一个进程有多个执行任务,而这每个任务都可以看成一个线程。

线程是程序的执行单元,执行路径。是程序使用CPU的·最基本单位。

单线程:如果程序只有一个执行路径

多线程:如果程序有多条执行路径,一个程序中有多个线程在同时执行。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

技术图片

 

通过下图来区别单线程程序与多线程程序的不同:

单线程程序:即,若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。如,去网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。

多线程程序:即,若有多个任务可以同时执行。如,去网吧上网,网吧能够让多个人同时上网。

技术图片

 

 

 多线程的意义:

单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。

举例:一边玩游戏(游戏进程),一边听音乐(音乐进程)。

也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。

并且呢,可以提高CPU的使用率。

问题:

一边玩游戏,一边听音乐是同时进行的吗?

不是。因为单CPU在某一个时间点上只能做一件事情。而我们在玩游戏,或者听音乐的时候,是CPU在做着程序间的高效切换让我们觉得是同时进行的。

多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。程序的执行其实都是在抢CPU的资源,CPU的执行权。

多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。

我们是不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。

程序原理

1、分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

2、抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

 

技术图片

 抢占式调度原理:

大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

技术图片

实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。

其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

Java程序运行原理

java 命令会启动 JVM,jvm启动相于一个进程,接着有该线程创建了一个主线程去调用一个main方法。

jvm虚拟机的启动是单线程的还是多线程的?

 多线程。原因是垃圾回收线程也要先启动,否则很容易会出现内存溢出。现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,jvm的启动其实是多线程的

大家注意两个词汇的区别:并行和并发。前者是逻辑上同时发生,指在某一个时间内同时运行多个程序。后者是物理上同时发生,指在某一个时间点同时运行多个程序。

 第二、创建多线程的方式

2.1Thread类

定义:

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。

start()方法是一个 native 方法,它会启动一个新线程,并执行 run()方法。这种方式实现多线程很简单,通过自己的类直接 extend Thread,并复写 run() 方法,就可以启动新线程并执行自己定义的 run()方法。

技术图片

 

 

 构造方法:

技术图片

 

 

 常用方法

 技术图片

线程常用的方法:

   Thread(String name)初始化线程的名字

 setName(String name)设置线程对象名

 getName()返回线程的名字

 start()使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

 sleep()线程睡眠指定的毫秒数。 静态的方法, 那个线程执行了sleep方法代码那么就是那个线程睡眠。

 currentThread()返回当前正在执行的线程对象,该方法是一个静态的方法, 注意: 那个线程执行了currentThread()代码就返回那个线程的对象。

 getPriority()返回当前线程对象的优先级   默认线程的优先级是5

 setPriority(int newPriority) 设置线程的优先级    虽然设置了线程的优先级,但是具体的实现取决于底层的操作系统的实现(最大的优先级是10 ,最小的1 , 默认是5)。

    Join()等待线程终止,有些线程结束了才能执行下面的线程

继续阅读,发现创建新执行线程有两种方法。

1、一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。创建对象,开启线程。run方法相当于其他线程的main方法。

2、另一种方法是声明一个实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

 2.2、创建线程方式一继承Thread

创建线程的步骤:

1 定义一个类继承Thread。

2 重写run方法, 把自定义线程的任务代码写在run方法中

3创建线程对象。

4 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。

注意: 一个线程一旦开启,那么线程就会执行run方法中的代码,run方法千万不能直接调用,直接调用run方法就相当调用了一个普通的方法而已并没有开启新的线程。

 2.3代码入门案例

public class ThreadDmo01 extends Thread


    @Override
    public void run() 
        System.out.println("当前线程是:"+Thread.currentThread().getName());
    
    public static void main(String[] args) 
        ThreadDmo01 threadDmo01 = new ThreadDmo01();
        threadDmo01.setName("AA");
        threadDmo01.start();
        ThreadDmo01 threadDmo02 = new ThreadDmo01();
        threadDmo02.setName("BB");
        threadDmo02.start();

    

 

 2.4继承Thread原理

为什么要继承Thread类,并调用其的start方法才能开启线程呢?

继承Thread类:因为Thread类用来描述线程,具备线程应该有功能。那为什么不直接创建Thread类的对象呢?如下代码:

Thread t1 = new Thread();

t1.start();//这样做没有错,但是该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码。

创建线程的目的是什么?

是为了建立程序单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定线程要执行的任务。

对于之前所讲的主线程,它的任务定义在main函数中。自定义线程需要执行的任务都定义在run方法中。

Thread类run方法中的任务并不是我们所需要的,只有重写这个run方法。既然Thread类已经定义了线程任务的编写位置(run方法),那么只要在编写位置(run方法)中定义任务代码即可。所以进行了重写run方法动作。

Thead类实现了Runnable接口,并重写了run()方法

部分源码

技术图片

 

 

 技术图片

 

 

 2.5多线程的内存图解

多线程执行时,到底在内存中是如何运行的呢?

以上个程序为例,进行图解说明:

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

技术图片

 

 

 当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

2.6、创建线程的方式二实现Runnable接口

创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

为何要实现Runnable接口,Runable是啥玩意呢?继续API搜索。

查看Runnable接口说明文档:Runnable接口用来指定每个线程要执行的任务。包含了一个 run 的无参数抽象方法,需要由接口实现类重写该方法。

技术图片

 

 

 构造方法

技术图片

创建线程的步骤。

1、定义类实现Runnable接口。

2、覆盖接口中的run方法。。

3、创建Thread类的对象

4、将Runnable接口的子类对象作为参数传递给Thread类的构造函数。

5、调用Thread类的start方法开启线程。

public class ThreadDemo02 implements Runnable 
    @Override
    public void run() 
        System.out.println("当前线程的名称"+Thread.currentThread().getName());
    

    public static void main(String[] args) 
        Thread thread1 = new Thread(new ThreadDemo02(),"AA");
        Thread thread2 = new Thread(new ThreadDemo02(),"BB");
        thread1.start();
        thread2.start();
    

 

2.7实现Runnable原理

为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?

实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。

创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

2.8实现Runnable的好处

第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。

继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,

类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

2.9线程的匿名的内部类使用

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

 匿名内部类的格式:

   new 类名或者接口名()

   重写方法;

;

   本质:是该类或者接口的子类对象。

public class ThreadDemo03 
    public static void main(String[] args) 
        new Thread(new Runnable() 
            @Override
            public void run() 
                System.out.println("当前线程:"+Thread.currentThread().getName());
            
        ,"AA").start();
        new Thread(new Runnable() 
            @Override
            public void run() 
                System.out.println("当前线程:"+Thread.currentThread().getName());
            
        ,"BB").start();
    

 

问题1: 请问Runnable实现类的对象是线程对象吗?

Runnable实现类的对象并 不是一个线程对象,只不过是实现了Runnable接口 的对象而已。

只有是Thread或者是Thread的子类才是线程 对象。

问题2: 为什么要把Runnable实现类的对象作为实参传递给Thread对象呢?作用是什么?

作用就是把Runnable实现类的对象的run方法作为了线程的任务代码去执行了。

推荐使用: 第二种。 实现Runable接口的。

原因: 因为java单继承 ,多实现的。

2.10.2种创建线程的比较

技术图片

第三、线程API

3.1线程休眠

线程休眠

public static void sleep(long millis) 线程睡眠指定的毫秒数。 静态的方法, 那个线程执行了sleep方法代码那么就是那个线程睡眠。

    @Override
    public void run() 
        for(int i=0;i<10;i++) 
            System.out.println("线程的名称:"+Thread.currentThread().getName());
            //让他睡上10分钟
            try 
                Thread.sleep(5000);
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
        
    

 

3.2线程加入

public final void join():等待该线程终止。有些线程结束了才能执行下面的线程

//创建对象
        MyTread mTread1 = new MyTread();
        MyTread mTread2 = new MyTread();
        MyTread mTread3 = new MyTread();
        MyTread mTread4 = new MyTread();
        MyTread mTread5 = new MyTread();
        mTread1.setName("李渊");
        mTread2.setName("李建成");
        mTread3.setName("李世民");
        mTread4.setName("李元吉");
        mTread5.setName("李元霸");
        //开启线程
        mTread1.start();
        //线程终止
        try 
            mTread1.join();
            mTread3.start();
            mTread4.start();
            mTread5.start();
         catch (InterruptedException e) 
            e.printStackTrace();
        
    

 

3.3线程礼让

线程礼让

public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。让多个线程更加和谐,但不能保证

public void run() 
        for(int i=0;i<100;i++) 
            System.out.println(getName());
            Thread.yield();
        
    

 

3.4守护线程

后台线程

public final void setDaemon(boolean on)

将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。

该方法必须在启动线程前调用。

技术图片

public class Thread07 
    public static void main(String[] args) 
        //创建一个线程对象
        ThreadDamon td = new ThreadDamon();
        ThreadDamon td1 = new ThreadDamon();
        Thread.currentThread().setName("刘备");
        td.setName("张飞");
        td1.setName("关于");
        //设置守护线程
        td.setDaemon(true);
        td1.setDaemon(true);
        td.start();
        td1.start();
        for(int i=0;i<5;i++)
    System.out.println(Thread.currentThread().getName());
        
    

class ThreadDamon extends Thread
    @Override
    public void run() 
        for(int i=0;i<100;i++)
            System.out.println(getName()+";"+i);
        
    

 

3.5线程终止

public final void stop():让线程停止,过时了,但是还可以使用。

public void interrupt():中断线程。 把线程的状态终止,并抛出一个InterruptedException。

public class Thread08 
    public static void main(String[] args) 
        ThreadStop ts =new ThreadStop();
        ts.start();
        //如果还没有苏醒就停止线程
        try 
            Thread.sleep(300);
            ts.interrupt();
         catch (InterruptedException e) 
            e.printStackTrace();
        
    

class ThreadStop extends Thread
    @Override
    public void run() 
        System.out.println("线程开启了"+new Date());
        try 
            Thread.sleep(10000);
         catch (InterruptedException e) 
            //e.printStackTrace();
            System.out.println("线程被终止了");
        
        System.out.println("线程中止了"+new Date());
    

 

3.6线程周期

技术图片

 

 技术图片

 

 第四、线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

  • 通过一个案例,演示线程的安全问题:

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “功夫熊猫3”,本次电影的座位共100个(本场电影只能卖100张票)。

我们来模拟电影院的售票窗口,实现多个窗口同时卖 “功夫熊猫3”这场电影票(多个窗口一起卖这100张票),需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟

测试类

/**
 * 多线程编写套路
 *   1、线程 操作(方法) 资源类
 *   2、高内聚 低耦合
 */
public class Ticket implements Runnable
    //100张票
    int ticket = 100;
    @Override
    public void run() 
       //模拟买票
        while (true)
            if(ticket>0)
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
            
        
    

    public static void main(String[] args) 
        Ticket ticket = new Ticket();
        Thread thread1 = new Thread(ticket);
        Thread thread2 = new Thread(ticket);
        Thread thread3 = new Thread(ticket);
        thread1.start();
        thread2.start();
        thread3.start();
    

 

 

技术图片

运行结果发现:上面程序出现了问题

  • 票出现了重复的票
  • 错误的票 0、-1

 实现Runnable接口的方式实现

  通过加入延迟后,就产生了连个问题:

  A:相同的票卖了多次

          CPU的一次操作必须是原子性的

  B:出现了负数票

         随机性和延迟导致的

问题1 :为什么50张票被卖出了100次?

出现 的原因: 因为num是非静态的,非静态的成员变量数据是在每个对象中都会维护一份数据的,三个线程对象就会有三份。

解决方案:把num票数共享出来给三个线程对象使用。使用static修饰。

问题2: 出现了线程安全问题 ?

线程 安全问题的解决方案:sun提供了线程同步机制让我们解决这类问题的。

其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

如何解决线程安全问题呢?

  要想解决问题,就要知道哪些原因会导致出问题:(而且这些原因也是以后我们判断一个程序是否会有线程安全问题的标准)

  A:是否是多线程环境

  B:是否有共享数据

  C:是否有多条语句操作共享数据

  我们来回想一下我们的程序有没有上面的问题呢?

  A:是否是多线程环境 是

  B:是否有共享数据 是

  C:是否有多条语句操作共享数据 是

  由此可见我们的程序出现问题是正常的,因为它满足出问题的条件。

  接下来才是我们要想想如何解决问题呢?

  A和B的问题我们改变不了,我们只能想办法去把C改变一下。

  思想:

   把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行。

  问题是我们不知道怎么包啊?其实我也不知道,但是Java给我们提供了:同步机制。

第五、线程同步

java中提供了线程同步机制,它能够解决上述的线程安全问题。

线程同步的方式有两种:

方式1:同步代码块

方式2:同步方法

5.1同步代码块

同步代码块: 在代码块声明上 加上synchronized

synchronized (对象)

可能会产生线程安全问题的代码

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

注意:

A:对象是什么呢?

   任意的一个对象都可以做为锁对象

   B:需要同步的代码是哪些呢?

   把多条语句操作共享数据的代码的部分给包起来

   注意:

   同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。多个线程必须是同一把锁。

在同步代码块中调用了sleep方法并不是释放锁对象的。

  举例:

   火车上厕所。

  同步的特点:

   前提:

   多个线程

  解决问题的时候要注意:

  多个线程使用的是同一个锁对象

  同步的好处

  同步的出现解决了多线程的安全问题。

  同步的弊端

  当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

    A:效率低

   B:容易产生死锁

使用同步代码块,对电影院卖票案例中Ticket类进行如下代码修改:

/**
 * 多线程编写套路
 *   1、线程 操作(方法) 资源类
 *   2、高内聚 低耦合
 */
public class Ticket implements Runnable
    //100张票
    private static int ticket = 100;
    //定义同一把锁
    private static Object lock = new Object();
    @Override
    public void run() 
       //模拟买票
        while (true)
            //同步代码块
            // t1,t2,t3都能走到这里
            // 假设t1抢到CPU的执行权,t1就要进来
            // 假设t2抢到CPU的执行权,t2就要进来,发现门是关着的,进不去。所以就等着。
            // 门(开,关)
            synchronized(lock)
                if(ticket>0)
                    try 
                        // 发现这里的代码将来是会被锁上的,所以t1进来后,就锁了。(关)
                        Thread.sleep(100);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                    System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
                
            
        
    

    public static void main(String[] args) 
        Ticket ticket = new Ticket();
        Thread thread1 = new Thread(ticket);
        thread1.setName("窗口1");
        Thread thread2 = new Thread(ticket);
        thread2.setName("窗口2");
        Thread thread3 = new Thread(ticket);
        thread3.setName("窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
    

 

当使用了同步代码块后,上述的线程的安全问题,解决了。

5.2同步方法

/**
 * 多线程编写套路
 *   1、线程 操作(方法) 资源类
 *   2、高内聚 低耦合
 */
public class Ticket implements Runnable
    //100张票
    private static int ticket = 100;
    //定义同一把锁
    private static Object lock = new Object();
    @Override
    public void run() 
       //模拟买票
        while (true)
           //同步方法
           method();
        
    

    /**
     * 同步方法 锁对象是this
     */
    private synchronized void method() 
        if(ticket>0)
            try 
                // 发现这里的代码将来是会被锁上的,所以t1进来后,就锁了。(关)
                Thread.sleep(100);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
        
    

    public static void main(String[] args) 
        Ticket ticket = new Ticket();
        Thread thread1 = new Thread(ticket);
        thread1.setName("窗口1");
        Thread thread2 = new Thread(ticket);
        thread2.setName("窗口2");
        Thread thread3 = new Thread(ticket);
        thread3.setName("窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
    

5.3静态同步方法: 在方法声明上加上static synchronized

public static synchronized void method()

可能会产生线程安全问题的代码

静态同步方法中的锁对象是 类名.class

 

以上是关于多线程基础总结的主要内容,如果未能解决你的问题,请参考以下文章

多线程基础总结

Java基础总结--多线程总结2

[Java] 多线程基础详细总结,附加详细实例

Java多线程基础总结

多线程基础知识总结

java 面试基础总结---多线程