同步(一)

一、同步的基本概念

1、同步的场景
在这里插入图片描述
线程获取同步锁,获取失败则阻塞等待,获取成功则执行任务,执行完毕后释放锁。

2、线程安全问题

(1)内存读取

  • cpu在内存读取数据时,顺序优先级:寄存器-高速缓存-内存
  • 计算完成后缓存数据写回内存中

(2)可见性

  • 每个线程都有独立的工作内存,并对其他线程是不可见的。线程执行如用到某变量,将变量从主内存复制到工作内存,对变量操作完成后再将变量写回主内存。
  • 可见性就是指线程A对某变量操作后,其他线程能够看到被修改的值,可以通过volidate等方式实现可见性。

(3)原子性

对于基本类型的赋值操作是原子操作,即这些操作是不可被中断的,要么执行,要么不执行。

x = 10;        //语句1(原子性操作)
y = x;         //语句2
x++;           //语句3
  • 语句1直接将10赋值给x,会直接将10写入到工作内存
  • 语句2需要先读x,然后将x写入工作内存,然后赋值,不是原子性操作
  • 语句3需要先读x,然后+1,然后赋值。

(4)有序性

  • Java内存模型中允许编译器和处理器对指令进行重排序,虽然重排序过程不会影响到单线程执行的正确性,但是会影响到多线程并发执行的正确性。
  • 可以通过volidate来保证有序性,synchronized和Lock保证每个时刻只有一个线程执行同步代码,这相当于是让线程顺序执行同步代码,从而保证了有序性。

(5)线程安全问题的原因
在这里插入图片描述
线程A和线程B需要将共享变量拷贝到本地内存中,并对各自的共享变量副本进行操作,操作完成后同步到主内存中,可能导致共享内存数据错乱的问题。

二、synchronized

1、基本特性:

  • synchronized可以用于修饰类的实例方法、静态方法和代码块
  • 可重入性:对同一个执行线程,它在获得了锁之后,在调用其他需要同样锁的代码时,可以直接调用。
  • 内存可见性:在释放锁时,所有写入都会写回内存,而获得锁后,都会从内存中读最新数据。
  • 当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

2、类锁和对象锁

public class SyncTest {
  private static int num;
    
  public void setClassText() {
     //类锁
     synchronized (SyncTest.class) {
       System.out.println("类锁开始执行");
       ++num;
       try {
         Thread.sleep(5000);
       } catch (InterruptedException e) {
         e.printStackTrace();
       }
       System.out.println("类锁执行完成");
     }
  }
  
  public static synchronized void setObject1Text() {
      //类锁
      System.out.println("类锁2开始执行");
      ++num;
      try {
        Thread.sleep(5000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("类锁2执行完成");
  }

  public synchronized void setObject1Text() {
      //对象锁
      System.out.println("对象锁1开始执行");
      ++num;
      try {
        Thread.sleep(5000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("对象锁1执行完成");
  }

  public void setObject2Text() {
    //对象锁
    synchronized(this) {
      System.out.println("对象锁2开始执行");
      ++num;
      try {
        Thread.sleep(5000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("对象锁2执行完成");
    }

  }
}

注:

  • 线程A持有对象锁,不会影响到线程B去持有类锁
  • 线程A和B操作同一个对象锁,线程A持有对象锁,线程B只能等到该对象锁释放
  • synchronized如果修饰static方法(类锁),不会影响到非static(对象锁)修饰的方法的调用

3、wait、notify

  • void notifyAl1( )
    解除那些在该对象上调用 wait 方法的线程的阻塞状态该方法只能在同步方法或同步块内部调用。 如果当前线程不是对象锁的持有者,该方法拋出一个IllegalMonitorStateException异常。
  • void notify()
    随机选择一个在该对象上调用 wait 方法的线程,解除其阻塞状态。 该方法只能在一个同步方法或同步块中调用。 如果当前线程不是对象锁的持有者, 该方法抛出一个 IllegalMonitorStateException 异常。
  • void wait(long mi11is)
    导致线程进人等待状态直到它被通知。该方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者,该方法拋出一个 IllegalMonitorStateException 异常。
  • void wait(long mi11Is, int nanos)
    导致线程进入等待状态直到它被通知或者经过指定的时间。 这些方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者该方法拋出一个 IllegalMonitorStateException 异常。

注意:

  • 调用notify会把在条件队列中等待的线程唤醒并从队列中移除,但它不会释放对象锁,只有在包含notify的synchronzied代码块执行完后,等待的线程才会从wait调用中返回。
  • 调用wait把当前线程放入条件等待队列,释放对象锁。等待时间到或被其他线程调用notify/notifyAll从条件队列中移除,此时要重新竞争对象锁。

三、锁对象

锁可以理解为进入某个门的钥匙,两个人都想进入门内,A持有钥匙可进入,B只能等待。A放下了钥匙,B才有机会获取到钥匙进入。

  • 锁用于保护代码片段,只有一个线程执行被保护的代码
  • 需要保证是同一个锁对象
  • 需要通过调用unlock去释放锁
  • 锁可以拥有一个或多个相关的条件对象

1、Lock

public interface Lock {

  /**
   * 获取锁,如果锁被另一线程拥有则发生阻塞
   */
  void lock();

  /**
   * 尝试获取锁,立即返回不阻塞
   * @return 获取成功返回true
   */
  boolean tryLock();
  
  /**
   * 尝试获取锁,如果成功立即返回,否则阻塞等待,阻塞时间不会超    * 过给定的值
   * @return 获取成功返回true
   */  
  boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

  /**
   * 释放锁
   */
  void unlock();

  /**
   * 获取一个与该锁相关的条件
   * @return
   */
  Condition newCondition();

    ......
}

synchronized和Lock对比:

  • 锁可以以非阻塞方式获取锁、可以响应中断、可以限时
  • synchronized会自动释放锁,而Lock一定要求程序员手工释放

2、ReentrantLock:

(1)构造:

public ReentrantLock() {
  sync = new NonfairSync();
}

/**
 * 构建一个公平策略的锁,公平锁偏爱等待时候最长的线程,会降低性能
 * @param fair
 */
public ReentrantLock(boolean fair) {
  sync = fair ? new FairSync() : new NonfairSync();
}

(2)条件对象

public Condition newCondition() {
......
}

一个锁对象可以有一个或多个条件对象,通过newCondition获取一个条件对象

(3)await、signalAll

  • void await( )
    将该线程放到条件的等待集中。
  • void signalAll( )
    解除该条件的等待集中的所有线程的阻塞状态。
  • void signal()
    从该条件的等待集中随机地选择一个线程, 解除其阻塞状态。

注意:

  • await()对应于Object的wait(),signal()对应于notify,signalAll()对应于notifyAll()
  • 调用await、signalAll等方法前需要先获取锁,如果没有锁,会抛出异常IllegalMonitorStateException

3、读写锁:

特性:

只要没有任何线程写入变量,并发读取可变变量通常是安全的。所以读锁可以同时被多个线程持有,只要没有线程持有写锁。这样可以提升性能和吞吐量,因为读取比写入更加频繁。

使用场景:多线程操作同一文件,读操作比较多,写操作比较少的情况。

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
//获取读、写锁
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

//对读的操作使用读锁
readLock.lock();
try {
  ......
} finally {
  readLock.unlock();
}

//对写的操作使用写锁
writeLock.lock();
try {
  ......
} finally {
  writeLock.unlock();
}

四、Volidate域

1、应用场景:

  • 如果声明一个域为 volatile, 它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
  • 对变量的写操作不会依赖于当前值。(不保证原子性)
  • 该变量没有包含在具有其他变量的不变式中

特性:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序。
  • Volatile变量不能提供原子性

例如

public class VolatileTest {

  volatile boolean flag;

  public void finish() {
    flag = true;
  }

  public void begin() {
    flag =false;
  }
}

通过volatile修饰保证标识位在多线程中的可见性

2、原理:

  • 它确保指令重排序时不会把其后面的指令排到被volidate修饰的变量之前,也不会把前面的指令排到该变量的后面;
  • 它会强制将对缓存的修改操作立即写入主存;
  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。

3、synchronized和volatile比较:

  • volatile是线程同步的轻量级实现,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。
  • 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞
  • volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性

五、死锁

有a, b两个线程,a持有锁A,在等待锁B,而b持有锁B,在等待锁A,a,b陷入了互相等待,最后谁都执行不下去。

public class DeadLockDemo {
  private static Object lockA = new Object();
  private static Object lockB = new Object();
  private static void startThreadA() {
    Thread aThread = new Thread() {
      @Override
      public void run() {
        synchronized (lockA) {
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
          }
          synchronized (lockB) {
          }
        }
      }
    };
    aThread.start();
  }
  private static void startThreadB() {
    Thread bThread = new Thread() {
      @Override
      public void run() {
        synchronized (lockB) {
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
          }
          synchronized (lockA) {
          }
        }
      }
    };
    bThread.start();
  }
  public static void main(String[] args) {
    startThreadA();
    startThreadB();
  }
}

解决方式:

  • 应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的顺序去申请锁
  • 显式锁接口Lock,它支持尝试获取锁(tryLock)和带时间限制的获取锁方法,可以在获取不到锁的时候释放已经持有的锁,然后再次尝试获取锁或干脆放弃,以避免死锁。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合;、下 4载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合;、下载 4使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合;、下载 4使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值