在上一篇博客里讲解了JAVA的线程的内存模型,见:JAVA并发编程2_线程安全&内存模型,接着上一篇提到的问题解决多线程共享资源的情况下的线程安全问题。

不安全线程分析

public class Test implements Runnable {private int i = 0;private int getNext() {return i++;}@Overridepublic void run() { // synchronizedwhile (true) {synchronized(this){if(i<10){System.out.println(getNext());}elsebreak;}}}public static void main(String[] args) {Test t = new Test();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();Thread.yield();}
}

与之前的代码的区别在于run方法被synchronized关键字修饰。

根据上一篇博客的分析:多线程在访问共享资源的时候由于CPU轮流给每个任务分配其占用的时间,而CPU的调度是随机的,因此就会发生某个线程正在访问该变量的时候CPU却将时间片分发给了其他的线程,这样就会发生这样的现象:一个线程从主内存读取到某个变量的值还没来得及修改(或者修改后刷新主内存),另一个线程就获得了CPU的执行权,也从主内存读取改变量的值。当CPU执行权再次回到第一个线程的时候会接着之前的中断处执行(修改变量等),执行权回到第二个线程时却不能看到第一个线程中改变了的值。归结起来就是说违背了线程内存的可见性。避免上看起来产生第一种输出的可能顺序如下图所示(实际上可能的情况非常多,因为i++不是单个的原子操作):

i++对应下面的JVM指令,因此在期间另一个线程都可能会修改这个变量。

4: aload_0

5: iconst_0

6: putfield      #2                  // Field i:I

为了体现内存的可见性,synchronized关键字能使它保护的代码以串行的方式来访问(同一时刻只能由一个线程访问)。保证某个线程以一种可预测的方式来查看另一个线程的执行结果。

线程同步

JAVA提供的锁机制包括同步代码块和同步方法。

每个Java对象都可以用做一个实现同步的锁,这些所成为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock),一个线程进入同步带吗快之前会自动获得锁,并且推出同步带吗快时自动释放锁。获得内置锁的位移途径就是进入由这个锁保护的同步代码块或方法并且该锁还未被其他线程获得。

Java内置锁相当于互斥体(互斥锁),意味着最多有一个线程持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果线程B永远不释放锁,那么线程A将永远等待下去。

每次只能有一个线程执行内置锁保护的代码块,因此这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码块时也不会相互干扰。

原子性的含义:一组语句作为一个不可分割的单元被执行。任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行由同一个锁保护的同步代码块。

千万注意:并不是说synchronized代码块或者synchronized方法是不可分割的整体,是原子的,因为,显然使用不同锁的话之间不存在互斥关系。

买票例子的引入

下面是模拟火车站卖票的程序,理论上是要将编号为1-10的票卖按照由大到小顺序卖出去,结果用两个窗口(线程)卖就出现了这样的结果,有些编号的票卖了两次,有些没卖出去,并且还有编号为0的票卖了出去。显然结果错误的。

public class Test implements Runnable {private int i = 10;private void sale(){while (true) {if (i >0){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread() + "正在卖第" + i + "张票");i--;} else
break;}}@Overridepublic void run() {sale();}public static void main(String[] args) {Test t = new Test();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();Thread.yield();}
}

出现这种结果的原因就是没有对多个线程共同访问的资源进行同步加锁。下面我们对其进行线程同步,达到想要的效果:

synchronized代码块:

synchronized (lock){//同步的代码}

lock必须是一个引用类型的变量。

使用synchronized同步代码块:

public class Test implements Runnable {private int i = 10;private void sale(){Object o = new Object();while (true) {synchronized(o){if (i >0){System.out.println(Thread.currentThread() + "正在卖第" + i + "张票");i--;}else
break;          }}}@Overridepublic void run() {sale();}public static void main(String[] args) {Test t = new Test();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();Thread.yield();}
} 

咦?使用了同步代码块了怎么结果还是不对呢??我们先看正确的同步:

public class Test implements Runnable {private int i = 10;Object o = new Object();// 通常使用:/*static*/ byte[] lock = new byte[0];private void sale(){while (true) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}synchronized(o){if (i >0){System.out.println(Thread.currentThread() + "正在卖第" + i + "张票");i--;}elsebreak;}}}@Overridepublic void run() {sale();}public static void main(String[] args) {Test t = new Test();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();Thread.yield();}

这里线程同步的原理是怎样的呢?因为任何一个Java对象都可以作为一个同步锁,上面代码的对象o就是一个同步锁。

一个线程执行到synchronized代码块,线程尝试给同步锁上锁,如果同步锁已经被锁,则线程不能获取到锁,线程就被阻塞;如果同步锁没被锁,则线程将同步锁上锁,并且持有该锁,然后执行代码块;代码块正常执行结束或者非正常结束,同步锁都将解锁。

所以线程执行同步代码块时,持有该同步锁。其他线程不能获取锁,就不能进入同步代码块(前提是使用同一把锁),只能等待锁被释放。

这时候回头看上上段代码中的同步代码块,由于两个线程使用的锁是不一样的(创建了两个对象),因此,就算线程A在执行同步代码块,当线程2获得CPU执行权时,检查到这个锁并未被其他线程锁定,因此不具有互斥性,不能达到线程同步的效果。

同步方法

将synchronized作为关键字修饰类的某个方法,这样该方法就变成了同步方法。

直接将sale函数改为synchronized方法的结果是虽然卖票不会乱序,但是只有一个线程在卖票。所以稍微做些调整:

public class Test implements Runnable {private int i = 10;private void sale(){while (true) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}f();}}private synchronized void f(){if (i >0){System.out.println(Thread.currentThread() + "正在卖第" + i + "张票");i--;}elsereturn;}@Overridepublic void run() {sale();}public static void main(String[] args) {Test t = new Test();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();Thread.yield();}
}

这时候的锁是哪个对象呢?

当修饰的方法是类方法时同步锁是该类对应的Class对象;

当修饰普通方法时,该同步锁是当前对象即this。

体会:不要滥用synchronized方法

在平时的编程中为了达到线程同步的目的,在不经认真思考的情况下,经常发生synchronized关键字的滥用,归根结底是没有理解同步的原理本质。

看下面的代码:

public class Test implements Runnable{     @Override  public void run() {  f();}  public synchronized void f(){  System.out.println(this);}public static void main(String[] args) {  Test t1=new Test();  Test t2=new Test();  // f()里面的代码无法达到同步的目的new Thread(t1).start();  new Thread(t2).start();  }
}
//Output
//Test@2073b879
//Test@d542094

根据打印的结果也可以看出来函数f()是无法同步的,因为这两个线程使用了两个同步锁。这就告诉我们,并不要看到一个方法是synchronized的就想当然的认为它是同步方法就在不同的线程里随便调用。

注:上面的代码里面多次使用到了Thread.sleep(long)方法,是让当前线程睡眠一会,这个方法会让当前线程放弃CPU的执行权,处于Time Waiting状态,CPU不在为其分配时间片。由于机器的不同可能不容易出现我们期望的线程切换,目这样做就可以强制的让线程切换。

另外,在synchronized代码里面使用sleep无效。因为该线程sleep后CPU不在为其分配时间片,但是这个时候线程已经拿到了同步锁,即使睡到天荒地老,它也不会把同步锁交出去,别的线程得到了CPU执行却却苦于没有同步锁而被拒之门外。后面学习线程的状态会讲到这些。

会写代码不一定理解了,理解了不一定能给别人讲清楚。想把一个东西用文字表述清楚真的挺不容易。

转载于:https://www.cnblogs.com/qhyuan1992/p/5385310.html

JAVA并发编程3_线程同步之synchronized关键字相关推荐

  1. Java并发编程之线程同步

    线程安全就是防止某个对象或者值在多个线程中被修改而导致的数据不一致问题,因此我们就需要通过同步机制保证在同一时刻只有一个线程能够访问到该对象或数据,修改数据完毕之后,再将最新数据同步到主存中,使得其他 ...

  2. Java 并发编程中使用 ReentrantLock 替代 synchronized 关键字原语

    标签: Java 5 引入的 Concurrent 并发库软件包中,提供了 ReentrantLock 可重入同步锁,用来替代 synchronized 关键字原语,并可提供更好的性能,以及更强大的功 ...

  3. Java并发编程学习笔记——volatile与synchronized关键字原理及使用

    Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令. 一.vo ...

  4. 【Java 并发编程】线程简介 ( 原子操作 | volatile 关键字使用场景 )

    文章目录 一.原子操作 二.volatile 关键字使用场景 一.原子操作 原子操作 : read : 从 主内存 中的线程共享变量中读取数据 ; load : 将从主内存读取到的数据 , 加载到 线 ...

  5. Java并发编程:线程的同步

    <?xml version="1.0" encoding="utf-8"?> Java并发编程:线程的同步 Java并发编程:线程的同步 Table ...

  6. 19、Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

    Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...

  7. java并发编程与线程安全

    2019独角兽企业重金招聘Python工程师标准>>> 什么是线程安全 如果对象的状态变量(对象的实例域.静态域)具有可变性,那么当该对象被多个线程共享时就的考虑线程安全性的问题,否 ...

  8. 【Java 并发编程】线程池机制 ( ThreadPoolExecutor 线程池构造参数分析 | 核心线程数 | 最大线程数 | 非核心线程存活时间 | 任务阻塞队列 )

    文章目录 前言 一.ThreadPoolExecutor 构造参数 二.newCachedThreadPool 参数分析 三.newFixedThreadPool 参数分析 四.newSingleTh ...

  9. 【Java 并发编程】线程池机制 ( 线程池示例 | newCachedThreadPool | newFixedThreadPool | newSingleThreadExecutor )

    文章目录 前言 一.线程池示例 二.newCachedThreadPool 线程池示例 三.newFixedThreadPool 线程池示例 三.newSingleThreadExecutor 线程池 ...

最新文章

  1. 最新数据:一图看清全球393家独角兽公司
  2. POJ3522Slim Span(最大边与最小边差值最小的生成树)
  3. Python中os库的使用
  4. PCGen的垃圾收集分析
  5. python迭代器使用_Python迭代器的用法
  6. .net redis定时_一场由fork引发的超时,让我们重新探讨Redis的抖动问题
  7. 鲜花海报,文字与花儿碰上的时候,美妙
  8. 配置iscsi服务器_在Windows Server 2016上安装和配置iSCSI目标服务器
  9. codewars--js--Happy numbers++无穷大判断
  10. LeetCode【1051. 高度检查器】
  11. 计算机定时关机教程,电脑定时关机怎么设置|如何让电脑定时关机
  12. JEECG框架创建项目使用步骤
  13. 晨魅--高拍仪二次开发
  14. m.微博各种接口分析
  15. 全网最细最全OLAP之clickhouse笔记|clickhouse文档|clickhouse揭秘文档(三)--clickhouse单机安装和clickhouse集群安装
  16. 罗赛塔石碑Rosetta Stone安装教程
  17. 如何群发邮件不进垃圾邮箱?群发邮件进了垃圾箱怎么办?
  18. 【深度学习中模型评价指标汇总(混淆矩阵、recall、precision、F1、AUC面积、ROC曲线、ErrorRate)】
  19. 14届蓝桥杯青少组选拔赛C++_2022.11.27
  20. php 带http的域名,php提取URL中的域名部分

热门文章

  1. leetcode 高薪_LeetCode 第 125 号问题:验证回文串
  2. Chapter7-8_Deep Learning for Constituency Parsing
  3. LeetCode 6061. 买钢笔和铅笔的方案数
  4. MapReduce 计算框架如何运作
  5. 01.神经网络和深度学习 W2.神经网络基础
  6. LeetCode MySQL 1511. Customer Order Frequency
  7. LeetCode 1441. 用栈操作构建数组
  8. 程序员面试金典 - 面试题 16.09. 运算(只用+法做乘除)
  9. php unicode 插入 mysql_关于MySQL的一些骚操作——提升正确性,抠点性能
  10. 7.排序、聚合函数、分组查询