详解Java并发编程之内置锁(synchronized)

简介

synchronized在JDK5.0的早期版本中是重量级锁,效率很低,但从JDK6.0开始,JDK在关键字synchronized上做了大量的优化,如偏向锁、轻量级锁等,使它的效率有了很大的提升。

synchronized的作用是实现线程间的同步,当多个线程都需要访问共享代码区域时,对共享代码区域进行加锁,使得每一次只能有一个线程访问共享代码区域,从而保证线程间的安全性。

因为没有显式的加锁和解锁过程,所以称之为隐式锁,也叫作内置锁、监视器锁。

如下实例,在没有使用synchronized的情况下,多个线程访问共享代码区域时,可能会出现与预想中不同的结果。

public class Apple implements Runnable {
 private int appleCount = 5;

 @Override
 public void run() {
  eatApple();
 }

 public void eatApple(){
  appleCount--;
  System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
 }

 public static void main(String[] args) {
  Apple apple = new Apple();
  Thread t1 = new Thread(apple, "小强");
  Thread t2 = new Thread(apple, "小明");
  Thread t3 = new Thread(apple, "小花");
  Thread t4 = new Thread(apple, "小红");
  Thread t5 = new Thread(apple, "小黑");
  t1.start();
  t2.start();
  t3.start();
  t4.start();
  t5.start();
 }
}

可能会输出如下结果:

小强吃了一个苹果,还剩3个苹果
小黑吃了一个苹果,还剩3个苹果
小明吃了一个苹果,还剩2个苹果
小花吃了一个苹果,还剩1个苹果
小红吃了一个苹果,还剩0个苹果

输出结果异常的原因是eatApple方法里操作不是原子的,如当A线程完成appleCount的赋值,还没有输出,B线程获取到appleCount的最新值,并完成赋值操作,然后A和B同时输出。(A,B线程分别对应小黑、小强)

如果改下eatApple方法如下,还会不会有线程安全问题呢?

public void eatApple(){
	System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + --appleCount + "个苹果");
}

还是会有的,因为--appleCount不是原子操作,--appleCount可以用另外一种写法表示:appleCount = appleCount - 1,还是有可能会出现以上的异常输出结果。

synchronized的使用

synchronized分为同步方法和同步代码块两种用法,当每个线程访问同步方法或同步代码块区域时,首先需要获得对象的锁,抢到锁的线程可以继续执行,抢不到锁的线程则阻塞,等待抢到锁的线程执行完成后释放锁。

1.同步代码块

锁的对象是object:

public class Apple implements Runnable {
 private int appleCount = 5;
 private Object object = new Object();

 @Override
 public void run() {
  eatApple();
 }

 public void eatApple(){
	//同步代码块,此时锁的对象是object
  synchronized (object) {
   appleCount--;
   System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
  }
 }

  //...省略main方法
}

2.同步方法,修饰普通方法

锁的对象是当前类的实例对象:

public class Apple implements Runnable {
 private int appleCount = 5;

 @Override
 public void run() {
  eatApple();
 }

 public synchronized void eatApple() {
  appleCount--;
  System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
 }

 //...省略main方法
}

等价于以下同步代码块的写法:

public void eatApple() {
	synchronized (this) {
		appleCount--;
		System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
	}
}

3.同步方法,修饰静态方法

锁的对象是当前类的class对象:

public class Apple implements Runnable {
 private static int appleCount = 5;

 @Override
 public void run() {
  eatApple();
 }

 public synchronized static void eatApple() {
  appleCount--;
  System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
 }

 //...省略main方法
}

等价于以下同步代码块的写法:

public static void eatApple() {
	synchronized (Apple.class) {
		appleCount--;
		System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
	}
}

4.同步方法和同步代码块的区别

a.同步方法锁的对象是当前类的实例对象或者当前类的class对象,而同步代码块锁的对象可以是任意对象。

b.同步方法是使用synchronized修饰方法,而同步代码块是使用synchronized修饰共享代码区域。同步代码块相对于同步方法来说粒度更细,锁的区域更小,一般锁范围越小效率就越高。如下情况显然同步代码块更适用:

public static void eatApple() {
	//不需要同步的耗时操作1
	//...
	synchronized (Apple.class) {
		appleCount--;
		System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
	}
	//不需要同步的耗时操作2
	//...
}

内置锁的可重入性

内置锁的可重入性是指当某个线程试图获取一个它已经持有的锁时,它总是可以获取成功。如下:

public static void eatApple() {
	synchronized (Apple.class) {
		synchronized (Apple.class) {
			synchronized (Apple.class) {
				appleCount--;
				System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
			}
		}
	}
}

如果锁不是可重入的,那么假如某线程持有了该锁,然后又需要等待持有该锁的线程释放锁,这不就造成死锁了吗?

synchronized可以被继承吗?

synchronized不可以被继承,如果子类中重写后的方法需要实现同步,则需要手动添加synchronized关键字。

public class AppleParent {
 public synchronized void eatApple(){

 }
}

public class Apple extends AppleParent implements Runnable {
 private int appleCount = 5;

 @Override
 public void run() {
  eatApple();
 }

 @Override
 public void eatApple() {
  appleCount--;
  System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
 }

 //...省略main方法
}

基于内置锁的等待和唤醒

基于内置锁的等待和唤醒是使用Object类中的wait()和notify()或notifyAll()来实现的。这些方法的调用前提是已经持有对应的锁,所以只能在同步方法或者同步代码块里调用。如果在没有获取到对应锁的情况下调用则会抛出IllegalMonitorStateException异常。下面介绍下相关的几个方法:

wait():使当前线程无限期地等待,直到另一个线程调用notify()或notifyAll()。

wait(long timeout):指定一个超时时间,超时时间过后线程将会被自动唤醒。线程也可以在超时时间之前被notify()或notifyAll()唤醒。注意,wait(0)等同于调用wait()。

wait(long timeout, int nanos):类似于wait(long timeout),主要区别是wait(long timeout, int nanos)提供了更高的精度。

notify():随机唤醒一个在相同锁对象上等待的线程。

notifyAll():唤醒所有在相同锁对象上等待的线程。

一个简单的等待唤醒实例:

public class Apple {
 //苹果数量
 private int appleCount = 0;

 /**
  * 买苹果
  */
 public synchronized void getApple() {
  try {
   while (appleCount != 0) {
    wait();
   }
  } catch (InterruptedException ex) {
   ex.printStackTrace();
  }

  System.out.println(Thread.currentThread().getName() + "买了5个苹果");
  appleCount = 5;
  notify();
 }

 /**
  * 吃苹果
  */
 public synchronized void eatApple() {
  try {
   while (appleCount == 0) {
    wait();
   }
  } catch (InterruptedException ex) {
   ex.printStackTrace();
  }

  System.out.println(Thread.currentThread().getName() + "吃了1个苹果");
  appleCount--;
  notify();
 }
}
/**
 * 生产者,买苹果
 */
public class Producer extends Thread{
 private Apple apple;

 public Producer(Apple apple, String name){
  super(name);
  this.apple = apple;
 }

 @Override
 public void run(){
  while (true)
  apple.getApple();
 }
}

/**
 * 消费者,吃苹果
 */
public class Consumer extends Thread{
 private Apple apple;

 public Consumer(Apple apple, String name){
  super(name);
  this.apple = apple;
 }

 @Override
 public void run(){
  while (true)
  apple.eatApple();
 }
}
public class Demo {
 public static void main(String[] args) {
  Apple apple = new Apple();
  Producer producer = new Producer(apple,"小明");
  Consumer consumer = new Consumer(apple, "小红");
  producer.start();
  consumer.start();
 }
}

输出结果:

小明买了5个苹果
小红吃了1个苹果
小红吃了1个苹果
小红吃了1个苹果
小红吃了1个苹果
小红吃了1个苹果
小明买了5个苹果
小红吃了1个苹果
    ......

到此这篇关于Java并发编程之内置锁(synchronized)的文章就介绍到这了,更多相关Java内置锁内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java并发包中CountDownLatch和线程池的使用详解

    1.CountDownLatch 现在做的这个华为云TaurusDB比赛中,参考的之前参加过阿里的PolarDB大赛的两个大佬的代码,发现都有用到CountDownLatch这个类,之前看代码的时候也看过,但是没有搞得很明白,自己写也写不出来,在此自己先学习一下. 字面理解:CountDownLatch:数量减少的门栓. 创建这样一个门栓 CountDownLatch countDownLatch = new CountDownLatch(count); 参数:count,门栓的计数次数. 在所

  • java迭代器移除元素出现并发修改异常的原因及解决

    迭代器(Iterator的对象)主要用于遍历集合,体现的就是迭代器模式. Iterator接口定义了以下四种方法. boolean hasNext():如果集合还没遍历完就返回true. Object next():返回集合里的下一个元素. void remove():删除集合里上一次next方法返回的元素. void forEachRemaining(Consumer action):这是java8新增的默认方法,可用Lambda表达式遍历数组. 使用迭代器遍历元素时不能不能通过Collect

  • Java 并发编程中如何创建线程

    简介 线程是基本的调度单位,它被包含在进程之中,是进程中的实际运作单位,它本身是不会独立存在.一个进程至少有一个线程,进程中的多个线程共享进程的资源. Java中创建线程的方式有多种如继承Thread类.实现Runnable接口.实现Callable接口以及使用线程池的方式,线程池将在后面文章中单独介绍,这里先介绍另外三种方式. 继承Thread类 优点:在run方法里可以用this获取到当前线程. 缺点:由于Java不支持多继承,所以如果继承了Thread类后就不能再继承其他类. public

  • Java工作中常见的并发问题处理方法总结

    问题复现 1. "设备Aの奇怪分身" 时间回到很久很久以前的一个深夜,那时我开发的多媒体广告播放控制系统刚刚投产上线,公司开出的第一家线下生鲜店里,几十个大大小小的多媒体硬件设备正常联网后,正由我一台一台的注册及接入到已经上线的多媒体广告播控系统中. 注册过程简述如下: 每一个设备注册到系统中后,相应的在数据库设备表中都会新增一条记录,来存储这个设备的各项信息. 本来一切都有条不紊的进行着,直到设备A的注册打破了这默契的宁静-- 设备A注册完成后,我突然发现,数据库设备表中,新增了两条

  • Java并发包之CopyOnWriteArrayList类的深入讲解

    前言 大家在学习Java的过程中,或者工作中,始终都绕不开集合.在单线程环境下,ArrayList就可以满足要求.多线程时,我们可以使用CopyOnWriteArrayList来保证数据安全.下面我们一起来看看CopyOnWriteArrayList类中的一些值得学习的方法. CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略实现的. 说明:代码部分,均基于JDK1.8 一.添加元素

  • 如何使用JCTools实现Java并发程序

    概述 在本文中,我们将介绍JCTools(Java并发工具)库. 简单地说,这提供了许多适用于多线程环境的实用数据结构. 非阻塞算法 传统上,在可变共享状态下工作的多线程代码使用锁来确保数据一致性和发布(一个线程所做的更改对另一个线程可见). 这种方法有许多缺点: 线程在试图获取锁时可能会被阻塞,在另一个线程的操作完成之前不会取得任何进展-这有效地防止了并行性 锁争用越重,JVM处理调度线程.管理争用和等待线程队列的时间就越多,实际工作就越少 如果涉及多个锁,并且它们以错误的顺序获取/释放,则可

  • java并发学习-CountDownLatch实现原理全面讲解

    CountDownLatch在多线程并发编程中充当一个计时器的功能,并且维护一个count的变量,并且其操作都是原子操作. 如下图,内部有下static final的Sync类继承自AQS. 该类主要通过countDown()和await()两个方法实现功能的,首先通过建立CountDownLatch对象,并且传入参数即为count初始值. 如果一个线程调用了await()方法,那么这个线程便进入阻塞状态,并进入阻塞队列. 如果一个线程调用了countDown()方法,则会使count-1:当c

  • Java利用Redis实现高并发计数器的示例代码

    业务需求中经常有需要用到计数器的场景:譬如一个手机号一天限制发送5条短信.一个接口一分钟限制多少请求.一个接口一天限制调用多少次等等.使用Redis的Incr自增命令可以轻松实现以上需求.以一个接口一天限制调用次数为例: /** * 是否拒绝服务 * @return */ private boolean denialOfService(String userId){ long count=JedisUtil.setIncr(DateUtil.getDate()+"&"+user

  • Java高并发BlockingQueue重要的实现类详解

    ArrayBlockingQueue 有界的阻塞队列,内部是一个数组,有边界的意思是:容量是有限的,必须进行初始化,指定它的容量大小,以先进先出的方式存储数据,最新插入的在对尾,最先移除的对象在头部. public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { /** 队列元素 */ final Object

  • 详解Java并发编程之内置锁(synchronized)

    简介 synchronized在JDK5.0的早期版本中是重量级锁,效率很低,但从JDK6.0开始,JDK在关键字synchronized上做了大量的优化,如偏向锁.轻量级锁等,使它的效率有了很大的提升. synchronized的作用是实现线程间的同步,当多个线程都需要访问共享代码区域时,对共享代码区域进行加锁,使得每一次只能有一个线程访问共享代码区域,从而保证线程间的安全性. 因为没有显式的加锁和解锁过程,所以称之为隐式锁,也叫作内置锁.监视器锁. 如下实例,在没有使用synchronize

  • 详解Java并发编程基础之volatile

    目录 一.volatile的定义和实现原理 1.Java并发模型采用的方式 2.volatile的定义 3.volatile的底层实现原理 二.volatile的内存语义 1.volatile的特性 2.volatile写-读建立的happens-before关系 3.volatile的写/读内存语义 三.volatile内存语义的实现 1.volatile重排序规则 2.内存屏障 3.内存屏障示例 四.volatile与死循环问题 五.volatile对于复合操作非原子性问题 一.volati

  • 详解Java并发编程之原子类

    目录 原子数组 AtomicIntegerArray 原子更新器 AtomicIntegerFieldUpdater 原子累加器 LongAdder 原子数组 原子数组有AtomicIntegerArray.AtomicLongArray.AtomicReferenceArray,主要是用来对数组中的某个元素进行原子操作.三个类的方法基本类似,这里只介绍一下AtomicIntegerArray的方法. AtomicIntegerArray 两个构造方法,第一个构造方法传入数组长度初始化一个所有值

  • 详解java并发编程(2) --Synchronized与Volatile区别

    1 Synchronized 在多线程并发中synchronized一直是元老级别的角色.利用synchronized来实现同步具体有一下三种表现形式: 对于普通的同步方法,锁是当前实例对象. 对于静态同步方法,锁是当前类的class对象. 对于同步方法块,锁是synchronized括号里配置的对象. 当一个代码,方法或者类被synchronized修饰以后.当一个线程试图访问同步代码块的时候,它首先必须得到锁,退出或抛出异常的时候必须释放锁.那么这样做有什么好处呢? 它主要确保多个线程在同一

  • 详解Java多线程编程中互斥锁ReentrantLock类的用法

    0.关于互斥锁 所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 而现在, Lock提供了比synchronized机制更广泛的锁定操作, Lock和synchronized机制的主要区别: synchronized机制提供了对与每个对象相关的隐式监视器锁的访问, 并强制所有锁获取和释放均要出现在一个块结构中, 当获取了多个锁时, 它们必须以相反的顺序释放. synchronized机制对锁的释放是

  • Java并发编程深入理解之Synchronized的使用及底层原理详解 上

    目录 一.线程安全问题 1.临界资源 2.线程安全问题 3.如何解决线程安全问题 二.synchronized使用介绍 三.synchronized实现原理 1.synchronized底层指令:monitorenter和monitorexit 2.Object Monitor(监视器锁)机制 一.线程安全问题 1.临界资源 多线程编程中,有可能会出现多个线程同时访问同一个共享.可变资源的情况,这个资源我们称之其为临界资源:这种资源可能是:对象.变量.文件等. 共享:资源可以由多个线程同时访问

  • Java并发编程深入理解之Synchronized的使用及底层原理详解 下

    目录 一.synchronized锁优化 1.自旋锁与自适应自旋 2.锁消除 逃逸分析: 3.锁粗化 二.对象头内存布局 三.synchronized锁的膨胀升级过程 1.偏向锁 2.轻量级锁 3.重量级锁 4.各种锁的优缺点 接着上文<Java并发编程深入理解之Synchronized的使用及底层原理详解 上>继续介绍synchronized 一.synchronized锁优化 高效并发是从JDK 5升级到JDK 6后一项重要的改进项,HotSpot虚拟机开发团队在这个版本上花费了大量的资源

  • 详解JAVA 函数式编程

    1.函数式接口 1.1概念: java中有且只有一个抽象方法的接口. 1.2格式: 修饰符 interface 接口名称 { public abstract 返回值类型 方法名称(可选参数信息); // 其他非抽象方法内容 } //或者 public interface MyFunctionalInterface { void myMethod(); } 1.3@FunctionalInterface注解: 与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解

  • 详解Java函数式编程和lambda表达式

    为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于某种语法或调用API去进行编程.例如,我们现在需要从一组数字中,找出最小的那个数字,若使用用命令式编程实现这个需求的话,那么所编写的代码如下: public static void main(String[] args) { int[] nums = new int[]{1, 2, 3, 4, 5,

  • 详解Java网络编程

    一.网络编程 1.1.概述 1.计算机网络是通过传输介质.通信设施和网络通信协议,把分散在不同地点的计算机设备互连起来,实现资源共享和数据传输的系统.网络编程就就是编写程序使联网的两个(或多个)设备(例如计算机)之间进行数据传输.Java语言对网络编程提供了良好的支持,通过其提供的接口我们可以很方便地进行网络编程. 2.Java是 Internet 上的语言,它从语言级上提供了对网络应用程 序的支持,程序员能够很容易开发常见的网络应用程序. 3.Java提供的网络类库,可以实现无痛的网络连接,联

随机推荐