细说多线程,如何解决线程安全问题

发布时间 2023-09-25 22:51:37作者: 从炼器到天人境

关于多线程,首先熟练分清楚线程和进程的关系:
进程:内存中正在运行的一个程序
线程:进程中的一个最小执行单元。一个进程最少得有一个线程(Java程序中一个请求就是一个线程)。
一、创建多线程

的方式有四种:
1.继承Thread类

1.定义一个子类继承Thread类,并重写run方法
2.创建Thread的子类对象
3.调用start方法启动线程(启动线程后,会自动执行run方法中的代码)
public class Test1 extends Thread {
    //2.必须重写Thread类的run方法
    @Override
    public void run() {
//        super.run();
        //描述线程的执行任务
        //任务体
        for (int i = 0; i <= 5; i++) {
            System.out.println("子线程MyThread输出:"+i);
        }
    }
 public static void main(String[] args) {
        //创建线程类的对象代表一个线程
        Test1 test1 = new Test1();
        //启动线程(自动执行run方法)
        test1.start();
        for (int i = 0; i <= 5; i++) {
            System.out.println("主线程MyThread输出:"+i);
        }
    }
}

2.实现Runable接口

public static void main(String[] args) {
        Test21 test21 = new Test21();
        new Thread(test21).start();
        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出 ===》" + i);
        }
        //匿名内部类
         // 1、直接创建Runnable接口的匿名内部类形式(任务对象)
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程1输出:" + i);
                }
            }
        };
        new Thread(target).start();

        // 简化形式1:
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程2输出:" + i);
                }
            }
        }).start();
    }

3.实现Callable接口

1.先定义一个Callable接口的实现类,重写call方法
2.创建Callable实现类的对象
3.创建FutureTask类的对象,将Callable对象传递给FutureTask
4.创建Thread对象,将Future对象传递给Thread
5.调用Thread的start()方法启动线程(启动后会自动执行call方法)
   等call()方法执行完之后,会自动将返回值结果封装到FutrueTask对象中
   
6.调用FutrueTask对的get()方法获取返回结果

public class MyCallable2 implements Callable<BigDecimal> {
    private int start1;
    private int end;
    public MyCallable2(int start1, int end) {
        this.start1 = start1;
        this.end = end;
    }
    @Override
    public BigDecimal call() throws Exception {
        BigDecimal sum= BigDecimal.valueOf(0);
        for (int i = start1; i <= end; i++) {
            sum=sum.add(BigDecimal.valueOf(i));
        }
        return sum;
    }
public static void main(String[] args) throws ExecutionException, InterruptedException {
        //3.创建一个Callable对象
        Callable<BigDecimal> c1 = new MyCallable2(1,2000);
        Callable<BigDecimal> c2 = new MyCallable2(2001,4000);
        //4.把Callable的对象封装成一个FutureTask对象(任务对象)
        //未来任务对象的作用?
        //4.1、是一个任务对象,实现了Runnable对象
        //4.2、可以在线程执行完毕之后,用未来任务调用get方法获取线程执行完毕后的结果。
        FutureTask<BigDecimal> bdft1 = new FutureTask<>(c1);
        FutureTask<BigDecimal> bdft2 = new FutureTask<>(c2);
        //5.把任务交给一个THread对象
        new Thread(bdft1).start();
        new Thread(bdft2).start();
        //6.获取线程执行完毕后返回的结果
        // 注意:如果执行到这儿,假如上面的线程还没有执行完毕
        // 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。
        BigDecimal b1 = bdft1.get();
        BigDecimal b2 = bdft2.get();

        String s = b1.add(b2).toString();
        System.out.println("线程计算的累加和"+s);

4.创建线程池
线程池的核心参数
核心线程数,最大线程数,临时线程的存活时间,阻塞队列,线程工厂,任务丢弃策略(拒绝策略);
拒绝策略分为四种:
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 (默认)
DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
CallerRunsPolicy:由调用线程处理该任务

 关于创建的线程池的工作执行流程,当有任务提交进来的时候,
(1)我们要判断线程池是否达到核心线程数最大了,没有达到,就利用核心线程来创建一个工作线程来执行任务。
(2)如果核心线程都在执行任务了,就判断工作队列是否都满了,没满则将新提交的任务存储在工作队列中。
(3)当队列满了,判断线程数是否达到最大线程,如果没有则创建新的线程来执行任务。
(4)如果达到最大线程数了,就执行拒绝策略。
(5)执行拒绝策略

线程状态

新建,就绪,运行,阻塞,死亡

图解释的很详细了。
***关于经常被问到的
1,线程池创建了,里面有线程吗?  没有
2、继承Thread类和实现Runnable接口,有什么区别?你一般选择哪个?为什么?
根据单一原则,选择实现Runable接口
3,实现Runable接口和实现Callable接口有什么区别?
Callable可以有返回结果

关于解决线程安全问题

在共享的环境中,线程往往是不安全的,解决多线程中的安全问题,我们一般就是加锁

Synchronized 在jvm层面,是关键字,出异常的时候会释放锁,不会出现死锁

不会手动释放锁,只能等同步代码块和方法结束的时候释放锁。

Lock 在API层面,是一个接口  ,出异常的时候不会释放锁,会出现死锁,需要在finally中手动释放,

可以调用api手动释放

 

 

//Lock加锁
public class Account {
    private String nameId;
    private double money;
//。。。构造器

//定义锁Lock
    private final Lock lk=new ReentrantLock();
    //对执行方法枷锁 用try-finally进行闭锁
    public void DrawMoney(double money){
        //Lock方法
        lk.lock();//加锁
        try {
            //谁来取钱
            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+"来取钱:余额不足");
            }
        } finally {
        //关锁
            lk.unlock();
        }
        }
// 。。get、set方法
}

Synchronized

同步方法

public synchronized void DrawMoney(double money){
        //同步方法 在方法中直接加,沈括乃日,对象方法。synchronized
            //谁来取钱
            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+"来取钱:余额不足");
            }
        }

同步代码块,锁是括号里面的对象【必须共享】

public  void DrawMoney(double money) {
//          synchronized (Account.class){//针对类
        synchronized (this) {
            //谁取钱
            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 + ",红包不足");
            }
        }
    }

同步代码块在底层的执行原理其实就是

使用 monitorenter 和 monitorexit  指令实现的,一个执行,一个释放

关于锁的定义