多线程【Java】【锁】

多线程

1.认识线程

什么是线程:线程是一个程序内部的一条执行流程,是操作系统能调度的最小单位,是进程的实际运作单位,一个进程可以包含多个线程

多线程:从软硬件上实现的多条执行流程的技术(多条线程由CPU复杂调度执行)

  • 多线程示例:
    在这里插入图片描述

2.创建线程

  • Thread

1.继承Thread类

  • 实现一个类继承Thread类
  • 重写Thread类的run()·方法
  • 创建该线程类的对象
  • 调用对象的start()方法启动一个线程
package com.itheima.demo1create;

public class test {
    public static void main(String[] args) {
        //3.创建线程类的对象:代表线程
        Thread myThread = new MyThread();
        //4.调用start()方法启动线程:还是调用run方法执行的
        myThread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程输出:" + i);
        }
    }
}
//1,定义一个子类继承Thread类
class MyThread extends Thread{
    //2.重写run方法,在run方法中编写线程的任务代码
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println("子线程输出:" + i);
        }
    }
}
  • 优点:编码简单
  • 缺点:继承了Thread类,由于单继承规则,无法再继承其他类
  • 注意事项:
    • 直接调用run方法会当成普通方法执行,此时还是相当于单线程执行
    • 只有调用start方法才是启动一个新的线程执行

2.实现Runnable接口

  • 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  • 创建MyRunnable对象
  • MyRunnable任务对象交给Thread线程对象处理
  • 调用线程对象的start()方法启动线程
package com.itheima.demo1create;

public class test1 {
    public static void main(String[] args) {
        //2.创建MyRunnable线程任务类对象
        MyRunnable r = new MyRunnable();
        //3.将线程任务类对象交给Thread线程对象处理
        Thread t1 = new Thread(r);
        //4.调用线程对象的start方法启动线程
        t1.start();

        //使用Runnable接口的匿名内部类来创建
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程2输出:" + i);
                }
            }
        };
        Thread t2 = new Thread(r2);

        //简化写法1:
        new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程3输出:" + i);
                }
            }
        }).start();

        //简化写法2:
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("子线程4输出:" + i);
            }
        }).start();




        for (int i = 0; i < 10; i++) {
            System.out.println("主线程输出:" + i);
        }
    }

}
//1.定义MyRunnable线程任务类,实现Runnable接口,重写run方法
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程1输出:" + i);
        };
    }
}
  • 优点:可扩展性强,实现该接口的同时还可以继承其他的类
  • 缺点:编程相对复杂,无法返回线程执行的结果

3.实现Callable接口,再用FutureTask进行包装

  • 1.实现Callable接口,并重写call方法,在call方法中执行返回结果的逻辑
  • 2.创建Callable接口实现类的对象,
  • 3.把Callable类型对象用FutureTask对象封装
  • 4.将FutureTask线程任务对象交给Thread线程对象
  • 5.调用线程对象的start()方法启动线程
  • 6.调用FutureTask对象的get()方法获取线程执行的返回结果
package com.itheima.demo1create;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class test2 {
    public static void main(String[] args) {
        //2.创建Callable接口实现类对象
        Callable<String> mc1 = new MyCallable(50);
        //3.把Callable类型的对象封装成FutureTask(线程任务对象)
        FutureTask<String> ft1 = new FutureTask<>(mc1);
        //4.将线程任务对象封装成线程对象
        Thread t1 = new Thread(ft1);
        //5.调用线程对象的start方法启动线程
        t1.start();

        Callable<String> mc2 = new MyCallable(100);
        FutureTask<String> ft2 = new FutureTask<>(mc2);
        Thread t2 = new Thread(ft2);
        t2.start();
        try {
            //如果主线程发现第一个线程还没有执行完毕,会让出CPU,等一个线程执行完毕后,才会往下执行
            System.out.println(ft1.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            //6.调用线程任务对象的get方法获取线程任务执行的结果
            System.out.println(ft2.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

//1.创建Callable接口的实现类,重写call方法,方法体写结果的处理方法,返回线程执行完毕需要返回的结果
class MyCallable implements Callable<String> {
    //创建成员变量
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "从1-" + n + "的结果是" + sum;
    }
}

  • 优点:可扩展性强,并且可以得到线程执行的结果
  • 缺点:编程相对复杂

三种创建方式的比较
在这里插入图片描述

3.线程的常用方法

在这里插入图片描述

  • 线程名字获取与设置:getName()setName
public class test {
    public static void main(String[] args) {
        Thread myThread = new MyThread("线程0");
        //调用Thread线程对象的getName方法获取线程名字
        System.out.println(myThread.getName());
        //调用线程对象的setName,给指定线程设置名字
        myThread.setName("线程1");
        System.out.println(myThread.getName());
        myThread.start();
        //哪个线程调用这个代码,这个代码就拿到哪个线程
        Thread m = Thread.currentThread();
        System.out.println(m.getName());
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程输出:" + i);
        }
    }
}
//1,定义一个子类继承Thread类
class MyThread extends Thread{
    //利用有参构造器给线程起名
    public MyThread(String name){
        super(name);
    }
    //2.重写run方法,在run方法中编写线程的任务代码
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println("子线程输出:" + i);
        }
    }
}
  • 线程睡眠:sleep()
    Thread.sleep(1000); 让当前执行的线程进入休眠状态,直到时间到了,才会继续执行

  • 线程插队:join()
    让调用的线程先执行完


4.线程安全

线程安全问题出现的原因:多个线程同时对一个共享的资源进行修改操作

1.案例模拟

两个人同时对一个账号进行取钱操作,使得账户余额变成负值

package com.itheima.threadsecuritydemo;

public class threadsecurity {
    public static void main(String[] args) {
        Account acc = new Account(123456, 100000.0);

        //小明和小红同时对一个共同账户对象操作
        Thread xm = new drawMoney("小明", acc);
        Thread xh = new drawMoney("小红", acc);

        //启动线程,执行取钱操作
        xm.start();
        xh.start();
    }
}

class drawMoney extends Thread{
    //将账户对象传给线程
    private Account acc;
    public drawMoney(String name, Account acc) {
        super(name);
        this.acc = acc;
    }
    @Override
    public void run(){
        //取钱
        acc.draw(100000);
    }
}

class Account {
    private int cardId;
    private double money;
    public Account(int cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }
    public void draw(double money) {
    	String name = Thread.currentThread().getName();
        if (this.money >= money) {
            System.out.println(name + "来取钱,吐出" + money + "!");
            this.money -= money;
            System.out.println(name + "取钱成功,还剩" + this.money + "余额!");
        } else {
            System.out.println(name + "取钱失败,余额不足!");
        }
    }

}

2.线程同步

线程同步是线程安全问题的解决方案*
思想

  • 让多个线程先后依次访问共享资源,从而避免线程安全问题
    常见方案
  • 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能继续加锁访问
1) 同步代码块
  • 把访问共享资源的核心代码块给上锁,以此保证线程安全
  • synchronized(同步锁) {
    访问共享资源的核心代码;
    }
  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁
package com.itheima.synchronized_code;

public class threadsecurity {
    public static void main(String[] args) {
        Account acc = new Account(123456, 100000.0);

        //小明和小红同时对一个共同账户对象操作
        Thread xm = new drawMoney("小明", acc);
        Thread xh = new drawMoney("小红", acc);

        //启动线程,执行取钱操作
        xm.start();
        xh.start();
    }
}

class drawMoney extends Thread{
    //将账户对象传给线程
    private Account acc;
    public drawMoney(String name, Account acc) {
        super(name);
        this.acc = acc;
    }
    @Override
    public void run(){
        //取钱
        acc.draw(100000);
    }
}

class Account {
    private int cardId;
    private double money;
    public Account(int cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }
    public  void draw(double money) {
        String name = Thread.currentThread().getName();
        //synchronized ("dlei") {   //使用唯一对象作为锁对象
        synchronized (this) {  //一般使用类对象作为锁对象
            if (this.money >= money) {
                System.out.println(name + "来取钱,吐出" + money + "!");
                this.money -= money;
                System.out.println(name + "取钱成功,还剩" + this.money + "余额!");
            } else {
                System.out.println(name + "余额不足,取钱失败!");
            }
        }
    }
}

扩展

  • 锁对象一般不选择唯一的对象,会影响其他无关线程的执行
  • 建议使用共享资源作为锁对象,对于实例方法使用this作为锁对象
  • 静态方法使用字节码(类名.class)作为锁对象:静态方法只有一份,是class文件所有的
2) 同步方法
  • 把访问共享资源的核心方法给上锁,以此保证线程安全
  • 修饰符 synchronized 返回值类型 方法名称(形参列表) {
    操作共享资源的代码
    }
  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁

package com.itheima.synchronized_code2;

public class threadsecurity {
    public static void main(String[] args) {
        Account acc = new Account(123456, 100000.0);

        //小明和小红同时对一个共同账户对象操作
        Thread xm = new drawMoney("小明", acc);
        Thread xh = new drawMoney("小红", acc);

        //启动线程,执行取钱操作
        xm.start();
        xh.start();
    }
}

class drawMoney extends Thread{
    //将账户对象传给线程
    private Account acc;
    public drawMoney(String name, Account acc) {
        super(name);
        this.acc = acc;
    }
    @Override
    public void run(){
        //取钱
        acc.draw(100000);
    }
}

class Account {
    private int cardId;
    private double money;
    public Account(int cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }
    public synchronized void draw(double money) {
        if (this.money >= money) {
            System.out.println(Thread.currentThread().getName() + "来取钱,吐出" + money + "!");
            this.money -= money;
            System.out.println(Thread.currentThread().getName() + "取钱成功,还剩" + this.money + "余额!");
        } else {
            System.out.println("余额不足,取钱失败!");
        }
    }
}

对比
同步代码块和同步方法:
举例:
锁整个方法,所有线程均要在方法外部排队,等加锁线程执行完在进入方法内部执行,
但是锁核心代码块,所有线程均可以先将处理核心共享资源之前的操作先执行完,等加锁线程执行完解锁之后立马能执行处理核心共享资源操作,性能会好一些。


3) lock锁

Lock锁是JDK5开始提供的一个新的锁定操作。
Lock是接口,不能直接实例化,可以采用它的实现了ReentrantLock来构建Lock锁对象
Lock lock = new ReentrantLock()

package com.itheima.synchronized_code2;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class threadsecurity {
    public static void main(String[] args) {
        Account acc = new Account(123456, 100000.0);

        //小明和小红同时对一个共同账户对象操作
        Thread xm = new drawMoney("小明", acc);
        Thread xh = new drawMoney("小红", acc);

        //启动线程,执行取钱操作
        xm.start();
        xh.start();
    }
}

class drawMoney extends Thread{
    //将账户对象传给线程
    private Account acc;
    public drawMoney(String name, Account acc) {
        super(name);
        this.acc = acc;
    }
    @Override
    public void run(){
        //取钱
        acc.draw(100000);
    }
}

class Account {
    private int cardId;
    private double money;
    private final Lock lock = new ReentrantLock();  //加final修饰防止锁被篡改
    public Account(int cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }
    public  void draw(double money) {
        String name = Thread.currentThread().getName();
        lock.lock(); //上锁
        try {
            if (this.money >= money) {
                System.out.println(name + "来取钱,吐出" + money + "!");
                this.money -= money;
                System.out.println(name + "取钱成功,还剩" + this.money + "余额!");
            } else {
                System.out.println(name + "余额不足,取钱失败!");
            }
        } finally {
            lock.unlock(); //解锁
        }
    }
}

5.线程池

1.认识线程池

线程池是一个可以复用线程的技术
不使用线程池会存在的问题:

  • 用户每发起一个请求就需要创建一个新线程来处理,创建新线程开销较大
  • 请求过多的话,会有多个线程同时执行的话,系统负载压力过大,严重影响系统性能
  • 形象举例:吃一顿饭扔一个碗,下一顿就需要花钱买新的碗

线程池工作原理:
在这里插入图片描述

2.创建线程池

  • 线程池接口: ExecutorService
方式1:通过ThreadPoolExecutor创建线程池

在这里插入图片描述

1.线程池创建:
package com.itheima.executorService;

import java.util.concurrent.*;

public class ExecutorServiceDemo1 {
    public static void main(String[] args) {

        //目标: 创建线程对象
        //1. 使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
        ExecutorService pool = new ThreadPoolExecutor(
                3,  //核心线程数量
                5,             //最大线程数量
                10,           //临时线程(最大线程数量-核心线程数量)的存活时间
                TimeUnit.SECONDS,  //指定临时线程存货的时间单位 SECONDS : 秒
                new ArrayBlockingQueue<>(3),  //任务队列: 最大排队的任务数量
                Executors.defaultThreadFactory(), //创建线程池的线程工厂
                new ThreadPoolExecutor.AbortPolicy()  //任务解决策略: 当核心线程与临时线程都被占用,且任务队列都排满了,再有线程进来时的处理办法
                );

        //2.使用线程池处理任务: 复用线程
        Runnable target = new MyRunnable();
        pool.execute(target);  //提交第1个任务  创建第1个线程, 自动启动线程处理这个任务
        pool.execute(target); //提交第2个任务  创建第2个线程, 自动启动线程处理这个任务
        pool.execute(target); //提交第3个任务  创建第3个线程, 自动启动线程处理这个任务
        //只要调用多少次execute就会执行几个任务,即使提交的任务对象是同一个

        //由于之前线程执行较快,执行完之后会复用核心线程去执行任务
        pool.execute(target); //复用线程
        pool.execute(target); //复用线程
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

2.处理Runnable任务

临时线程的创建时机:

  • 新任务提交时,发现核心线程都在忙,并且任务队列也满了,这时候会创建临时线程来缓解压力。

拒绝新任务的时机:

  • 核心线程和临时线程都被占用,并且任务队列也满了,新的任务再过来就会开始拒绝新任务。
  • 在这里插入图片描述
package com.itheima.executorService;

import java.util.concurrent.*;

public class ExecutorServiceDemo1 {
    public static void main(String[] args) {

        //目标: 创建线程对象
        //1. 使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
        ExecutorService pool = new ThreadPoolExecutor(
                3,  //核心线程数量
                5,             //最大线程数量
                10,           //临时线程(最大线程数量-核心线程数量)的存活时间
                TimeUnit.SECONDS,  //指定临时线程存货的时间单位 SECONDS : 秒
                new ArrayBlockingQueue<>(3),  //任务队列: 最大排队的任务数量
                Executors.defaultThreadFactory(), //创建线程池的线程工厂
                new ThreadPoolExecutor.AbortPolicy()  //任务解决策略: 当核心线程与临时线程都被占用,且任务队列都排满了,再有线程进来时的处理办法
                );

        //2.使用线程池处理任务
        Runnable target = new MyRunnable();
        pool.execute(target);  //提交第1个任务  创建第1个线程, 自动启动线程处理这个任务
        pool.execute(target); //提交第2个任务  创建第2个线程, 自动启动线程处理这个任务
        pool.execute(target); //提交第3个任务  创建第3个线程, 自动启动线程处理这个任务
        //只要调用多少次execute就会执行几个任务,即使提交的任务对象是同一个


        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        //核心线程都在忙,并且已经达到任务队列的最大等待数量,此时会创建临时线程进行新任务处理
        pool.execute(target);
        pool.execute(target); //仍然可以继续创建临时线程进行处理
        //此时核心线程与临时线程均被占用,并且任务队列已经排满,开始执行任务拒绝策略:new ThreadPoolExecutor.AbortPolicy(),打印出拒绝日志
        pool.execute(target);
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 0) {
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

ThreadPoolExecutor.CallerRunsPolicy():老板亲自服务
当核心线程和临时线程都被占用,并且任务队列满了,有新任务来时,主线程亲自执行。


3.处理Callable任务
  • 使用ExecutorService方法: Future<T> submit(Callable<T> command)
package com.itheima.executorService;

import java.util.concurrent.*;

public class ExecutorServiceDemo2 {
    public static void main(String[] args) {

        //目标: 创建线程对象
        //1. 使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
        ExecutorService pool = new ThreadPoolExecutor(
                3,  //核心线程数量
                5,             //最大线程数量
                10,           //临时线程(最大线程数量-核心线程数量)的存活时间
                TimeUnit.SECONDS,  //指定临时线程存货的时间单位 SECONDS : 秒
                new ArrayBlockingQueue<>(3),  //任务队列: 最大排队的任务数量
                Executors.defaultThreadFactory(), //创建线程池的线程工厂
                new ThreadPoolExecutor.AbortPolicy()  //任务解决策略: 当核心线程与临时线程都被占用,且任务队列都排满了,再有线程进来时的处理办法
        );

        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));

        try {
            System.out.println(f1.get());
            System.out.println(f2.get());
            System.out.println(f3.get());
            System.out.println(f4.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

//1.创建Callable接口的实现类,重写call方法,方法体写结果的处理方法,返回线程执行完毕需要返回的结果
class MyCallable implements Callable<String> {
    //创建成员变量
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "从1-" + n + "的结果是" + sum;
    }
}

方式2.通过Executors 创建线程池

在这里插入图片描述

package com.itheima.executorService;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class executors_demo {
    public static void main(String[] args) {

        //目标: 创建线程对象
        //1. 使用Executors的静态方法创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(3);

        //2.使用线程池处理任务:
        Future<String> f1 = pool.submit(new MyCallable2(100));
        Future<String> f2 = pool.submit(new MyCallable2(200));
        Future<String> f3 = pool.submit(new MyCallable2(300));
        Future<String> f4 = pool.submit(new MyCallable2(400));

        try {
            System.out.println(f1.get());
            System.out.println(f2.get());
            System.out.println(f3.get());
            System.out.println(f4.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
class MyCallable2 implements Callable<String> {
    private int n;
    public MyCallable2(int n) {
        this.n = n;
    }
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "从1-" + n + "的结果是" + sum;
    }
}

在这里插入图片描述


3.扩充

线程池的核心线程数量和最大线程数量一般如何配置:

  • CUP密集型任务(计算密集型):
    线程池大小通常设置为与系统可用处理器数量相等或者多一点: 线程处理数为16,那么则设置为16+1
  • IO密集型任务:
    线程等待IO操作时间较久,应设置更多线程来充分利用CPU等待的时间。
    最大线程数量 = 核心线程数量 * 2 //或者更高

6.并发和并行

进程:一个正在运行的程序或者软件就是一个独立的进程

  • 一个进程可以同时运行很多个进程

并发:进程中的线程是由CPU进行调度的,但CPU同时处理线程的能力有限,为了保证全部线程对能往前执行,CPU会轮询为系统的每个线程服务。由于CPU在线程之间的切换速度很快,让我们感觉这些线程在同时执行,这就是并发。
在这里插入图片描述

并行: 多个线程在同时执行。比如CPU的逻辑处理器(线程处理数量)是16,那么CPU同时可以操作16个线程,这16个线程在CPU统一执行的时候就称为并行。
在这里插入图片描述

多线程是并发和并行同时进行的


7.案例

100个员工抢200个红包雨
在这里插入图片描述

package com.itheima.executorService;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class redPacket {

    public static void main(String[] args) {
        List<Integer> redPacket = getRedPacket();

        for (int i = 1; i <= 100; i++) {
            new GetRedPacket(redPacket,  i + "").start();
        }
    }

    //准备两百个红包
    public static List<Integer> getRedPacket() {
        Random random = new Random();
        List<Integer> redPacket = new ArrayList<Integer>();
        for (int i = 1; i <= 160; i++) {
            redPacket.add(random.nextInt(30) + 1);
        }
        for (int i = 1; i <= 40; i++) {
            redPacket.add(random.nextInt(70) + 31);
        }
        return redPacket;
    }
}


class GetRedPacket extends Thread {
    private List<Integer> redPacket;
    public GetRedPacket(List<Integer> redPacket, String name) {
        super(name);
        this.redPacket = redPacket;
    }
    @Override
    public void run() {
        while (true) {
            //拿到锁的人进去抢红包
            synchronized (redPacket) {
                if (redPacket.size() == 0) {
                    break; //没有红包了,直接跳出
                }
                String name = Thread.currentThread().getName();
                int index = (int) (Math.random() * redPacket.size());
                Integer money = redPacket.remove(index);
                System.out.println(Thread.currentThread().getName() + "号员工抢到了: " + money);
                //如果是最后一个红包,那么活动结束
                if (redPacket.size() == 0) {
                    System.out.println("红包抢完了,活动结束!");
                    break;
                }
            }
            try {
                Thread.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值