互斥同步
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock
线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果
同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能
知识兔synchronized
修饰代码块
知识兔public void func() { synchronized (this) { // ... } }
它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步
修饰一个方法
知识兔public synchronized void func () { // ... }
它和同步代码块一样,作用于同一个对象
同步一个类
知识兔public class SynchronizedExample { public void func2() { synchronized (SynchronizedExample.cl ass) { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } } }
作用的对象是这个类的所有对象
同步一个静态方法
知识兔public synchronized static void fun() { // ... }
静态的同步函数的锁是:字节码对象,用于整个类
ReentrantLock
重入锁(ReentrantLock)是一种递归无阻塞的同步机制。它是 java.util.concurrent(J.U.C)包中的锁
public class LockExample {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
知识兔public static void main(String[] args) { LockExample lockExample = new LockExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> lockExample.func()); executorService.execute(() -> lockExample.func());}// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
相比于 synchronized,它多了以下高级功能:
等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
可实现公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
锁绑定多个条件
一个 ReentrantLock 对象可以同时绑定多个 Condition 对象。
synchronized 和 ReentrantLock 比较
1. 锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
2. 性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。
3. 功能
ReentrantLock 多了一些高级功能。
4. 使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
常见问题:synchronized与lock的区别,使用场景。看过synchronized的源码没?
- (用法)synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized 可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
- (用法)lock(显示锁):需要显示指定起始位置和终止位置。一般使用 ReentrantLock 类做为锁,多个线程中必须要使用一个 ReentrantLock 类做为对象才能保证锁的生效。且在加锁和解锁处需要通过 lock() 和 unlock() 显示指出。所以一般会在 finally 块中写 unlock() 以防死锁。
- (性能)synchronized 是托管给 JVM 执行的,而 lock 是 Java 写的控制锁的代码。在 Java1.5 中,synchronize 是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用 Java 提供的 Lock 对象,性能更高一些。但是到了 Java1.6 ,发生了变化。synchronize 在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致 在 Java1.6 上 synchronize 的性能并不比 Lock 差。
- (机制)synchronized 原始采用的是 CPU 悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。Lock 用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是 CAS 操作(Compare and Swap)。
什么是乐观锁和悲观锁
- 为什么需要锁(并发控制)
- 在多用户环境中,在同一时间可能会有多个用户更新相同的记录,这会产生冲突。这就是著名的并发性问题。
- 典型的冲突有:
- 丢失更新:一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失。例如:用户 A 把值从 6 改为 2,用户 B 把值从 2 改为 6,则用户 A 丢失了他的更新。
- 脏读:当一个事务读取其它完成一半事务的记录时,就会发生脏读取。例如:用户 A,B 看到的值都是6,用户 B 把值改为 2,用户 A 读到的值仍为 6。
- 为了解决这些并发带来的问题。 我们需要引入并发控制机制。
- 并发控制机制
- 悲观锁:假定会发生并发冲突,独占锁,屏蔽一切可能违反数据完整性的操作。
- 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。乐观锁不能解决脏读的问题。
参考资料:
Synchronized(对象锁)和Static Synchronized(类锁)区别
一个是实例锁(锁在某一个实例对象上,如果该类是单例,那么该锁也具有全局锁的概念),一个是全局锁(该锁针对的是类,无论实例多少个对象,那么线程都共享该锁)。
实例锁对应的就是 synchronized关 键字,而类锁(全局锁)对应的就是 static synchronized(或者是锁在该类的 class 或者 classloader 对象上)。
/** * static synchronized 和synchronized的区别! * 关键是区别第四种情况! */public class StaticSynchronized { /** * synchronized方法 */ public synchronized void isSynA(){ System.out.println("isSynA"); } public synchronized void isSynB(){ System.out.println("isSynB"); } /** * static synchronized方法 */ public static synchronized void cSynA(){ System.out.println("cSynA"); } public static synchronized void cSynB(){ System.out.println("cSynB"); } public static void main(String[] args) { StaticSynchronized x = new StaticSynchronized(); StaticSynchronized y = new StaticSynchronized(); /** * x.isSynA()与x.isSynB(); 不能同时访问(同一个对象访问synchronized方法) * x.isSynA()与y.isSynB(); 能同时访问(不同对象访问synchronized方法) * x.cSynA()与y.cSynB(); 不能同时访问(不同对象也不能访问static synchronized方法) * x.isSynA()与y.cSynA(); 能同时访问(static synchronized方法占用的是类锁, * 而访问synchronized方法占用的是对象锁,不存在互斥现象) */ }}
线程之间的协作
当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调
join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。/*对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。*/public class JoinExample { private class A extends Thread { @Override public void run() { System.out.println("A"); } } private class B extends Thread { private A a; B(A a) { this.a = a; } @Override public void run() { try { a.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("B"); } } public void test() { A a = new A(); B b = new B(a); b.start(); a.start(); }}public static void main(String[] args) { JoinExample example = new JoinExample(); example.test();}//输出: A B
wait() notify() notifyAll()
调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify()(随机叫醒一个) 或者 notifyAll() (叫醒所有 wait 线程,争夺时间片的线程只有一个)来唤醒挂起的线程。它们都属于 Object 的一部分,而不属于 Thread。
只能用在同步方法或者同步控制块中使用!否则会在运行时抛出 IllegalMonitorStateExeception。使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。使用 Lock 来获取一个 Condition 对象。
sleep和wait有什么区别
- sleep 和 wait
- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
- wait() 会释放锁,sleep() 不会。
- 有什么区别
- sleep() 方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)。
- wait() 是 Object 类的方法,调用对象的 wait() 方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的 notify() 方法(或 notifyAll() 方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。
- sleep 和 wait