本文主要对Java多线程同步与通信以及相关锁的介绍。

1 .Java多线程安全问题

Java多线程安全问题是实现并发最大的问题,可以说多线程开发其实就是围绕多线程安全问题开发,涉及之深,不是简简单单一两篇博客能够讲解清楚,如果想要更深层次认识多线程安全问题,需要自己查阅量更多资料,潜入书籍中去学习,作者和大家一样还在学习的路上。

先通过一个例子认识Java多线程安全问题。

 public class MyThread {

     public static int count = 0;

     public static void main(String[] args) {
// 保证所有线程执行完毕.
final CountDownLatch cdl = new CountDownLatch(10);
for (int i = 0; i < 10; i++)
new Thread() {
public void run() {
for (int j = 0; j < 100; j++) {
count++;
try {
Thread.sleep(10);
System.out.println(count);
} catch (InterruptedException e) { }
}
cdl.countDown();//cdl减1
}
}.start(); try {
cdl.await();//等待一段时间直到cdl等于0,继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("static count: " + count);
}
}

定义一个等于0的变量count,开启10条线程,每条线程循环100次对count进行自增,正确的结果count=1000,运行后控制台输出:

可以看出虽然最终结果正确,但控制台打印大量重复数据,再执行一次:

可以看出这次执行结果并不是正确结果,而控制台同样输出大量重复数据。以上两次执行,无论是最终结果与否都存在线程安全问题,正确的执行结果不但执行过程不能出现重复数据,而且最终结果也必须是正确的,这就是多线程安全问题。

为什么会出现多线程安全问题?问题出在哪里?究其根源发现之此处出现多线程安全问题是因为 count++并不是一个原子性操作,而是分为三步完成:(1) 从内存中读出count的值(2)执行加1操作 (3)重新对count赋值。只有经过这三步自增操作才完成,而在多线程环境下,可能出现第一条线程未完成赋值之前失去cpu时间片,第二条线程读取到的是第一条线程没有自增操作之前的数值,那么就会出现重复结果。

上例只是一个简单的线程安全问题,但其具有线程不安全的所有主要因素,结合本例对线程不安全问题可以归纳为以下主要原因:

a. 多线程环境(10条线程)

b. 多线程环境存在共享数据 (count变量)

c. 多线程对同一个共享数据操作  (count++)

为了保证线程安全,Java采用同步机制以及引入锁的概念对共享数据的操作进行原子性封装,保证共享数据在一段代码内只能被一条线程处理,这样就可以避免count++因非原子性操作带来的线程安全问题。而锁是用来决定哪条线程能够进入操作共享数据的那段代码(被称为同步代码块),再执行完同步代码块之后释放锁,下一个获取到锁的线程才能再次进入同步代码块,以上就是Java保证线程安全的简单思路。

Java提供了众多方式保证代码同步,最早出现,普遍使用就是关键字synchronized,它可以用来修饰方法和代码块,而synchronized修饰方法这种方式并不友好,它在保证线程安全的同时牺牲了效率,所以我们可以选择对共享数据操作的代码块使用synchronized修饰。synchronized需要配合锁的使用保证线程安全,锁具有互斥性,同时可分为对象锁和类锁,对象锁是指类的实例对象,synchronized修饰代码块时可以选取任意对象作为锁,修饰非静态方法时锁为类的实例对象,这两种锁均被成为对象锁;修饰静态方法时的锁为类的class对象,此时锁被称为类锁,两种锁均为互斥锁,但在某些方面具有不同的用途。

使用synchronized对上例进行改造,保证线程安全的代码如下:

 public class MyThread {

     public static volatile int count = 0;

     private static final Object lock = new Object();

     public static void main(String[] args) {
// 保证所有线程执行完毕.
final CountDownLatch cdl = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
//加锁
new Thread() {
public void run() {
synchronized (lock) {
for (int j = 0; j < 100; j++) {
count++;
try {
Thread.sleep(100);
System.out.println(count);
} catch (InterruptedException e) {
//异常处理
}
}
}
cdl.countDown();//cdl减1
}
}.start();
}
try {
cdl.await();//等待一段时间直到cdl等于0,继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("static count: " + count);
}
}

再次执行结果为:

可以看出与正确结果一致,此时多线程是安全的。

2 .synchronized的性能优化

synchronized作为官方推荐使用的同步关键字,其重要性不言而喻。最初synchronized的性能效率比较差,是不折不扣的重量级锁,但随着版本升级经过数次变革synchronized性能逐渐优化,我们来看下synchronized是怎么一步步优化的。

synchronized字面意思同步的,重量级锁,对比Lock来说又是隐式锁,为什么成为隐式锁呢?上例实现代码同步的过程可以发现,我们知道加锁的位置,但并没有看到代码执行完毕释放锁的位置,这就是synchronized的特点,不需要开发人员关心在哪里释放锁,什么时候时候释放锁,synchronized自动完成锁的释放,而Lock锁则需要手动加锁和释放锁,所以其常被称为显式锁。还需要了解的是synchronized是JVM层面的,是作为一个关键字供开发使用的,而Lock则是JDK层面的,是作为一个接口供开发使用,大概这就是为什么官方推荐使用synchronized的原因吧。

重量级锁

synchronized为什么是重量级锁?这是因为锁的实现是依赖底层的监视器(暂不了解),监视器依赖操作系统底层的互斥锁,Java线程状态是内核态(操作系统)的映射,是Java特有的模型。当线程没有获取到锁,那么必将发生内核态和用户态(可以理解Java线程状态)的转换,操作系统线程状态转换的成本是很高的,所以synchronized效率比较低,被称为重量级锁。

当前版本synchronized锁的状态共有四种 :

  • 无锁
  • 偏向锁
  • 轻量级锁
  • 重量级锁

很显然锁的性能从上到下依次降低,轻量级锁和偏向锁是为了尽可能的向无锁状态靠拢,尽可能减少重量。在介绍锁的状态之前需要了解两个概念Mark Word和CAS.

Markword

Java对象实例是由对象头和实例数据组成,对象头是由Markword和类型指针组成,如果为数组还会包括数组长度。简单理解对象头就是为了保存对象的一些必要信息,而Markword就是一种数据结构用来保存数据,随着JVM数位的不同,Markword也分为32bit和64bit。为了节省空间并不是每个字段都有空间,锁的状态不同,字段的含有也不相同。比如说32位的Markword,这几位是干什么的,别的几位是干什么的都代表不同的含义,在这里我们仅仅需要了解不同的锁状态在Markword中会记录不同的字段信息。

锁标志位(Markword字段):他的标志位包括 无锁、偏向锁、轻量级锁、重量级锁

轻量级锁时会记录:指向栈中锁记录的指针

重量级锁时会记录:指向重量锁的指针

偏向锁时记录:线程ID

CAS

compareAndSwap,比较与替换,它是一种实现并发算法常用的技术,CAS需要三个参数:内存地址V、旧的预期值A、即将更新的目标值B 。 当你对一个变量进行操作时,变量的初始值为A,你想要将它修改为B,当你修改后没有重新赋值之前,它会再次确定此时变量是否仍为A,如果是,那么完成修改,此时变量为B;如果不是,说明变量已经被修改为C,那么将对修改后的变量重新操作,以此循环。需要注意的是在此过程中并没有加锁,所以没有互斥访问但是能保证数据安全,可以理解为CAS只是逻辑上的加锁,避免了真正加锁带来的效率问题。这是CAS的核心理论,同时也是轻量级锁的底层实现。

 轻量级锁

上面已经说过轻量级锁的实现是基于CAS操作,对于竞争不激烈的场景下,可以减少重量级所得使用。

线程需要访问同步代码块时,会判断当前状态是否时无锁状态。如果无锁,尝试通过CAS操作,复制一份Mark Word并且将锁标记位修改位指向当前线程中锁记录的指针

--修改成功,说明没有竞争,那么执行同步代码块

--修改失败,说明存在竞争,那么锁会升级为重量级锁,Mark Word修改为指向重量级锁指针,此后请求锁的线程会被堵塞。

当持有锁的线程执行结束后,会再次借助CAS操作恢复Mark Word:

--恢复成功,说明此次CAS操作成功,锁释放完成

--恢复失败,说明仍存在竞争,锁升级为重量级锁,修改Mark Word字段后,释放锁并且唤醒被堵塞的线程

对于轻量级锁,核心就是CAS操作,通过比对Mark Word中锁标记位的新值和旧值后操作,CAS操作失败说明存在竞争,会自动升级为重量级锁,其他请求锁的线程被堵塞,该线程执行结束后唤醒其他堵塞线程。

偏向锁

对于轻量级锁,需要对Mark Word中复制的字段进行维护,已经多次CAS操作,但当场景中只有一条线程来回访问,那么轻量级锁的维护相对来说也没必要了,这样做也不是最优方式,而偏向锁就是一种优化方案。

对于这种不但没有竞争而且总是一条线程来回访问,锁会偏向于这条线程,这也是偏向的概念,它的核心思想就是:锁会偏向第一个获取它的线程,如果不存在竞争,只有一个线程,则持有偏向锁的线程永远不需要同步。如果没有竞争,可以看到出来,偏向锁可以约等于是无锁。

原理:当线程访问同步代码块,会记录存储锁偏向的线程ID,后续该线程在进入和退出时不再需要CAS操作进行加锁和解锁,只需简单地判断一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果不是当前线程ID,继续执行CAS操作,一旦CAS失败,锁会自动升级,然后执行同步代码块;如果成功,还是执行同步代码块。

自旋性、适应性自旋

所谓自旋,不是获取不到锁就堵塞,而是原地等待一会(时长和次数有限),再次尝试,以牺牲CPU为代价换取内核态和用户态转换的开销。

适应性自旋则对自旋的限制,比如时长(或者次数限制)的一种优化,如果本次自旋成功,下次可以多等待一会,如果经常自旋失败,那就不需要自旋,直接堵塞。借助于适应性自旋,可以在CPU时间片的损耗和内核状态的切换开销之间相对的找到一个平衡,进而能够提高性能,从原来的一旦获取不到就阻塞、状态切换,转变为在有的时候可以借助于较小的CPU浪费避免状态切换的开销,所以显然可以提升性能。

锁消除

锁消除是指删除非必要的同步,根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,没必要加锁。

比如方法A,调用B方法,B将内部创建的局部对象返回给A,那么这个局部变量就属于逃逸,存在被其他线程操作的可能。而锁消除是一种通过算法,将没有必要实现同步的代码消除synchronized取消同步。实际上JDK提供的方法,别人的jar包中有很多代码用到synchronized,所以你的代码中synchronized远比你想象中的多,锁消除就显得尤为重要了。

锁粗化

     如一个A方法,中有三个对象b,c,d,分别调用他们的方法而且都是同步方法
      void A(){
        b.function();
        c.function();
        d.function();
    }  
    每个方法都加锁和解锁,是不是很烦很费电!如果他们碰巧使用的是同一把锁,其实大可将他们合并,减少加锁和解锁操作。也就是说,虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,如此必会减少加锁和解锁带来的消耗。
 
    结束:
      以上就是synchronized优化过的地方,从最初的重量级锁,这会小青年经历一次次优化已经成为一位可以独当一面的领袖,而且它自身有很多优势,比如隐式锁带来的方便,所以我们没有必要放弃使用它,除非场景特殊,或者对程序分析后,业务适合,否则尽可能的选择synchronized吧!

从零开始学习Java多线程(三)的更多相关文章

  1. 从零开始学习Java多线程(二)

    前面已经简单介绍进程和线程,为后续学习做铺垫.本文讨论多线程传参,Java多线程异常处理机制. 1. 多线程的参数传递 在传统开发过程中,我们习惯在调用函数时,将所需的参数传入其中,通过函数内部逻辑处 ...

  2. 从零开始学习Java多线程(一)

    1. 什么是进程? 对其概念需要自行goole,简单理解就是:进程是计算机系统进行资源分配和调度的基本单位,是正在运行程序的实体:每一个进程都有它自己的内存空间和系统资源:进程是线程的容器.如:打开I ...

  3. 从零开始学习java一般需要多长时间?

    从零开始学习java一般需要多长时间? 其实学java一般要多久?因人而异,例如一个零基础的小白自学java,每天学习8个小时来算,而且在有学习资料的基础上,每天学习,从零到找到工作,起码要半年起步, ...

  4. 从零开始学习JAVA(入门基础)

    目录 博主从零开始学习JAVA(入门基础) 1.搭建JAVA开发环境 卸载JDK(未安装的请忽略) 安装JDK 2.编程语言中,何为编译型与解释型 编译型 解释型 3.第一个JAVA应用程序 4.JA ...

  5. java 多线程三

    java 多线程一 java 多线程二 java 多线程三 java 多线程四 注意到 java 多线程一 中 MyThread2 运行结果出现0.-1,那是因为在操作共享数据时没有加锁导致. 加锁的 ...

  6. 从火箭发场景来学习Java多线程并发闭锁对象

    从火箭发场景来学习Java多线程并发闭锁对象 倒计时器场景 在我们开发过程中,有时候会使用到倒计时计数器.最简单的是:int size = 5; 执行后,size—这种方式来实现.但是在多线程并发的情 ...

  7. java多线程三之线程协作与通信实例

    多线程的难点主要就是多线程通信协作这一块了,前面笔记二中提到了常见的同步方法,这里主要是进行实例学习了,今天总结了一下3个实例: 1.银行存款与提款多线程实现,使用Lock锁和条件Condition. ...

  8. Java多线程——<三>简单的线程执行:Executor

    一.概述 按照<Java多线程——<一><二>>中所讲,我们要使用线程,目前都是显示的声明Thread,并调用其start()方法.多线程并行,明显我们需要声明多个 ...

  9. java多线程(三)-Executors实现的几种线程池以及Callable

    从java5开始,类库中引入了很多新的管理调度线程的API,最常用的就是Executor(执行器)框架.Executor帮助程序员管理Thread对象,简化了并发编程,它其实就是在 提供了一个中间层, ...

随机推荐

  1. VC6中函数点go to definition报告the symbol XXX is undefined

    删除Debug中的bsc文件,再重建所有文件即可,在该函数处点击go to definition会提示重建.bsc文件,如果不行,多操作几次.

  2. 三极管(如NPN)集电极正偏 发射极反偏会怎么样呢? 电流会倒流吗? 其他三种都知道,就是不知道这种情况

    三极管除了你知道的放大,饱和和截止三种工作状态之外,还有一种用得极少的“倒置”工作状态,就是集电结正偏发射结反偏,这时跟对比放大状态的发射结正偏集电结反偏来理解,“倒置状态”的集电结,发射结分别充当了 ...

  3. PowerDesigner 15的Table表视图的列显示Code

    PowerDesigner 15的图表的Table表视图一般显示成这样: 现在,我要将Code显示到Table表视图上,该怎么做?选择菜单:Tools→Display Preferences,弹出对话 ...

  4. Java -- 构造函数 & this & 方法重写和方法重载的区别

    JAVA: 今天总结一下构造方法.关键字.方法重载和方法重写的异同   一.构造方法(构造函数)1.构造方法的作用:一是创建对象时调用构造方法创建对象,二是可以初始化多个属性 [学生类创建一个学生对象 ...

  5. 【Access】数据库四门功课--[增删改查]基础篇

    一.增 以userinfo为例 1.增加一条完整的数据 INSERT INTO userinfo VALUES (1, 2, 3, 4); 基本格式:INSERT INTO AAA VALUES (X ...

  6. centos7.4卸载再安装mariadb服务无法启动问题

    今天yum安装MariaDB完成后,启动服务时一直报以下错误 Job for mariadb.service failed. See ‘systemctl status mariadb.service ...

  7. 对比剖析Swarm Kubernetes Marathon编排引擎

    Docker Native Orchestration 基本结构 Docker Engine 1.12 集成了原生的编排引擎,用以替换了之前独立的Docker Swarm项目.Docker原生集群(S ...

  8. 使用 lsyncd 同步文件

    https://unix.stackexchange.com/questions/307046/real-time-file-synchronization https://github.com/ax ...

  9. ECS之Git服务器搭建

    最简教程 ### . 安装Git 安装Git服务,命令如下: ```Shell $ yum install curl-devel expat-devel gettext-devel openssl-d ...

  10. Xcode9,cocoaPod执行pod install时报错,一行命令即可解决。