Java多线程-----线程安全及解决机制

   1.什么是线程安全问题?

     从某个线程开始访问到访问结束的整个过程,如果有一个访问对象被其他线程修改,那么对于当前线程而言就发生了线程安全问题;

如果在整个访问过程中,无一对象被其他线程修改,就是线程安全的,即存在两个或者两个以上的线程对象共享同一个资源

   2.线程安全问题产生的根本原因

     首先是多线程环境,即同时存在有多个操作者,单线程环境不存在线程安全问题。在单线程环境下,任何操作包括修改操作都是操作者自己发出的,

 操作者发出操作时不仅有明确的目的,而且意识到操作的影响。

     多个操作者(线程)必须操作同一个对象,只有多个操作者同时操作一个对象,行为的影响才能立即传递到其他操作者。

     多个操作者(线程)对同一对象的操作必须包含修改操作,共同读取不存在线程安全问题,因为对象不被修改,未发生变化,不能产生影响。

综上可知,线程安全问题产生的根本原因是共享数据存在被并发修改的可能,即一个线程读取时,允许另一个线程修改

   3.有线程安全的实例

模拟火车站售票窗口,开启三个窗口售票,总票数为20张

实例一:

package com.practise.threadsafe;

//模拟火车站售票窗口,开启三个窗口售票,总票数为100张
//存在线程的安全问题
class Window extends Thread {
  static int ticket = 20;

  public void run() {
      while (true) {
          if (ticket > 0) {
              try {
                  Thread.sleep(200);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--);
          } else {
              break;
          }
      }
  }
}

public class TestWindow {
  public static void main(String[] args) {
      Window w1 = new Window();
      Window w2 = new Window();
      Window w3 = new Window();

      w1.setName("窗口1");
      w2.setName("窗口2");
      w3.setName("窗口3");

      w1.start();
      w2.start();
      w3.start();

  }

}
运行结果的一种:出现重复售票及负数票

窗口3售票,票号为:20
窗口2售票,票号为:18
窗口1售票,票号为:19
窗口1售票,票号为:17
窗口3售票,票号为:16
窗口2售票,票号为:17
窗口1售票,票号为:15
窗口3售票,票号为:14
窗口2售票,票号为:13
窗口2售票,票号为:12
窗口3售票,票号为:11
窗口1售票,票号为:10
窗口3售票,票号为:8
窗口2售票,票号为:9
窗口1售票,票号为:7
窗口1售票,票号为:6
窗口2售票,票号为:6
窗口3售票,票号为:5
窗口1售票,票号为:4
窗口3售票,票号为:4
窗口2售票,票号为:3
窗口2售票,票号为:2
窗口1售票,票号为:2
窗口3售票,票号为:2
窗口2售票,票号为:1
窗口1售票,票号为:-1
窗口3售票,票号为:0

 实例二:

扫描二维码关注公众号,回复: 4578745 查看本文章
package com.practise.threadsafe;

//使用实现Runnable接口的方式,售票
/*
* 此程序存在线程的安全问题
*/

class Window1 implements Runnable {
  int ticket = 20;

  public void run() {
      while (true) {
          if (ticket > 0) {
              try {
                  Thread.sleep(200);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--);
          } else {
              break;
          }
      }
  }
}

public class TestWindow1 {
  public static void main(String[] args) {
      Window1 w = new Window1();
      Thread t1 = new Thread(w);
      Thread t2 = new Thread(w);
      Thread t3 = new Thread(w);

      t1.setName("窗口1");
      t2.setName("窗口2");
      t3.setName("窗口3");

      t1.start();
      t2.start();
      t3.start();
  }
}
运行结果的一种:出现重复售票

窗口2售票,票号为:20
窗口1售票,票号为:20
窗口3售票,票号为:20
窗口1售票,票号为:19
窗口3售票,票号为:18
窗口2售票,票号为:17
窗口2售票,票号为:16
窗口3售票,票号为:14
窗口1售票,票号为:15
窗口1售票,票号为:13
窗口2售票,票号为:12
窗口3售票,票号为:11
窗口2售票,票号为:10
窗口1售票,票号为:10
窗口3售票,票号为:10
窗口1售票,票号为:9
窗口3售票,票号为:7
窗口2售票,票号为:8
窗口2售票,票号为:6
窗口3售票,票号为:4
窗口1售票,票号为:5
窗口3售票,票号为:3
窗口1售票,票号为:3
窗口2售票,票号为:3
窗口1售票,票号为:2
窗口3售票,票号为:0
窗口2售票,票号为:1

   4.线程安全解决机制Lock和synchronized

4.1  同步代码块synchronized

package com.practise.threadsafe;

/*      同步代码块
 *         synchronized(同步监视器){
 *             //需要被同步的代码块(即为操作共享数据的代码)
 *         }
 *         1.共享数据:多个线程共同操作的同一个数据(变量)
 *         2.同步监视器:由一个类的对象来充当。哪个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁
 *         要求:所有的线程必须共用同一把锁!
 *         注:在实现的方式中,考虑同步的话,可以使用this来充当锁。但是在继承的方式中,慎用this!
 */
class Window implements Runnable {
    int ticket = 20;// 共享数据

    public void run() {
        while (true) {
            // this表示当前对象,本题中即为w
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--);
                }
            }
        }
    }
}

public class TestWindow {
    public static void main(String[] args) {
        Window w = new Window();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
运行结果的一种:

窗口1售票,票号为:20
窗口3售票,票号为:19
窗口3售票,票号为:18
窗口2售票,票号为:17
窗口2售票,票号为:16
窗口2售票,票号为:15
窗口2售票,票号为:14
窗口2售票,票号为:13
窗口2售票,票号为:12
窗口3售票,票号为:11
窗口3售票,票号为:10
窗口1售票,票号为:9
窗口1售票,票号为:8
窗口3售票,票号为:7
窗口2售票,票号为:6
窗口2售票,票号为:5
窗口3售票,票号为:4
窗口3售票,票号为:3
窗口3售票,票号为:2
窗口3售票,票号为:1

4.2  同步方法synchronized

package com.practise.threadsafe;

/*    
 * 同步方法
 * 将操作共享数据的方法声明为synchronized。即此方法为同步方法,能够保证当其中一个线程执行
 * 此方法时,其它线程在外等待直至此线程执行完此方法 
 */

class Window1 implements Runnable {
    int ticket = 20;// 共享数据

    public void run() {
        while (true) {
            show();
        }
    }

    public synchronized void show() {
        if (ticket > 0) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--);
        }

    }
}

public class TestWindow1 {
    public static void main(String[] args) {
        Window1 w = new Window1();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
运行结果的一种:

窗口2售票,票号为:20
窗口1售票,票号为:19
窗口3售票,票号为:18
窗口1售票,票号为:17
窗口2售票,票号为:16
窗口1售票,票号为:15
窗口3售票,票号为:14
窗口1售票,票号为:13
窗口2售票,票号为:12
窗口1售票,票号为:11
窗口3售票,票号为:10
窗口1售票,票号为:9
窗口2售票,票号为:8
窗口1售票,票号为:7
窗口3售票,票号为:6
窗口3售票,票号为:5
窗口1售票,票号为:4
窗口2售票,票号为:3
窗口1售票,票号为:2
窗口3售票,票号为:1

4.3  同步锁Lock

package com.practise.threadsafe;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Window2 implements Runnable {
    int ticket = 20;// 共享数据
    Lock lock = new ReentrantLock();

    public void run() {
        while (true) {
            lock.lock(); // 获取锁
            try {
                if (ticket > 0) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--);
                }
            } finally {
                lock.unlock(); // 释放锁
            }
        }
    }
}

public class TestWindow2 {
    public static void main(String[] args) {
        Window2 w = new Window2();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
运行结果的一种:

窗口3售票,票号为:20
窗口3售票,票号为:19
窗口3售票,票号为:18
窗口3售票,票号为:17
窗口3售票,票号为:16
窗口3售票,票号为:15
窗口3售票,票号为:14
窗口3售票,票号为:13
窗口3售票,票号为:12
窗口1售票,票号为:11
窗口2售票,票号为:10
窗口3售票,票号为:9
窗口3售票,票号为:8
窗口3售票,票号为:7
窗口3售票,票号为:6
窗口3售票,票号为:5
窗口3售票,票号为:4
窗口3售票,票号为:3
窗口3售票,票号为:2
窗口1售票,票号为:1

   5.synchronized 的局限性 与 Lock 的优点

   如果一个代码块被synchronized关键字修饰,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待直至占有锁的线程释放锁。事实上,占有锁的线程释放锁一般会是以下三种情况之一:

  • 占有锁的线程执行完了该代码块,然后释放对锁的占有;
  • 占有锁线程执行发生异常,此时JVM会让线程自动释放锁;
  • 占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。

   synchronized 是Java语言的内置特性,可以轻松实现对临界资源的同步互斥访问。那么,为什么还会出现Lock呢?试考虑以下三种情况:

Case 1 :

    在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。

Case 2 :

    我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。

Case 3 :

    我们可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。

   上面提到的三种情形,我们都可以通过Lock来解决,但 synchronized 关键字却无能为力。事实上,Lock 是 java.util.concurrent.locks包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。也就是说,Lock提供了比synchronized更多的功能。但是要注意以下几点:

  • 1)synchronized是Java的关键字,因此是Java的内置特性,是基于JVM层面实现的。而Lock是一个Java接口,是基于JDK层面实现的,通过这个接口可以实现同步访问;
  • 2)采用synchronized方式不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致死锁现象

   6.Lock和synchronized的选择

   总结来说,Lock和synchronized有以下几点不同:

  •   1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
  •   2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
  •   3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
  •   4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
  •   5)Lock可以提高多个线程进行读操作的效率

  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择

猜你喜欢

转载自www.cnblogs.com/fengfuwanliu/p/10147945.html