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();
}
}
情况二:在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);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
lockDemo.print2();
},"B").start();
}
}
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();
}
}
情况四:给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();
}
}
情况五:
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();
}
}
情况六:
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();
}
}
1 execution
2 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();
},"A").start();
new Thread(()->{
lockDemo.print2();
},"B").start();
}
}

对同一个对象加锁的解释:
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) {
this.wait();
}
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) {
this.wait();
}
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,如果不是就生产了。
解决方案:
改if为while
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) {
this.wait();
}
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();
}
}
6 总结
多线程的环境下,尤其是生产者消费者问题,一定要注意虚假唤醒。所以条件判断,要注意:
要是用while。唤醒条件那里,一定是while不是if。
多线程:
线程 资源类 线程操作资源类
加锁(synchornized) 有等待有唤醒
通知所有等待人,醒来只一个
等待条件要while,if出虚假唤醒