多线程(下)
传送门:多线程(上)
11. 线程同步
(1)线程安全
其实,在以后的开发中,所有的项目都会运行在服务器上,而服务器已经将线程的定义,线程对象的创建,线程的启动等都实现完了。而我们更需要关注的是在多线程并发的环境下,数据的安全性!
- 这里拿两个人同时去银行对一个用户取钱为例
-
什么时候数据在多线程并发的环境下会存在安全问题呢?
- 条件一:多线程并发
- 条件二:有共享数据
- 条件三:共享数据存在修改行为
满足以上三个条件之后,就会存在线程安全问题
-
如何解决线程安全问题呢?
使用“线程同步机制”,线程同步就是线程排队了,不能并发了,所以会牺牲一部分效率,但是安全更重要
-
线程同步的两个基本术语
- 异步编程模型:线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1。谁也不需要等谁,多线程并发。
- 同步编程模型:线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,效率较低,线程排队执行
(2)线程安全问题实例
银行账户:不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题
Test类
public class Test {
public static void main(String[] args) {
Account account = new Account("act-001",10000);
Thread t1 = new Thread(new AccountThread(account,5000));
Thread t2 = new Thread(new AccountThread(account,5000));
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
Account类
public class Account {
private String id;
private double balance;
public Account() {
}
public Account(String id, double balance) {
this.id = id;
this.balance = balance;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public void withDraw(double money){
//t1和t2并发执行这个方法(两个栈操作堆中的同一个对象)
double before = balance;
double after = balance - money;
//模拟网络延迟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//t1执行到这个,还没有来得及执行这行代码,t2线程进来withDraw这个方法,就会出现问题
balance = after;
}
}
AccountThread类
public class AccountThread implements Runnable{
//两个线程必须共享同一个对象
private Account act;
private double money;
public AccountThread(Account act, double money) {
this.act = act;
this.money = money;
}
@Override
public void run() {
act.withDraw(money);
System.out.println(Thread.currentThread().getName()+"对账户"+act.getId()+"取款"+money+"成功,余额为"+act.getBalance());
}
}
输出结果:
显然,从输出结果上来看就出现了错误。两个用户同时取款5000,最后余额应为0,而结果还剩5000。
(3)利用线程同步解决(2)问题
线程同步机制的语法:
//一个线程把这里的代码全部执行结束,另一个线程才能进来,必须线程排队,不能并发
synchronized(){
//线程同步代码块
}
()中写的要看你想让哪些线程同步。假设t1,t2,t3,t4,t5有5个线程。只希望t1,t2,t3线程排队,t4,t5不需要排队。这时,要在()中写一个t1,t2,t3共享的对象,而这个对象对于t4,t5来说是不共享的。
-
对(2)进行线程同步,只需要对withDraw方法进行修改
public void withDraw(double money){ //账户对象是共享的,那么this就是账户对象 //不一定是this,这里只要是多线程共享的那个对象就行 synchronized (this) { double before = balance; double after = balance - money; //模拟网络延迟 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //t1执行到这个,还没有来得及执行这行代码,t2线程进来withDraw这个方法,就会出现问题 balance = after; } }
-
分析
public void withDraw(double money){ Object obj = new Object() synchronized (obj) { double before = balance; double after = balance - money; //模拟网络延迟 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //t1执行到这个,还没有来得及执行这行代码,t2线程进来withDraw这个方法,就会出现问题 balance = after; } }
将obj写入到括号内,这样可以吗?
不可以,因为obj不是共享对象,是在withDraw方法内的局部变量,每一个线程来执行该方法时,都会创建一个新的Object对象
-
分析
public void withDraw(double money){ synchronized ("abc") { double before = balance; double after = balance - money; //模拟网络延迟 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //t1执行到这个,还没有来得及执行这行代码,t2线程进来withDraw这个方法,就会出现问题 balance = after; } }
将字符串“abc”写入到括号内,可以吗?
可以,因为“abc”在字符串常量池中,所有线程共享,这样写的话就是所有线程都同步
-
分析
public synchronized void withDraw(double money){ double before = balance; double after = balance - money; //模拟网络延迟 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //t1执行到这个,还没有来得及执行这行代码,t2线程进来withDraw这个方法,就会出现问题 balance = after; }
直接将synchronized写在实例方法上,一定锁的是this,不能是其他对象。
这种方式不灵活,还有可能扩大同步范围,不推荐使用
-
总结
Java中三大变量
- 实例变量:堆中
- 静态变量:方法区中
- 局部变量:栈中
以上三大变量中,局部变量永远不会存在线程安全问题。因为局部变量不共享(一个线程一个栈),局部变量在栈中。实例变量在堆中,堆只有1个。静态变量在方法区中,方法区只有一个。堆和方法区都是多线程共享的,所以可能存在安全问题。
-
在StringBuilder和StringBuffer之间选择时,如果使用局部变量的话,可以选择用StringBuild,因为局部变量不存在线程安全问题,使用带有同步机制的StringBuffer效率比较低
ArrayList是非线程安全的,Vector是线程安全的
HashMap、HashSet是非线程安全的、HashTable是线程安全的
(4)synchronized三种写法总结
-
第一种:同步代码块
synchronized(线程共享对象){ //同步代码块 }
-
第二种:在实例方法上使用synchronized,表示共享对象一定是this,并且同步代码块是整个方法体
-
第三种:在静态方法上使用synchronized,表示找类锁,类锁永远只有一把,创建多少个对象都是一把类锁
12. 死锁
-
概念:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去;此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
-
代码实现
public class DeadLock { public static void main(String[] args) { Object o1 = new Object(); Object o2 = new Object(); //两个线程共享o1,o2 Thread t1 = new MyThread1(o1,o2); Thread t2 = new MyThread2(o1,o2); t1.setName("t1"); t2.setName("t2"); t1.start(); t2.start(); } } class MyThread1 extends Thread{ Object o1; Object o2; public MyThread1(Object o1, Object o2){ this.o1 = o1; this.o2 = o2; } @Override 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 MyThread2(Object o1, Object o2){ this.o1 = o1; this.o2 = o2; } @Override public void run() { synchronized (o2){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized ((o1)){ } } } }
所以,synchronized在开发中最好不要嵌套使用,有可能不小心就会出现死锁
13. 如何解决线程安全问题
开发中应该怎么解决线程安全问题呢?直接使用synchronized可以吗?
最好不要,因为synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量降低
-
第一种方案:
尽量使用局部变量代替“实例变量和静态变量”
-
第二种方案:
如果必须是实例变量,那么可以创建多个对象,这样实例变量的内存就不共享了,就没有数据安全问题了
-
第三种方案:
如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synochronized,线程同步机制
14. 守护线程
-
Java语言中线程分为两大类:用户线程和守护线程,典型的用户线程就是main方法,守护线程就是垃圾回收线程
-
特点
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程就会自动结束
-
守护线程用在什么地方呢?
例如每天在固定时间进行备份,就可以使用定时器,并设置成守护线程。每到那个时间就备份一次,所有的用户线程结束了,守护线程自动退出,没有必要进行数据备份了
-
语法
线程对象.setDaemon(true);
-
示例
public class Test { public static void main(String[] args) { Thread t = new MyThread(); t.setName("t1"); //在启动线程之前,设置为守护线程 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 MyThread extends Thread{ @Override public void run() { int i=0; //即使是死循环,但由于该线程是守护线程,所以用户线程结束后,守护线程也会自动终止 while(true){ System.out.println(Thread.currentThread().getName()+"是一个守护线程--->"+(++i)); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
15. 定时器
-
作用
间隔特定的时间,执行特点的程序
-
在实际开发中,每隔多久执行一段特定的程序,这种需求是很常见的。在Java中也有多种实现方式
- 可以使用sleep方法,设置睡眠时间,每到这个时间点就醒来执行程序,但是不推荐这么使用
- 在java类库中有已经写好的定时器:java.util.Timer,可以直接用
- 但是在实际开发中,更常用的是框架来进行配置定时器任务,但是底层实现原理还是Timer
-
实例
public class Test { public static void main(String[] args) throws ParseException { //创建定时器对象 Timer timer = new Timer(); //Timer timer = new Timer(true); 设置成守护线程 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date firstIme = sdf.parse("2021-12-20 21:13:00"); timer.schedule(new LogTimerTask(),firstIme,1000*10); } } //编写一个定时任务 //假设这是一个记录日志的定时任务 class LogTimerTask extends TimerTask{ @Override public void run() { //编写索要执行的任务 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String strTime = sdf.format(new Date()); System.out.println(strTime+"完成了一次数据备份"); } }
输出结果:
TimerTask是一个抽象类,编写定时任务类时要继承TimerTask并重写其中的run方法
timer.schedule(定时任务,第一次执行时间,间隔时间)
-
上面例子也可以采用匿名内部类
public class Test { public static void main(String[] args) throws ParseException { Timer timer = new Timer(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date firstIme = sdf.parse("2021-12-20 21:13:00"); //匿名内部类 timer.schedule(new TimerTask() { @Override public void run() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String strTime = sdf.format(new Date()); System.out.println(strTime+"完成了一次数据备份"); } }, firstIme, 1000 * 10); } }
16. wait和notify
-
wait和notify方法不是线程对象的方法,是Java中任何一个Java对象都有的方法,因为这两个方法是Object类中自带的,不是通过线程对象调用的
-
wait()方法的作用?
Object o = new Object(); o.wait();
让正在o对象上活动的线程(当前线程)进入等待状态,无限期等待,直到被唤醒为止,并且会同时释放t线程之前占有o对象的锁(详细看17中的例子)
-
notify()方法的作用?
Object o = new Object(); o.notify();
唤醒正在o对象上等待的线程,还有一个notifyAll()方法唤醒o对象上处于等待的所有线程,不会释放o对象之前占有的锁(详细看17中的例子)
17. 生产者与消费者模式
使用wait方法和notify方法实现“生产者和消费者模式”
需求:仓库使用List集合,List集合中假设只能存储一个元素,一个元素就表示仓库满了,如果List集合中元素个数为0,就表示仓库空了。保证List集合永远都是最多存储一个元素
public class Test {
public static void main(String[] args) {
//创建一个共享仓库对象
List list = new ArrayList();
//创建两个线程对象
Thread pro = new Thread(new Producer(list));
Thread con = new Thread(new Consumer(list));
pro.setName("Producer");
con.setName("Consumer");
pro.start();
con.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.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName()+"生产了"+obj);
//唤醒消费者消费,但不释放锁
list.notifyAll();
}
}
}
}
//消费者线程
class Consumer implements Runnable{
//仓库
private List list;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
while(true){
synchronized (list){
if(list.size()==0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName()+"消费了"+obj);
list.notifyAll();
}
}
}
}
运行结果:
-
为什么调用wait方法后会释放锁?
因为调用wait方法后,当前线程已经进入了等待状态,如果再不释放锁的话,可能会导致死锁的发生
-
那调用notify方法后,当前线程还会继续抢锁吗?
会继续抢锁,但并没有关系,因为即使当前线程真的抢到了锁,但是会因为条件不满足,而调用wait方法进入等待状态,而wait方法会释放锁,这时另一个线程就又得到了拿到锁的机会