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版权协议,转载请附上原文出处链接和本声明。