可重入性:
两者都是可重入的。同一个现成计入一次,锁的计数器进行自增,等到锁的计数器下降为零时,才能释放锁。
锁的实现:
synchronized 依赖于 JVM 实现,而 ReentrantLock 基于 JDK 实现。区别类似于操作系统控制实现与用户使用代码实现。
性能区别:
在 synchronized 优化前性能比 ReentrantLock 差很多, 但自从引入了偏向锁、轻量级锁(自选锁)后,也就是自循锁后,两者性能差不多(JDK1.6以后)。官方更推荐使用 synchronized, 因为写法更容易,synchronized 的优化其实借鉴了 ReentrantLock 中的 CAS 技术,都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
功能区别:
便利性
synchronized 更便利,它是由编译器保证加锁与释放。ReentrantLock 是需要手动声明与释放锁,所以为了避免忘记手动释放锁造成死锁,最好在 finally 种声明释放所。
锁的细粒度和灵活度
ReentrantLock 优于 synchronized
ReentrantLock 独有的功能
- 可配置公平锁,ReentrantLock 可以指定是公平锁还是非公平锁,synchronized 只能是非公平锁。(公平锁就是先等待的线程先获得锁)
//创建一个非公平锁,默认是非公平锁
Lock lock = new ReentrantLock();
Lock lock = new ReentrantLock(false);
//创建一个公平锁,构造传参true
Lock lock = new ReentrantLock(true);
- 分组唤醒,提供了一个 Condition 类,可以实现分组唤醒需要唤醒的线程。不像是 synchronized 要么随机唤醒一个线程,要么全部唤醒
- 等待可中断,提供能够终端等待锁的线程的机制,通过lock.lockInterruptibly()实现,这种机制 ReentrantLock 持有锁的线程长期不释放的时候,正在等待的线程可以通过lock.lockInterruptibly()选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
ReentrantLock源码分析
lock()方法在公平锁中
- 先调用acquire方法尝试获取锁
final void lock() {
acquire(1);
}
- acquire方法
public final void acquire(int arg) {
//尝试获取锁失败,进入等待队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//
selfInterrupt();
}
static void selfInterrupt() {
//中断当前线程
Thread.currentThread().interrupt();
}
lock()方法在非公平锁中
1.先比较并设置状态,成功则设置独有线程,否则去获取锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
2.acquire方法同公平锁
为什么非公平锁比公平锁更高效
个人理解,公平锁都要排队,上一个用完这把锁后,去唤醒下一个,但是唤醒需要一定时间。
非公平锁就是在还未唤醒时插队,抢先一步拿到锁,这就相当于节省了一部分唤醒时间。
CAS无锁技术
compare and swap,比较并切换,是一种实现并发算法时常用到的技术。
在多线程高并发编程的时候,最关键的问题就是保证临界区的对象的安全访问。通常是加锁来处理,其实加锁本质上是将并发转变为串行来实现的,会影响吞吐量。
对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。而无锁是一种乐观策略。无锁的策略采用一种比较交换技术CAS来鉴别线程冲突。
与锁相比,CAS 会使得程序设计比较负责,由于其性能优势,天生免疫死锁,没有竞争带来的开销和线程间频繁调度带来的开销,比基于锁的方式有着更优越的性能。
CAS算法
CAS方法包含三个参数CAS(V,E,N),V表示当前内存的是,E表示预期值,N表示更新的值,只有当V等于E的时候,才会将V值改为N,如果V不等于E,说明已经有其他线程对它做了更新,则当前线程直接返回V值。
J.U.C并发包下的atomic包里的类都是CAS实现的
CAS缺点
CAS存在一个很明显的问题,即ABA问题。
问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?
如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。改进后的CAS(V,E,N,ES,NS),ES表示期望的标记,NS表示新的标记
可重入性的定义
举例:方法A和方法B都加了同一把锁,方法A加锁后调用方法B,如果锁可充入,那么方法B会获得锁,锁的计数器+1,如果不可重入,那么就会死锁。可重入锁的计数器全部释放后,锁才会释放。
死锁的四个必要条件
- 互斥:至少有一个资源必须处于非共享模式,即一次只有一个进程可使用。如果另一进程申请该资源,那么申请进程应等到该资源释放为止。
- 占有并等待:—个进程应占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有。
- 非抢占:资源不能被抢占,即资源只能被进程在完成任务后自愿释放。
- 循环等待:有一组等待进程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。
public class DeadLock {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args){
Thread a = new Thread(new Lock1());
Thread b = new Thread(new Lock2());
a.start();
b.start();
}
}
class Lock1 implements Runnable{
@Override
public void run(){
try{
System.out.println("Lock1 running");
while(true){
synchronized(DeadLock.obj1){
System.out.println("Lock1 lock obj1");
Thread.sleep(3000);//获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2
synchronized(DeadLock.obj2){
System.out.println("Lock1 lock obj2");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
class Lock2 implements Runnable{
@Override
public void run(){
try{
System.out.println("Lock2 running");
while(true){
synchronized(DeadLock.obj2){
System.out.println("Lock2 lock obj2");
Thread.sleep(3000);
synchronized(DeadLock.obj1){
System.out.println("Lock2 lock obj1");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
用jstack查看
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f9dcf818ca8 (object 0x00000007959dbee0, a java.lang.String),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f9dcf814c08 (object 0x00000007959dbf10, a java.lang.String),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at Study20191021.Lock2.run(DeadLock.java:52)
- waiting to lock <0x00000007959dbee0> (a java.lang.String)
- locked <0x00000007959dbf10> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at Study20191021.Lock1.run(DeadLock.java:33)
- waiting to lock <0x00000007959dbf10> (a java.lang.String)
- locked <0x00000007959dbee0> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
synchronized的几种用法
- 同步类
下面提供了两种同步类的方法,锁住效果和同步静态方法一样,都是类级别的锁,同时只有一个线程能访问带有同步类锁的方法。
/**
* 用在类
*/
private void synchronizedClass() {
synchronized (TestSynchronized.class) {
System.out.println("synchronizedClass");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 用在类
*/
private void synchronizedGetClass() {
synchronized (this.getClass()) {
System.out.println("synchronizedGetClass");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 同步方法
这个也是我们用得最多的,只要涉及线程安全,上来就给方法来个同步锁。这种方法使用虽然最简单,但是只能作用在单例上面,如果不是单例,同步方法锁将失效。
/**
* 用在普通方法
*/
private synchronized void synchronizedMethod() {
System.out.println("synchronizedMethod");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 同步静态方法,不管你有多少个类实例,同时只有一个线程能获取锁进入这个方法。
/**
* 用在静态方法
*/
private synchronized static void synchronizedStaticMethod() {
System.out.println("synchronizedStaticMethod");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
同步静态方法是类级别的锁,一旦任何一个线程进入这个方法,其他所有线程将无法访问这个类的任何同步类锁的方法。
4. 同步对象实例
这也是同步块的用法,和上面的锁住当前实例一样,这里表示锁住整个 LOCK 对象实例,只有获取到这个 LOCK 实例的锁才能进入这个方法。
/**
* 用在对象
*/
private void synchronizedInstance() {
synchronized (LOCK) {
System.out.println("synchronizedInstance");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 同步代码块
这也是同步块的用法,表示锁住整个当前对象实例,只有获取到这个实例的锁才能进入这个方法。
/**
* 用在this
*/
private void synchronizedThis() {
synchronized (this) {
System.out.println("synchronizedThis");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
另外,类锁与实例锁不相互阻塞,但相同的类锁,相同的当前实例锁,相同的对象锁会相互阻塞。
synchronized不能被继承;
不能使用synchronized关键字修饰接口方法;
构造方法也不能用synchronized;
Monitor监视器
monitor 是一个同步工具,提供线程(进程)被阻塞和被唤醒的管理机制
在 jvm 中,每一个对象头都关联着 Monitor,每一个监视器和一个对象引用相关联,为了实现监视器的互斥功能,每个对象都管理者一把锁。
一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监事区域,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码。
monitor的基本元素
- 临界区--被 synchronized 修饰的方法、代码块
- monitor 对象以及锁
- 条件变量以及定义在 monitor 对象上的 wait signal 操作
monitor object 作用,保存被阻塞的队列,提供 wait() 和 notify() 进行阻塞和唤醒。java.lang.Object 都能作为 monitor object.
Java 对象存储在内存中,分为三个部分,对象头、实例数据、对齐填充,在对象头中,保存了锁标识(其中的重量级锁指的就是 synchronized 锁)。

当一个线程需要获取 Object 的锁时,会被放入 EntrySet 中进行等待,如果该线程获取到了锁,成为当前锁的 owner。如果根据程序逻辑,一个已经获得了锁的线程缺少某些外部条件,而无法继续进行下去(例如生产者发现队列已满或者消费者发现队列为空),那么该线程可以通过调用 wait 方法将锁释放,进入 wait set 中阻塞进行等待,其它线程在这个时候有机会获得锁,去干其它的事情,从而使得之前不成立的外部条件成立,这样先前被阻塞的线程就可以重新进入 EntrySet 去竞争锁。这个外部条件在 monitor 机制中称为条件变量。
守护线程
Java提供两种类型的线程:用户线程和守护程序线程
用户线程是高优先级的线程。JVM将在终止任务之前等待任何用户线程完成其任务。
守护线程是低优先级线程,其唯一作用是为用户线程提供服务。
守护线程对于后台支持任务非常有用,例如垃圾收集,释放未使用对象的内存一级从缓存中删除不需要的条目。大多数JVM线程都是守护线程。
创建守护线程
public class DaemonTest {
public static void main(String[] args){
DaemonThread daemonThread = new DaemonThread();
Thread thread = new Thread(daemonThread);
thread.setDaemon(true);
thread.start();
}
}
class DaemonThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().isDaemon());
}
}
任何线程都继承创建它的线程的守护进程状态。由于主线程是用户线程,因此在main方法内创建的任何线程默认为用户线程。