[关闭]
@Tyhj 2019-03-15T00:36:39.000000Z 字数 4003 阅读 718

Java并发问题

Java


并发问题

Java并发问题就是多个线程共享资源引起的问题;举个例子,两个线程同时修改一个对象的值,就会出现并发问题;就这个例子去写的话感觉也不是一下就能写出来,得有一些依据来写

一个对象存在并发问题,在于它的值可以被修改,构建一个可以修改自身值的对象

  1. public class Count implements Runnable {
  2. int data = 0;
  3. @Override
  4. public void run() {
  5. for (int i = 0; i < 100000; i++) {
  6. data++;
  7. }
  8. }
  9. }

然后把它作为公共资源,两个线程同时修改它的值,就会引起并发问题

  1. public static void main(String[] args) throws InterruptedException {
  2. Count count = new Count();
  3. new Thread(count).start();
  4. new Thread(count).start();
  5. Thread.sleep(1000);
  6. System.out.println(count.data);
  7. }

我们希望最终输出的值是20万,但是实际上输出的值是不确定的,每次的结果都可能不一样;

引起问题的原因

每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息;线程去访问这个公共对象的时候,通过对象的引用找到对应在堆内存的变量的值,建立一个变量副本;然后对这个副本进行操作,在修改完值后的一个时刻,线程会把副本的值再次赋给原变量;这样当两个线程都拿到变量去改变值,就会出现问题;

解决办法

对象本身

从对象的角度看的话,要保证对象是线程安全的,是因为自身保存了一些数据导致的,如果对象本身不保存数据就行了;

如果对象一定需要保存数据的话,又不能让数据变化,那数据设置为final属性就好了

如果对象的数据一定需要改变的话,可以通过返回一个新的对象而不改变自身的值,所以基本类型和String等都是线程安全的

对象加锁

给这个对象加锁也是常见的手段,分为两种,一种是悲观锁,一种是乐观锁;乐观锁就是乐观的认为我操作这个对象的时候是不会有其他线程来操作它的,我直接去操作,操作完了以后我再去看一下原来这个对象的值有没有被改变,没有的话就赋于原对象新值,有的话会重新拿对象的最新值去操作;

悲观锁是认为我使用这个对象的时候会有其他线程来使用,于是我把它锁起来,我先用完了其他人才能继续使用;

synchronized

常用的加锁方式是使用synchronized,是一种悲观锁,使用它的确就可以解决上面的问题,然后理所当然的这样去加了锁,感觉是没有问题的呀,count加锁,其他线程无法去使用它,但是一运行发现结果还是不确定的

  1. public static void main(String[] args) throws InterruptedException {
  2. Count count = new Count();
  3. synchronized (count){
  4. new Thread(count).start();
  5. }
  6. new Thread(count).start();
  7. Thread.sleep(1000);
  8. System.out.println(count.data);
  9. }

改这个地方不对在Count的run方法里面改才行

  1. @Override
  2. public void run() {
  3. synchronized (this) {
  4. for (int i = 0; i < 100000; i++) {
  5. data++;
  6. }
  7. }
  8. }

synchronized的作用是,在执行这个代码块的时候,给相应的对象加锁,代码块执行完就没有锁了;第一种写法,加锁以后新开线程,然后不管之后线程有没有运行完,这个代码块已经算是执行完了,锁自然就没有了,第一种写法类似下面这种

  1. @Override
  2. public void run() {
  3. synchronized (this) {
  4. new Thread(new Runnable() {
  5. @Override
  6. public void run() {
  7. for (int i = 0; i < 100000; i++) {
  8. data++;
  9. }
  10. }
  11. }).start();
  12. }
  13. }

锁顺序死锁

一般的情况并发情况synchronized可以解决,但是synchronized使用不当可能造成死锁,当多个线程去访问多个公共资源,然后加锁顺序不对就可能引发死锁

随便创建两个对象

  1. Count count1 = new Count();
  2. Count count2 = new Count();

线程1,先拿count1再拿count2

  1. Thread thread1 = new Thread(new Runnable() {
  2. @Override
  3. public void run() {
  4. synchronized (count1) {
  5. System.out.println("thread1拿到了线程count1");
  6. synchronized (count2) {
  7. System.out.println("thread1拿到了线程count2");
  8. }
  9. }
  10. }
  11. });

线程2,和线程1相反,先拿count2再拿count1

  1. Thread thread2 = new Thread(new Runnable() {
  2. @Override
  3. public void run() {
  4. synchronized (count2) {
  5. System.out.println("thread2拿到了线程count2");
  6. synchronized (count1) {
  7. System.out.println("thread2拿到了线程count1");
  8. }
  9. }
  10. }
  11. });

然后同时执行线程

  1. thread1.start();
  2. thread2.start();

运行结果,发现程序一直不会停止运行,也无法继续执行下去,也不报错;这就发生了死锁,thread1和thread2各持有一个对象,互不相让,为了获取另一个对象而等待,导致程序无法执行下去

  1. //输出结果
  2. thread1拿到了线程count1
  3. thread2拿到了线程count2

所以我们可能会为获取这两个对象的方法设置一个顺序,新建一个方法用于获取这两个对象

  1. public void getData(Count count1, Count count2) {
  2. synchronized (count1) {
  3. System.out.println("拿到了线程count1");
  4. synchronized (count2) {
  5. System.out.println("拿到了线程count2");
  6. }
  7. }
  8. }

动态锁顺序死锁

但是也存在一个问题,可能传至的时候会传错,传值顺序不一样了,还是导致死锁,如下

  1. Thread thread1 = new Thread(new Runnable() {
  2. @Override
  3. public void run() {
  4. new Main().getData(count1, count2);
  5. }
  6. });
  7. Thread thread2 = new Thread(new Runnable() {
  8. @Override
  9. public void run() {
  10. new Main().getData(count2, count1);
  11. }
  12. });
  13. thread1.start();
  14. thread2.start();

都是靠写代码的时候去注意,不是很靠谱,两个对象的hash值一般是不同的,可以根据两者的hash值来判断先后顺序;看程序可以知道直接给这个getData方法加锁是没用的,因为是不同的对象来执行的

  1. public void getData(Count count1, Count count2) {
  2. if(count1.hashCode()>count2.hashCode()){
  3. synchronized (count1) {
  4. System.out.println("拿到了线程count1");
  5. synchronized (count2) {
  6. System.out.println("拿到了线程count2");
  7. }
  8. }
  9. }else if(count2.hashCode()>count1.hashCode()){
  10. synchronized (count2) {
  11. System.out.println("拿到了线程count2");
  12. synchronized (count1) {
  13. System.out.println("拿到了线程count1");
  14. }
  15. }
  16. }
  17. }

这样来写就差不都了,两个对象的hash值相同几乎是不可能的,但是针对这种情况,也有办法,就是再加一把锁,再设置一个对象,让线程先去争夺这个对象的锁,谁拿到谁获取后两把锁

  1. /**
  2. * 加时赛锁
  3. */
  4. private final Object tieLock = new Object();
  5. public void getData(Count count1, Count count2) {
  6. if (count1.hashCode() > count2.hashCode()) {
  7. synchronized (count1) {
  8. System.out.println("拿到了线程count1");
  9. synchronized (count2) {
  10. System.out.println("拿到了线程count2");
  11. }
  12. }
  13. } else if (count2.hashCode() > count1.hashCode()) {
  14. synchronized (count2) {
  15. System.out.println("拿到了线程count2");
  16. synchronized (count1) {
  17. System.out.println("拿到了线程count1");
  18. }
  19. }
  20. } else {
  21. synchronized (tieLock) {
  22. synchronized (count1) {
  23. System.out.println("拿到了线程count1");
  24. synchronized (count2) {
  25. System.out.println("拿到了线程count2");
  26. }
  27. }
  28. }
  29. }
  30. }

感觉稍微是有点麻烦,肯定也还有其他写法

ReetrantLock(重入锁)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注