JAVA入门——多线程

一、基本概念

并发和并行:

  • 并发:同一时刻,多个指令在CPU上交替运行
  • 并行:同一时刻,多个指令在CPU上同时运行

二、多线程的实现方式(重点)

1.继承Thread类

  • 要重写run方法,里面书写线程要执行的代码
  • 然后创建这个类的对象,调用start方法,就可以打开这个线程(注意别直接调用run方法,直接调用就相当于用对象的方法,不是线程)

2.实现Runnable接口

  • 也要重写run方法,里面书写要执行的代码
  • 创建实现了Runnable接口的对象,这个对象表示任务
  • 创建Thread类对象,把任务对象传入,调用start方法启动线程

测试类测试代码: 

//主函数代码
public class ThreadTest1 {

    public static void main(String[] args) {
        //创建任务对象
        MyThread1 m1 = new MyThread1();

        //创建线程对象
        Thread t1 = new Thread(m1);//任务传入
        Thread t2 = new Thread(m1);

        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();
    }

}

实现了Runnable接口的类的代码:

public class MyThread1 implements Runnable {
    
    @Override
    public void run() {
        //线程要执行的代码
        for (int i = 0; i < 50; i++) {
            //这里用不了getName方法,因为是在Thread类中的
            //所以我们要先获取当前线程对象
            System.out.println(Thread.currentThread().getName() + "114514");
        }
    }
}

3.利用Callable接口和Future接口实现

  • 这种方式能获得多线程运行的结果

步骤:

  • 创建一个类MyCallable实现Callable接口(注意这个接口有泛型)
  • 重写call方法(有返回值,表示多线程的运行结果)
  • 创建MyCallable对象(表示多线程的任务)
  • 创建FutureTask对象(管理多线程的运行结果)
  • 创建Thread类对象,启动多线程

4.三种方法的优缺点

  • 第一种:代码简单,但是可扩展性弱,不能再继承其他类,但是可以直接使用Thread类中的方法
  • 第二种和第三种:扩展性强,代码可能复杂一些,而且不能直接用Thread类中的方法

三、常见的成员方法

1.4个基本方法

public class ThreadTest1 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //1.获取线程名字,如果没设置,线程有默认名字
        System.out.println(t1.getName());
        //2.设置线程名字
            //构造方法中也能设置名字,但是注意是Thread中的构造方法,子类要用super调用这个方法
        t1.setName("线程四");
        //3.获取当前线程对象
        Thread t = Thread.currentThread();
        System.out.println(t);
        //4.睡眠()ms
        //时间到后,线程会自动醒来,继续往下运行
        System.out.println(114);
        Thread.sleep(1000);
        System.out.println(514);
        
    }

}

2.线程的优先级

线程的调度方法:

  • 抢占式调度:线程随机执行(JAVA中是这种)
    • 线程的优先级越高,越容易抢到
    • 默认优先级是5,最低是1,最高是10
  • 非抢占式调度:线程轮流执行

两个成员方法:

getPriority()                 获取优先级

setPriority()                 设置优先级

3.守护线程

成员方法:

setDeamon()        设置为守护线程

作用:

  • 当其他非守护线程结束后,守护线程也会陆续结束(不管有没有执行完,都会陆续结束,但注意这个结束需要时间)

4.出让线程和插入线程(用的很少)

yield()        出让线程,即让出CPU的占有

join()          插入线程,把调用这个方法的线程,插入到当前线程之前

                (当前线程就看写在哪)

四、同步代码块

  • 在操作共享数据(static)时,多个线程可能得到重复/超出的结果,这是因为在执行中,需要执行的语句未被完全执行,CPU执行权就被抢走了
  • 同步代码块就可以进行锁的操作,只有锁中的代码全部被执行完了,其他线程才能抢CPU

锁的特点:

  • 锁默认打开,有一个线程进入后,锁自动关闭
  • 锁内代码全部执行完毕,线程出来后,锁自动打开

格式:

synchronized(锁的对象){

        //代码

}

//锁对象是任意的,写Object都可以

//但是锁对象一定要是唯一的,即加static修饰

//我们可以把这个对象当成锁的钥匙来理解

//我们一般用当前类的字节码文件当做锁对象,即xxx.class

五、同步方法

  • 把synchronized关键字加到方法上
  • 特点:
    • 同步方法锁住方法里的所有代码
    • 锁的对象不能自己指定
      • 非静态方法:this
      • 静态方法:当前类的字节码文件

Javabean类: 

public class MyRun implements Runnable {
    int ticket = 0;

    @Override
    public void run() {
        while(true){
            if (extracted()) break;
        }
    }

    private synchronized boolean extracted() {
        if(ticket == 100){
            return true;
        }else{
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            ticket++;
            System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");

        }
        return false;
    }
}

测试类:

  • 因为我们采用多线程实现的第二种方式,是把一个任务传入多个线程,这个任务是确定唯一的,所以无需再声明为static了。(如果用第一种,创建三个线程对象,公共数据就要声明为static)
public class ThreadTest2 {

    public static void main(String[] args) {
        MyRun mr = new MyRun();
        //创建线程
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        Thread t3 = new Thread(mr);

        t1.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");

        t1.start();
        t2.start();
        t3.start();
    }
}

六、Lock锁(JDK5以后,能够手动上锁)

  • Lock是一个接口,创建对象时要用它的实现类ReenTrantLock
  • 一定要注意unlock的位置,如果在解锁前出现了return,break这类语句,可能导致一条线程一直独占(没解锁),解决方法之一是用try,catch包围,finally里面解锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyRun implements Runnable {
    int ticket = 0;

    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            lock.lock();
            try {
                if (extracted()) break;
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
    }

    private boolean extracted() {
        if(ticket == 100){
            return true;
        }else{
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            ticket++;
            System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");
        }
        return false;
    }
}

死锁:出现了锁的嵌套,容易出BUG,在用到synchronized和lock时都要注意

七、等待唤醒机制(生产者消费者模式)

  • 核心思想:线程分成消费者和生产者,通过第三者(用桌子比喻)来控制二者轮流执行

1.消费者:

  • 判断桌子上是否有数据
    • 如果没有就等待
    • 如果有就消费数据
  • 消费完成后,唤醒生产者继续生产

2.生产者:

  • 判断桌子上是否有数据
    • 如果有就等待
    • 如果没有就生产数据
  • 把数据放到桌子上
  • 唤醒消费者来消费

3.涉及的方法

wait()                当前线程等待,直到被唤醒

notify()              随机唤醒单个线程

notifyAll()          唤醒全部线程

4.一个简单例子:

生产者类:

public class Cook extends Thread{

    @Override
    public void run() {

        while(true){

           synchronized(Desk.lock){
               if(Desk.count == 0){
                   //消耗完所有数据
                   break;
               }else{
                   //核心业务逻辑
                   if(Desk.foodflag == 1){
                       //有食物,生产者等待
                       try {
                           Desk.lock.wait();
                       } catch (InterruptedException e) {
                           throw new RuntimeException(e);
                       }
                   }else{
                       //没有食物
                       System.out.println("厨师做了一碗面条");
                       Desk.foodflag = 1;
                       Desk.lock.notifyAll();
                   }
               }

           }
        }

    }
}

消费者类:

public class Foodie1 extends Thread{

    @Override
    public void run() {

        while(true){

            synchronized(Desk.lock){
                if(Desk.count == 0){
                    break;
                }else{

                    if(Desk.foodflag == 0){
                        //等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        //把当前线程和锁进行绑定
                        //这样唤醒时,会唤醒所有和锁对象相关的线程
                    }else{
                        //总数减少,消耗了一个数据
                        Desk.count--;
                        //如果有就吃
                        System.out.println("吃了,还剩" + Desk.count + "个");
                        //唤醒厨师
                        Desk.lock.notifyAll();
                        //修改桌子状态
                        Desk.foodflag = 0;
                    }

                }

            }

        }

    }
}

桌子类:

public class Desk {
    //控制生产者与消费者执行

    //0:没有 1:有
    public static int foodflag = 0;

    //当前总个数
    public static int count = 10;

    //锁对象
    public static final Object lock = new Object();

}

测试类:

public class ThreadTest3 {

    public static void main(String[] args) {
        Foodie1 f = new Foodie1();
        Cook c = new Cook();

        c.setName("生产者");
        f.setName("消费者");

        c.start();
        f.start();

    }

}

八、等待唤醒机制的阻塞队列式实现

阻塞队列的顶层接口:

Iterable

Collection

Queue

BlockingQueue

实现类:

ArrayBlockingQueue        底层是数组,有界

LinkedBlockingQueue      底层是链表,相对无界,但也不超过int最大值

小例子:

测试类:

public class ThreadTest4 {

    public static void main(String[] args) {
        ArrayBlockingQueue<String> q1 = new ArrayBlockingQueue<>(1);//这里要传上界

        Cook c = new Cook(q1);
        Foodie f = new Foodie(q1);

        c.start();
        f.start();

    }
}

生产者类:

public class Cook extends Thread{

    ArrayBlockingQueue<String> queue;

    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while(true){
            try {
                queue.put("面条");//底层有锁,不用自己写
                System.out.println("诶!给你整碗面吃");//注意锁在put里,而这条语句在锁外面,不过数据是安全的,只是我们打印出来看着不对劲
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }

    }
}

消费者类:

public class Foodie extends Thread{

    ArrayBlockingQueue<String> queue;

    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while(true){
            try {
                String food = queue.take();//底层有锁,不用自己写
                System.out.println(food);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
    }
}

九、初识线程池

  • 上面我们写的线程代码都是一次性的,每个线程用完就没用了,这样很浪费资源,线程池就能帮我们解决这个问题

原理:

  • 创建一个空的线程池
  • 提交任务时会创建新的线程对象,任务执行完毕,把线程还给池子,下次再提交任务时就还用这个线程
  • 如果提交任务时,池子中没有空的线程,那就创建新的
  • 如果没有空线程也无法创建,那就排队等待

代码实现:

1.创建线程池:工具类Executors

public static ExecutorService newCachedThreadPool();                  //没有上线

public static ExecutorService newFixedThreadPool(int nThreads);  //有上限

2.提交任务

submit() 

3.关闭线程池(服务器上24小时运行,所以一般不会关)

shutdown()

小例子:

public class MyThreadPool {

    public static void main(String[] args) {

        //1.获取线程池对象
        ExecutorService pool1 = Executors.newCachedThreadPool();


        //2.提交任务
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());
        pool1.submit(new MyRunnable());

        //3.销毁线程池
        pool1.shutdown();
    }
}

十、自定义线程池——创建ThreadPoolExecutor对象

重点考虑七个参数

  • 1.核心线程数量(不能小于0)
  • 2.线程池中最大线程数量
  • 3.空闲时间(值)
  • 4.空闲时间(单位)   
    • 要用TimeUnit指定
  • 5.阻塞队列(不能为null)
  • 6.创建线程的方式(不能为null)
  • 7.执行的任务过多时的解决方案(不能为null)
    • ThreadPoolExecutor的静态内部类AbortPolicy中

执行过程:

  • 任务加入线程池时,创建核心线程,当任务数比核心线程数大时,会进入阻塞队列来排队,当排队的任务达到阻塞队列最大值时,新的任务会进入临时线程来执行,当临时线程也满时,新的任务会被拒绝服务

小例子:

public class MyThreadPool2 {

    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3, //核心线程数;
                6, //最大线程数
                60, //60
                TimeUnit.SECONDS, //秒
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(), //默认线程创建
                new ThreadPoolExecutor.AbortPolicy()  //拒绝策略

        );
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值