zoukankan      html  css  js  c++  java
  • 【Java多线程 34】

    一、线程的死锁问题

    • 不同的线程分别占用对方需要的同步资源不放,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
    • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

      解决方法

    • 专门的算法、原则
    • 尽量减少同步资源的定义
    • 尽量避免嵌套同步
    package com.csii.day03;
    
    /**
     * @Author wufq
     * @Date 2020/12/22 10:55
     * 死锁演示
     */
    
    class A{
    
        public synchronized void foo(B b){
            System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入A实例的foo方法");
    
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("当前线程名:"+Thread.currentThread().getName()+"企图调用B实例的bar方法");
    
            b.last();
        }
    
        public synchronized void last(){
            System.out.println("进入了A类的内部last方法");
        }
    }
    
    
    
    
    class B{
    
        public synchronized void bar(A a){
            System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入B实例的bar方法");
    
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("当前线程名:"+Thread.currentThread().getName()+"企图调用A实例的foo方法");
    
            a.last();
        }
    
    
        public synchronized void last(){
            System.out.println("进入了B类的内部last方法");
        }
    
    }
    
    
    public class BeadLock implements Runnable{
    
        A a = new A();
        B b = new B();
    
        public void init(){
            Thread.currentThread().setName("主线程");
            a.foo(b);
            System.out.println("进入主线程之后");
        }
    
        public void run(){
            Thread.currentThread().setName("副线程");
            b.bar(a);
            System.out.println("进入副线程之后");
        }
    
        public static void main(String[] args){
    
            BeadLock bl = new BeadLock();
            new Thread(bl).start();
            bl.init();
    
    
        }
    }

    二、解决线程安全问题的方式三:lock锁  ----   JDK5.0新增

    • 从JDK5.0开始,Java提供了更强大的线程同步机制---通过显式定义同步锁对象来实现同步。同步锁使用lock对象充当
    • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
    • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁。
    package com.csii.day03;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * @Author wufq
     * @Date 2020/12/22 15:14
     * lock锁解决线程安全问题
     */
    
    
    
    class Window5 implements Runnable{
    
        private int ticket = 100;
    
        private ReentrantLock lock = new ReentrantLock();
        @Override
        public void run() {
            while (true){
                try {
                    //调用lock()方法,获取同步监视器
                    lock.lock();
    
                    Thread.sleep(100);
                    if(ticket>0){
                        System.out.println(Thread.currentThread().getName()+"票号:"+ticket);
                        ticket --;
                    }else {
                        break;
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
    
                    //调用解锁方法
                    lock.unlock();
                }
            }
    
        }
    }
    
    
    public class LockTest {
        public static void main(String[] args){
    
            Window5 w5=new Window5();
            for(int i=0;i<3;i++) {
                Thread tt= new Thread(w5);
                tt.setName("窗口"+i+" ");
    
                tt.start();
            }
    
    
        }
    
    }

    synchronized与Lock的对比

    1、Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放

    2、Lock只有代码块锁,synchronized有代码块锁和方法锁

    3、使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

    优先使用顺序:

    Lock >  同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)

    三、练习1

    银行有一个账户

    有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

    问题:该程序是否有安全问题,如果有,如何解决?

    【提示】

    1、明确哪些代码是多线程运行代码,须写入run()方法

    2、明确什么是共享数据

    3、明确多线程运行代码中哪些语句是操作共享数据的

    package com.csii.day03;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * @Author wufq
     * @Date 2020/12/22 17:20
     * 银行有一个账户
        有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
    
        分析:
        是否涉及到多线程? 两个储户线程
        是否有共享数据?  账户或者余额是共享数据
        是否存在线程安全问题? 有线程安全问题
        如何解决?(同步机制:三种方法)
     */
    
    
    //账户类
    class Account{
    
        //定义账户
        private double balance;
    
        private ReentrantLock lock = new ReentrantLock();
    
        public Account(double balance) {
            this.balance = balance;
        }
    
        //amt是存的钱数
        public void deposit(double amt) {
            try {
    
                //调用锁定方式lock()
                lock.lock();
                if(amt>0){
    
                    balance += amt;
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:"+balance);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //调用解锁方式unlock()
                lock.unlock();
            }
    
        }
    }
    
    //储户
    class Customer implements Runnable{
    
        private Account acc;
    
        public Customer(Account acc) {
            this.acc = acc;
        }
    
        @Override
        public void run() {
    
            for(int i=0;i<3;i++) {
                acc.deposit(1000);
            }
        }
    }
    
    
    
    public class AccountTest {
        public static void main(String[] args){
    
            Account acc = new Account(0);
            Customer c1 = new Customer(acc);
            Customer c2 = new Customer(acc);
    
            Thread t1 = new Thread(c1);
            t1.setName("甲");
    
            Thread t2 = new Thread(c2);
            t2.setName("乙");
    
            t1.start();
            t2.start();
    
        }
    
    }
    
    ====执行结果====
    甲存钱成功,余额为:1000.0
    甲存钱成功,余额为:2000.0
    甲存钱成功,余额为:3000.0
    乙存钱成功,余额为:4000.0
    乙存钱成功,余额为:5000.0
    乙存钱成功,余额为:6000.0

    四、线程的通信

    1、

     涉及到的三个方法:
    wait():一旦执行此方法,当前线程就进入阻塞状态
    ,并释放同步监视器
    notify():一旦执行此方法,就会唤醒被wait的一个线程
    如果多个线程被wait,就唤醒优先级高的那个
    totifuAll():一旦执行此方法,就会唤醒被wait的所有线程


    注意点:
    1、wait(),notify(),notifyAll()三个方法
    必须使用在同步代码块或同步方法中
    2、wait(),notify(),notifyAll()三个方法的调用者必须
    是同步代码块或者同步方法中的同步监视器,否则,
    会出现IlllegalMonitorStateException异常
    3、wait(),notify(),notifyAll()三个方法是定义在
    java.lang.Object类中

    package com.csii.day04;
    
    /**
     * @Author wufq
     * @Date 2020/12/24 10:35
     *
     * 线程通信:使用两个线程打印1-100,线程1,线程2 交替打印
     */
    
    class Num implements Runnable{
    
        private int number = 1;
    
        /*
        * 1、wait方法和notify方法其实前面是有this对象的,这就表明同步监视器,wait方法,notify方法是有被对象调用的,并且是谁调用对象就是谁
        * 2、这里把同步监视器(锁)设置成obj的对象,而wait和notify方法依然采用this方法,执行后会报IllegalMonitorStateException异常
        * 但是wait和notyfy方法都设置成obj对象,是可以运行成功的,这就表明:wait、notyfy、notyfyAll三个方法的调用必须是同步监视器内
        * 方法,否则会报异常
        *
        * */
        Object obj = new Object();
    
        @Override
        public void run() {
    
    //        synchronized (this) {
            synchronized (obj) {
                while (true){
    
                    //一旦执行此方法,就会唤醒被wait的一个线程,如果多个线程被wait,就唤醒优先级高的那个
    //                this.notify();
                    obj.notify();
                    try {
                        Thread.sleep(100);
                        if(number<=100) {
                            System.out.println(Thread.currentThread().getName() + ":" + number);
                            number++;
    
                            //一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
    //                       this.wait();
                            obj.wait();
                        }else {
                            break;
                        }
                    } catch (Exception e) {
                            e.printStackTrace();
                    }
                    }
                }
            }
    
        }
    
    
    public class Communication {
    
        public static void main(String[] args){
            Num num =new Num();
    
            Thread t1 = new Thread(num);
            Thread t2 = new Thread(num);
    
            t1.setName("线程1:");
            t2.setName("线程2:");
    
            t1.start();
            t2.start();
    
        }
    
    }
    
    =====执行结果=====
    线程1::1
    线程2::2
    线程1::3
    线程2::4
    线程1::5
    线程2::6

    2、sleep和wait方法的异同

    相同点:一旦执行方法,都可以使的当前的线程进入阻塞状态

    不同点:

    1)、两个方法声明的位置不同:Thread类中声明sleep()方法,Object类中声明wait()方法

    2)、调用的要求不同:sleep可以在任意需要的场景下调用,wait必须使用在同步代码块或者同步方法中

    3)、关于释放同步监视器:如果两个方法都使用在同步代码块或者同步方法中,sleep()不释放,wait()会释放   --即一旦使用了wait必须使用notify进行唤醒被阻塞的线程

    3、线程通信的金典例题:生产者和消费者的问题

    生产者(productor)将产品交给店员(clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者视图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了在通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了在通知消费者来取走产品。

    这里可能出现两个问题:

    1)、生产者比消费者快时,消费者会漏掉一些数据没有收到

    2)、消费者比生产者快时,消费者会取相同的数据

    package com.csii.day04;
    
    /**
     * @Author wufq
     * @Date 2020/12/25 16:20
     *
     */
    
    
    //共享数据
    class Clerk{
    
        private int productCount = 0;
        //生产的产品
        public synchronized void produceProduct() throws InterruptedException {
    
                if(productCount <20){
    
                    productCount++;
                    System.out.println(Thread.currentThread().getName()+"开始生产第"+productCount+"个");
    
                    notify();
    
                }
                else {
                    wait();
                }
        }
    
        //消费的产品
        public synchronized void customerProduct() throws InterruptedException {
    
    
                if(productCount >0){
                    System.out.println(Thread.currentThread().getName()+"开始消费第"+productCount+"个");
                    productCount--;
    
                    notify();
                }else {
                    wait();
                }
        }
    }
    
    //生产者和消费者是两个多线程
    class Producer implements Runnable{
    
        private Clerk clerk;
    
        public Producer(Clerk clerk) {
            this.clerk = clerk;
        }
    
        @Override
        public void run() {
    
            System.out.println(Thread.currentThread().getName()+"开始生产产品...");
    
            while (true){
    
                try {
                    Thread.sleep(100);
                    clerk.produceProduct();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            }
    
        }
    }
    
    class Customer implements Runnable{
    
        private Clerk clerk;
    
        public Customer(Clerk clerk) {
            this.clerk = clerk;
        }
    
        @Override
        public void run() {
    
            System.out.println(Thread.currentThread().getName()+"开始消费产品...");
    
            while (true){
    
                try {
                    Thread.sleep(200);
                    clerk.customerProduct();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            }
    
        }
    }
    
    
    public class ProductTest {
        public static void main(String[] args){
    
            Clerk clerk = new Clerk();
    
            Producer p1 = new Producer(clerk);
            Customer c1 = new Customer(clerk);
    
            Thread t1 = new Thread(p1);
            Thread t2 = new Thread(c1);
    
            t1.setName("生产者");
            t2.setName("消费者");
    
            t1.start();
            t2.start();
        }
    }

    五、JDK5.0新增线程创建方式

    新增方式一:实现Callable接口

    1、与使用Runable相比,Callable功能更强大些

    • 相比run()方法,可以有返回值
    • 方法可以抛出异常
    • 支持泛型的返回值
    • 需要借助FutureTask类,比如获取返回结果

    2、Future接口

    • 可以对具体Runable、Callable任务的执行结果进行取消,查询是否完成,获取结果等
    • FutureTask是Future接口的唯一实现类
    • FutureTask同时实现了Runable,Future接口。它即可以作为Runable被线程执行,又可以作为Future得到Callable的返回值
    package com.csii.day04;
    
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    /**
     * @Author wufq
     * @Date 2020/12/28 11:29
     */
    
    //1、创建一个实现Callable接口的实现类
    class NumTest implements Callable<Integer>{
    
    //    2、实现call方法,将此线程需要执行的操作声明在call()中
    
        /*
        * 实现20以内偶数的和
        * */
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            for(int i=0;i<20;i++){
                if(i%2==0){
                    System.out.println(i);
                    sum += i ;
                }
            }
    
            return sum;
        }
    }
    
    
    public class Test001 {
        public static void main(String[] args){
    //    3、创建Callable实现类的对象
            NumTest numTest = new NumTest();
    
    //    4、将此Callable实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
            FutureTask<Integer> task = new FutureTask<>(numTest);
    
    //    5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法
            new Thread(task).start();
    
            try {
    //    6、如果需要返回call方法的值,需要调用get方法来获取
                Integer sum = task.get();  // get()方法返回值即为FutureTask构造器Callable
                             实现类重写call()的返回值
    System.out.println(
    "总和:"+sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } ====执行结果==== 0 2 4 6 8 10 12 14 16 18 总和:90

    如何理解实现Callable接口的方式创建多线程比实现
    Runable接口创建多线程方式强大?
    1、call()可以有返回值
    2、call() 可以跑出异常,被外面的操作铺货,
    获取异常的信息
    3、Callable是支持泛型的

    新增方式二:使用线程池

    |-- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大

    |-- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具

    |-- 好处:

     1)提高响应速度(减少了创建新线程的时间)

    2)降低资源消耗(重复利用线程池中线程,不需要每次都创建)

    3)便于线程管理:

      corePoolSize:核心池的大小

      maximumPoolSIze:最大线程数

      keepAliveTime:线程没有任务时最多保持多长时间后会终止

    4)JDK5.0提供了线程池相关API:ExecutorService和Executors

    5)ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

       --> void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runable

     --> <T>Futuure<T>submit(Callable<T>task):执行任务,有返回值,一般用来执行Callable

     --> void shutdown():关闭连接池

    6)Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

     --> Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池

       --> Executors.newFixedThreadPool(n);:创建一个可重用固定线程数的线程池(常用的一个普通的线程池)

     --> Executors.newSingleThreadPool():创建一个只有一个线程的线程池

     --> Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

    package com.csii.day04;
    
    import java.util.concurrent.*;
    
    /**
     * @Author wufq
     * @Date 2020/12/28 16:33
     * 创建线程的方式四:使用线程池
     */
    class NumberTest implements Runnable{
    
        @Override
        public void run() {
            int sum = 0;
    
            for(int i=0;i<20;i++){
                if(i%2 == 0){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                    sum += i;
                }
            }
    
            System.out.println("偶数和:"+sum);
    
        }
    }
    
    class NumberTest2 implements Runnable{
    
        @Override
        public void run() {
            int sum = 0;
    
    
            for(int i=0;i<10;i++){
                if(i%2 == 0){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                    sum += i;
                }
            }
            
            System.out.println("偶数和:"+sum);
        }
    }
    
    class NumberTest1 implements Callable<Integer>{
    
        @Override
        public Integer call() throws Exception {
    
            int sum = 0;
    
            for(int i=0;i<20;i++){
                if(i%2 != 0){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                    sum += i;
                }
            }
    
            return sum;
        }
    }
    
    
    public class ThreadPool {
    
        public static void main(String[] args){
    
    //        1、提供指定线程数量的线程池
            ExecutorService service = Executors.newFixedThreadPool(10);
    
            /*
            * 验证ExecutorService接口常用的子类
            * */
            System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor
    
    
    //        2、执行指定的线程的操作,需要提供实现Runable接口或Callable接口实现类的对象
            service.execute(new NumberTest());
    
            service.execute(new NumberTest2());
    
            FutureTask<Integer> task = new FutureTask<>(new NumberTest1());
    
            service.submit(task);
    
            try {
                /*
                * ExecutorService类的两个常用方法:
                * 1、execute用来执行Runable接口 -->没有返回值
                * 2、submit用来执行Callable接口 -->有返回值
                * */
                Integer sum = task.get();
                System.out.println("奇数和:"+sum);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
    
    //        3、关闭连接池
            service.shutdown();
        }
    }
    =====执行结果====
    
    class java.util.concurrent.ThreadPoolExecutor
    pool-1-thread-1:0
    pool-1-thread-1:2
    pool-1-thread-1:4
    pool-1-thread-1:6
    pool-1-thread-1:8
    pool-1-thread-1:10
    pool-1-thread-1:12
    pool-1-thread-1:14
    pool-1-thread-1:16
    pool-1-thread-1:18
    偶数和:90
    pool-1-thread-2:0
    pool-1-thread-2:2
    pool-1-thread-2:4
    pool-1-thread-2:6
    pool-1-thread-2:8
    偶数和:20
    pool-1-thread-3:1
    pool-1-thread-3:3
    pool-1-thread-3:5
    pool-1-thread-3:7
    pool-1-thread-3:9
    pool-1-thread-3:11
    pool-1-thread-3:13
    pool-1-thread-3:15
    pool-1-thread-3:17
    pool-1-thread-3:19
    奇数和:100
    package com.csii.day04;
    
    import java.util.concurrent.*;
    
    /**
     * @Author wufq
     * @Date 2020/12/28 16:33
     * 创建线程的方式四:使用线程池
     */
    class NumberTest implements Runnable{
    
        @Override
        public void run() {
            int sum = 0;
    
            for(int i=0;i<20;i++){
                if(i%2 == 0){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                    sum += i;
                }
            }
    
            System.out.println("偶数和:"+sum);
    
        }
    }
    
    class NumberTest2 implements Runnable{
    
        @Override
        public void run() {
            int sum = 0;
    
    
            for(int i=0;i<10;i++){
                if(i%2 == 0){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                    sum += i;
                }
            }
    
            System.out.println("偶数和:"+sum);
        }
    }
    
    class NumberTest1 implements Callable<Integer>{
    
        @Override
        public Integer call() throws Exception {
    
            int sum = 0;
    
            for(int i=0;i<20;i++){
                if(i%2 != 0){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                    sum += i;
                }
            }
    
            return sum;
        }
    }
    
    
    public class ThreadPool {
    
        public static void main(String[] args){
    
    //        1、提供指定线程数量的线程池
            ExecutorService service = Executors.newFixedThreadPool(2);
    
            /*
            * 验证ExecutorService接口常用的子类
            * */
            System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor
    
    
    //        2、执行指定的线程的操作,需要提供实现Runable接口或Callable接口实现类的对象
            service.execute(new NumberTest());
    
            service.execute(new NumberTest2());
    
            FutureTask<Integer> task = new FutureTask<>(new NumberTest1());
    
            service.submit(task);
    
            try {
                /*
                * ExecutorService类的两个常用方法:
                * 1、execute用来执行Runable接口 -->没有返回值
                * 2、submit用来执行Callable接口 -->有返回值
                * */
                Integer sum = task.get();
                System.out.println("奇数和:"+sum);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
    
    //        3、关闭连接池
            service.shutdown();
        }
    }
    ====执行结果====
    class java.util.concurrent.ThreadPoolExecutor
    pool-1-thread-1:0
    pool-1-thread-1:2
    pool-1-thread-1:4
    pool-1-thread-1:6
    pool-1-thread-1:8
    pool-1-thread-1:10
    pool-1-thread-1:12
    pool-1-thread-1:14
    pool-1-thread-1:16
    pool-1-thread-1:18
    偶数和:90
    pool-1-thread-2:0
    pool-1-thread-2:2
    pool-1-thread-2:4
    pool-1-thread-2:6
    pool-1-thread-2:8
    偶数和:20
    pool-1-thread-1:1
    pool-1-thread-1:3
    pool-1-thread-1:5
    pool-1-thread-1:7
    pool-1-thread-1:9
    pool-1-thread-1:11
    pool-1-thread-1:13
    pool-1-thread-1:15
    pool-1-thread-1:17
    pool-1-thread-1:19
    奇数和:100

    通过以上两段代码发现:

    第一段代码:设置的线程连接池内的线程数大于实际运行线程数时,线程会按照实际的线程运行

    第二段代码:设置的线程连接池内的线程数小于实际运行线程数时,超出的线程会重新按照线程1开始运行,映射到压测时,这也是为什么连接数少的时候,而又并发的线程数大的时候,请求少

    导致TPS低的原因

  • 相关阅读:
    面试题|Docker的优缺点
    【华为出品】物联网全栈开发实战营来啦!送海思双目开发板
    Nginx实战|Nginx健康检查
    Linux中几个正则表达式的用法
    盘点提高国内访问 Github 的速度的 9 种方案
    一行代码如何隐藏 Linux 进程?
    (二)类加载机制与反射:类加载器
    (一)类加载机制与反射:类的加载,连接和初始化
    (八)多线程:线程相关类
    (七)多线程:线程池
  • 原文地址:https://www.cnblogs.com/frankruby/p/14179235.html
Copyright © 2011-2022 走看看