文章目录
区分并行并发同步异步
并行:一起进行,两个任务可以一起运行、进行。
并发:一起分发分配时间片,两个任务交替地获得时间片,你执行几秒换我执行,再换你执行。宏观看就是并行在执行,微观是并发。
串行:完成一个任务再完成下一个任务
同步异步是两种不同的编程模型:
同步要等前一个任务做完了再做下一个,有个相互的关系。同步模型是为了解决并发问题的。
异步 不需要等待,两个线程可以一起做。
io操作频繁的就需要同步,而不是多线程异步,因为会造成很大的浪费如时间片的浪费。
总结就是一句话,同步并发 异步并行
线程同步问题的解决方案(加锁)
分类区分
类锁、对象锁都属于对象监视器,而对象监视器是基于互斥锁的。
类锁、对象锁层次: java层次
类锁即锁住了类,对象锁即锁住实例对象
synchronized使用场景分以下几种:
- 一般方法
对象锁,锁住当前对象
- 静态方法
类锁,锁住此类
- synchronized(object)
锁住object,object可为对象,可为类
常见:
synchronized(this) 对象锁
synchronized(*.class) 类锁
锁的本质:
逻辑上来说,锁其实是对象内存堆中头部的一部分数据。当线程获得一个锁,即是在锁内存区域设置一些标志。线程释放锁也是改变这些标记。
以下是两种锁
synchronized关键字
1、Java中每个对象
都可以作为锁。当线程试图访问同步代码时,必须先获得对象锁,退出或抛出异常
时必须释放锁。
synchronized原理:
方法同步和代码块同步,两者虽然实现细节不同,但本质上都是对一个对象的监视器(monitor)的获取
。任意一个对象都拥有自己的监视器
,当执行同步代码块或同步方法时,执行方法的线程
必须先获得该对象的监视器
才能进入同步块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,状态变为BLOCKED。 当监视器的线程释放了锁后,会唤醒阻塞在同步队列的线程,使其重新尝试对监视器的获取。
ReentrantLock锁
可重入的互斥锁,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。(重入锁后面介绍)
Lock接口:
它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁,缺点就是缺少像synchronized那样隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性
,可中断的获取锁
以及超时获取
锁等多种synchronized关键字所不具备的同步特性。
Lock接口的主要方法:
void lock(): (我称为非要锁不可)
如果锁空闲当前线程就能获得锁否则就禁用当前线程直到获得锁。
boolean tryLock():(我称为尝试要锁)
如果锁可用,则获取锁,并立即返回true,否则返回false.
未获取锁仍然可以继续往下执行代码。
通常采用如下的代码形式调用tryLock()方法:
void unlock():
执行此方法时,当前线程将释放持有的锁. 锁只能由持有者释放,如果线程并不持有锁,却执行该方法,可能导致异常的发生.
Condition newCondition():
条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁。
【**PS:】**关键字synchronized与wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock锁的newContition()方法返回Condition对象,Condition类也可以实现等待/通知模式。
用notify()通知时,JVM会随机唤醒某个等待的线程,而使用Condition类可以进行选择性通知, Condition比较常用的两个方法:
● await()会使当前线程等待,同时会释放锁,当其他线程调用signal()时,线程会重新获得锁并继续执行。
● signal()用于唤醒一个等待的线程。
使用方法
(放在class里的静态变量!)ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
.....................
lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
try {
//操作
} finally {
lock.unlock(); //释放锁
}
重入锁
可重入(自己可以再次
获取自己的内部锁)的锁。
Java里面内置锁(synchronized)和Lock(ReentrantLock)都是可重入的。
public class SynchronizedTest {
public void method1() {
synchronized (SynchronizedTest.class) {
System.out.println("方法1获得ReentrantTest的锁运行了");
method2();
}
}
public void method2() {
synchronized (SynchronizedTest.class) {
System.out.println("方法1里面调用的方法2重入锁,也正常运行了");
}
}
public static void main(String[] args) {
new SynchronizedTest().method1();
}
}
上面便是synchronized的重入锁特性,即调用method1()方法时,已经获得了锁,此时内部调用method2()方法时,由于本身已经具有该锁,所以可以再次获取。
public class ReentrantLockTest {
private Lock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
System.out.println("方法1获得ReentrantLock锁运行了");
method2();
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
System.out.println("方法1里面调用的方法2重入ReentrantLock锁,也正常运行了");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new ReentrantLockTest().method1();
}
}
上面便是ReentrantLock的重入锁特性,即调用method1()方法时,已经获得了锁,此时内部调用method2()方法时, 由于本身已经具有该锁,所以可以再次获取。
公平锁
非公平锁:如synchronized控制的锁就是这种CPU在调度线程的时候从等待队列里随机
挑一个线程,有可能会导致优先级低的线程的饥饿现象。之前练习的模拟卖票系统就是默认非公平锁,所以导致了某几个窗口经常卖票,有的窗口可能一次都轮不上。
而ReentrantLock可以选择成为公平锁或者非公平锁:
通过在构造方法中传入true就是公平锁,传入false,就是非公平锁。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
如此一来公平锁可以保证线程按照时间的先后顺序执行,避免饥饿现象的产生。但公平锁的效率比较低,因为要实现顺序执行,需要维护一个有序队列。
synchronized和ReentrantLock的比较
【PS】同步是两个任务的执行有一个先后到过程,某某执行完,某某才能执行;而异步是两个任务可以一起执行,就用到了多线程编程,可以实现真正的并行。并发是类似单核cpu通过分配时间片来宏观上让你觉得在并行(其实是在并发),而多核cpu则是真正在多个核心上执行,实现并行。
1.区别:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
总结:ReentrantLock相比synchronized,增加了一些高级的功能。但也有一定缺陷。
在ReentrantLock类中定义了很多方法,比如:
isFair() //判断锁是否是公平锁
isLocked() //判断锁是否被任何线程获取了
isHeldByCurrentThread() //判断锁是否被当前线程获取了
hasQueuedThreads() //判断是否有线程在等待该锁
2.两者在锁的相关概念上区别:
1)可中断锁
顾名思义,就是可以响应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
lockInterruptibly()的用法体现了Lock的可中断性。
2)公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁(并不是绝对的,大体上是这种顺序),这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。ReentrantLock可以设置成公平锁。
3)读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作可以并发进行,不需要同步,而写操作需要同步进行,提高了效率。
ReadWriteLock
就是读写锁,它是一个接口
,ReentrantReadWriteLock实现了这个接口。
可以通过readLock()获取读锁,通过writeLock()获取写锁。
4)绑定多个条件
一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多余一个条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这么做,只需要多次调用new Condition()方法即可。
3.性能比较
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而 当竞争资源非常激烈时(即有大量线程同时竞争),此时ReentrantLock的性能要远远优于synchronized 。所以说,在具体使用时要根据适当情况选择。
在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的ReentrankLock对象,性能更高一些。到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。