并发线程的数据安全 java_关于多线程并发环境下的数据安全的问题

1.为什么是重点?

以后在开发中,项目都是运行在服务器当中,而服务器已经将线程的定义、线程对象的创建、线程的启动等,都已经实现完了。这些代码都不需要编写,最重要的是要知道:编写的程序需要放到一个多线程的环境下运行,更需要关注这些数据在多线程并发的环境下是否是安全的。

2.什么时候数据在多线程并发的环境下会存在安全问题?

三个条件:

(1)条件1:多线程并发

(2)条件2:多线程有共享的数据

(3)条件3:共享的数据有修改的行为

满足以上3个条件之后,就会存在线程安全问题。

3.如何解决线程安全问题?

当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题。

此时应当使线程排队执行(不能并发)。用排队执行解决线程安全问题,这种机制称为:线程同步机制。(这是专业术语的叫法,实际上就是线程不能并发了,必须排队执行)。线程同步(也就是线程排队)会牺牲一部分效率,但是数据安全是第一位的。

4.线程同步涉及到的两个专业术语

4.1 异步编程模型

线程t1和t2各自执行各自的,相互不管,谁也不需要等谁。(其实就是多线程并发,效率较高)

4.2 同步编程模型

线程t1和t2,在某一线程执行的时候,另一个线程必须等待正在执行的线程,一直到其执行完毕为止。两个线程之间发生了等待关系,线程排队执行,效率较低。

5.例子分析

1 packagethread_safe;2

3 public classAccount {4

5 //账户

6 privateString sctno;7

8 //余额

9 private doublebalance;10

11 publicAccount() {12

13 }14

15 public Account(String sctno, doublebalance) {16 this.sctno =sctno;17 this.balance =balance;18 }19

20 publicString getSctno() {21 returnsctno;22 }23

24 public voidsetSctno(String sctno) {25 this.sctno =sctno;26 }27

28 public doublegetBalance() {29 returnbalance;30 }31

32 public void setBalance(doublebalance) {33 this.balance =balance;34 }35

36 //取款方法

37 public void withdraw(doublemoney){38 //t1和t2并发这个方法;t1、t2是两个栈。两个栈操作堆中同一个对象39 //取款前的余额

40 double before=this.getBalance();41 double after=before-money;//取款后的余额42 //更新余额43 //若t1执行到这里,但还没来得及执行第44行代码,t2线程进来withdraw()方法了,此时一定出现问题。

44 this.setBalance(after);45

46 }47

48 }

1 packagethread_safe;2

3 public class AccountThread extendsThread {4

5 //两个线程必须共享同一个账户对象

6 privateAccount act;7

8 //通过构造方法传递过来账户对象

9 publicAccountThread(Account act){10 this.act=act;11 }12 public voidrun(){13 //run()方法执行取款操作14 //假设取款5000

15 double money=5000;16 //取款

17 act.withdraw(money);18 System.out.println(Thread.currentThread().getName()+"对账户"+act.getSctno()+"取款成功,余额:"+act.getBalance());19 }20 }

1 packagethread_safe;2

3 public classTest {4 public static voidmain(String[] args){5

6 //创建一个账户对象

7 Account act=new Account("act-001",10000);8

9 //创建两个线程

10 Thread t1=newAccountThread(act);11 Thread t2=newAccountThread(act);12

13 //设置名字

14 t1.setName("t1");15 t2.setName("t2");16

17 //启动线程取款

18 t1.start();19 t2.start();20

21 }22

23 }

运行结果:

6071c0f46a4ea9054ff0e0102f2bef48.png

可以看到,两个余额都是5000,这就说明出现了问题。但是要注意但是,出现问题是个概率事件,即这种情况可能发生,也可能不发生,关键在于当某个线程即将执行 this.setBalance(after);这行代码的时候,另一个线程是否已经执行withdraw()方法了,倘若另一个线程已经执行了withdraw()方法,那么即将执行this.setBalance(after)这行代码的线程执行完这行代码后,肯定会出现问题。

为了放大这个问题,可以在this.setBalance(after)之前设置一个延时,这样一定出错,问题显示的也更加明显:

1 packagethread_safe;2

3 public classAccount {4

5 //账户

6 privateString sctno;7

8 //余额

9 private doublebalance;10

11 publicAccount() {12

13 }14

15 public Account(String sctno, doublebalance) {16 this.sctno =sctno;17 this.balance =balance;18 }19

20 publicString getSctno() {21 returnsctno;22 }23

24 public voidsetSctno(String sctno) {25 this.sctno =sctno;26 }27

28 public doublegetBalance() {29 returnbalance;30 }31

32 public void setBalance(doublebalance) {33 this.balance =balance;34 }35

36 //取款方法

37 public void withdraw(doublemoney){38 //t1和t2并发这个方法;t1、t2是两个栈。两个栈操作堆中同一个对象39 //取款前的余额

40 double before=this.getBalance();41 double after=before-money;//取款后的余额42

43 //进行1秒的睡眠,模拟网络延时

44 try{45 Thread.sleep(1000);46 } catch(InterruptedException e) {47 //TODO Auto-generated catch block

48 e.printStackTrace();49 }50 //更新余额51 //若t1执行到这里,但还没来得及执行第44行代码,t2线程进来withdraw()方法了,此时一定出现问题。

52 this.setBalance(after);53

54 }55

56 }

这样之前所说的概率事件就成了一个肯定发生的事件,即每次运行都会出现问题。

解决方法:

1 public void withdraw(doublemoney){2

3 //一下几行代码必须是线程排队的,不能并发4 //一个线程将这里的代码全部执行完毕后,另一个代码才能进来

5 /*

6 * 线程同步机制的语法是:7 * synchronized(){8 * 线程同步代码块9 *10 * }11 *12 * synchronized后面小括号中传的这个数据是相当重要的,这个数据必须是多线程共享的数据,才能达到多线程排队13 * ()中写的是想让同步的线程,这里让t1,t2两个线程同步。14 *15 *16 * 这里的共享对象是:账户对象 。账户对象是共享的,而这里的this就是账户对象17 *18 * 在java中,任何一个对象都有“一把锁”,其实这把锁就是一个标记(100个对象100把锁)19 *20 * 下面代码 的原理:21 * 1.假设t1和t2线程并发,开始执行一下代码的时候,肯定有一个先一个后22 * 2.假设t1先执行了,遇到synchronized,这个时候自动找后面“共享对象”(也就是这里的Account)的对象锁。找到之后并占有这把锁,23 * 然后执行同步代码块中的程序,在程序执行过程中一直占有这把锁,直到同步代码块代码结束,这把锁才会释放。24 * 3. 当t2想占有对象的锁的时候,发现t1已经占有,所以只能在同步代码块外等候t1执行完同步代码块里的程序,归还对象锁后,t2再占用对象的锁,25 * 然后进入同步代码块执行程序26 *27 * 注:共享对象的选择一定要选好,这个对象一定要是需要排队执行的这些线程对象所共享的28 */

29 synchronized(this){30 double before=this.getBalance();31 double after=before-money;//取款后的余额

32

33 try{34 Thread.sleep(1000);35 } catch(InterruptedException e) {36

37 e.printStackTrace();38 }39 this.setBalance(after);40 }41

42 }

ebbe17167084a2d4744b447e2772f14a.png

运行结果:

0ba16d0ae77ca202e78b068d1ac11116.png

思考:

a.  在Account对象中再添加一个Obj对象(全局对象)

1 public classAccount {2

3 //账户

4 privateString sctno;5

6 //余额

7 private doublebalance;8

9

10 //对象

11 Object obj=new Object(); //实例变量,(Account)对象是多线程共享的,所以Account对象中的实例变量obj也是被这些线程共享的

然后在synchronized()的括号内放入obj对象

1 synchronized(obj){2 double before=this.getBalance();3 double after=before-money;//取款后的余额

4

5 try{6 Thread.sleep(1000);7 } catch(InterruptedException e) {8

9 e.printStackTrace();10 }11 this.setBalance(after);12 }13

14 }

运行结果:

895ef838e690d16357e0384c4bc98ff3.png

b.在withdraw()方法里添加一个对象(局部对象)。

1 packageThread_safe2;2

3 public classAccount {4

5 //账户

6 privateString sctno;7

8 //余额

9 private doublebalance;10

11

12 //全局对象 obj

13 Object obj=new Object(); //实例变量,(Account)对象是多线程共享的,所以Account对象中的实例变量obj也是被这些线程共享的

14

15 publicAccount() {16

17 }18

19 public Account(String sctno, doublebalance) {20 this.sctno =sctno;21 this.balance =balance;22 }23

24 publicString getSctno() {25 returnsctno;26 }27

28 public voidsetSctno(String sctno) {29 this.sctno =sctno;30 }31

32 public doublegetBalance() {33 returnbalance;34 }35

36 public void setBalance(doublebalance) {37 this.balance =balance;38 }39

40 //取款方法

41 public void withdraw(doublemoney){42 //局部对象 obj2

43 Object obj2=newObject();44 synchronized(obj2){45 double before=this.getBalance();46 double after=before-money;//取款后的余额

47

48 try{49 Thread.sleep(1000);50 } catch(InterruptedException e) {51

52 e.printStackTrace();53 }54 this.setBalance(after);55 }56

57 }58

59 }

运行结果:

0d6f0bea7d15a8f5005525ae7d362ad8.png

分析原因 :obj2是局部变量,所以它不是共享对象

c.在withdraw()方法里添加字符串“abc”

synchronized("abc"){double before=this.getBalance();double after=before-money;//取款后的余额

try{

Thread.sleep(1000);

}catch(InterruptedException e) {

e.printStackTrace();

}this.setBalance(after);

}

这种方法也行,因为“abc”在字符串常量池中,但是需要注意的是,写“abc”是所有线程都同步,也就是说,如果这里再有几个线程t3、t4。。。。。。,它们也会同步,如果只需要t1和t2同步,其他线程不希望同步,那么就不能采用这样写法。如:

1 packageThread_safe2;2

3 public classTest {4 public static voidmain(String[] args){5

6 //创建一个账户对象

7 Account act=new Account("act-001",10000);8 Account act1=new Account("act-002",10000000);9

10 //创建两个线程

11 Thread t1=newAccountThread(act);12 Thread t2=newAccountThread(act);13

14 //额外创建一个线程

15 Thread t3=newAccountThread(act1);16

17 //设置名字

18 t1.setName("t1");19 t2.setName("t2");20

21 t3.setName("t3");22

23 //启动线程取款

24 t1.start();25 t2.start();26 t3.start();27

28 }29

30 }

(1)synchronized()中写this,这种情况下,只有t1与t2同步,也就是说,它们需要排队,后执行者等前面的归还对象锁(t1与t2共享的对象是act,所以它们两个要排队使用act的对象锁)后,才执行,而此时t3的对象是act1,括号中的this指的是当前对象,所以此时,t3与t1和t2都不同步。

(2)synchronized()中写“abc”,这种情况下,只有t1、t2、t3都同步,它们共同使用对象“abc”的锁,所以需要排队。

6.java中的三大变量

实例变量:存在堆中

静态变量:存在方法区中

局部变量:存在栈中

以上三大变量中,局部变量永远不会存在线程安全问题,因为局部变量在栈中,而一个线程一个栈,所以局部变量永远不会被共享所以也就不会存在安全问题了。

实例变量在堆中,静态变量在方法区中,而堆和方法区都只有一个,所以会堆和方法区都是多线程共享的,所以可能存在安全问题。

综上所述:局部变量和常量(因为常量不可修改)都没有线程安全问题,而成员变量(实例变量+静态变量)会存在线程安全问题

7. 如果使用局部变量的话,使用StringBuilder还是StringBuffer?

建议使用:StringBuilder。

虽然StringBuffer是线程安全的,StringBuilder是非线程安全的,然而将它们用作局部变量的时候,因为局部变量不存在线程安全问题,StringBuffer因为是线程安全的,反而效率会降低(因为StringBuffer的方法上都加synchronized了,每次运行到这里的时候都会去“锁池”里走一趟,所以会降低效率),所以这个时候应该使用StringBuilder。

非线程安全:ArrayList、HashMap、HashSet

线程安全:Vector、Hashtable

8. 总结

synchronized有两种写法

第一种:同步代码块。

synchronized(线程共享对象){

同步代码块

}

第二种:在实例方法上使用synchronized。表示共享对象一定是this,并且同步代码块 是整个方法体

第三种:在静态方法上,使用synchronized。表示使用类锁,类锁永远只有1把,就算创建了100个对象,那类锁也只有一把。

对象锁:一个对象一把锁,100个对象100把锁

类锁:100个对象,也可能只是一把锁

4个例子:

a.

1 packageexam;2

3 /*

4 * doOther()的执行是否需要等doSome()结束?5 * 不需要,因为doOther()不需要占用this的锁,即当doSome()占用this锁执行的时候,不影响doOther()的执行6 */

7 public classExam01 {8

9 public static void main(String[] args) throwsException{10

11 MyClass mc=newMyClass();12 Thread t1=newMyThread(mc);13 Thread t2=newMyThread(mc);14

15 t1.setName("t1");16 t2.setName("t2");17

18 t1.start();19 //睡眠的作用是保证t1先执行

20 Thread.sleep(1000);21 t2.start();22 }23

24 }25 class MyThread extendsThread{26 MyClass mc=newMyClass();27 publicMyThread(MyClass mc){28 this.mc =mc;29 }30 public voidrun(){31 if(Thread.currentThread().getName().equals("t1")){32 mc.doSome();33 }34 if(Thread.currentThread().getName().equals("t2")){35 mc.doOther();36 }37 }38 }39 classMyClass{40 public synchronized voiddoSome(){41 System.out.println("doSome begin");42

43 try{44 Thread.sleep(1000*10);45 } catch(InterruptedException e) {46 //TODO Auto-generated catch block

47 e.printStackTrace();48 }49 System.out.println("doSome over");50 }51

52 public voiddoOther(){53 System.out.println("doOther begin");54 System.out.println("doOther over");55 }56 }

b.

1 packageexam;2

3

4 /*

5 * doOther()的执行是否需要等doSome()结束?6 * 需要,因为等doSome()执行结束了,才会释放this的锁,然后doOther()拿到this的锁了,才能继续执行7 */

8 public classExam2 {9 public static void main(String[] args) throwsException{10

11 MyClass1 mc=newMyClass1();12 Thread t1=newMyThread1(mc);13 Thread t2=newMyThread1(mc);14

15 t1.setName("t1");16 t2.setName("t2");17

18 t1.start();19 //睡眠的作用是保证t1先执行

20 Thread.sleep(1000);21 t2.start();22 }23

24 }25 class MyThread1 extendsThread{26 MyClass1 mc=newMyClass1();27 publicMyThread1(MyClass1 mc){28 this.mc =mc;29 }30 public voidrun(){31 if(Thread.currentThread().getName().equals("t1")){32 mc.doSome();33 }34 if(Thread.currentThread().getName().equals("t2")){35 mc.doOther();36 }37 }38 }39 classMyClass1{40 public synchronized voiddoSome(){41 System.out.println("doSome begin");42

43 try{44 Thread.sleep(1000*10);45 } catch(InterruptedException e) {46 //TODO Auto-generated catch block

47 e.printStackTrace();48 }49 System.out.println("doSome over");50 }51

52 public synchronized voiddoOther(){53 System.out.println("doOther begin");54 System.out.println("doOther over");55 }56 }

c.

1 packageexam;2

3 /*

4 * doOther()的执行是否需要等doSome()结束?5 * 不需要,因为MyClass对象是两个,两把锁6 */

7

8 public classExam3 {9 public static void main(String[] args) throwsException{10

11 MyClass2 mc1=newMyClass2();12 MyClass2 mc2=newMyClass2();13 Thread t1=newMyThread2(mc1);14 Thread t2=newMyThread2(mc2);15

16 t1.setName("t1");17 t2.setName("t2");18

19 t1.start();20 //睡眠的作用是保证t1先执行

21 Thread.sleep(1000);22 t2.start();23 }24

25 }26 class MyThread2 extendsThread{27 MyClass2 mc=newMyClass2();28 publicMyThread2(MyClass2 mc){29 this.mc =mc;30 }31 public voidrun(){32 if(Thread.currentThread().getName().equals("t1")){33 mc.doSome();34 }35 if(Thread.currentThread().getName().equals("t2")){36 mc.doOther();37 }38 }39 }40 classMyClass2{41 public synchronized voiddoSome(){42 System.out.println("doSome begin");43

44 try{45 Thread.sleep(1000*10);46 } catch(InterruptedException e) {47 //TODO Auto-generated catch block

48 e.printStackTrace();49 }50 System.out.println("doSome over");51 }52

53 public synchronized voiddoOther(){54 System.out.println("doOther begin");55 System.out.println("doOther over");56 }57

58 }

d.

1 packageexam;2 /*

3 * doOther()的执行是否需要等doSome()结束?4 * 需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把5 */

6 public classExam4 {7 public static void main(String[] args) throwsException{8

9 MyClass3 mc1=newMyClass3();10 MyClass3 mc2=newMyClass3();11 Thread t1=newMyThread3(mc1);12 Thread t2=newMyThread3(mc2);13

14 t1.setName("t1");15 t2.setName("t2");16

17 t1.start();18 //睡眠的作用是保证t1先执行

19 Thread.sleep(1000);20 t2.start();21 }22

23 }24 class MyThread3 extendsThread{25 MyClass3 mc=newMyClass3();26 publicMyThread3(MyClass3 mc){27 this.mc =mc;28 }29 public voidrun(){30 if(Thread.currentThread().getName().equals("t1")){31 mc.doSome();32 }33 if(Thread.currentThread().getName().equals("t2")){34 mc.doOther();35 }36 }37 }38 classMyClass3{39 //synchronized 出现在静态方法上找的是类锁

40 public synchronized static voiddoSome(){41 System.out.println("doSome begin");42

43 try{44 Thread.sleep(1000*10);45 } catch(InterruptedException e) {46 //TODO Auto-generated catch block

47 e.printStackTrace();48 }49 System.out.println("doSome over");50 }51

52 public synchronized static voiddoOther(){53 System.out.println("doOther begin");54 System.out.println("doOther over");55 }56

57

58

59 }

9.开发中如何解决线程安全问题?

虽然使用synchronized()会解决线程安全问题,保证线程的同步,但是这种方法也有缺点:会降低用户的吞吐量(并发量),使得程序执行效率降低,所以尽量避免使用这种方法。

方法一:尽量使用局部变量代替实例变量和静态变量。

方法二:如果必须使用实例变量,可以考虑创建多个对象,这样就可以避免共享实例变量的内存。(1个线程对应1个对象,100个线程对应100个对象,若线程之间不存在共享对象,也就不会出现安全问题)。

方法三:如果不能使用局部变量,对象也不能创建多个,这个时候就只能使用synohronized.

10. 线程部分的其他内容

10.1 守护线程

java语言中,线程分为两类,一类叫用户线程,另一类叫守护线程(后台线程)。

守护线程的特点:一般是个死循环;只要用户线程结束,守护线程就会自动结束。垃圾回收线程就是一个守护线程,main方法就是一个用户线程。

守护线程用在什么地方?

10.2 定时器

10.3 实现线程的第三种方式:FutureTask方式,实现Callable接口。(JDK8新特性)

10.4 关于Object类中的wait和notify方法。(生产者和消费者模式)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值