多线程(续集)

线程同步机制(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,消费者线程会进入等待状态并释放对象锁,生产线程进行生产。

posted @ 2020-08-02 22:28  莫哈德  阅读(83)  评论(0编辑  收藏  举报
/*地址栏logo*/