一、线程不安全的条件
二、怎么解决线程安全问题
——同步和异步的理解
——模拟两个线程对同一个账户取款(不安全的 并发取款)
——解决以上代码不安全问题 synchronized关键字:
——synchronized关键字出现在实例方法上:
三、总结: synchronized的三种写法
四、哪些变量有线程安全问题
五、死锁(要会写 有可能面试写)
六、守护线程
七、计时器
三个条件:
条件1:多线程并发。
条件2:有共享数据。(方法区和堆区资源是共享的,new 一个银行账户是在堆区开辟的对象内存)
条件3:共享数据有修改的行为。
满足以上3个条件之后,就会存在线程安全问题
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在
线程安全问题,怎么解决这个问题?
线程排队执行。(不能并发)。
用排队执行解决线程安全问题。
这种机制被称为:线程同步机制。
专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。
怎么解决线程安全问题呀?
使用"线程同步机制"。
线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全
第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,
谁也不需要等谁,这种编程模型叫做:异步编程模型。
其实就是:多线程并发( 效率较高。)
异步就是并发。
同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行
结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,
两个线程之间发生了等待关系,这就是同步编程模型。
效率较低。线程排队执行。
同步就是排队。
账户类:
package com.bipowernode.javase.thread.threadsafe;
/*
银行账户
不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题
*/
public class Account {
// 属性
// 账号
private String actNo;
// 余额
private double balance;
// 构造方法
// 有参构造
public Account(String actNo, double balance) {
this.actNo = actNo;
this.balance = balance;
}
// getter and setter
public String getActNo() {
return actNo;
}
public void setActNo(String actNo) {
this.actNo = actNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
// 取款的方法
// 线程t1和t2并发这个取款的方法.....
public void withdraw(double money){
// 取款之前的余额
double before =this.balance;
// 取款之后的余额
double after =before -money; // 到此处用户已经取到钱了
// 更新余额
// 思考:t1执行到这里了,但还没有来得及执行这行代码,t2线程进来withdraw方法了 此时一定出问题
// 假设t1走到这里还没有到更新余额(没更新账户余额还是10000) 那么t2也并发过来了 那么t1和t2都取到了5000 而且账户余额还剩5000
try {
// 睡眠5s (检验一定会出毛病)
Thread.sleep(1000*5); // 假设先进来的t1 让t1睡眠5s再更新余额(此时账户余额还是10000) 那么此时在更新余额睡眠的情况下t2线程过来调用取款方法 最终t1和t2都取了5000元 最终银行账户余额还有5000 就出安全问题了
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setBalance(after);
}
}
两个线程同时对同一个银行账户取款(线程并发):
package com.bipowernode.javase.thread.threadsafe;
public class ThreadAccount extends Thread{
// 两个线程必须共享同一个账户对象
private Account act; // 把银行账户类指向act引用
// 通过构造方法传递过来账户对象
public ThreadAccount(Account act) {
this.act = act;
}
// 重写run()方法
@Override
public void run() {
// 这里 当两个线程开启start的时候,就会并发这里面的代码(线程并发)
// 这里跑的是子线程的代码
// run()方法执行表示取款操作
// 假设取款5000
// 多线程并发这个取款方法
double money =5000;
act.withdraw(money);
System.out.println("账户:"+act.getActNo()+"取款"+money+"成功"+"账户剩余:"+act.getBalance());
}
}
程序测试:
package com.bipowernode.javase.thread.threadsafe;
// 程序测试
public class Test01 {
public static void main(String[] args) {
// 创建银行账户对象(只创建1个)
Account account =new Account("act-001",10000);
// 创建两个线程
ThreadAccount ta1 =new ThreadAccount(account);
ThreadAccount ta2 =new ThreadAccount(account); // 当ta1线程更新账户余额不及时的时候 ta2拿到的也是10000元 二者都是10000元的情况下取5000 最终还剩5000 银行账户安全有问题了
// 设置name
ta1.setName("线程ta1");
ta2.setName("线程ta2");
// 启动线程取款
ta1.start();
ta2.start();
}
}
输出结果:
银行账户类:
package com.bipowernode.javase.thread.threadsafe;
/*
银行账户
*/
public class Account {
// 账户
private String actNo;
// 账户余额
private double money;
// 构造方法
public Account(String actNo, double money) {
this.actNo = actNo;
this.money = money;
}
// setter and getter方法
public String getActNo() {
return actNo;
}
public void setActNo(String actNo) {
this.actNo = actNo;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
// 取款的方法
public void withdraw(double money){
// 以下这几行代码必须是线程排队的,不能并发
// 一个线程把这里的代码全部执行结束之后,另一个线程才能进来。
/*
线程同步机制的语法是:
synchronized(){ // (线程共享对象)
// 线程同步代码块
}
synchronized后面小括号中传的这个“数据”是相当关键的。
这个数据必须是多线程共享的数据,才能达到多线程排队(如在此程序当中,因为两个线程t1和t2都是对同一个账户Account类进行取款操作的
所以这个括号里面(线程共享对象)应该是this, 因为在Account类当中this代表Account这个对象)
()中写什么?
那要看你想让哪些线程同步。
假设t1、t2. t3. t4、t5 ,有5个线程
你只希望t1 t2 t3排队, t4 t5不需要排队。怎么办?
你一定要在()中写一个t1 t2 t3共享的对象。而这个
对象对于t4 t5来说不是共享的。
这里的共享对象是:账户对象。
账户对象是共享的,那么this就是账户对象吧! ! !(从Test01测试代码当中可以看出当创建两个线程的时候,传的是同一个Account对象)
不一定是this ,这里只要是多线程共享的那个对象就行。
在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁)
[也就是说synchronized后面的括号是线程共享对象 而对象都有一把锁]
100个对象, 100把锁。1个对象1把锁。
以下代码的执行原理?
1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,
找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是
占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
3、假设t1线程已经占有这把锁,此时t2线程也遇到synchronized关键字,也会去占有后面
共享对象的这把锁,结果这把锁被t1占有, t2只能在同步代码块外面等待t1的结束,
直到t1把同步代码块执行结束了, t1会归还这把锁,此时t2终于等到这把锁,然后
t2占有这把锁之后,进入同步代码块执行程序。【也就是说一个茅坑两个人抢着占用,先到的进去锁着门拉,结束后开锁第二个人关上门拉】
这样就达到了线程排队执行。
这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队
执行的这些线程对象所共享的。
*/
synchronized (this){
// 取款前账户余额
double before =this.getMoney();
// 取款后的余额
double after =before -money;
// 模拟更新账户余额延迟
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 更新账户余额
this.setMoney(after);
}
}
}
创建线程:
package com.bipowernode.javase.thread.threadsafe;
public class ThreadAccount extends Thread{
// 拿账户对象
private Account act;
// 通过构造方法拿到账户对象
public ThreadAccount(Account act){
this.act =act;
}
public void setAct(Account act) {
this.act = act;
}
// 重写run()方法 这里是子线程的运行
@Override
public void run() {
// 进行取款的操作
// 两个线程start之后,并发执行run()方法
// 我们在这里模拟两者对账户余额的取款操作
// 思考: 首先我们肯定要拿到对象才能调用取款的操作把
// 调用银行账户的取款方法
double money =5000;
this.act.withdraw(money);
System.out.println("账户:"+act.getActNo()+"取款"+money+"成功,"+"账户余额:"+act.getMoney());
}
}
程序测试:
package com.bipowernode.javase.thread.threadsafe;
// 程序测试
public class Test01 {
public static void main(String[] args) {
// 创建账户对象
Account account =new Account("act-01",10000);
// 一个对象"一把锁" 这里new的Account是一个对象 里面两个线程 那么这两个线程见到synchronized关键字就需要进行抢锁排队
// 当线程没有碰到synchronized关键字的时候不需要进行抢锁排队
// 创建两个线程
ThreadAccount ta1 =new ThreadAccount(account);
ThreadAccount ta2 =new ThreadAccount(account);
Account account1 =new Account("act-02",10000);
// 这里又重新创建了一个对象,那么这个对象就有"一把锁"供ta3线程使用
// 创建线程
ThreadAccount ta3 =new ThreadAccount(account1);
// 设置name
ta1.setName("线程ta1");
ta2.setName("线程ta2");
// 开启线程
ta1.start();
ta2.start();
ta3.start();
// 开启线程后,ta1和ta2线程会抢时间片 同时并发执行run()方法里面的代码
// 我们这里让run()方法里面进行取款的操作
}
}
输出结果:
package com.bipowernode.javase.thread.threadsafe;
/*
银行账户
*/
public class Account {
// 账户
private String actNo;
// 账户余额
private double money;
// 构造方法
public Account(String actNo, double money) {
this.actNo = actNo;
this.money = money;
}
// setter and getter方法
public String getActNo() {
return actNo;
}
public void setActNo(String actNo) {
this.actNo = actNo;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
/*
在实例方法上可以使用synchronized吗? 可以的
synchronized出现在实例方法上,一定锁的是this
没得挑,只能是this了 不能是其他的对象了
所以这里方式不灵活
另外还有一个缺点:synchronized出现在实例方法上,
表示整个方法体都需要同步,可能会无故扩大同步的范围,
导致程序的执行效率降低
*/
// 取款的方法
public synchronized void withdraw(double money){
// 取款前账户余额
double before =this.getMoney();
// 取款后的余额
double after =before -money;
// 模拟更新账户余额延迟
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 更新账户余额
this.setMoney(after);
}
}
第一种:同步代码块
灵活
synchronized (线程共享对象) {
同步代码块;
}第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体。执行效率会降低第三种:在静态方法上使用synchronized
表示找类锁。
类锁永远只有1把。
就算创建了100个对象,那类锁也只有一把
对象锁: 1个对象1把锁,100个 对象100把锁。
类锁: 100个对象,也可能只是1把类锁。
java中的三大变量:
静态变量:在方法区
局部变量:在栈中
实例变量:在堆中
以上三大变量中:
局部变量永远都不会存在线程安全问题(因为开辟线程就是重新开辟一个栈,多线程就是多个栈执行代码)
因为局部变量不共享。(一个线程一个栈。)
局部变量在栈中。所以局部变量永远都不会共享。
实例变量在堆中,堆只有1个。
静态变量在方法区中,方法区只有1个。
堆和方法区都是多线程共享的,所以可能存在线程安全问题。
package com.bipowernode.javase.thread;
/*
死锁代码一定要会写
一般面试的时候要求你会写
只有会写,才能在以后的开发中注意到这个事儿
因为死锁很难调试
*/
public class DeadLock {
public static void main(String[] args) {
// 创建两个对象
Object o1 =new Object();
Object o2 =new Object();
// 创建两个线程
// my和my1两个线程同时共享o1和o2对象
MyThreadL my =new MyThreadL(o1,o2);
MyThreadL1 my1 =new MyThreadL1(o1,o2);
// 开启线程
my.start();
my1.start();
}
}
class MyThreadL extends Thread{
Object o1;
Object o2;
// 通过构造方法拿到o1和o2对象
public MyThreadL(Object o1,Object o2){
this.o1 =o1;
this.o2 =o2;
}
@Override
public void run() {
synchronized (o1){
// 模拟一下5s延迟
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
}
}
}
}
class MyThreadL1 extends Thread{
Object o1;
Object o2;
// 通过构造方法拿到o1和o2对象
public MyThreadL1(Object o1,Object o2){
this.o1 =o1;
this.o2 =o2;
}
@Override
public void run() { // 因为两个线程是共享o1和o2的 同时并发o1,o2
synchronized (o2){
// 模拟5s延迟 让线程1和线程2的锁僵持住
// (也就是 第一个线程执行5s后需要锁o2 而o2锁被第二个线程占用着等待着第一个线程o1锁释放,那么就僵持住了)
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
}
}
}
}
守护线程
java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)。
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。
(假设保洁阿姨,当我们开会时制造的垃圾[用户线程运行]的时候保洁阿姨[守护线程]开始打扫卫生,
咱们离开会场后保洁阿姨也离开了会场)
注意:主线程main方法是一个用户线程。
守护线程用在什么地方呢?
每天00:00的时候系统数据自动备份。
这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
一直在那里看着,每到00:00的时候就备份一次,所有的用户线程如
果结束了,守护线程自动退出,没有必要进行数据备份了
当没有被守护都是两个线程的时候输出结果: (一直死循环)
当成守护后,(用户线程结束,守护线程也结束):
t1.setDaemon(true); 方法
package com.bipowernode.javase.thread;
// 守护线程
public class ThreadTest10 {
public static void main(String[] args) {
// 创建备份数据线程对象
BakDataTread bak =new BakDataTread();
// 创建线程对象
Thread t1 =new Thread(bak);
t1.setName("备份数据的线程");
// 设置为守护 [当用户线程main结束后,守护线程虽然是死循环但也会跟着结束]
t1.setDaemon(true);
// 开启线程
t1.start();
// 主线程(用户线程)
for (int i=0;i<=9;i++){
System.out.println("用户线程 ---->"+i);
// 睡1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 假设一个备份数据的线程
class BakDataTread implements Runnable{
@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();
}
}
}
}
Timer类当中的
schedule(定时任务,第一次执行的时间,间隔多久执行一次);方法
定时任务:(TimerTask task) 是一个实现Runnable的抽象类
定时器的作用:
间隔特定的时间,执行特定的程序。
每周要进行银行账户的总账操作。
每天要进行数据的备份操作。在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的,
那么在java中其实可以采用多种方式实现:
可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行
任务。这种方式是最原始的定时器。
(比较low)
在java的类库中已经写好了一个定时器: jalya.util.Timer,可以直接拿来用。
不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持
定时任务的。
在实际的开发中,目前使用较多的是spring框架中提供的SpringTask框架,
这个框架只要进行简单的配置,就可以完成定时器的任务。
package com.bipowernode.javase.thread;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
// 使用定时器指定定时任务
public class ThreadTest11 {
public static void main(String[] args) throws ParseException {
// 创建定时器对象
Timer timer =new Timer();
// Timer timer1 =new Timer(true); // 守护线程的方式
// 指定定时任务
// timer.schedule(定时任务,第一次执行的时间,间隔多久执行一次);
// 设置第一次执行的时间
// 假设指定到 2022-4-6 11:10:30 开始第一次的执行
SimpleDateFormat simpleDateFormat =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date FirstTime =simpleDateFormat.parse("2022-4-6 11:22:30");
// 指定定时任务 (也可以用匿名内部类)
timer.schedule(new LogTimer(),FirstTime,1000*10);
// 定时任务:(TimerTask task) 是一个实现Runnable的抽象类 TimerTask task =new LogTimer(); // 多态
}
}
// 编写一个定时任务类
// 假设这是一个记录日志的定时任务
class LogTimer extends TimerTask{
@Override // 要重写抽象类当中的抽象方法
public void run() {
// 编写你需要执行的任务就行了
SimpleDateFormat simpleDateFormat =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date =new Date();
String s =simpleDateFormat.format(date);
System.out.println(s + "成功完成了一次数据备份~");
}
}