(八)多线程

10、线程安全:

 为什么会出现线程安全的名词:

        目前为了提高运行效率,多线程的使用越来越广泛;

        但他也带来了许多问题,如果有多个线程,它们在一段时间内,并发访问堆区中的同一个变量(含写入操作),由于线程的速度很快,那么最终可能会出现数据和预期结果不符的情况,这种情况就是线程安全问题。

(1)概述:

        线程安全问题都是由全局变量及静态变量引起的

        若多个线程同时执行写操作,就很可能出现线程安全问题

(2)解决方法:

        线程同步技术

11、线程同步:
(1)概述:

        由关键字synchronized实现

(2)线程同步方式:

        利用锁机制同步代码块,同步方法

(3)同步代码块:
        <1>定义:

                利用锁机制实现共享资源的互斥访问;

                保证同步代码块中的操作不会被别的线程打断,要么不执行要么就全部执行完;

        <2>工作原理:锁机制

                ①获取锁:进入同步代码块之情,必须先获得锁对象的使用权

                ②互斥访问:同一时间,只有一个线程可以持有锁,其他线程必须等待锁释放

                ③释放锁:线程执行完代码块之后,自动释放锁,其他等待的线程可以争夺锁了

        <3>格式:

                在方法中写synchronized(锁对象){

                                        需要同步执行的操作;

                                   }

                锁对象可以是Object类或者还他的子类对象

                这里我们对象的选取,一般是所有线程公有的对象。

        <4>流程:

                抢占到共享资源后(上锁成功),线程才进入代码块,这时其他抢占失败的线程会进入blocked状态;

                同步代码块中的代码执行完毕或者遇到break,return,异常这种情况,都会自动释放锁,其他线程争抢到锁后会由阻塞转为运行中。

        <5>注意:

                要求所有的线程都参与线程同步,且锁对象一致时,才有用

                同步代码块不会阻止cpu切换进程,只是只要这个线程的同步代码块没执行完,即使时间片分配给别的线程,别的线程也不能进入代码块,只能执行其他的。

(4)同步方法:
        <1>定义:

                将整个方法都作为同步代码块处理。

                就是这个方法被调用了之后,只要没调用完,别的线程就不能调用

        <2>定义格式:

           普通方法:public synchronized void methodName(){

                                        //方法体

                             }

                             相当于:

                              public void methodName(){

                                        synchronized(this){

                                                //方法体

                                        }

                             }

           抽象方法:public static synchronized void methodName(){

                                        //方法体

                             }

                             相当于:

                              public void methodName(){

                                        synchronized(xxx.class){

                                                //方法体

                                        }

                             }

                             对于静态方法,默认使用定义该方法的类的class对象。

        <3>作用:

                由于方法的锁是自己本身的对象,因此这是线程对同一个对象的竞争,而不是方法;如果线程操作不同对象,那同步方法互不干扰。

                静态方法就是把对象换成类,只要是同一个类就要争夺。(不管是使用普通方法还是类调用静态,只要是这个类的,就要抢)。

class Printer {
    //普通同步方法: 锁对象默认为this
    public synchronized void print1() {
        System.out.print("天");
        System.out.print("天");
        System.out.print("向");
        System.out.print("上");
        System.out.println();
    }
    public void print2() {
        //同步代码块,也使用this作为锁对象
        //测试时,可以注释同步代码块,或使用其他锁对象,然后观察程序运行效果
        //synchronized (Printer.class) {
        synchronized (this) {
                System.out.print("努");
                System.out.print("力");
                System.out.print("学");
                System.out.print("习");
                System.out.println();
        }
    }
}

public class Test16_Funtion {
    public static void main(String[] args) {
        //准备一个对象
        final Printer p = new Printer();
        //创建子线程1,输出100次 "好好学习"
        Thread th1 = new Thread() {
            @Override
            public void run() {
                for(int i = 0; i < 100; i++)
                    p.print1();
            }
        };
        //创建子线程2,输出100次 "天天向上"
        Thread th2 = new Thread() {
            @Override
            public void run() {
                for(int i = 0; i < 100; i++)
                    p.print2();
            }
        };
        th1.start();
        th2.start();
    }
}

这样写保证了线程1抢到对象,调用方法时,线程2即使拿到时间片也无法使用对象,无法调用方法,这样就可以使“好好学习”这四个字永远是连在一起的。

12、线程通信:

出现通信线程的原因:

        只有线程同步,我们仅能保证线程在执行的过程中不被打断,但是我们无法保证线程执行的顺序。

        例如我们上面的“好好学习”后面我们不能保证是“好好学习”还是“天天向上”

(1)定义:

        多个线程一起执行,在默认情况下,cpu是随机切换进程的

        但是我们需要这些线程有顺序的协作完成任务,那么多线程之间就需要协调通信

(2)实现原理:等待唤醒机制

        wait()与notify()、notifyAll()

                这三个方法全都在Object类中的方法,必须由锁对象调用

                只有在同步代码块或者方法中才可以使用

                wait():是线程进入无限等待状态,等待锁对象调用notify将它唤醒

                notify():选取随机一个线程唤醒

                notifyAll():将所有执行了wait()方法的线程唤醒

                即使被唤醒也要争夺锁对象,挣不到就阻塞。

                状态由waiting变为runnable

                调用wait与notify的必须是同一个锁对象。

案例展示:这是一个双线程的:

//包子类
class Bum {
    //包子数量
    int num = 0;
    //包子存在标识
    boolean flag = false;
}

//生产者
class Producer extends Thread {
    private Bum bum;
    public Producer(String name, Bum bum) {
        super(name);
        this.bum = bum;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            //同步
            synchronized (bum){
                //根据flag判断包子是否存在,如果存在则 线程进行等待
                if(bum.flag){
                    try {
                        bum.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //生产包子
                System.out.println("第" + i + "次," + this.getName() + ": 开始生产包子...");
                bum.num++;
                System.out.println("生产完成,包子数量: " + bum.num + ",快来吃!");
                //生产完成,修改flag存在标识为true
                bum.flag = true;
                //通知 消费者线程吃包子
                bum.notify();
            }
        }
    }
}
//消费者
class Customer extends Thread {
    private Bum bum;
    public Customer(String name, Bum bum) {
        super(name);
        this.bum = bum;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            //同步
            synchronized (bum){
                //根据flag判断包子是否存在,如果不存在则线程等待
                if(bum.flag == false){
                    try {
                        bum.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("第" + i + "次," + this.getName() + " 开始吃包子...");
                bum.num--;
                System.out.println("消费完成,包子数量: " + bum.num + ",快生产吧!");
                //消费完成,修改flag存在标识为false
                bum.flag = false;
                //通知 消费者线程吃包子
                bum.notify();
            }
        }
    }
}

//测试类
public class Test17_TwoCommunication {
    public static void main(String[] args) {
        //准备共享对象
        Bum bum = new Bum();
        //生产者线程
        Thread th1 = new Producer("打工人",bum);
        //消费者线程
        Thread th2 = new Customer("吃货",bum);
        //启动线程
        th1.start();
        th2.start();
    }
}

案例展示:多线程案例:

//包子类
class Bum2 {
    // 包子数量
    int num = 0;
    // 线程执行标识: 0表示线程1执行 1表示线程2执行 2表示线程3执行
    int flag = 0;
}


//生产者
class Producer1 extends Thread {
    private Bum2 bum;
    public Producer1(String name, Bum2 bum) {
        super(name);
        this.bum = bum;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            // 同步
            synchronized (bum) {
                // 根据flag判断包子是否存在,如果存在则 线程进行等待
                // 注意,此处必须改为while,用if无法实现功能
                while (bum.flag != 0) {
                    try {
                        bum.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 生产包子
                System.out.println("第" + i + "次," + this.getName() + ": 开始生产包子...");
                // 每次生产2个包子
                bum.num += 2;
                System.out.println("生产完成,包子数量: " + bum.num + ",快来吃!");
                // 生产完成,修改flag存在标识为true
                bum.flag = 1;
                // 通知 其他所有线程转入运行    
                bum.notifyAll();
            }
        }
    }
}

//消费者线程
class Customer2 extends Thread {
    private Bum2 bum;
    public Customer2(String name, Bum2 bum) {
        super(name);
        this.bum = bum;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            // 同步
            synchronized (bum) {
                // 根据flag判断包子是否存在,如果不存在则线程等待
                // 注意,此处必须改为while,用if无法实现功能
                while (bum.flag != 1) {
                    try {
                        bum.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(this.getName() + " 开始吃包子...");
                bum.num--;
                System.out.println("消费完成,包子剩余数量: " + bum.num);
                // 消费完成,修改flag存在标识为false
                bum.flag = 2;
                // 通知 其他所有线程转入运行
                bum.notifyAll();
            }
        }
    }
}

class Customer3 extends Thread {
    private Bum2 bum;
    public Customer3(String name, Bum2 bum) {
        super(name);
        this.bum = bum;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            // 同步
            synchronized (bum) {
                // 根据flag判断包子是否存在,如果不存在则线程等待
                // 注意,此处必须改为while,用if无法实现功能
                while (bum.flag != 2) {
                    try {
                        bum.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(this.getName() + " 开始吃包子...");
                bum.num--;
                System.out.println("消费完成,包子剩余数量: " + bum.num);
                // 消费完成,修改flag存在标识为false
                bum.flag = 0;
                // 通知 其他所有线程转入运行
                bum.notifyAll();
            }
        }
    }
}

//测试类:    
public class Test17_MoreCommunication {
    public static void main(String[] args) {
        Bum2 bum = new Bum2();
        // 生产者线程
        Thread th1 = new Producer1("打工人", bum);
        // 消费者线程
        Thread th2 = new Customer2("1号吃货", bum);
        Thread th3 = new Customer3("2号吃货", bum);
        th1.start();
        th2.start();
        th3.start();
    }
}
13、死锁:

        简单的描述死锁就是:俩个线程t1t2t1拿着t2需要等待的锁不释放,而t2又拿着t1需要等待的锁不释放,俩个线程就这样一直僵持下去。

        是指两个或多个线程在执行的过程中,因争夺资源而陷入互相等待的状态,且没有任何一个线程可以主动释放资源,导致所有线程无法继续执行。

案例展示:

//结论:不要嵌套上锁(synchronized)
//容易造成死锁
public class Test18_DeadLock {
    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Thread th1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    synchronized (obj1) {
                        System.out.println("th1拿到左筷子");
                        synchronized (obj2) {
                            System.out.println("th1拿到右筷子");
                            System.out.println("th1吃 水盆羊肉");
                        }
                    }
                }
            }
        };
        Thread th2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    synchronized (obj2) {
                        System.out.println("th2拿到右筷子");
                        synchronized (obj1) {
                            System.out.println("th2拿到左筷子");
                            System.out.println("th2吃到了水盆羊肉");
                        }
                    }
                }
            }
        };
        th1.start();
        th2.start();
    }
}

建议不要随意嵌套上锁,但是当我们制定了合理的要求是,比如任何一个线程执行时都必须先拿到obj1才可以拿obj2,这样嵌套可以实现我们的功能,还防止了死锁的发生

案例展示:

public class Transfer {
     public boolean transfer(Account from, Account to, double amount) {
        // 验证转账金额是否有效
        if (amount <= 0) {
            System.out.println("转账金额必须大于0,转账失败");
            return false;
        }

        // 按账户名称字典序排序,确定加锁顺序,避免死锁
        Account firstLock = from;
        Account secondLock = to;
        
        if (from.getName().compareTo(to.getName()) > 0) {
            firstLock = to;
            secondLock = from;
        }

        // 按照排序后的顺序加锁
        synchronized (firstLock) {
            synchronized (secondLock) {
                // 验证转出账户余额是否充足
                if (from.getBalance() < amount) {
                    System.out.println(from.getName() + "余额不足,转账失败。余额:" + from.getBalance() + ",需转账:" + amount);
                    return false;
                }

                // 执行转账操作
                from.setBalance(from.getBalance() - amount);
                to.setBalance(to.getBalance() + amount);

                // 输出转账结果
                System.out.println(from.getName() + "向" + to.getName() + 
                                "转账" +amount + "元," + from.getName()
                                 +"余额:" + from.getBalance()+ "," + to.getName() 
                                 + "余额:" + to.getBalance());
                return true;
            }
        }
    }
    
}

上述例子是转账时,永远都先给名字排序靠前的人先上锁,这样不管是A给B转,还是B给A,线程都要先拿到A、B中名字排序靠前的人的锁,就不会出现,有人拿了A的锁,有人拿了B的锁,造成死锁。

14、线程池:
(1)概念:

        可以容纳多个线程的容器,其中的线程可以重复使用。

(2)优点:

                1. 降低资源消耗: 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

                2. 提高响应速度 :当任务到达时,任务可以不需要的等到线程创建就能立即执行。

                3. 提高线程的可管理性

(3)线程池的使用:

        Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。

        创建线程池的方法:

  • public static ExecutorService newFixedThreadPool(int nThreads)

        返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最 大数量)

        使用线程池对象方法:

  • public Future<?> submit(Runnable task)

        想线程池提交一个任务,并返回一个future对象进行跟踪

        Future接口:用来记录线程任务执行完毕后产生的结果

        future对象.get():阻塞当前进程直到任务完成

        future对象.cancel():任务未执行前取消

        future对象.isDone():判断是否执行完毕

        线程池操作步骤:

                1. 创建线程池对象(ExecutorService类对象)

                2. 创建Runnable接口子类对象

                3. 提交Runnable接口子类对象(借助submit方法实现)

15、callable类:用于创建线程(有兴趣的自己了解)
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐