java进阶 第十四讲 线程安全问题

java进阶 第十四讲 线程安全问题

1 关于sleep

结束sleep()的办法:
1.interrupt()
强行打断,利用了异常机制。这种方式不好。
有没有什么办法让一个睡眠线程结束睡眠。
更好的办法是:利用标识位!

2 同步机制

线程安全问题的实质:多个线程操作一个资源类对象。
多个线程,是不是多个栈?为什么要多个栈?
局部变量在栈和栈之间是不能通信的。也就是说,栈内的数据是不共享的。
所以栈里的数据是不存在安全问题的。

在JVM的哪些内存空间中的数据存在线程安全问题? 
栈内存、堆内存、方法区
栈内存空间中存放什么?局部变量:有线程安全问题吗?没有
堆内存空间中存放什么?成员变量:成员变量是共享的,堆中数据是共享的
方法区内存空间中存放什么?静态变量:方法区的数据也是共享的

怎样尽量避免线程安全问题?
	尽量多使用局部变量,少使用成员变量或者是静态变量。
	局部变量是不能出作用域的,出去之后就没了。怎么办呢?
	你们有什么办法吗?可以结合IO流来写出去。
	
如何在多线程中,确保数据安全:
synchronized: 同步的意思。它实际上是加锁。
用法:
	可以用在方法上
	也可以用在方法内
	用在方法上:public synchronized T m1(){}
	用在方法内:
	synchronized(参数) {
		java语句
	}
用在方法上:意味着,线程A进入到m1()方法以后,一直会执行完毕,下一个线程才有可能
获得该方法的执行权。这就意味着,给方法加了一把锁。谁进去,谁锁住这个方法。
直到方法执行完毕后,释放锁。
在同步方法中(也就是被synchronized修饰的方法),遇到了sleep方法,不会释放锁。
这叫做抱锁睡。直到sleep方法被打断或者是执行结束,同步方法还没执行完,就要继续
执行。直到这个线程将同步方法执行完毕。
 public synchronized void getMoney(int money) { // 成员方法上加锁
        int before = getBalance();//取款之前的余额
        int after = before - money;//取款之后的余额
        try {
            Thread.sleep(10);// 抱锁睡,不会释放锁
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        setBalance(after);//修改账户余额

        System.out.println(Thread.currentThread().getName() + "取出了" + money + "账户余额:" + after);
    }

synchronized用法二:
 public void getMoney(int money) {
        synchronized (this) {
            int before = getBalance();//取款之前的余额
            int after = before - money;//取款之后的余额
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            setBalance(after);//修改账户余额
            System.out.println(Thread.currentThread().getName() + "取出了" + money + "账户余额:" + after);
        }
    }

语法:
synchronized(共享的资源对象){} 
计算机中锁的本质到底是什么?标识位
对象:放在内存中的
    00000001 00000000 00000000 00000000 ....
锁方法:方法怎么锁呢?锁门,锁住的是开门的方法吗?
    
记住:锁只能锁对象。也就是说只有对象才能加锁。

public synchronized static void p1() {// 这又是锁什么呢?锁的是类对象。
    // 一个类可以产生无数个对象,但只有一个类对象。
    // 一个对象一把锁,对象锁是有很多的,但是类锁只有一把。
   System.out.println("hello");
}
静态方法加锁,锁的是什么呢?锁对象。
静态方法有对象吗?不是"类名."--->对象 : 类构造对象的过程
    类:在方法区内存中,只是一堆字节码
    对象:实实在在的数据,在堆中
    
    对象要构造成什么样子?是不是得有一个模板?是的。
    一个类的模板,也是一个对象,叫做类对象。它也在堆中,只有一份。
    一个类有一个且仅有一个类对象在堆中。它负责刻画该类的实例对象长什么样子
    构造方法只是负责分配空间、赋值。
    这个类对象,包含了该类所有的信息。包含了这个类的类名、修饰符、
    返回值、属性、方法等等。

3 线程的各种问题

  • 最原始的并发
public class LockDemo {

    public void print1() {
        System.out.println("1 execution");
    }

    public void print2() {
        System.out.println("2 execution");
    }
}

情况一:
public class Test {

    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        new Thread(()->{
            lockDemo.print1();
        },"A").start();
        
        new Thread(()->{
            lockDemo.print2();
        },"B").start();

        // 以上代码,打印结果是先 1 还是先 2 不一定,1和2都有可能先执行。因为CPU调度。
    }
}

情况二:在A和B之间睡眠1秒钟
public class Test {

    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        new Thread(()->{
            lockDemo.print1();
        },"A").start();
        
        try {
            Thread.sleep(1000);//main在睡,当前线程睡
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        new Thread(()->{
            lockDemo.print2();
        },"B").start();

        // 以上代码,打印结果是先 1 还是先 2:一定先打印1 再打印2
        // 因为main线程执行到创建A线程的时候,有两个线程并发
        // 这时候main线程和A线程都是就绪状态
        // 执行顺序由CPU调度。这时候,可能main先执行,那么main方法继续执行
        // 遇到sleep,main让出执行权,等待睡眠结束后恢复就绪状态
        // 此时,只有A线程是就绪状态,那么A肯定执行,打印"1 execution"
        // 也有可能A先执行,直接打印"1 execution",然后A线程执行结束,死掉
        // 只有main线程,main线程睡眠1秒钟,睡醒向下执行
        // 然后创建B线程,main和B并发,也就是说,main和B线程处于就绪状态
        // CPU调度执行顺序,如果main先获得执行权,main向下执行,方法结束
        // 此时,B仍然处于就绪状态,CPU调度B执行,打印"2 execution"
    }
}

// 补充说明 以下代码的执行顺序:
 new Thread(()->{lockDemo.print2();},"B").start();
// 要知道执行顺序,先要知道上述包含了哪些内容?
1. new Thread()
2. Thread 实例的 start()方法
3. 重写的Thread 实例的 run()方法
4. 重写的run方法中调用了lockDemo的print2()方法
// 上述代码的执行顺序应该是这样子的:
1. 先执行 new
2. 执行Thread()的构造
3. 执行Runnable接口实现类的构造 
    这两个构造其实是由内而外的:人要构造自己,先打扮还是先充实自己的内心
4. 执行Thread实例的start()方法,创建一个线程
5. 在新创建的线程栈中,执行run方法
6. 在run方法中执行print2()方法
情况三:
public class Test {

    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        new Thread(()->{
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lockDemo.print1();
        },"A").start();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            lockDemo.print2();
        },"B").start();

        // 以上代码,打印结果是先 1 还是先 2
        // 睡眠时间短的情况下AB线程不一定谁先执行。
    }
}
情况四:给print1()加上synchronized
public class LockDemo {

    public synchronized void print1() {
        System.out.println("1 execution");
    }

    public void print2() {
        System.out.println("2 execution");
    }
}


public class Test {

    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        new Thread(()->{
            lockDemo.print1();
        },"A").start();

        new Thread(()->{
            lockDemo.print2();
        },"B").start();

        // 以上代码,打印结果是先 1 还是先 2
        // 结果还是不一定。都有可能先执行。为什么?
        // synchronized关键字在print1()方法上,只是保证了方法执行的原子性
        // 也就是说,print1()一旦执行,就一定会执行完。
    }
}

情况五:
public class Test {

    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lockDemo.print1();
        },"A").start();

        new Thread(()->{
            lockDemo.print2();
        },"B").start();

        // 以上代码,打印结果是先 1 还是先 2:
        // 一定是先2 再1
        // 同步一个方法,print2()只是普通的成员方法。
    }
}
情况六:
public class LockDemo {

    public  void print1() {
        System.out.println("1 execution");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1 weak up!");
    }

    public  void print2() {
        System.out.println("2 execution");
    }
}

public class Test {

    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        new Thread(()->{
            lockDemo.print1();
        },"A").start();

        new Thread(()->{
            lockDemo.print2();
        },"B").start();
    }
}

// 这里演示了print1()方法中可能答应了1 execution之后,sleep
// 此时,A 进入阻塞,CPU调度main和B,如果B先执行,打印"2 execution"
// 然后 A醒来以后答应 "1 weak up!"
// 这是没锁的情况,sleep之后,会让出CPU执行权。
1 execution
2 execution
1 weak up!
    
// 给print1()加上synchronized关键字后,1 execution 和 1 weak up一定是
// 连续出现的。
    
情况七:
public class LockDemo {

    public synchronized static void print1() {
        System.out.println("1 execution");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1 weak up!");
    }

    public synchronized void print2() {
        System.out.println("2 execution");
    }
}

public class Test {

    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();

        new Thread(()->{
            lockDemo.print1();// LockDemo.print1()
        },"A").start();

        new Thread(()->{
            lockDemo.print2();
        },"B").start();

        // 以上代码,仍然不能保证print1()方法执行的原子性。
        // 也就是说,还会出现:"1 execution" "2 execution" "1 weak up!"
        // 为什么?
        // 1 同一把锁的情况下,一定会保证原子性
        // 2 说明这不是一把锁。
        //  public synchronized static void print1() 这个锁的是静态方法
        //  也就是说它锁住的是类对象
        //  public synchronized void print2() 这个锁的是成员方法
        //  锁住的是实例对象。
    }
}

在这里插入图片描述

对同一个对象加锁的解释:
public class LockDemo {

    public synchronized void print1() {
        System.out.println("1 execution");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1 weak up!");
    }

    public synchronized void print2() {
        System.out.println("2 execution");
    }
}


public class Test {

    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();

        new Thread(()->{
            lockDemo.print1();
        },"A").start();

        new Thread(()->{
            lockDemo.print2();
        },"B").start();
    }
}

在这里插入图片描述

4 守护线程

线程分为两类:用户线程,守护线程
守护:守护神,你的守护神,如果你都不存在你的守护神还存在吗?
汉朝的守护者,汉朝都没了,守护者守护啥?守护者也就不存在了。
守护线程,它是守护用户线程的,用户线程执行结束,守护线程自动结束。
一个用户线程结束,它的守护线程的职责和义务已经完成,就会自动结束。

怎么创建守护线程呢?
public class DaemonTest {
    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(10000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A");
        thread.setDaemon(true);
        thread.start();

        try {
            ....
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

将thread设置为main线程的守护线程,main线程结束,守护线程自动结束。
这有什么用?在应用中可以做很多事情,比如日志的收集、存储打印等等。

5 生产者消费者问题

这是一个很常见的应用场景:
也就是生产者,生产了产品之后,才能出售。
生产多少,消费者消费多少。不能超卖。
比如生产手机,生产一部卖一部。不积压也不超卖。

分析:
	资源类:手机
	线程:生产线程、消费线程
	操作:生产、卖
	深入讨论:
		生产出一部手机之后,要通知消费者来买
		消费者买了一部手机之后,要通知生产者生产
		这样就能做到不积压,不超卖
		这就是生产者消费者问题。
写代码:
	
public class Phone {
    private int num;

    public Phone() {
    }

    public Phone(int num) {
        this.num = num;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return "Phone{" +
                "num=" + num +
                '}';
    }

    public synchronized void produce() throws InterruptedException {
        // 判断,是否有货,有货就不生产
        if (num != 0) {
            // 手机还有货,就不生产,就等着手机卖掉为0的时候生产
            this.wait();// 这是谁的方法?Object
        }
        // 没货就生产一部手机
        num++;
        System.out.println(Thread.currentThread().getName() + "\t --->生产 " + num);
        // 告诉销售,我这里已经生产好一部手机了,怎么通知?

        this.notifyAll();
    }

    public synchronized void sale() throws InterruptedException {
        // 判断是否有货,如果没有货,等生产
        if (num == 0) {
            //等
            this.wait();
        }
        // 有货,卖货
        num--;
        System.out.println(Thread.currentThread().getName() + "\t --->卖后剩余 " + num);
        // 卖完通知生产
        this.notifyAll();
    }
}

public class Client {
    public static void main(String[] args) {
        Phone phone = new Phone();
        // 线程 操作 资源类的方法
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                try {
                    phone.produce();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"生产").start();

        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                try {
                    phone.sale();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"消费").start();

    }
}
总结:
	使用Object类中的wait和notifyAll方法的时候,一定要加锁,要加上synchornized关键字
	他们一起出现。否则会报异常:IllegalMonitorStateException

重要结论:
	wait不是抱锁等,一旦wait执行就会释放锁,当有notifyAll执行的时候,wait就会从阻塞状态进入就绪状态,等待CPU的调度
	notifyAll是通知所有操作当前Phone对象的线程,如果有执行wait处于阻塞状态的,进入就绪状态了。
如果再增加一对生产和消费线程,就会出现数据不一致。
public class Phone {
    private int num;

    public Phone() {
    }

    public Phone(int num) {
        this.num = num;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return "Phone{" +
                "num=" + num +
                '}';
    }

    public synchronized void produce() throws InterruptedException {
        // 判断,是否有货,有货就不生产
        if (num != 0) {
            // 手机还有货,就不生产,就等着手机卖掉为0的时候生产
            this.wait();// 这是谁的方法?Object
        }
        // 没货就生产一部手机
        num++;
        System.out.println(Thread.currentThread().getName() + "\t --->生产 " + num);
        // 告诉销售,我这里已经生产好一部手机了,怎么通知?

        this.notifyAll();
    }

    public synchronized void sale() throws InterruptedException {
        // 判断是否有货,如果没有货,等生产
        if (num == 0) {
            //等
            this.wait();
        }
        // 有货,卖货
        num--;
        System.out.println(Thread.currentThread().getName() + "\t --->卖后剩余 " + num);
        // 卖完通知生产
        this.notifyAll();
    }
}   
    
public class Client {
    public static void main(String[] args) {
        Phone phone = new Phone();
        // 线程 操作 资源类的方法
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                try {
                    phone.produce();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"生产1").start();

        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                try {
                    phone.sale();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"消费1").start();
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                try {
                    phone.produce();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"生产2").start();

        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                try {
                    phone.sale();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"消费2").start();
    }
}

消费1	 --->卖后剩余 0
生产1	 --->生产 1
生产2	 --->生产 2
生产1	 --->生产 3
消费1	 --->卖后剩余 2
消费1	 --->卖后剩余 1
消费1	 --->卖后剩余 0
生产1	 --->生产 1
生产2	 --->生产 2
生产1	 --->生产 3
消费1	 --->卖后剩余 2

原因:
	s1执行,卖了一部 num=0
	通知所有人,我卖了。你们可以生产了。
	这时候,P2拿到了执行权,它进去看,num==0
	num++,此时有了一部手机。它说,大家可以去工作了,我生产了一部。
    他通知了所有人。
    包括P1,这时候P1处于等待状态,它之前已经验证过了,有一台,所以等待。
    等待被唤醒是因为有notifyAll这个方法,它就会往下走,
    他不会再回来进行if判断了!所以,P1被调度之后,就会再次生产。
	于是num=2
    
问题出在:if语句,这叫做虚假唤醒。也就是说,if语句不会再回去判断num != 0
    只要被唤醒,他就往下执行。执行num++,所以就会超量生产了。
    
 怎么解决?
    思路:一旦生产者被唤醒,那么它一定要去看看是不是有货 (还要回去进行判断)。
    也就是看看num的值是否!=0
    如果是,继续执行wait,如果不是就生产了。
    解决方案:
    	改ifwhile
    
public class Phone {
    private int num;

    public Phone() {
    }

    public Phone(int num) {
        this.num = num;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return "Phone{" +
                "num=" + num +
                '}';
    }

    public synchronized void produce() throws InterruptedException {
        // 判断,是否有货,有货就不生产
        while (num != 0) {
            // 手机还有货,就不生产,就等着手机卖掉为0的时候生产
            this.wait();// 这是谁的方法?Object
        }
        // 没货就生产一部手机
        num++;
        System.out.println(Thread.currentThread().getName() + "\t --->生产 " + num);
        // 告诉销售,我这里已经生产好一部手机了,怎么通知?

        this.notifyAll();
    }

    public synchronized void sale() throws InterruptedException {
        // 判断是否有货,如果没有货,等生产
        while (num == 0) {
            //等
            this.wait();
        }
        // 有货,卖货
        num--;
        System.out.println(Thread.currentThread().getName() + "\t --->卖后剩余 " + num);
        // 卖完通知生产
        this.notifyAll();
    }
}
// notifyAll是全部通知
// 可不可以定点通知?

6 总结

多线程的环境下,尤其是生产者消费者问题,一定要注意虚假唤醒。所以条件判断,要注意:
要是用while。唤醒条件那里,一定是while不是if。

多线程:
	线程 资源类 线程操作资源类
	加锁(synchornized) 有等待有唤醒
	通知所有等待人,醒来只一个
	等待条件要while,if出虚假唤醒
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值