线程:synchronized原理及使用

1. synchronized作用

能够保证原子性的Atomic变量和保证可见性、有序性的 volatile关键字是轻量级的实现并发同步的方式,但是都存在着一定的局限,具体参考博文

synchronized是重量级的同步实现方式,synchronized 作用域中的代码是同步执行的。并发的情况下,执行到对同一个对象加锁的 synchronized 代码块时,多线程会转为串行执行的。这里注意,不是同一个同步代码块,而是对同一个对象上锁的同步代码块。这意味着范围更广。

此外 JMM关于synchronized的两条规定:

  1. 线程解锁前,必须把共享变量的最新值刷新到主内存中
  2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值

由此可见synchronized 可以确保可见性,在一个线程执行完 synchronized 代码后,所有代码中对变量值的变化都能立即被其它线程所看到。

2. synchronized使用

同步代码块

以下面的代码为例,假设存在一个队列对象 tasks,里面存放着需要执行的任务(即Runnable的实现类),

// 消费者run方法中的同步代码块
synchronized (tasks) {
    if (tasks.size() > 0) {
        task = tasks.removeFirst();
        sleep(100);
        tasks.notifyAll();
   } else {
        tasks.wait();
    }
}

// 生产者run方法中的同步代码块
synchronized (tasks) {
    if (tasks.size() < MAX) {
         Task task = new Task(new Random().nextInt(3) + 1, getPunishedWord());
        tasks.addLast(task);
        System.out.println(threadName + "留了作业,抄写" + task.getWordToCopy() + " " + task.getLeftCopyCount() + "次");
        tasks.notifyAll();
    } else {
        System.out.println(threadName+"开始等待");
        tasks.wait();
        System.out.println("teacher线程 " + threadName + "线程-" + name + "等待结束");
    }
}

synchronized (tasks) ,这行代码小括号里的 tasks 对象和 synchronized 实现的方式相关。小括号里的对象是可以是任意的对象。这个对象相当于是同步代码块的看门人,每个对其 synchronized 的线程,它都会记录下来,然后等到同步代码块没有线程执行的时候,它就会通知其它线程来执行同步代码块。所以并不是括号内的对象加锁,只是让它来维护秩序

例子中,并发的线程并不是同样类型的 Thread(因为存在生产者和消费者两种线程),一个是 Student,还一个是 Teacher。对于不同线程对象的同步控制,一定要选用两种线程都持有的对象才行。否则各自使用不同的对象,相当于聘用了两个看门人,各看各的门,毫无瓜葛。那么原本想要串行执行的代码仍旧会并行执行。

同步方法

对于非静态方法,

public synchronized void eat(){
	.......
  .......
}

同步方法的锁对象是this,等效于下面的代码块,

public void eat(){
	synchronized(this){
		.......
  	.......
	}
}

对于静态方法,

public static synchronized void eat(){
	.......
  .......
}

此时同步方法为类的 Class 对象。如果上述静态方法所在的类为 Test。那么锁对象就是 Test.class。

synchronized使用总结

  1. 选用一个锁对象,可以是任意对象
  2. 锁对象锁住的是同步代码块,而不是自己
  3. 不同类型的线程如果执行相同的同步代码,锁对象要使用所有线程共同持有的一个对象
  4. sychronized代码块可以同时满足并发的三个特性

3. synchronized原理

synchronized 的秘密其实都在同步对象上,这个对象就是一个看门人,每次只允许一个线程进来,进门后此线程可以做任何自己想做的事情,然后再出来。线程出来后,看门人会提示其它线程,总有个敏捷值最高的线程先冲入门内,那么其它线程只好继续等待。

每个对象Java都关联了一个 monitor lock。当一个线程获取了 monitor lock 后,其它线程如果运行到获取同一个 monitor 的时候就会被 block 住。当这个线程执行完同步代码,则会释放 monitor lock。在后一个线程获取锁后,happens-before 原则生效,前一个线程所做的任何修改都会被这个线程看到。

再深入底层一点,每个 Java 对象在 JVM 的对等对象的头中保存锁状态,指向 ObjectMonitor。该模式是JVM内部基于C++实现的,模式图如下,
image

  1. ObjectMonitor 保存了当前持有锁的线程引用
  2. EntryList 中保存目前等待获取锁的线程,WaitSet 保存 wait 的线程
  3. 此外还有一个计数器,每当线程获得 monitor 锁,计数器 +1,当线程重入此锁时,计数器还会 +1。当计数器不为0时,其它尝试获取 monitor 锁的线程将会被保存到EntryList中,并被阻塞
  4. 当持有锁的线程释放了monitor 锁后,计数器 -1。当计数器归位为 0 时,所有 EntryList 中的线程会尝试去获取锁,但只会有一个线程会成功,没有成功的线程仍旧保存在 EntryList 中。

当一个线程需要获取 Object 的锁时,会被放入 EntrySet 中进行等待,如果该线程获取到了锁,成为当前锁的 owner。如果根据程序逻辑,一个已经获得了锁的线程缺少某些外部条件,而无法继续进行下去(例如生产者发现队列已满或者消费者发现队列为空),那么该线程可以通过调用 wait 方法将锁释放,进入 wait set 中阻塞进行等待,其它线程在这个时候有机会获得锁,去干其它的事情,从而使得之前不成立的外部条件成立,这样先前被阻塞的线程就可以重新进入 EntrySet 去竞争锁。这个外部条件在 monitor 机制中称为条件变量。引自博文

4. synchronized注意事项

  1. synchronized 使用的为非公平锁,如果需要公平锁,可以使用 ReentrantLock,设置为公平锁
  2. 锁对象不能为 null。如果锁对象为 null,就不存在与其关联的 monitor 锁
  3. 只把需要同步的代码放入 synchronized 代码块。如果不思考,为了线程安全把方法中全部代码都放入同步代码块,那么将会丧失多线程的优势。再多的线程也只能串行执行,这完全违背了并发的初衷
  4. 只有使用同一个对象作为锁对象,才能同步。是同一个对象,而不是同一个类。有一种常犯的错误是,不同线程持有的是同一个类的不同实例。那么该对象实例用作锁对象的话,多个线程并不会同步。还一种错误是使用不同类的实例作为锁对象,但是期望不同位置的同步代码块能够同步执行
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值