【Java】深入理解Java线程

1 相关概念

并发:两个或多个事件在同一时间段内发生【多个任务交替执行】
并行:两个或多个事件在同一时刻发生【多个任务同时执行】
进程:进入内存的程序
内存:所有应用程序都要进入到内存中执行 临时存储RAM
线程:进程的一个执行单元,负责程序的执行
一个程序至少有一个进程,一个进程可以包含多个线程
CPU:中央处理器,对数据进行计算,指挥软件和硬件
单线程:CPU在多个线程之间做高速的切换,轮流执行多个线程,效率低
多线程:多个线程在多个任务之间做高速的切换,速度是单线程的多倍,多个线程之间互不影响

线程调度

  • 分时调度:所有线程轮流使用CPU,平均分配每个线程占用CPU的时间
  • 抢占式调度:优先让优先级高的线程使用CPU,如果优先级相同,则随机选择一个,Java中使用抢占式调度。

2 主线程

主线程:执行main方法的线程
主线程的过程:JVM执行main方法,main方法进入到栈内存,JVM会找操作系统开辟一条main方法的执行路径,CPU根据路径来执行main方法,这个路径就是主线程。
单线程:执行从main方法开始自上而下依次执行

public class Person {
    
    
    private String name;
    
    public Person(String name) {
    
    
        this.name = name;
    }
    
    public void run(){
    
    
        for (int i = 0; i < 3; i++) {
    
    
            System.out.println(name + " " + i);
        }
    }
}
public class MainThread {
    
    
    public static void main(String[] args) {
    
    
        Person p1 = new Person("张三");
        p1.run();
        Person p2 = new Person("李四");
        p2.run();
    }
}

在这里插入图片描述

3 创建多线程程序-法1

3.1 创建Thread类的子类

  1. 创建Thread类的子类
  2. 在子类中重写run方法,设置线程任务
  3. 创建子类对象
  4. 调用start方法,开始新的线程,执行run方法

main压栈执行后,在堆内存中创建线程子类对象,栈中保存对象地址。如果调用run方法,run方法压栈执行,则是单线程处理。如果调用start方法,会开辟新的栈空间执行run方法。CPU可以选择不同的栈空间。

start使线程开始执行,JVM调用线程的run方法,两个线程并发运行
(main线程)<----->(创建新线程执行run)

public class MyThread extends Thread {
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 5; i++) {
    
    
            System.out.println("run " + i);
        }
    }
}
public class MyThreadTest {
    
    
    public static void main(String[] args) {
    
    
        MyThread mt = new MyThread();
        mt.start();
        for (int i = 0; i < 5; i++) {
    
    
            System.out.println("main " + i);
        }
    }
}

同优先级下,随机抢占,谁抢到谁执行
在这里插入图片描述

3.2 Thread类常用方法

获取线程名称:getName()、Tread.currentTread()

Tread t = new Tread();
sout(t.getName());//名称
Tread t = Tread.currentTread();
sout(t);//名称

设置线程名称:setName()、构造方法参数传递线程名称

public class MyThreadTest {
    
    
    public static void main(String[] args) {
    
    
        MyThread mt1 = new MyThread("张三");
        mt1.start();
        MyThread mt2 = new MyThread();
        mt2.setName("李四");
        mt2.start();
    }
}
public class MyThread extends Thread {
    
    
    public MyThread(String name) {
    
    
        super(name);
    
    public MyThread() {
    
    
    }
    @Override
    public void run() {
    
    
        System.out.println(getName());
    }
}

线程休眠:sleep(long millis) 毫秒结束后程序继续执行

模拟秒表 示例:

public class MyThreadTest {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        for (int i = 1; i <= 60; i++) {
    
    
            System.out.println(i);
            Thread.sleep(1000);
        }
    }
}

4 创建多线程程序-法2【推荐使用】

4.1 创建Runnable实现类

  1. 创建Runanable接口的实现类
  2. 实现类中重写run方法,设置线程任务
  3. 创建实现类的对象
  4. 创建Thread类对象,构造方法中传递Runnale的实现类对象
  5. 调用Thread类中的start方法
public class RunnableImpl implements Runnable {
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 5; i++) {
    
    
            System.out.println(Thread.currentThread()+" "+ i);
        }
    }
}
public class MyThreadTest {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        RunnableImpl run = new RunnableImpl();
        Thread t = new Thread(run);
        t.start();
        for (int i = 0; i < 5; i++) {
    
    
            System.out.println(Thread.currentThread() + " " + i);
        }
    }
}

在这里插入图片描述

4.2 两种实现方法的区别

Runnable的优点

  • 避免了单继承的局限性,类继承了Thread类就不能继承其他类了,实现Runnable接口还可以实现其他接口。
  • 增强了程序的扩展性,降低了程序的耦合性(解耦)。把设置线程任务开启线程进行了分离。实现类中重写run方法来设置线程任务,创建Thread类对象调用start来开启新线程。想要什么任务,就传递什么实现类对象。

5 匿名内部类创建线程

匿名内部类:简化代码。
把1.子类继承父类 2.重写父类 3.创建子类对象 —> 一步完成
把1.实现实现类接口 2.重写接口方法 3.创建实现类对象 —> 一步完成

public class MyThreadTest {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        //Thread法
        new Thread(){
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 5; i++) {
    
    
                    System.out.println(Thread.currentThread() + " " + i);
                }
            }
        }.start();

        //Runnable法
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                for (int i = 0; i < 5; i++) {
    
    
                    System.out.println(Thread.currentThread() + " " + i);
                }
            }
        }).start();
    }
}

6 线程安全

共享资源产生安全问题

public class RunnableImpl implements Runnable {
    
    
    //共享票源
    private int tickets = 100;
    @Override
    public void run() {
    
    
   		//重复卖票
        while(true){
    
    
            if(tickets > 0){
    
    
                try {
    
    
                    Thread.sleep(10);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("卖第"+tickets+"张票");
                tickets--;
            }
        }
    }
}
public class MyThreadTest {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        RunnableImpl r = new RunnableImpl();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        Thread t3 = new Thread(r);
        t1.start();
        t2.start();
        t3.start();
    }
}

出现了重复的票

窗口Thread-2在卖第100张票
窗口Thread-0在卖第100张票
窗口Thread-1在卖第100张票
窗口Thread-1在卖第97张票
窗口Thread-2在卖第97张票
窗口Thread-0在卖第97张票
窗口Thread-0在卖第94张票
窗口Thread-2在卖第94张票

出现了不存在的票

窗口Thread-2在卖第0张票
窗口Thread-1在卖第-1张票

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

注意:访问共享数据的时候,无论是否时区CPU执行权,其他线程只能等待,等当前线程完全结束后,其他线程再继续。

7 同步技术的原理

使用了一个锁对象,这个对象叫同步锁,也叫对象监视器。多个线程一起抢夺CPU执行权,谁抢到了,谁执行run方法,遇到同步代码块。

此时,抢到CPU的当前线程T0会检查同步代码块是否有锁对象,如果有,则获取锁对象,进入到同步中进行。

另一进程T1抢到CPU后发现没有锁对象了,则进入阻塞状态,等待锁对象的归还,直到上一进程T0执行完同步代码块才归还锁对象T1进程才可以获取到锁对象,进入到同步中执行。

同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁对象,无法进入同步代码块。同步保证了只有一个线程再同步中执行共享数据,保证安全,但牺牲了效率。

7.1 同步方法

定义同步方法解决线程安全问题

  1. 把访问了共享数据的代码取出来,放到一个方法中
  2. 在方法上添加synchronized

同步方法也会锁住方法内部,只让一个线程执行,锁对象是实现类对象new RunnableImpl(),也就是this。

public class RunnableImpl implements Runnable {
    
    
    //共享票源
    private int tickets = 100;
    @Override
    public void run() {
    
    
        while(true){
    
    
            sell();
        }
    }
    public synchronized void sell(){
    
    
        if(tickets > 0){
    
    
            try {
    
    
                Thread.sleep(10);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            String name = Thread.currentThread().getName();
            System.out.println("窗口"+name+"在卖第"+tickets+"张票");
            tickets--;
        }
    }
}

7.2 静态同步方法

加static关键字,锁对象不是this,this是创建对象之后产生的,static优先于对象的创建,静态同步方法的锁对象是本类的class属性—>class文件对象(反射)

RunnableImpl.class

7.3 Lock锁

JDK1.5之后出现Lock接口,实现了synchronized方法和语句,可获得更广泛的锁操作。

  1. 在成员位置创建一个ReentrantLock对象
  2. 在可能会出现安全问题的代码前调用Lock接口的lock方法获取锁
  3. 在可能会出现安全问题的代码后调用Lock接口的unlock方法释放锁
public class RunnableImpl implements Runnable {
    
    
    //共享票源
    private static int tickets = 100;
    Lock lock = new ReentrantLock();
    @Override
    public void run() {
    
    
        while(true){
    
    
            lock.lock();
            if(tickets > 0){
    
    
                try {
    
    
                    Thread.sleep(10);
                    String name = Thread.currentThread().getName();
                    System.out.println("窗口"+name+"在卖第"+tickets+"张票");
                    tickets--;
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }finally {
    
    
                    lock.unlock();
                }
            }
        }
    }
}

8 线程状态

  • 新建状态:new
  • 运行状态:runnable
  • 阻塞状态:blocked
  • 死亡状态:terminated
  • 休眠状态:time_waiting【等待时间】
  • 永久等待:waiting【等待唤醒】

new —> start() + CPU —> runnable
new —> start() - CPU —> blocked
在这里插入图片描述

runnable —> stop() / run(over) —> terminated
runnable —> sleep / wait —> timed_waiting
在这里插入图片描述
timed_waiting —> time over - CPU—> blocked
timed_waiting —> time over + CPU—> runnable
在这里插入图片描述
runnable —> Object.wait() —> waiting
waiting —> Object.notify() + CPU —> runnable
waiting —> Object.notify() - CPU —> blocked

9 线程通信

9.1 等待唤醒案例

  1. 创建消费者线程:申请资源种类和数量,调用wait方法,放弃CPU,进入waiting状态。
  2. 创建生产者线程:产生资源之后,调用notify唤醒消费者。
  3. 两个线程必须被同步代码块包裹,确保只有一个在执行。
  4. 同步的锁对象必须唯一,只有锁对象可以调用wait和notify方法。
public class WaitAndNotify {
    
    
    public static void main(String[] args) {
    
    
    	//锁对象
        Object obj = new Object();
        //消费者
        new Thread(){
    
    
            @Override
            public void run() {
    
    
                while(true){
    
    
                    synchronized (obj){
    
    
                        System.out.println("申请资源");
                        try {
    
    
                            obj.wait();
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                    }
                    System.out.println("拿到资源");
                    System.out.println("------------");
                }
            }
        }.start();
        //生产者
        new Thread(){
    
    
            @Override
            public void run() {
    
    
                while (true){
    
    
                    try {
    
    
                        System.out.println("准备资源");
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    synchronized (obj){
    
    
                        obj.notify();
                        System.out.println("资源已备好");
                    }
                }
            }
        }.start();
    }
}

Object.wait(long m):无参数的wait需要等待notify唤醒,有参数的wait等到时间结束后,进入到runnable(有CPU)或者blocked(无CPU)状态,相当于sleep(long m),但如果时间结束前,notify被调用,则提前醒来。

Object.notifyAll():唤醒监视器上所有的线程。

9.2 生产者和消费者案例

  1. 包子
public class BaoZi {
    
    
    String pi;//包子皮
    String xian;//包子馅
    boolean flag = false;//是否有包子
}
  1. 包子铺
public class BaoZiPu extends Thread {
    
    
    private BaoZi bz;//锁对象

    public BaoZiPu(BaoZi bz){
    
    
        this.bz = bz;
    }

    @Override
    public void run() {
    
    
        int count = 0;
        //持续生产包子
        while(true){
    
    
            synchronized (bz){
    
    
                if(bz.flag == true){
    
    
                    try {
    
    
                        bz.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                //被唤醒后执行 包子铺生产包子
                if(count % 2 == 0){
    
    
                    bz.pi = "薄皮";
                    bz.xian = "三鲜";
                }else {
    
    
                    bz.pi = "厚皮";
                    bz.xian = "牛肉";
                }
                count++;
                System.out.println("包子铺正在生产:"+bz.pi+bz.xian+"包");
                try {
    
    
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                bz.flag = true;
                bz.notify();
                System.out.println(bz.pi+bz.xian+"包已生产好,吃货可以开始吃包子了");
            }
        }
    }
}
  1. 吃货
public class ChiHuo extends Thread {
    
    
    private BaoZi bz;
    public ChiHuo(BaoZi bz){
    
    
        this.bz = bz;
    }
    @Override
    public void run() {
    
    
        while(true){
    
    
            synchronized (bz){
    
    
                if(bz.flag == false){
    
    
                    try {
    
    
                        bz.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                System.out.println("吃货正在吃"+bz.pi+bz.xian+"包");
                //吃完包子
                bz.flag = false;
                //唤醒包子铺
                bz.notify();
                System.out.println("吃货已经吃完了"+bz.pi+bz.xian+"包,包子铺开始生产包子");
                System.out.println("===================================================");
            }
        }
    }
}
  1. 测试类
public class Test {
    
    
    public static void main(String[] args) {
    
    
        BaoZi bz = new BaoZi();
        BaoZiPu bzp = new BaoZiPu(bz);
        ChiHuo ch = new ChiHuo(bz);
        bzp.start();//生产包子
        ch.start();//吃包子
    }
}

10 线程池

10.1 概念

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

线程池的优点

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

10.2 线程池的使用

JDK1.5出现线程池的工厂类Executor用来生产线程池

Executors类的静态方法:

  • newFixedThreadPool(int nThread):创建可重用固定线程数的线程池,返回值是ExecutorService接口的实现类对象,使用ExecutorService接口接收【面向接口编程】

ExecutorService接口:

  • shutdown:关闭销毁线程池
  • submit(Runnable task):提交一个Runnable任务用于执行

使用步骤

  1. 使用工厂类Executors里面的静态方法newFixedThreadPool生产一个线程池
  2. 创建一个实现类,实现Runnable,重写run,设置线程任务
  3. 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法。
  4. 调用ExecutorService中的方法shutdown销毁线程池【不建议销毁线程池】
public class RunnableImpl implements Runnable {
    
    
    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName()+"创建了一个新的线程执行");
    }
}
public class ThreadPool {
    
    
    public static void main(String[] args) {
    
    
        ExecutorService es = Executors.newFixedThreadPool(2);
        es.submit(new RunnableImpl());
        es.submit(new RunnableImpl());
        es.submit(new RunnableImpl());
        es.submit(new RunnableImpl());
    }
}

pool-1-thread-2创建了一个新的线程执行
pool-1-thread-1创建了一个新的线程执行
pool-1-thread-2创建了一个新的线程执行
pool-1-thread-1创建了一个新的线程执行

猜你喜欢

转载自blog.csdn.net/xd963625627/article/details/105306243
今日推荐