多线程的同步

在多线程的环境中, 经常会遇到数据的共享问题, 即当多个线程需要访问同一资源时, 他们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用,否则, 程序的运行结果将会是不可预料的, 在这种情况下, 就必须对数据进行同步。
在 Java 中, 提供了四种方式来实现同步互斥访问: synchronized 和 Lock和 wait()/notify()/notifyAll()方法和 CAS。

一. synchronized 的用法。
同步代码块 synchronized(object){ }
线程在执行的时候会将 object 对象上锁。 (注意这个对象可以是任意类的对象, 也可以使用 this 关键字或者是 class 对象)
修饰非静态的方法(同步方法)
当 synchronized 关键字修饰一个方法的时候, 该方法叫做同步方法。
Java 中的每个对象都有一个锁(lock) , 或者叫做监视器(monitor) ,当一个线程访问某个对象的 synchronized 方法时, 将该对象上锁, 其他任何线程都无法再去访问该对象的 synchronized 方法了(这里是指所有的同步方法, 而不仅仅是同一个方法) , 直到之前的那个线程执行方法完毕后(或者是抛出了异常) , 才将该对象的锁释放掉, 其他线程才有可能再去访问该对象的synchronized 方法。
修饰静态的方法
当一个 synchronized 关键字修饰的方法同时又被 static 修饰, 之前说过,非静态的同步方法会将对象上锁, 但是静态方法不属于对象, 而是属于类, 它会将这个方法所在的类的 Class 对象上锁。 一个类不管生成多少个对象, 它们所对应的是同一个 Class 对象。
synchronized 方法是一种粗粒度的并发控制, 某一时刻, 只能有一个线程执行该 synchronized 方法。

二. Lock 的用法
使用 Lock 必须在 try-catch-finally 块中进行, 并且将释放锁的操作放在finally 块中进行, 以保证锁一定被释放, 防止死锁的发生。

Lock 是一个接口, 而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
synchronized 在发生异常时, 会自动释放线程占有的锁, 因此不会导致死锁现象发生; 而 Lock 在发生异常时, 如果没有主动通过 unLock()去释放锁, 则很可能造成死锁现象, 因此使用 Lock 时需要在 finally 块中释放锁;
Lock 可以让等待锁的线程响应中断(可中断锁) , 而 synchronized却不行, 使用 synchronized 时, 等待的线程会一直等待下去, 不能够响应中断(不可中断锁);
通过 Lock 可以知道有没有成功获取锁(tryLock() 方法: 如果获取了锁, 则返回 true; 否则返回 false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
Lock 可以提高多个线程进行读操作的效率(读写锁) 。
Lock 可以实现公平锁, synchronized 不保证公平性。
在性能上来说, 如果线程竞争资源不激烈时, 两者的性能是差不多的, 而当竞争资源非常激烈时(即有大量线程同时竞争) ,此时 Lock 的性能要远远优于 synchronized。 所以说, 在具体使用时要根据适当情况选择。

扩展 1: volatile 和 synchronized 区别

volatile 是变量修饰符, 而 synchronized 则作用于代码块或方法。
volatile 不会对变量加锁, 不会造成线程的阻塞; synchronized 会对变量加锁, 可能会造成线程的阻塞。
volatile 仅能实现变量的修改可见性, 并不能保证原子性; 而synchronized 则 可 以 保 证 变 量 的 修 改 可 见 性 和 原 子 性 。
只需要保证共享资源的可见性的时候可以使用 volatile 替代,synchronized 保证可操作的原子性, 一致性和可见性。
三.wait()\notify()\notifyAll()的用法
在 Java 发展史上曾经使用 suspend()、 resume()方法对于线程进行阻塞唤醒, 但随之出现很多问题, 比较典型的还是死锁问题。

解决方案可以使用以对象为目标的阻塞, 即利用 Object 类的 wait()和 notify()方法实现线程阻塞.

wait、 notify 方法是针对对象的, 调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁, 相应地, 调用任意对象的 notify()方法则将随机解除该对象阻塞的线程, 但它需要重新获取改对象的锁, 直到获取成功才能往下执行;
wait、notify 方法必须在 synchronized 块或方法中被调用, 并且要保证同步块或方法的锁对象与调用 wait、 notify 方法的对象是同一个, 如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁, 执行 wait 阻塞后当前线程就将之前获取的对象锁释放。
扩展 1: 为什么 wait(),notify(),notifyAll()等方法都定义在 Object 类中?

因为这三个方法都需要定义在同步代码块或同步方法中, 这些方法的调用是依赖锁对象的, 而同步代码块或同步方法中的锁对象可以是任意对象, 那么能被任意对象调用的方法一定定义在 Object 类中。

扩展 2: notify()和 notifyAll()有什么区别?

notify()和 notifyAll()都是 Object 对象用于通知处在等待该对象的线程的方法。
void notify(): 唤醒一个正在等待该对象的线程, 进入就绪队列等待 CPU 的调度。
void notifyAll(): 唤醒所有正在等待该对象的线程, 进入就绪队列等待 CPU 的调度。
两者的最大区别在于:

notifyAll 使所有原来在该对象上等待被 notify 的线程统统退出 wait 的状态, 变成等待该对象上的锁, 一旦该对象被解锁, 他们就会去竞争。
notify 他只是选择一个 wait 状态线程进行通知, 并使它获得该对象上的锁, 但不惊动其他同样在等待被该对象 notify 的线程们, 当第一个线程运行完毕以后释放对象上的锁, 此时如果该对象没有再次使用 notify 语句, 即便该对象已经空闲, 其他 wait 状态等待的线程由于没有得到该对象的通知, 继续处在 wait 状态, 直到这个对象发出一个 notify 或 notifyAll,它们等待的是被 notify 或 notifyAll, 而不是锁。
四. CAS
它是一种非阻塞的同步方式

猜你喜欢

转载自blog.csdn.net/jcsyl_mshot/article/details/80246155
今日推荐