开始时间:2021-01-07
线程安全
关于多线程并发环境下,数据的安全问题。
- 为什么这个是重点?
以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编马.
最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据
在多线程并发的环境下是否是安全的。 - 什么时候数据在多线程并发的环境下会存在安全问题呢?
个条件:
条件1:多线程并发。
条件2:有共享数据。
条件3:共享薮据有修改的行为。
满足以上3个条件之后,就会存在线程安全问题。
怎么解决线程安全问题呢?
-
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?
线程排队执行(不能并发) .用排队执行解决线程安全问题。这种机制被称为:线程同步机制。
专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。 -
使用"线程同步机制"-
线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。 -
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。
其实就是:多线程并发(效率较高。)
异步就是并发。 -
同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。
效率较低。线程排队执行。
同步就是排队。
两个线程对同一账户进行取款
首先写一个Account类
package BUOT20210110;
public class Account {
//账号
private String no;
private double balance;
public Account(String no, double balance) {
this.no = no;
this.balance = balance;
}
public Account() {
}
public String getNo() {
return no;
}
public void setNo(String no) {
this.no = no;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Account{" +
"no='" + no + '\'' +
", balance=" + balance +
'}';
}
public void withDraw(double money) {
double before = this.getBalance();
double after = before - money;
this.setBalance(after);
}
}
再写一个 Account线程类
package BUOT20210110;
public class AccountThread extends Thread {
//两个线程共享同一个对象
private Account act;
public AccountThread(Account act) {
this.act = act;
}
public void run() {
//取款操作
//多线程并发执行
double money = 5000;
act.withDraw(money);
System.out.println(Thread.currentThread().getName() + "取款成功,当前余额为" + act.getBalance());
}
}
写一个测试类来进行测试
package BUOT20210110;
public class AccountTest {
public static void main(String[] args) {
Account act = new Account("act-001", 10000);
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
//设置name
t1.setName("t1");
t2.setName("t2");
//启动线程
t1.start();
t2.start();
}
}
如果在一个线程没有走完,另一个线程就进去了,那么会导致余额可能出现错误信息的情况
正常情况
错误情况
改写withDraw方法
添加一个睡眠,一定会出问题
public void withDraw(double money) throws InterruptedException {
double before = this.getBalance();
double after = before - money;
//模拟一下网络延迟,一定报错
Thread.sleep(1000);
this.setBalance(after);
}
同步代码块来解决上述问题
只需对上面的Account进行修改
package ThreadSafe2;
public class Account {
//账号
private String no;
private double balance;
// Object obj=new Object();
//如果添加这一条语句,那么后面传参不传this,传obj也是可以的,只要是共享对象就行
//并且,这个对象一定要唯一,不能写成方法体中的局部变量
public Account(String no, double balance) {
this.no = no;
this.balance = balance;
}
public Account() {
}
public String getNo() {
return no;
}
public void setNo(String no) {
this.no = no;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Account{" +
"no='" + no + '\'' +
", balance=" + balance +
'}';
}
public void withDraw(double money) throws InterruptedException {
//为了保证线程安全
//以下代码必须是线程同步的,不允许并发
//线程同步代码块
/*
synchronized () {
//线程同步代码块
//小括号传的数据必须是多线程共享的数据,才能达到多线程排队
//账户对象是共享的,这里就是this
}
*/
//在这里而言,括弧里传个 "abc"的字符串常量也行,因为指向的都是常量池中的对象
synchronized (this){
double before = this.getBalance();
double after = before - money;
//模拟一下网络延迟,但因为线程同步了,所以也不报错
Thread.sleep(1000);
this.setBalance(after);
}
}
}
对synchronized的理解
在java语言中,任何一个对象都有一把锁”,其实这把锁就是标记。(只是把它叫做锁。)100个对象,100把锁。1个对象1把锁。
- 假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
- 假设t1先执行了,遇到了synchronized,这个时候自动找后面共享对象"”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一亘都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
- 假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,疸到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后t2占有这把锁之后,进入同步代码块执行程序。
这样就达到了线程排队执行。 - 这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。
可以这样来理解以上过程
在火车上排队上厕所,第一个人进去后,手里会握有下一个上厕所人的资格,只有等第一个人解决完后,第二个人才能进去,进去后,握有第三个上厕所人的资格。如果第一个不解决完,第二个就不能进去
前提是要蹲的是同一个坑位
在这里找共享对象的对象锁线程进入谈池找共享对象的对象锁的时候会释放之前占有的CPU时间片,有可能找到了,有可能没找到,没找到测在锁也中等待,如果找到了会进入就绪状态继续抢夺CPU时间片,进入锁池,也能理解为一种阻塞状态
我们刚才讨论的情况,是同一个对象,被两个线程执行。所以用this 用"abc"都行
但是如果是有两个对象,第一个对象被两个线程执行,第二个对象被第三个线程执行,这时候用this就只针对第一个对象,用"abc",则是三条线程全开
比如取钱,对同一个账户取才需要等待,多个用户对自己的卡同时取,互不干扰
Java中三大变量
实例变量:在堆中
静态变量:在方法区。
局部变量:在栈中
以上三大变量中:局部变量永远都不会存在线程安全问题。
局部变量不共享,一个线程一个栈
其他两种,即成员变量,有可能共享
常量没有线程安全问题,因为都不会涉及到修改
回到上面的问题,如果不对Account里面的withdraw方法进行同步
也可以直接对线程中的run方法进行同步
public void run() {
double money = 5000;
try {
//这里不能用this,只能用act,用this代表的是AccountThread对象,并不共享的
synchronized (act){
act.withDraw(money);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "取款成功,当前余额为" + act.getBalance());
}
这样的坏处就是效率要低,因为同步代码块太大了,覆盖了整个withdraw方法,之前只是withdraw里面的一部分
也可以在实例方法上进行加锁
/*
在实例方法上进行加锁,锁的一定是this,这样操作不太灵活
*/
public synchronized void withDraw(double money) throws InterruptedException {
double before = this.getBalance();
double after = before - money;
//模拟一下网络延迟,一定报错
Thread.sleep(1000);
this.setBalance(after);
}
回到之前的一点内容,
StringBuilder和StringBuffer
前者线程不安全,后者线程安全
对于局部变量,选择前者,因为不需要处理线程安全问题,StringBuffer效率低
ArrayList是非线程安全的.
vector是线程安全的
HashMap Hashset是非线程安全的
Hashtable是线程安全的。
synchronized的几种写法
第一种:同步代码块
灵活
synchronized(线程共享对象){
同步代码块;
}
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
表示找类锁,类锁只有一把,创建多少个对象都是一把
面试题1
package exam;
//doOther方法的执行需要等待doSome方法的结束吗?
//不需要
public class exam1 {
public static void main(String[] args) throws InterruptedException {
MyClass myClass = new MyClass();
Thread t1 = new MyThread(myClass);
Thread t2 = new MyThread(myClass);
t1.setName("t1");
t2.setName("t2");
t1.start();
Thread.sleep(1000 );
t2.start();
}
}
class MyThread extends Thread {
private MyClass mc;
public MyThread(MyClass mc) {
this.mc = mc;
}
public void run() {
if (Thread.currentThread().getName() == "t1") {
mc.doSome();
}
if (Thread.currentThread().getName() == "t2") {
mc.doOther();
}
}
}
class MyClass {
//synchronized锁 this
public synchronized void doSome() {
System.out.println("doSome begins");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
//没有锁synchronized,直接就执行了
public void doOther() {
System.out.println("doOther begins");
System.out.println("doOther over");
}
}
doSome begins
doOther begins
doOther over
doSome over
题目2
//锁synchronized,就要等t1搞完再执行了
public synchronized void doOther() {
System.out.println("doOther begins");
System.out.println("doOther over");
}
doSome begins
doSome over
doOther begins
doOther over
题目3
new两个对象,就不需要等待了,有synchronized也不行
MyClass myClass = new MyClass();
MyClass myClass1=new MyClass();
Thread t1 = new MyThread(myClass);
Thread t2 = new MyThread(myClass1);
题目4
synchronized出现在静态方法上,是类锁
虽然是两个对象,但都是MyClass类,所以一并锁住
package exam4;
//doOther方法的执行需要等待doSome方法的结束吗?
//不需要
public class exam4 {
public static void main(String[] args) throws InterruptedException {
MyClass myClass = new MyClass();
MyClass myClass1 = new MyClass();
Thread t1 = new MyThread(myClass);
Thread t2 = new MyThread(myClass1);
t1.setName("t1");
t2.setName("t2");
t1.start();
Thread.sleep(1000);
t2.start();
}
}
class MyThread extends Thread {
private MyClass mc;
public MyThread(MyClass mc) {
this.mc = mc;
}
public void run() {
if (Thread.currentThread().getName() == "t1") {
mc.doSome();
}
if (Thread.currentThread().getName() == "t2") {
mc.doOther();
}
}
}
class MyClass {
//synchronized出现在静态方法上,是类锁
// 虽然是两个对象,但都是MyClass类,所以一并锁住
public synchronized static void doSome() {
System.out.println("doSome begins");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
//没有锁synchronized,直接就执行了
public synchronized static void doOther() {
System.out.println("doOther begins");
System.out.println("doOther over");
}
}
结束时间 2021-01-10