多线程

1. 概念

  • 进程:当一个程序正在运行时,就是一个进程
  • 线程:进程中的一个执行单元,负责控制进程的执行
  • 线程分为用户线程和守护线程
    一个程序至少有一个进程,一个进程可以有多个线程

2. 普通方法调用和多线程的区别

  • 普通方法的执行
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200925063632419.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0RfTmt5,size_16,color_FFFFFF,t_70#pic_center
  • 多线程的执行
    在这里插入图片描述

3. 实现多线程的三种方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

3.1 Thread类

优点:编写简单,想要访问当前线程直接this就可以获取
缺点:线程类无法继承其他类

步骤:

  1. 自定义线程类继承Thread类
  2. 重写run()方法,编写线程执行体
  3. 创建线程对象,调用start()方法启动线程
//继承Thread类,重写run()方法,调用start()方法
public class TestThread extends Thread {
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i <10 ; i++) {
            System.out.println("学习");
        }
    }

    public static void main(String[] args) {
        //main线程,主线程
        TestThread testThread =new TestThread();
        testThread.start();

        for (int i = 0; i <200 ; i++) {
            System.out.println("写代码");
        }
    }

虽然在代码中是先让start()方法先执行的,但是输出的结果是main主线程先输出;
run()方法的输出结果穿插在中间,并且每次输出的位置不一样;
所以,线程开启时不一定会立即执行,由cpu调度执行

3.2 Runnable接口

优点:实现接口类,还可以继承其他类
缺点:编写稍复杂,如果想访问当前线程需要使用Thread.currentThread()方法

步骤:

  1. 实现Runnable接口
  2. 重写run()方法
  3. new Thread类,并把runnable接口实现类当作参数
  4. thread.start()启动线程
//Runnable接口,重写run()方法,执行线程需要丢入runnable接口实现类
public class MyRunnable implements Runnable {
    public void run() {
        //run线程
        for (int i = 0; i <10 ; i++) {
            System.out.println("学习");
        }
    }

    public static void main(String[] args) {
    	//创建runnable接口实现类对象
        MyRunnable myRunnable=new MyRunnable();
        //创建线程对象,通过线程对象来开启线程
        Thread thread =new Thread(myRunnable);
        thread.start();
        for (int i = 0; i <100 ; i++) {
            System.out.println("写代码");
        }
    }
}

3.3 Callable接口

与Runnable接口不同点:

  • Runnable的重写方法是run(),Callable重写方法是call()
  • Callable有返回值,Runnable没有
  • call方法可以抛异常

步骤:

  1. 实现Callable接口,需要定义泛型类型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(线程数量);
  5. 提交执行:Futrue<泛型类型> r1 = ser.submit(目标对象);
  6. 获取结果:boolean rs1 = r1.get();
  7. 关闭服务:ser.shutdownNow();

注:(ExecutorService是一个线程池接口,Executors有四种线程池)

public class MyCallable implements Callable<Boolean> {
    public MyCallable(String url,String file) {
        this.url=url;
        this.file=file;
    }

    String url;
    String file;
	//上面不用管,看下面的
	
	//call方法相当于Runnable接口中的run方法
    public Boolean call() throws Exception {
        WebDowload webDowload=new WebDowload(url,file);
        webDowload.dowloader();
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建目标对象
        MyCallable myCallable1 =new MyCallable("https://i0.hdslb.com/bfs/archive/622017dd4b0140432962d3ce0c6db99d77d2e937.png","D://1.png");
        MyCallable myCallable2 =new MyCallable("https://i0.hdslb.com/bfs/archive/622017dd4b0140432962d3ce0c6db99d77d2e937.png","D://2.png");
        MyCallable myCallable3 =new MyCallable("https://i0.hdslb.com/bfs/archive/622017dd4b0140432962d3ce0c6db99d77d2e937.png","D://3.png");

        //创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(3);

        //提交执行
        Future<Boolean> r1 = ser.submit(myCallable1);
        Future<Boolean> r2 = ser.submit(myCallable2);
        Future<Boolean> r3 = ser.submit(myCallable3);

        //获取结果
        boolean rs1 =r1.get();
        boolean rs2 =r2.get();
        boolean rs3 =r3.get();

        //关闭服务
        ser.shutdownNow();
    }
}

4. 线程状态

  1. 创建状态:生成线程对象,没有启动,这是创建状态

  2. 就绪状态:线程对象在start开启之后就处于就绪状态,如果线程对象从等待或者睡眠结束之后也会变成就绪状态

  3. 运行状态:线程调度程序将处于"就绪状态" 的线程对象设置为当前线程,此时状态就变成了运行状态,并运行run()中的代码

  4. 阻塞状态:线程在运行时被暂停,sleep、suspend、wait等方法都可以导致线程阻塞

  5. 死亡状态:线程在run()方法结束之后,或者调用stop方法之后就进入死亡状态

阻塞的5种状态:

1. 线程休眠: Thread.sleep(long millis)参数为毫秒数

  • sleep(时间)指定当前线程的毫秒数;
  • sleep存在异常InterruptedException;
  • sleep时间达到后线程进入就绪状态;
  • sleep可以模拟网络延时,倒计时等;
  • 每一个对象都有一个锁,sleep不会释放锁;

2. 线程礼让: Thread.yield();让别的线程先运行,让当前线程暂停从运行状态变成就绪状态,但不进入阻塞状态,礼让不一定成功

3. 合并线程: thread.join();Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞,可以想象成插队

4.改变线程优先级:getPriority();获取线程优先级,返回值为int,setPriority(int i);设置线程优先级,参数为int

线程优先级最大值为10,最小值为1,标准值为5;
优先级低,不代表不会被调用,只是调用的概率变低了,都得看cpu的调度;
(注意要先设置优先级,再start)

public class TestPriority {

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程1启动");
        },"线程1");
        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程2启动");
        },"线程2");
        
        //设置线程优先级
        thread1.setPriority(1);
        thread1.start();
        System.out.println(thread1.getPriority());
        thread2.setPriority(10);

        thread2.start();
        System.out.println(thread2.getPriority());
    }
}

5.将线程设置成守护线程:thread.setDaemon(Boolean on)设置线程为守护线程,参数为布尔类型,true为设置成守护

  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如,后台记录操作日志,内存监控,垃圾回收等

当用户线程结束之后,守护线程也会自动结束

4.1 观测线程状态

Thread.State state = thread.getState();创建观测状态
state=thread.getState();更新状态,进入下一种状态的时候再获取一次

public class TestState {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread( () -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("/");
        });
        //观察线程状态
        //New
        Thread.State state = thread.getState();//创建观测状态
        System.out.println(state);
        //启动线程
        thread.start();
        //Run
        state=thread.getState(); //更新状态
        System.out.println(state);

        while (state!=Thread.State.TERMINATED) {//只要线程不终止,就一直输出状态
        Thread.sleep(100);
        state=thread.getState();//更新状态
            //Terminated,线程终止
            System.out.println(state);
        }
    }
}

几种线程状态的输出结果:

  1. new: new Thread() 后处于新生状态,尚未启动
  2. Runnable: 在Java虚拟机中执行的线程处于此状态
  3. Blocked: 被阻塞,等待监视器锁定的线程处于此状态
  4. Waiting: 等待另一个线程执行特定动作的线程处于此状态
  5. Time_Waiting: 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
  6. Terminated: 退出的线程处于此状态

5. synchronized

使用线程同步可以保证安全性,但是会导致性能降低的问题

比如:

  • 一个线程持有锁会导致其他需要此锁的线程全部挂起;
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时
  • 如果优先级较高的线程等待一个优先级较低线程释放锁,会导致优先级倒置

synchronized可以使用在方法上和代码块中;

作用在方法上:

锁类的实例对象;

private synchronized void method(){}

锁类对象(即new多个实例对象,但他们仍然是属于同一个类依然会被锁住);

private static synchronized void method(){}

作用在代码块中:

锁类的实例对象;

synchronized(this){
}

锁类对象

synchronized(SynchronizeDemo.class){
}

锁任意的object实例对象,(公共资源)

String lock;
synchronized(lock){
} 

6. 死锁

两个(或多个)线程各自占有一些共享资源,并且互相等待对方线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题

产生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个线程使用
  • 请求与保持条件:一个线程因请求资源和阻塞时,对已获得的资源保持不放
  • 不剥夺条件:线程已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

7. ReentrantLock

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁

public class TestLock {
    public static void main(String[] args) {
        TestLock2 testLock2 =new TestLock2();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
    }
}

class TestLock2 implements Runnable{
    private int ticket = 10;
    //获取锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        try {
            //加锁
            lock.lock();
            Thread.sleep(1000);
            while (ticket>=0){
                System.out.println(ticket--);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }
}

8. synchronized与Lock的对比

  • Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的拓展性(提供更多的子类)
  • 优先使用顺序
    • Lock > 同步代码块(已经进入了方法体,分配了相应资源 )> 同步方法(在方法体之外)

9. 线程通信

并发协作模型“生产者 / 消费者模式”
必须在synchronized修饰的方法中

利用缓冲区解决(管程法):
生产者发现仓库满了,则自己wait(),同时唤醒消费者notify(),如果仓库空了则唤醒自己;
消费者发现仓库空了,则自己wait(),同时唤醒生产者notify(),如果仓库满了则唤醒自己;

/**
 * 生产者消费者问题,创建缓冲区,管程法
 * */
public class TestPC {
    public static void main(String[] args) {
        SynContainer container=new SynContainer();
        Producer producer = new Producer(container);
        Consumer consumer = new Consumer(container);
        new Thread(producer).start();
        new Thread(consumer).start();
    }

}

//生产者
class Producer implements Runnable{
    //拿到缓冲区;
    SynContainer container;
    
    Producer(SynContainer container){
        this.container=container;
    }

    @Override
    public void run() {
        for (int i = 0; i <=100 ; i++) {
            //传入第i个产品
            try {
                //循环放入产品,并把id传进产品
                container.push(new Product(i));
                System.out.println("生产了第"+i+"个产品");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//消费者
class Consumer implements Runnable{
    //拿到缓冲区;
    SynContainer container;

    Consumer(SynContainer container){
        this.container=container;
    }

    @Override
    public void run() {
        for (int i = 0; i <=100 ; i++) {
            //获取第i个产品
            try {
                //获取产品类的id
                System.out.println("消费了第"+container.take().id+"个产品");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//产品
class Product{
    //产品id
    int id;

    public Product(int id) {
        this.id = id;
    }
}

//缓冲区
class SynContainer{
    //创建容器
    Product product ;
    Product[] products =new Product[100];
    int count=0;

    //生产者放入产品
    public synchronized void push(Product product) throws InterruptedException {
        //如果容器满了,通知消费者拿取产品
        if(count==products.length){
            //通知消费者拿取产品,生产者等待
            this.wait();
        }
        //如果容器未满,生产产品放入容器
        products[count]=product;
        count++;
        this.notify();

    }
    
    //消费者拿取产品
    public synchronized Product take() throws InterruptedException {
        //如果容器没产品,通知生产者生产
        if (count==0){
            //消费者等待,生产者生产
            this.wait();
        }
        //如果有产品,拿取产品
        this.notify();
        count--;
        Product product=products[count];
        return product;
    }
}

信号灯法: 创建一个布尔类型的标志位flag,如果为true,就让他等待、如果为false,就让他去通知另外一个人、把两人衔接起来


10. 线程池

为什么使用: 频繁的创建和销毁使用量大的线程,对性能影响很大

好处:

  • 提高响应速度,减少了创建新线程的时间
  • 降低资源消耗,重复利用线程池中的线程,不需要每次创建
  • 便于线程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

10.1 使用线程池

  • JDK5.0提供了线程池相关的API:ExecutorServiceExecutors
  • ExecutorService: 真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute (Runnable command)Reunnable的线程池执行方法,没有返回值
    • <T> Future<T> submit (Callable<T> task)Callable接口的线程
    • void shutdown()关闭线程池
  • **Executors:**工具类、线程池的工厂类,用于创建并返回不同类型的线程池
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值