关于多线程并发环境下,数据的安全问题。
为什么线程安全这个是重点
以后在开发中,我们的项目都是运行在服务器当中,
而服务器已经将线程的定义,线程对象的创建,线程
的启动等,都已经实现完了。这些代码我们都不需要
编写。
最重要的是:你要知道,你编写的程序需要放到一个
多线程的环境下运行,你更需要关注的是这些数据
在多线程并发的环境下是否是安全的(重点:*****)
什么时候数据在多线程并发的环境下会存在安全问题呢?
三个条件:
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。
满足以上3个条件之后,就会存在线程安全问题。
怎么解决线程安全问题呢?
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在
线程安全问题,怎么解决这个问题?
线程排队执行。(不能并发)。
用排队执行解决线程安全问题。
这种机制被称为:线程同步机制。
专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。
使用“线程同步机制”。
线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全
第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。
说到线程同步这块,涉及到这两个专业术语:
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,
谁也不需要等谁,这种编程模型叫做:异步编程模型。
其实就是:多线程并发(效率较高。)
异步就是并发。
同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行
结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,
两个线程之间发生了等待关系,这就是同步编程模型。
效率较低。线程排队执行。
同步就是排队。
Java中有三大变量?【重要的内容。】
实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。
以上三大变量中:
局部变量永远都不会存在线程安全问题。
因为局部变量不共享。(一个线程一个栈。)=
局部变量在栈中。所以局部变量永远都不会共享
实例变量在堆中,堆只有1个
静态变量在方法区中,方法区只有1个
堆和方法区都是多线程共享的,所以可能存在线程安全问题
局部变量+常量:不会有线程安全问题
成员变量:可能会有线程安全问题。
如果使用局部变量的话:
建议使用:StringBuilder。
因为局部变量不存在线程安全问题。选择StringBuilder。
StringBuffer效率比较低。
ArrayList是非线程安全的。
Vector是线程安全的。
HashMap HashSet是非线程安全的。
Hashtable是线程安全的。
synchronized有三种写法:
第一种:同步代码块
灵活
synchronized(线程共享对象){ 同步代码块; }
synchronized后面小括号中传的这个“数据”是相当关键的。
这个数据必须是多线程共享的数据。才能达到多线程排队。
()中写什么?
那要看你想让哪些线程同步。
假设t1、t2、t3、t4、t5,有5个线程,
你只希望t1 t2 t3排队,t4 t5不需要排队。怎么办?
你一定要在()中写一个t1 t2 t3共享的对象。而这个
对象对于t4 t5来说不是共享的。
//Object obj2 = new Object(); //synchronized (this){ //synchronized (obj) { //synchronized ("abc") { // "abc"在字符串常量池当中。 //synchronized (null) { // 报错:空指针。 //synchronized (obj2) { // 这样编写就不安全了。因为obj2不是共享对象。 //synchronized(this){
第二种:在实例方法上使用synchronized
表示共享对象一定是this
并且同步代码块是整个方法体。
第三种:在静态方法上使用synchronized
表示找类锁。
类锁永远只有1把。
就算创建了100个对象,那类锁也只有一把。
对象锁:1个对象1把锁,100个对象100把锁。
类锁:100个对象,也可能只是1把类锁。
开发中应该怎么解决线程安全问题?
是一上来就选择线程同步吗?synchronized
不是,synchronized会让程序的执行效率降低,用户体验不好。
系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择
线程同步机制。
第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样
实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,
对象不共享,就没有数据安全问题了。)
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候
就只能选择synchronized了。线程同步机制。
示例代码01:
/* 银行账户 不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题。 */ public class Account implements Serializable{ private static final long serialVersionUID = 1655286831056942086L; //账户 private String actno; //余额 private double balance; public Account() { } public Account(String actno, int balance) { this.actno = actno; this.balance = balance; } 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; } //取款方法 public void withdraw(double money){ //取款之前的余额 // t1和t2并发这个方法。。。。(t1和t2是两个栈。两个栈操作堆中同一个对象。) double before = this.getBalance(); //取款之后的余额 double after = before - money; // 在这里模拟一下网络延迟,100%会出现问题 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //更新余额 // 思考:t1执行到这里了,但还没有来得及执行这行代码,t2线程进来withdraw方法了。此时一定出问题。 this.setBalance(after); } } public class AccountThread extends Thread{ // 两个线程必须共享同一个账户对象。 private Account act; // 通过构造方法传递过来账户对象 public AccountThread(Account act){ this.act = act; } public Account getAct() { return act; } public void setAct(Account act) { this.act = act; } public void run(){ //取款5000 // run方法的执行表示取款操作。 // 假设取款5000 double money = 5000; // 取款 // 多线程并发执行这个方法。 act.withdraw(money); System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款" + money + "成功," + "账户余额为:" + act.getBalance()); } } public class Test { public static void main(String[] args) { // 创建账户对象(只创建1个) Account act = new Account("act-no",10000); // 创建两个线程 Thread t1 = new AccountThread(act); Thread t2 = new AccountThread(act); // 设置name t1.setName("t1"); t2.setName("t2"); // 启动线程取款 t1.start(); t2.start(); } }
运行结果:
示例代码02:
/* 银行账户 不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题。 */ public class Account { //账户 private String actno; //余额 private double balance; //对象 Object obj = new Object(); // 实例变量。(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。) public Account() { } public Account(String actno, int balance) { this.actno = actno; this.balance = balance; } 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; } //取款方法 public void withdraw(double money){ // 以下这几行代码必须是线程排队的,不能并发。 // 一个线程把这里的代码全部执行结束之后,另一个线程才能进来。 /* 线程同步机制的语法是: synchronized(){ // 线程同步代码块。 } synchronized后面小括号中传的这个“数据”是相当关键的。 这个数据必须是多线程共享的数据。才能达到多线程排队。 ()中写什么? 那要看你想让哪些线程同步。 假设t1、t2、t3、t4、t5,有5个线程, 你只希望t1 t2 t3排队,t4 t5不需要排队。怎么办? 你一定要在()中写一个t1 t2 t3共享的对象。而这个 对象对于t4 t5来说不是共享的。 这里的共享对象是:账户对象。 账户对象是共享的,那么this就是账户对象吧!!! 不一定是this,这里只要是多线程共享的那个对象就行。 在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁。) 100个对象,100把锁。1个对象1把锁。 以下代码的执行原理? 1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。 2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁, 找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是 占有这把锁的。直到同步代码块代码结束,这把锁才会释放。 3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面 共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束, 直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后 t2占有这把锁之后,进入同步代码块执行程序。 这样就达到了线程排队执行。 这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队 执行的这些线程对象所共享的。 */ //Object obj2 = new Object(); //synchronized (this){ //synchronized (obj) { //synchronized ("abc") { // "abc"在字符串常量池当中。 //synchronized (null) { // 报错:空指针。 //synchronized (obj2) { // 这样编写就不安全了。因为obj2不是共享对象。 //synchronized(this){ double before = this.getBalance(); double after = before - money; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.setBalance(after); } } public class AccountThread extends Thread{ // 两个线程必须共享同一个账户对象。 private Account act; // 通过构造方法传递过来账户对象 public AccountThread(Account act){ this.act = act; } public Account getAct() { return act; } public void setAct(Account act) { this.act = act; } public void run(){ //取款5000 // run方法的执行表示取款操作。 // 假设取款5000 double money = 5000; // 取款 // 多线程并发执行这个方法。 //synchronized (this){//这里的this是AccountThread对象,这个对象不共享! synchronized (act) {//这种方式也可以,只不过扩大了同步的范围,效率更低了 act.withdraw(money); } System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款" + money + "成功," + "账户余额为:" + act.getBalance()); } } public class Test { public static void main(String[] args) { // 创建账户对象(只创建1个) Account act = new Account("act-no",10000); // 创建两个线程 Thread t1 = new AccountThread(act); Thread t2 = new AccountThread(act); // 设置name t1.setName("t1"); t2.setName("t2"); // 启动线程取款 t1.start(); t2.start(); } }
运行结果:
示例代码03:
/* 银行账户 不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题。 */ public class Account implements Serializable{ private static final long serialVersionUID = 1655286831056942086L; //账户 private String actno; //余额 private double balance; public Account() { } public Account(String actno, int balance) { this.actno = actno; this.balance = balance; } 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; } //取款方法 /* 在实例方法上可以使用synchronized吗?可以的。 synchronized出现在实例方法上,一定锁的是this。 没得挑。只能是this。不能是其他的对象了。 所以这种方式不灵活。 另外还有一个缺点:synchronized出现在实例方法上, 表示整个方法体都需要同步,可能会无故扩大同步的 范围,导致程序的执行效率降低。所以这种方式不常用。 synchronized使用在实例方法上有什么优点? 代码写的少了。节俭了。 如果共享的对象就是this,并且需要同步的代码块是整个方法体, 建议使用这种方式。 */ public synchronized void withdraw(double money){ double before = this.getBalance(); double after = before - money; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.setBalance(after); } } public class AccountThread extends Thread{ // 两个线程必须共享同一个账户对象。 private Account act; // 通过构造方法传递过来账户对象 public AccountThread(Account act){ this.act = act; } public Account getAct() { return act; } public void setAct(Account act) { this.act = act; } public void run(){ //取款5000 // run方法的执行表示取款操作。 // 假设取款5000 double money = 5000; // 取款 // 多线程并发执行这个方法。 act.withdraw(money); System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款" + money + "成功," + "账户余额为:" + act.getBalance()); } } public class Test { public static void main(String[] args) { // 创建账户对象(只创建1个) Account act = new Account("act-no",10000); // 创建两个线程 Thread t1 = new AccountThread(act); Thread t2 = new AccountThread(act); // 设置name t1.setName("t1"); t2.setName("t2"); // 启动线程取款 t1.start(); t2.start(); } }
运行结果:
synchronized面试题01:
doOther方法执行的时候需要等待doSome方法的结束吗?不需要
因为doOther方法没有sychronized关键字,所以doOther方法不需要到锁池中获取锁
示例代码04:
public class Exam01 { public static void main(String[] args) { MyClass c = new MyClass(); MyThread t1 = new MyThread(c); MyThread t2 = new MyThread(c); t1.setName("t1"); t2.setName("t2"); t1.start(); try { Thread.sleep(1000);//这个睡眠的作用是:为了保证t1线程先执行。 } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); } } class MyThread extends Thread { private MyClass mc; public MyThread(MyClass mc) { this.mc = mc; } public void run() { if(Thread.currentThread().getName().equals("t1")){ mc.doSome(); } if(Thread.currentThread().getName().equals("t2")){ mc.doOther(); } } } class MyClass { public synchronized void doSome() { System.out.println("doSome beign"); try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("doSome over"); } public void doOther() { System.out.println("doOther beign"); System.out.println("doOther over");//因为doOther方法没有sychronized关键字,所以doOther方法不需要到锁池中获取锁 } }
运行结果:
synchronized面试题02:
doOther方法执行的时候需要等待doSome方法的结束吗?需要
因为doOther方法有sychronized关键字,所以doOther方法需要排队(等待doSome方法执行玩释放锁之后)获取锁
示例代码05:
public class Exam02 { public static void main(String[] args) { MyClass1 a = new exam.MyClass1(); MyThread1 t3 = new exam.MyThread1(a); MyThread1 t4 = new exam.MyThread1(a); t3.setName("t3"); t4.setName("t4"); t3.start(); try { Thread.sleep(1000);//这个睡眠的作用是:为了保证t1线程先执行。 } catch (InterruptedException e) { e.printStackTrace(); } t4.start(); } } class MyThread1 extends Thread { private MyClass1 mc; public MyThread1(MyClass1 mc) { this.mc = mc; } public void run() { if(Thread.currentThread().getName().equals("t3")){ mc.doSome(); } if(Thread.currentThread().getName().equals("t4")){ mc.doOther(); } } } class MyClass1 { public synchronized void doSome() { System.out.println("doSome beign"); try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("doSome over"); } public synchronized void doOther() { System.out.println("doOther beign"); System.out.println("doOther over");//因为doOther方法有sychronized关键字,所以doOther方法需要排队(等待doSome方法执行玩释放锁之后)获取锁 } }
运行结果:
synchronized面试题03:
doOther方法执行的时候需要等待doSome方法的结束吗?不需要
因为MyClass对象是两个,两把锁。
示例代码06:
public class Exam03 { public static void main(String[] args) { MyClass2 c1 = new MyClass2(); MyClass2 c2 = new MyClass2();//创建两个对象就没有共享对象了,不需要排队获取锁 MyThread2 t1 = new MyThread2(c1); MyThread2 t2 = new MyThread2(c2); t1.setName("t1"); t2.setName("t2"); t1.start(); try { Thread.sleep(1000);//这个睡眠的作用是:为了保证t1线程先执行。 } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); } } class MyThread2 extends Thread { private MyClass2 mc; public MyThread2(MyClass2 mc) { this.mc = mc; } public void run() { if(Thread.currentThread().getName().equals("t1")){ mc.doSome(); } if(Thread.currentThread().getName().equals("t2")){ mc.doOther(); } } } class MyClass2 { public synchronized void doSome() { System.out.println("doSome beign"); try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("doSome over"); } public synchronized void doOther() { System.out.println("doOther beign"); System.out.println("doOther over"); } }
运行结果:
synchronized面试题04:
doOther方法执行的时候需要等待doSome方法的结束吗?需要
因为静态方法是类锁,不管创建了几个对象,类锁只有。
示例代码07:
public class Exam04 { public static void main(String[] args) { MyClass2 c = new MyClass2(); MyThread2 t1 = new MyThread2(c); MyThread2 t2 = new MyThread2(c); t1.setName("t1"); t2.setName("t2"); t1.start(); try { Thread.sleep(1000);//这个睡眠的作用是:为了保证t1线程先执行。 } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); } } class MyThread3 extends Thread { private MyClass3 mc; public MyThread3(MyClass3 mc) { this.mc = mc; } public void run() { if(Thread.currentThread().getName().equals("t1")){ mc.doSome(); } if(Thread.currentThread().getName().equals("t2")){ mc.doOther(); } } } // synchronized出现在静态方法上是找类锁。 class MyClass3 { public synchronized static void doSome() { System.out.println("doSome beign"); try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("doSome over"); } public synchronized static void doOther() { System.out.println("doOther beign"); System.out.println("doOther over"); } }
运行结果: