问题引入

多个线程执行的不确定性引起执行结果的不稳定
多个线程对数据的共享,会造成操作的不完整性,会破坏数据。

举例:
创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式

  • 问题:卖票过程中,出现了重票、错票 —>出现了线程的安全问题
  • 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
  • 如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来(即上锁)。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。

Java解决方案:同步机制

在Java中,我们通过同步机制,来解决线程的安全问题。

方式一:同步代码块

  1. synchronized (对象){
  2. // 需要被同步的代码;
  3. }

说明:

  • 操作共享数据的代码,即为需要被同步的代码。
  • 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
  • 同步监视器,俗称:锁。任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。

补充:
在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类(如:类名.class)充当同步监视器。

继承thread方式:

  1. class Window2 extends Thread{
  2. private static int ticket = 100;
  3. private static Object obj = new Object();
  4. @Override
  5. public void run() {
  6. while(true){
  7. //正确的
  8. // synchronized (obj){
  9. synchronized (Window2.class){//Class clazz = Window2.class,Window2.class只会加载一次
  10. //错误的方式:this代表着t1,t2,t3三个对象
  11. // synchronized (this){
  12. if(ticket > 0){
  13. try {
  14. Thread.sleep(100);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. System.out.println(getName() + ":卖票,票号为:" + ticket);
  19. ticket--;
  20. }else{
  21. break;
  22. }
  23. }
  24. }
  25. }
  26. }

方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

  1. public synchronized void show (String name){
  2. // 需要被同步的代码;
  3. }

关于同步方法的总结:

  • 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
  • 非静态的同步方法,同步监视器是:this
  • 静态的同步方法,同步监视器是:当前类本身(类名.class)

同步机制中的锁

  • 同步锁机制:

对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。

  • 注意:

必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

方式三:Lock锁

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

    1. class A{
    2. //1.实例化ReentrantLock
    3. private final ReentrantLock lock = new ReentrantLock();
    4. public void m(){
    5. //2.调用锁定方法lock()
    6. lock.lock();
    7. try{
    8. //保证线程安全的代码;
    9. }
    10. finally{
    11. //3.调用解锁方法:unlock()
    12. lock.unlock();
    13. }
    14. }
    15. }
    16. 注:如果同步代码有异常,要将unlock()写入finally语句块

    synchronized 与 Lock 的对比 :

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放

  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序: Lock —->同步代码块(已经进入了方法体,分配了相应资源)——->同步方法 (在方法体之外)