Java线程的概念与使用

  • Post author:
  • Post category:java




1 多线程

我们在之前, 学习的程序在没有跳转的前提下, 都是由上至下一次执行, 那么现在想要设计一个程序 , 边打游戏边听歌, 怎么设计, 要解决上述问题, 就得使用多进程或者多线程解决



并发与并行


  • 并发

    : 指两个或多个事件在同一个时间段内发生

  • 并行

    : 指两个或多个事件在同一时刻发生

在操作系统中, 安装了多个程序, 并发指的是在一段时间内宏观上有多个程序同时运行; 这在单CPU系统中, 每一个时刻只能有一个程序运行, 即微观上这些程序是分时的交替运行, 只不过给人的感觉是同时运行, 那是因为交替运行的时间是非常短的

而在多个CPU系统中, 则这些可以并发执行的程序便可以分配到多个处理器上(CPU), 实现多任务的并行执行, 即利用每个处理器来处理一个可以并发执行的程序, 这样多个程序便可以同时运行, ,目前电脑市场说的多核CPU, 便是多核处理器, 核越多, 并行处理的程序越多, 能大大的提高电脑运行的效率

注意 : 单核处理器的计算机是不能并行的处理多个任务的, 只能是多个任务在单个CPU并发执行; 同理, 线程也是一样的, 从宏观角度上理解线程是并行运行的, 但是从微观角度上分析确实串行执行的, 即一个线程一个线程的去运行, 当系统只有一个CPU时, 线程会以某种顺序执行多个线程, 我们把这种情况称之为线程的调度



线程与进程


进程

: 是指一个内存中运行的应用程序 , 每个进程都有一个独立的内存空间, 每个应用程序可以同时运行多个进程, 进程也是程序的一次执行过程, 是系统运行程序的基本单位; 系统运行一个程序即是一个进程从创建, 运行 到消亡的过程


线程

: 线程是进程中的一个执行单元, 负责当前进程中程序的执行, 一个进程中至少有一个线程; 一个进程中是可以有多个线程的, 这个应用程序也可以称之为多线程程序

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


线程调度

:

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

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

设置线程的优先级 : 右键 –> 设置优先级越高 –> 越先执行

抢占式调度 :

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

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

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



创建线程类

Java使用

java.lang.Thread

类代表线程, 所有的线程对象都必须是Thread类或其子类的实例; 每个线程的作用是完成一定的任务, 实际上就是执行一段程序流即一段顺序执行的代码; Java使用线程执行体来代表这段程序流

Java中通过继承Thread类创建并启动多线程的步骤如下:

  1. 定义Thread类的子类, 并重写该类的run方法, 该run()方法的方法体就代表了线程需要完成的任务, 因此把run() 方法称为线程执行体
  2. 创建Thread子类的实例, 即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程, 去执行run()
// 自定义线程类
public class MyThread extends Thread{

	@Override
	public void run(){
		for (int i = 0; i < 20; i++) {
			System.out.println("run: "+i);
		}
	}
}


// 测试类
public class Demo01Thread {
	public static void main(String[] args) {
		MyThread mt = new MyThread();
		mt.start();

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

// 打印的结果mian()与 run()交替执行

多线程执行的方法, 他们处于

栈的不同内存空间, 互不影响



Thread类

Thread类中有一些常用的方法


构造方法


  • public Thread()

    : 分配一个新的线程对象

  • public Thread(String name)

    : 分配一个指定名字的线程对象

  • public Thread(Runnable target)

    : 分配一个带有指定目标的新线程对象

  • public Thread(Runnable target,String name)

    :分配一个带有指定目标新的线程对象并指定名字


常用方法


  • public Strinf getName()

    : 获取当前线程的名称


  • public void star()

    : 导致该线程开始运行 ,JVM调用此线程的run方法


  • public void run()

    : 此线程要执行的任务再此处定义代码


  • public static void sleep(long millis)

    : 使当前正在执行的线程以指定的毫秒数暂停(暂停停止执行)


  • public static Thread currentThread()

    : 返回对当前正在执行的线程对象的引用



创建线程的第二种方法

实现线程可以通过Thread类的子类, 还可以通过Runnable接口实现线程

实现步骤如下:

  1. 创建一个Runnable接口的实现类
  2. 在实现类中重写Runnable接口的run方法, 设置线程任务
  3. 创建一个Runnable接口的实现类对象
  4. 创建一个Thread类对象, 构造方法中传递Runnable接口实现的类对象
  5. 调用Thread类中的start方法, 开启新的线程执行run方法
// Runnbale类的实现类
public class MyRunnable implements Runnable{
	
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println("run" +i);
		}
	}
}


// 测试类
public class Demo01Runnable {
	public static void main(String[] args) {

		// Runnable实现类的对象
		Runnable r = new MyRunnable();

		// 创建Thread类对象, 把Runnable实现类的对象作为参数传递到Thread类的go偶在方法中
		Thread td = new Thread(r);
		td.start();

		for (int i = 0; i < 20; i++) {
			System.out.println(Thread.currentThread().getName()+
					"-->"+i);
		}
	}
}



Thread和Runnable的区别

如果一个类继承Thread, 则不适合资源共享; 但是如果实现了Runnbale接口的话, 则很容易的实现资源共享

实现Runnable接口比继承Thread类所具有的优势

  1. 适合多个相同的程序代码的线程去共享同一个资源
  2. 可以避免Java中的单继承的局限性
  3. 增加程序的健壮性, 实现解耦操作, 代码可以被多个线程共享, 代码和线程独立
  4. 线程池只能放入实现Runnable或者Callable类的线程, 不能直接放入基础Thread的类
  5. 在java中, 每次程序运行至少启动两个线程, 一个是main线程, 一个垃圾收集线程; 因为每当使用java命令执行一个类的时候, 实际上都会启动一个JVM, 每一个JVM其实就是在操作系统中启动了一个进程



匿名内部类实现线程的创建

实现线程的匿名内部类的方式 , 可以方便的实现每个线程执行不同的线程任务操作; 使用匿名内部类的方式实现Runnable接口, 重写Runnable接口中的run()方法

public class NoNameInnerClassThread{
    public static void main(String[] args){
        
        Runnable r = new Runnable(){
            public void run(){
                for(int i=0;i<20;i++){
                    System.out.println("name"+i);
                }
            }
        };
        new Thread(r).start();
        for (int i = 0; i < 20; i++) {
         System.out.println("费玉清:"+i);
     }
        
    }
}



2 线程安全

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

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

线程同步 : 当我们使用多个线程访问同一资源的时候, 且多个线程中对资源有写的操作, 就容易出现线程安全问题; 要解决上述多线程并发访问一个资源的安全性问题; 也就是解决重复票与不存在票问题, Java中提供了**同步机制(synchronized)**来解决, 包括如下

  1. 同步代码块
  2. 同步方法
  3. 锁机制


同步代码块

synchronized 关键字可以用于方法中的某个区块中, 表示只对这个区块的资源实行互斥访问

格式 : syschronized(同步锁)(需要同步的代码)

同步锁 : 对象的同步锁只是一个概念, 可以想象为在对象上标记了一个锁

  1. 锁对象可以是任意类型
  2. 多个线程对象要使用同一把锁
  3. 在任何时候, 最多只允许一个线程拥有同步锁, 谁拿到锁就进入代码块, 其他的线程只能在外等着(BLOCKED)


同步方法

使用 synchronized 关键字修饰的方法, 就叫做同步方法, 保证A线程执行该方法的时候, 其他的线程只能在外面等着

格式 : public synchronized void method(){ 可能会产生安全问题的代码 }

public class Ticket implements Runnable{
    private int ticket = 100;
    
    // 买票
    @Override
    public void run(){
        while(true){
        // 每个买票的窗口要永远开启
            sellTicket();
        }
    }
    
    // 锁对象, 谁调用这个方法, 谁就隐含锁对象 , 就是this
    public synchronized void sellTicket(){
        if(ticket>0){
            try{
                Thread.sleep(100);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            
            // 获取当前线程对象的名字
            String name = Thread.currentThread().getName();
            System.out.printl(name +"正在卖"+ ticket--);
        }
    }
}

// 频繁的释放锁, 可能会降低程序的效率


锁机制


java.util.concurrent.locks.ReentranLock implements Lock

机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有, 除此之外更强大, 更体现面向对象

Lock锁也称为同步锁, 加锁与释放锁方法化了


  • public void lock()

    : 加同步锁

  • public void unlock()

    : 释放同步锁
public class Ticket implements Runnable{
    private int ticket = 100;
    Lock lock = new ReentrantLock();
    
    @Override
    public void run() {
        while(true){
            lock.lock();
            if(ticket>0){
                try{
                    Thread.sleeep(50);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }finally{
                    lock.unlock();
                }
            }            
    }
}



线程状态剖析

线程状态分析图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zLWfwWWk-1589289456436)(assets/线程状态剖析图.png)]



3 等待唤醒机制



线程间通信

概念 : 多个线程在处理同一个资源, 但是处理的动作 (线程的任务) 却不相同

比如 : 线程A用来生成包子的, 线程B用来吃包子的, 包子可以理解为同一资源; 线程A与线程B处理的动作, 一个是生成, 一个是消费; 那么线程A与线程B之间就存在线程通信问题


为什么要处理线程间通信

多个线程并发执行时, 在默认情况下CPU是随时切换线程的, 当我们需要多个线程来共同完成一件任务, 并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信, 以此来帮我们达到多线程共同操作一份数据


如何保证让线程通信有效有效利用资源

多个线程在处理同一个资源, 并且任务不同时, 需要线程通信来帮助解决线程之间对同一个变量的使用或操作; 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺; 也就是我们需要通过一定的手段使各个线程能有效的利用资源; 而这种手段 —— 等待唤醒机制



等待唤醒机制


什么是等待唤醒机制

这是多个线程间的一种协作机制; 谈到线程我们经常想到的是线程间的竞争, 比如去争夺锁, 但这并不是故事的全部, 线程间也会有协作机制, 就好比在公司里你和你的同事们, 你们可能存在晋升时的竞争, 但更多时候你们是一起合作完成某些任务

就是在一个线程进行了规定操作后, 就进入等待状态wait(), 等待其他线程执行完他们的指定代码过后, 再将其唤醒 notify ; 在有多个线程进行等待时, 入股偶需要, 可以使用 notifyAll() 唤醒所有等待的线程

wait/notify 是线程间的一种协作机制


等待唤醒中的方法

等待唤醒机制中的方法就是用于解决线程间的通信的问题, 使用的三个方法的含义如下:

  1. wait() : 线程不再活动, 不再参与调度, 进入wait set中, 因此不会浪费CPU资源了, 也不会去竞争锁了, 这时的线程状态即是

    WAITING

    , 它还要等着别的线程去执行一个

    特别的动作

    , 也即是

    通知(notify)

    , 在这个对象上等待的线程从 wait set 中释放出来, 重新进入到调度队列(ready queue) 中,
  2. notify : 则选取通知对象的wait set 中的一个线程释放; 例如, 餐馆有空位置后, 等候就餐最久的顾客最先入座
  3. notifyAll : 则释放通知对象的 wait set 上的全部线程

注意 :

哪怕只通知了一个等待的线程, 被通知的线程也不能立即恢复执行, 因为它当初中断的地方是在同步代码块内, 而此刻它已经不持有锁, 所以它再次尝试去获取锁(很有可能面临其他线程的竞争), 成功后才能在当初调用 wait 方法之后的地方恢复执行

总结如下:

如果能获取锁, 线程就从

WAITING

状态变成

RUNNABLE

状态; 否则, 从wait set 出来, 又进去 entry set, 线程就从

WAITING

状态又变成

BLOCKED

状态


调用wait和notify方法需要注意的细节

  1. wait方法与notify方法必须要有同一个锁对象调用; 因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用 wait方法后的线程
  2. wait方法与notify方法是属于Object类的方法, 因为 锁对象可以是任意对象, 而任意对象的所属类都是继承了Object类的
  3. wait方法与 notify方法必须要在同步代码块或者是同步函数中使用; 因为必须要通过锁对象调用这两个方法



生产者与消费者的问题

就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:

包子铺线程生成包子,吃货线程消费包子

当没包子时(包子状态为false), 吃货线程等待, 包子铺线程生成包子(包子状态为true),并通知吃货线程(接触吃货的等待状态), 因为已经有包子了, 那么包子铺线程进入到等待状态; 接下来, 吃货线程能否进一步执行取决于锁的获取情况, 如果吃货取到锁, 那么执行吃包子动作, 包子吃完(包子状态为false), 并通知包子铺线程(解除包子铺的等待状态), 吃货线程进入等待, 包子铺线程能否进一步执行取决于 锁 的获取情况

分析图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AwD2wmg8-1589289456441)(assets/包子问题分析图.png)]

代码演示 :

// 包子铺资源类
public class BaoZi{
    String pier;
    String xianer;
    boolean flag = fasle;
}


// 吃货线程类
public class ChiHuo extends Thread{
    private BaoZi bz;
    
    // 有参构造
    public ChiHuo(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }
    
    @Override
    public void run(){
        while(true){
            synchronized (bz){
                if(ba.flag == fasle){// 没包子
                    try{
                        bz.wait()
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                }
                System.out.println("吃货正在吃"+bz.pier+bz.xianer+"包子");
                bz.flag = false;
                ba.notify();
            }
        }
    }
}


// 包子铺线程类
public class BaoZiPu extends Thread{
    private BaoZi bz;
    
    public BaoZiPu(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }
    
    @Override
    public void run(){
        int count = 0;
        // 做包子
        while(true){
            // 同步
            synchronized(bz){
                if (bz.flasg == true){
                    // 包子 存在
                    try{
                        bz.wait();
                    }catch(InterruptedException e){
                         e.printStackTrace();
                    }
                }
                
                // 没有包子, 做包子
                System.out.println(""包子铺开始做包子)
                if(count%2 == 0){
                     // 冰皮  五仁
                    bz.pier = "冰皮";
                    bz.xianer = "五仁";
                }else{
                    // 薄皮  牛肉大葱
                    bz.pier = "薄皮";
                    bz.xianer = "牛肉大葱";
                }
                count++;
                
                ba.flag = true;
                System.out.println("包子造好了:"+bz.pier+bz.xianer);
                System.out.println("吃货来吃吧");
                //唤醒等待线程 (吃货)
                bz.notify();
            }
        }
    }
}


// 测试类
public class Demo{
    public static void main(String[] args){
        // 等待唤醒案例
        BaoZi bz = new BaoZi();
        
        ChiHuo ch = new ChiHuo("吃货,bz);
        BaoZiPu bzp = new BaoZiPu("包子铺",bz)
        
       ch.start();
       bzp.start()
    }
}



4 线程池



线程池思想抛出

我们在使用线程的时候就去创建一个线程, 这样实现起来很方便, 但是就会有一个问题

如果并发的线程数量很多, 并且每个线程都是执行一个事件很短的任务就结束了, 这样频繁创建线程就会大大降低系统的效率, 因为频繁创建线程和销毁线程需要时间

那么有没有一种办法使得线程可以复用, 就是执行完一个任务, 并不被销毁, 而是继续执行其他的任务

在Java中可以通过线程池来达到这样的效果, 今天就来详细讲解一下Java的线程池



线程池的概念


线程池

: 其实就是一个容纳多个线程的容器, 其中的线程可以反复使用, 省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源

由于线程池有很多操作都是与优化资源相关的


合理使用线程池的好处

  1. 降低资源消耗, 减少了创建和销毁线程的次数, 每个工作线程都可以被重复利用, 可执行多个任务
  2. 提高响应速度, 当任务达到时, 任务可以不需的等到线程创建就能立即执行
  3. 提高线程的可管理性, 可根据系统的承受能力 调整线程池中工作线程的数目, 防止因为小号过多的内存, 二八服务器累趴下(每个线程需要大约1MB内存, 线程开的越多, 消耗的内存也就越大, 最后死机)



线程池的使用

Java里面线程池的顶级接口是

java.util.concurrent.Executor

, 但是严格意义上讲

Excetuor

并不是一个线程池, 而只是一个执行线程的工具, 真正的线程池接口是

java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的, 尤其是对于线程池的原理不是很清楚的情况下, 很有可能配置的线程池不是较优的, 因此在

java.util.concurrent.Executors

线程工厂类里面提供了一些静态工厂, 生成一些常用的线程池, 官方建议使用Executors工程类来创建线程池对象

Executors类中有个创建线程池的方法如下 :


public static ExecutorService newFixedThreadPool(int nThreads)

: 返回线程池对象(创建的是有界线程池, 也就是池中的线程个数可以指定最大数量)

获取到一个线程池ExecutorService对象, 那么怎么使用呢, 在这里定义了一个使用线程池对象的方法如下 :


public Future<?> submit(Runnable task)

: 获取线程池中的某一个线程对象, 并执行

Future接口 : 用来记录线程任务执行完毕后产生的结果, 线程池创建与使用

使用线程池中线程对象的步骤

  1. 创建线程池对象
  2. 创建Runnable接口类子类对象 (task)
  3. 提交Runnable接口类子类对象 (take task)
  4. 关闭线程池
// Runnable实现类代码
public class MyRunnable implements Runnable{
    @Override
    public void run(){
        System.out.println("我要一个教练");
        try{
            Thread.sleep(2000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("教练来了 :"+Thread.currentThread().getName());
        System.out.println("教我游泳完, 教练回到了游泳池");
    }
}


// 线程池测试类
public class ThreadPoolDemo{
    public static void main(String[] args){
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2)  // 创建两个线程对象
        // 创建Runnable实例对象
        MyRunnable r = new MyRunnable();
        
        // 自己创建线程对象的方式
        // Thread t = new Thread(r); // r MyRunnable中的接口类的子类对象
        // t.start();  // 调用MyRunnable中的run()
        
        // 从线程中获取线程对象, 然后调用MyRunable中的run()
        service.submit(r);
        // 再从线程中获取线程对象, 调用MyRunable中的run()
        service.submit(r);
        // 注意 : submit方法调用结束之后, 程序并不终止, 是因为线程池控制了线程的关闭
        // 将使用完的线程又归还到线程池中
        // service.shutdown()
        
    }
}

hreadPool(2) // 创建两个线程对象

// 创建Runnable实例对象

MyRunnable r = new MyRunnable();

    // 自己创建线程对象的方式
    // Thread t = new Thread(r); // r MyRunnable中的接口类的子类对象
    // t.start();  // 调用MyRunnable中的run()
    
    // 从线程中获取线程对象, 然后调用MyRunable中的run()
    service.submit(r);
    // 再从线程中获取线程对象, 调用MyRunable中的run()
    service.submit(r);
    // 注意 : submit方法调用结束之后, 程序并不终止, 是因为线程池控制了线程的关闭
    // 将使用完的线程又归还到线程池中
    // service.shutdown()
    
}

}




版权声明:本文为weixin_42269739原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。