zoukankan      html  css  js  c++  java
  • 多线程(续集)

    线程同步机制(synchronized)

    //线程同步机制代码格式
    synchronized(排队线程共享的对象){ 线程同步代码块 }
    /*
    ():括号中填的是,排队线程共享的对象,比如有t1,t2,t3,t4,t5线程,只要线程t1,t2,t3排队执行,那么要在括号内写线程t1,t2,t3共享的对象,这个对象对于线程t4,t5是不共享的
    */
    
    //取款的方法(写在账户类中)
        public void widthBalance(double money){
            //共享对象是账户,this代表当前账户
           synchronized(this){
            //取款前的账户余额
            double before = this.getBalance();
            //取款后的账户余额
            double after = before - money;
             
               try { //哪个线程先进来,哪个线程先睡1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
               //更新账户余额
            this.setBalance(after);
            }
        }
    

    线程执行过程:

    当线程 t1 执行到synchronized(this)会自动找“共享对象”的对象锁并占用 this(即线程共享的Account对象) 的对象锁,每一个对象都有一个特定的对象锁(锁就是标记),此时线程 t2 也执行到synchronized(this)获取对象锁,因为线程 t1 还在占用该对象锁,所以线程 t2 会等待,直到线程 t1 执行完同步代码块中的代码释放对象锁线程 t2 才继续往下执行。

    image-20200728163052411

    线程执行到synchronized代码处会在锁池中找共享对象的对象锁,线程进入锁池找共享对象的对象锁时,会释放之前占有的CPU时间片,如果没找到对象锁则在锁池中等待,如果找到了会进入就绪状态抢夺CPU时间片。(进入锁池可以理解为一种阻塞状态)

    共享对象的深层理解:

    public class Account {
        private String actno;
        private double balance;
    
        public Account(){
    
        }
    
        public Account(String actno,double balance){
            this.actno = actno;
            this.balance = balance;
        }
    
        public void setActno(String actno){
            this.actno = actno;
        }
    
        public String getActno(){
            return actno;
        }
    
        public void setBalance(double balance){
            this.balance = balance;
        }
    
        public double getBalance(){
            return balance;
        }
        Object obj1 = new Object();
        //取款的方法
        public void widthBalance(double money){
            Object obj2 = new Object();
            //synchronized(this) {
            //synchronized(obj1){
                synchronized(obj2){
                //取款前的账户余额
                double before = this.getBalance();
                //取款后的账户余额
                double after = before - money;
    
                //模拟网络延迟
                try { //哪个线程先进来,哪个线程先睡1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                //更新账户余额
                this.setBalance(after);
            }
        }
    }
    

    问题:

    1、为什么使用synchronized(obj1)能正常执行,而使用synchronized(obj2)就不能正常执行呢?

    答:obj1是全局实例对象(Account 对象是多线程共享的,Account 对象中的实例变量 obj1 也是共享的),创建Account实例对象时会同创建obj1这个实例对象,此时这两个对象都只有一个,使用synchronized(obj1)时线程会占用obj1对象的锁,在同步代码块没有执行完时其他线程只能等待,所以obj1也可以看成是共享对象。然而,obj2是局部对象,每个线程执行widthBalance()方法时都会创建一个obj2对象,当执行synchronized(obj2)时每个线程都可以找到相对应的对象锁,此时obj2不是共享对象。

    2、为什么synchronized("ac")也能正常执行?

    答:字符串常量池中ac是唯一的,只有一个,但是此时字符串ac是所有线程的共享对象,所有线程都会同步。

    synchronized("ac")synchronized(this)的区别:

    //创建一个账户对象
    Account act1 = new Account();
    //创建线程
    Thread t1 = new AccountThread(act1);
    Thread t2 = new AccountThread(act1);
    
    //创建另一个账户对象
    Account act2 = new Account();
    //创建线程
    Thread t3 = new AccountThread(act2);
    Thread t4 = new AccountThread(act2);
    

    synchronized("ac")时,字符串ac是 t1、t2、t3、t4 四个线程的共享对象。

    synchronized(this)时,act1 是线程 t1、t2 的共享对象,act2 是线程 t3、t4 的共享对象。

    synchronized(){}中的同步代码块代码越少效率就越高。

    同步代码的另一种写法:将widthBalance()的整个方法作为同步代码块,这种方式增加了同步代码块的代码,效率更低 。

    public class AccountThread extends Thread {
        //线程共享的账户
        private Account act;
    
        //通过构造方法把账户对象传递过来
        public AccountThread(Account act){
            this.act = act;
        }
    
        /**
         * 取款时调用的方法
         */
        public void run(){
          //取款的金额
            double money = 5000;
    	    
            synchronized(act){ //不能写this,this代表当前线程,不是共享对象       
              act.widthBalance(money);
                            }
    
            System.out.println(Thread.currentThread().getName()+"对账户"
                    +act.getActno()+"取款"+money+",账户余额:"+act.getBalance());
        }
    }
    

    synchronized出现在实例方法上:(不常用)

      public synchronized void widthBalance(double money){
            Object obj2 = new Object();
          
                //取款前的账户余额
                double before = this.getBalance();
                //取款后的账户余额
                double after = before - money;
    
                //模拟网络延迟
                try { //哪个线程先进来,哪个线程先睡1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                //更新账户余额
                this.setBalance(after);
            }
    

    synchronized出现在实例方法上时,共享对象只能是this,这种方式不灵活;这种方式表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低,所以这种方式不常用。

    如果共享对象就是this并且需要同步的代码块是整个方法体,则建议synchronized使用在实例方法上,这样代码少,更加简洁。

    例如:StringBuffer的源代码中很多都是synchronized出现在实例方法上,是线程安全的,而StringBuilder是非线程安全的。

    问题:使用局部变量(没有线程安全问题)时是用StringBuffer还是StringBuilder

    答:使用StringBuilder。如果使用StringBuffer每次都会进入锁池放弃CPU时间片或等待,这样执行效率大大降低,所以局部变量中尽量使用StringBuilder

    补充: Vector、Hashtable是线程安全的, ArrayList、HashMap、HashSet 是非线程安全的。

    总结:synchronized的三种写法:

    第一种:同步代码块(灵活)

    synchronized(线程共享对象){
        同步代码块;
    }
    

    第二种:在实例方法上使用synchronized,表示共享对象一定是 this ,并且同步代码块是整个方法体。

    第三种:在静态方法上使用synchronized,表示找类锁。(类锁只有一把)

    面试题:doOther方法执行的时候需要等待doSome方法结束吗?

    public class Test{
        public static void main(String[] args){
            //创建共享对象
            TestClass tc = new TestClass();
            //创建线程
            Thread t1 = new TestThread(tc);
            Thread t2 = new TestThread(tc);
            t1.start();
            try{
                Thread.sleep(1000);//让主线程睡1秒,保证线程t1先执行
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    
    //测试类
    class TestClass{
        public synchronized void doSome(){
            System.out.println("doSome begin");
            try{
                Thread.sleep(1000 * 10);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            System.out.println("doSome over");
        }
        
        public void doOther(){
            System.out.println("doOther begin");
            System.out.println("doOther over");
        }
    }
    
    //线程类
    class TestThread extends Thread{
        private TestClass tc;
        public TestThread(TestClass tc){
            this.tc = tc;
        }
        public void run(){
            if(Thread.currentThread().getName().equals("t1")){
                tc.doSome();
            }
             if(Thread.currentThread().getName().equals("t2")){
                tc.doOther();
            }
        }
    }
    
    1. doOther 方法没有synchronized,此时doOther 方法执行的时候不需要等待 doSome方法结束,线程 t1 调用 doSome 方法时占用 TestClass 的对象锁,当线程 t1 占用对象锁时线程 t2 调用 TestClass 对象的 doOther 方法,因为doOther 方法没有synchronized调用时不需要等待 doSome 方法释放对象锁可以直接执行。

    2)doOther 方法有synchronized,此时线程 t2 调用 doOther 方法需要获取对象锁,所以必须等待 doSome 方法结束。

    3)当synchronized出现在静态方法上(找的是类锁),这时需要等,因为静态方法找类锁,虽然两个线程执行的是两个对象,但这两个对象同属于一个 TestClass 类,所以线程 t1 占用类锁时,线程 t2 必须等待。

    public class Test{
        public static void main(String[] args){
            //创建共享对象
            TestClass tc1 = new TestClass();
             TestClass tc2 = new TestClass();
            //创建线程
            Thread t1 = new TestThread(tc1);
            Thread t2 = new TestThread(tc2);
            t1.start();
            try{
                Thread.sleep(1000);//让主线程睡1秒,保证线程t1先执行
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    
    //测试类
    class TestClass{
        public synchronized static void doSome(){
            System.out.println("doSome begin");
            try{
                Thread.sleep(1000 * 10);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            System.out.println("doSome over");
        }
        
        public synchronized static void doOther(){
            System.out.println("doOther begin");
            System.out.println("doOther over");
        }
    }
    
    //线程类
    class TestThread extends Thread{
        private TestClass tc;
        public TestThread(TestClass tc){
            this.tc = tc;
        }
        public void run(){
            if(Thread.currentThread().getName().equals("t1")){
                tc.doSome();
            }
             if(Thread.currentThread().getName().equals("t2")){
                tc.doOther();
            }
        }
    }
    

    死锁(重点)

    image-20200729133148104

    线程1,2都需要同时锁住对象1,2才能顺利执行下去,且线程1是先锁住对象1再锁对象2,线程2是先锁对象2再锁对象1,但线程1,2同时执行时就无法同时将对象1,2同时锁住,此时程序不出现异常,也不出现错误,程序一直僵持很难调试出错误。

    死锁代码:(必须会手写)

    public class DeadLock{
        public static void main(String[] args){
            Object o1 = new Object();
            Object o2 = new Object();
            //线程t1,t2共享对象o1,o2
            Thread t1 = new MyThread1(o1,o2);
            Thread t2 = new MyThread2o1,o2);
            t1.start();
            t2.start();
        }
    }
    
    //线程类
    class MyThread1 extends Thread{
        Object o1;
        Object o2;
        public MyThread1(Object o1,Object o2){
            this.o1 = o1;
            this.o2 = o2;
        }
        public void run(){
            synchronized(o1){
                  try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                synchronized(o2){
                    
                }
            }
        }
    }
    
    class MyThread2 extends Thread{
        Object o1;
        Object o2;
        public MyThread1(Object o1,Object o2){
            this.o1 = o1;
            this.o2 = o2;
        }
        public void run(){
            synchronized(o2){
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                synchronized(o1){
                    
                }
            }
        }
    }
    

    怎样合理的解决线程安全问题?

    使用线程同步机制(synchronized)会降低程序执行效率,系统的用户吞吐量(并发量)降低,用户体验差,在不得已的情况下才选择线程同步机制。

    方案一:尽量使用局部变量代替“实例变量和静态变量”。(局部变量不共享)

    方案二:当必须使用实例变量时,可以考虑创建多个对象,一个线程对应一个对象,这样实例变量的内存就不共享了,就没有数据安全问题了。

    方案三:如果不能使用局部变量,对象也不能创建多个,此时只能使用synchronized线程同步机制。

    守护线程

    Java 中的线程分为两大类:用户线程(如主线程main)、守护线程(后台线程,如垃圾回收线程)

    守护线程的特点:一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。

    守护线程用在哪?怎么用?

    答:例如每天零点时系统数据自动备份。我们可以将定时器设置为守护线程,每次一到零点的时候就备份一次,当所有的用户线程结束后,守护线程自动退出。

    //守护线程测试
    public class Test{
        public static void main(String[] args){
            Thread t = new DataThread();
            t.setName("备份数据的线程");
            //将备份数据的线程设置为守护线程
            t.setDaemon(true);
            t.start();
            //用户线程(主线程)
            for(int i = 0;i<10;i++){
                System.out.println(Thread.currentThread().getName()+"-->"+i);
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
    
    //线程类
    class DataThread extends Thread{
        public void run(){
            int i = 0;
            //当该线程是守护线程时,即使是死循环当用户线程结束时,守护线程也会结束
            while(true){//死循环
                 System.out.println(Thread.currentThread().getName()+"-->"+(++i));
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }
    

    定时器

    定时器的作用:间隔特定的时间,执行特定的程序。

    比如每周进行银行账户的总账操作;每天进行数据的备份工作。

    实际开发中,每隔一定时间执行特定的程序在Java 中可以采用多种方式实现:

    1、使用sleep睡眠方法,这是最原始的定时器。

    2、java.util.TimerJava类库中已经写好的定时器,开发中很少用,因为很多高级框架都是支持定时任务的。

    3、实际开发中使用较多的是 Spring 框架中提供的 SpringTask 框架,只要进行简单配置就可以完成定时的任务。

    public class TimerTest{
        public static void main(String[] args){
            //创建定时器对象
            Timer timer = new Timer();
    		/*
    		//创建守护线程
            Timer timer = new Timer(true);
            */
            /*
            //指定定时任务
            timer.schedule(定时的任务,第一次执行时间,间隔多久执行一次(填毫秒))
            */
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date firstTime = sdf.parse("2020-07-30 10:00:00");
            timer.schedule(new LogTimerTask(),firstTime,1000 * 10);
        }
    }
    
    //编写一个定时任务类,假设这是个记录日志的定时任务
    class LogTimerTask extends TimerTask{
        @Override
        public void run(){
            //在此处编写需要执行的任务
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String steTime = sdf.format(new Date());
            System.out.println(strTime+":成功完成了一次数据备份!");
        }
    }
    

    结果:从2020-07-30 10:00:00开始,每隔10秒完成一次数据备份。

    实现线程的第三种方式

    拿到线程的执行结果,可以通过实现callable接口的方式(JDK8 的新特性)。

    优点:可以获取线程的执行结果。

    缺点:效率较低,在获取线程 t 执行结果时,当前线程受阻塞效率低。

    call()方法相当于run()方法,但call()方法有返回值。

    public static void main(String[] args){
        //创建一个“未来任务类”对象,参数是Callable接口实现类对象
        FutureTask task = new FutureTask(new Callable(){
           @Override
            public Object call() throws Exception{//call()方法相当于run方法
               //模拟执行
                System.out.println("call method begin");
                Thread.sleep(1000 * 10);
                System.out.println("call method end");
                int a = 1;
                int b = 4;
                return (a+b);//结果是Integer类型(自动装箱)
            }
        });
        //创建线程对象
        Thread t = new Thread(task);
        t.start();
        //在主线程中获取线程t的返回结果
        Object obj = task.get();//此处抛出一个异常
        System.out.println("线程t执行结果:"+obj);
        System.out.println("主线程");
        
    }
    

    问题:在主线程中获取线程t的返回结果的get()方法会不会导致主线程阻塞?

    答:会。主线程要继续执行下去必须等待get()方法结束,返回另一个线程的执行结果需要一定的时间,所以会阻塞主线程。

    2.8.10 关于 Object 类中的 wait 和 notify 方法

    1、wait()方法作用

    Object obj = new Object();
    obj.wait();
    

    表示让正在 obj 对象上活动的线程进入等待状态,并且释放之前占有的 obj 对象的锁,无限期等待,直到被唤醒为止。

    2、notify()方法作用

    Object obj = new Object();
    obj.notify();
    

    唤醒正在 obj 对象上等待的线程(如果有多个线程处于等待状态则会随机唤醒一个线程),不会释放之前占有的 obj 对象的锁。

    还有一个notifyAll()方法,唤醒 obj 对象上处于等待的所有线程。

    生产者和消费者模式:

    1、什么是生产者和消费者模式

    生产线程负责生产,消费线程负责消费,生产线程和消费线程达到均衡。

    2、使用wait()notify()方法实现“生产者和消费者模式”

    image-20200731111801336

    因为多线程要同时操作一个仓库(共享对象),有线程安全问题,所以wait()notify()方法建立在线程同步(排队执行)的基础上。

    练习:

    public class WakeThread {
        public static void main(String[] args) {
            /**
             *  创建集合
             */
            List list = new ArrayList();
            /**
             * 创建线程
             */
            Thread t1 = new Thread(new Producer(list));
            Thread t2 = new Thread(new Comsumer(list));
            /**
             * 给两个线程起名字
             */
            t1.setName("生产线程");
            t2.setName("消费线程");
            /**
             * 启动线程
             */
            t1.start();
            t2.start();
        }
    }
    
    /**
     * 生产线程
     */
    class Producer implements Runnable{
    
        private List list;
    
        public Producer(List list){
            this.list = list;
        }
    
        @Override
        public void run(){
            //生产者一直生产
            while(true){
                synchronized (list){
                    if(list.size() > 0){
                        try {
                            //集合中有元素生产者线程进入等待状态,并释放list对象锁
                            list.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //当集合中没有元素时进行生产
                    Object obj = new Object();
                    list.add(obj);
                    System.out.println(Thread.currentThread().getName()+"生产了元素:"+obj);
                    //唤醒消费线程消费
                    list.notify();
                }
            }
        }
    }
    
    image-20200731155240600

    问题:

    1、为什么上述代码中唤醒线程可以用list.notifyAll();代替list.notify();

    答:因为生产线程和消费线程都进行了集合中元素的判断,并且都有wait()方法,所以即使唤醒全部线程也不会造成线程并发。

    2、消费者中唤醒生产者时,是否会再次立即抢到锁?

    答:消费者线程可能会再次立即抢到锁。但此时集合中元素为0,消费者线程会进入等待状态并释放对象锁,生产线程进行生产。

  • 相关阅读:
    Node.js Net 模块+DNS 模块
    php程序报500错误
    Node.js 工具模块-OS模块+path模块
    Node.js GET/POST请求
    Canvas动画+canvas离屏技术
    Python OS 模块
    Python random 模块
    Python time 模块
    Python 迭代器
    Python 生成器
  • 原文地址:https://www.cnblogs.com/gyunf/p/13423613.html
Copyright © 2011-2022 走看看