Java学习之synchronized关键字

前言一、线程基础知识1.1 进程 · 线程 · 多线程1.2 多线程实现的方式二、synchronized 关键字2.1 实例变量与线程安全2.2 synchronized 使用场景2.3 synchronized 同步方法2.3.1 方法内的变量为线程安全(代码详见class211)2.3.2 实例变量非线程安全(代码详见class211)2.3.3 多个对象多个锁(代码详见class213)2.3.4 synchronized 方法与锁对象(代码详见class214)2.3.5 脏读(代码详见class215)2.3.6 synchronized锁重入(代码详见class216)2.3.7 出现异常,锁自动释放(代码详见class217)2.3.8 同步不具有继承性(代码详见class218)2.4 synchronized同步语句块2.4.1 synchronized方法的弊端(代码详见class221)2.4.2 synchronized同步代码块的使用(代码详见class222)2.4.3 用同步代码块解决同步方法的弊端(代码详见class223)2.4.4 一半异步,一半同步(代码详见class224)2.4.5 synchronized代码块间的同步性(代码详见class225)2.4.6 验证同步synchronized(this)代码块是锁定当前对象的(代码详见class226)2.4.7 将任意对象作为对象监视器(代码详见class227)2.4.8 细化验证3个结论(代码详见class228)2.4.9 静态同步synchronized方法与synchronized(class)代码块2.4.10 数据类型String的常量池特性2.4.11 同步synchronized方法无限等待与解决2.4.12 多线程的死锁2.4.13 内置类与静态内置类2.4.14 内置类与同步:实验12.4.15 内置类与同步:实验22.4.16 锁对象的改变三、volatile 关键字3.1 volatile 关键字与死循环3.2 解决同步死循环3.3 解决异步死循环3.4 volatile 非原子的特性3.5 使用原子类进行i++操作3.6 原子类也并不完全安全3.7 synchronized 代码块有volatile同步的功能四、作业

前言

本人主要是结合《Java多线程编程核心技术》这本书的第二章内容,对synchronized关键字的知识进行梳理,其中会把比较抽象的概念通过生活上的例子进行说明,以便更好理解。在讲解synchronized关键字的知识点之前,先来总结线程的一些基础知识。

一、线程基础知识

1.1 进程 · 线程 · 多线程

进程:通俗点来讲,就是“Windows任务管理器”中的列表里面运行在内存中的.exe文件。一个进程,至少包含一条线程。

线程:进程中独立运行的子任务,比如QQ.exe运行时,就有很多子任务在同时进行,显示界面、显示歌词、发出声音和播放广告等这些都是线程完成的。程序执行的一条路径,一条线程肯定会被包含在一个进程中。

多线程:QQ.exe运行时,就有很多子任务在同时进行,显示界面、显示歌词、发出声音和播放广告等这些都是线程完成的。这些不同的多条线程是异步的,而且CPU分配给每一条线程的时间,具备随机性。

单线程与多线程:通过下载的例子说明,比如,第一个人来下载,那么我这个服务员器给你提供下载服务,有可能花费30分钟,如果是单线程的话,那么几百人来下载,会发生这样的情况:第一个人先来下载,等下载完成后,第二个人才能下载,等下载完成后,第三个人才能下载。而如果是多线程,是几百条线程为每一个人提供下载服务,互不干扰,这样效率大大提升。

1.2 多线程实现的方式

方式一:继承Thread

  1. 自定义class继承Thread
  2. 重写run方法
  3. 把新线程要做的事写在run方法中
  4. 创建线程对象
  5. 开启新线程,内部会自动执行run方法
public class Demo2_Thread {
/**
* @param args
*/

public static void main(String[] args) {
MyThread mt = new MyThread(); //4,创建自定义类的对象
mt.start(); //5,开启线程
for(int i = 0; i < 3000; i++) {
System.out.println("bb");
}
}
}
class MyThread extends Thread { //1,定义类继承Thread
public void run() { //2,重写run方法
for(int i = 0; i < 3000; i++) { //3,将要执行的代码,写在run方法中
System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
}
}
}
复制代码

方式二:实现Runnable

  1. 自定义class实现Runnable接口
  2. 实现run方法
  3. 把新线程要做的事写在run方法中
  4. 创建自定义的Runnable的子类对象
  5. 创建Thread对象,传入Runnable
  6. 调用start()开启新线程,内部会自动调用Runnable的run()方法
public class Demo3_Runnable {
/**
* @param args
*/

public static void main(String[] args) {
MyRunnable mr = new MyRunnable(); //4,创建自定义类对象
//Runnable target = new MyRunnable();
Thread t = new Thread(mr); //5,将其当作参数传递给Thread的构造函数
t.start(); //6,开启线程
for(int i = 0; i < 3000; i++) {
System.out.println("bb");
}
}
}
class MyRunnable implements Runnable { //1,自定义类实现Runnable接口
@Override
public void run() { //2,重写run方法
for(int i = 0; i < 3000; i++) { //3,将要执行的代码,写在run方法中
System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
}
}
}
复制代码

二、synchronized 关键字

2.1 实例变量与线程安全

自定义线程类中的实例变量针对其他线程可以有共享与不共享之分。

不共享数据:每条线程都对各自的实例变量进行数据操作,互不干扰,不会产生线程安全问题。

共享数据:多条线程访问同一个实例变量,比如实现投票功能的软件时,多条线程可以同时处理同一个人的票数。如果是i++、i--的操作,那么一定会出现非线程安全问题。

非线程安全:主要是指多条线程对同一个对象中的同一个实例变量进行操作时出现值被更改、值不同步的情况,进而影响程序的执行流程。

2.2 synchronized 使用场景

经典示例:5个销售员,每个销售员卖出一个货品后不可以得出相同的剩余数量,必须在每一个销售员卖完一个货品后其他销售员才可以在新的剩余物品数上继续减1操作,这时就需要多个线程之间进行同步,也就是用按顺序排队的方式进行减1操作。

public class MyThread extends Thread {
private int count = 5;
@Override
synchronized public void run() {
super.run();
count--;
System.out.println("由 " + this.currentThread().getName() + " 计算,count= " + count);
}
}
复制代码

通过在run方法前加入synchronized关键字,使多条线程在执行run方法时,以排队的方式进行处理。当一条线程调用run方法前,先判断run方法有没有被上锁,如果上锁,说明有其他线程正在调用方法,必须等其他线程对run方法调用结束后才可以执行run方法。这样实现了排队调用run方法的目的,也达到了按顺序对count变量减1的效果。synchrinized可以在任意对象及方法上枷锁,而加锁的这段代码称为“互斥区”或“临界区”。当一条线程想要执行同步方法里面的代码时,线程首先会尝试去拿这把锁,如果能拿到这把锁,那么这个线程可以执行synchronized里面的代码。如果不能拿到这把锁,那么这条线程就会不断地尝试拿这边锁,直到能够拿到为止,而且是有多条线程同时去争抢这把锁。通俗点来讲就是,一群人等着上洗手间,那么你总要等到里面的人出来,并且抢在其他人之前进到洗手间的门(拿到锁),才可以方便吧。

2.3 synchronized 同步方法

  • “非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据其实是被更改过的。
  • “线程安全”是以获得的实例变量的值是经过同步处理的,不会出现脏读的现象。
2.3.1 方法内的变量为线程安全(代码详见class211)
  • 方法中的变量不存在非线程安全问题,永远都是线程安全的,这是因为方法内部的变量是私有的特性造成的。
2.3.2 实例变量非线程安全(代码详见class211)
  • 如果多个线程共同访问一个对象中的实例变量,则有可能出现“非线程安全”问题,有可能出现覆盖的情况,但是访问同一个对象中的同步方法(在方法前使用关键字synchronized)时一定是线程安全的,例子如上所示,只要在public void addI(String username)方法前加关键字synchronized即可。
  • 如果线程访问的对象中有多个实例变量,则运行的结果有可能出现交叉的情况。
2.3.3 多个对象多个锁(代码详见class213)
  • 两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,效果却是以异步的方式运行的。关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法当作锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。但如果多个线程访问多个对象,则JVM会创建多个锁,上面的示例就是创建了2个HasSelfPrivateNum.java类的对象所以产生2个锁。同步的单词为synchronized,异步的单词为asynchronized。
2.3.4 synchronized 方法与锁对象(代码详见class214)
  • 调用用关键字synchronized声明的方法一定是排队运行的,另外需要牢牢记住“共享”这两个字,只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本就没有同步的必要。
  • 两个线程A、B访问同一个对象object的两个不同的方法(一个同步方法,一个普通方法),虽然A线程持有了object对象的Lock锁,但是B线程完全可以异步调用非synchronized类型的方法。
  • 两个线程A、B访问同一个对象object的两个同步的方法,A线程持有了object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法则需要等待,也就是同步。
2.3.5 脏读(代码详见class215)
  • 多个线程调用同一个方法时,为了避免数据出现交叉的情况,使用synchronized关键字来进行同步,虽然在赋值时进行了同步,但是在取值时有可能出现在读取实例变量时,此值已经被其他线程更改过了,这种情况就是脏读。
  • 通过class215案例不仅要知道脏读是通过synchronized关键字解决的,还要知道如下内容:
    • 当A线程调用anyObject对象加入synchronized关键字的XXX方法时,A线程就获得了XXX方法锁,更准确地讲,是获得了对象的锁,所以其他线程必须等A线程执行完毕才可以调用XXX方法,但B线程可以任意调用其他的非synchronized同步方法。
    • 当A线程调用anyObject对象加入synchronized关键字的XXX方法时,A线程就获得了方法所在对象的锁,所以其他线程必须等A线程执行完毕才可以调用XXX方法,而B线程如果调用声明了synchronized关键字的非XXX方法时,必须等A线程将XXX方法执行完,也就是释放对象锁后才可以调用。这时A线程已经执行了一个完整的任务,也就是说username和password这两个实例变量已经同时被赋值,不存在脏读的基本环境。脏读一定会出现在操作实例变量的情况下,这就是不同线程“争抢”实例变量的结果。
2.3.6 synchronized锁重入(代码详见class216)
  • synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的,这也证明了在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。(例子1)
  • “可重入锁”的概念是:自己可以再次获取自己的内部锁,比如有一条线程获取了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。可重入锁也支持在父子类继承的环境中。(例子2)此实验说明,当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的。
2.3.7 出现异常,锁自动释放(代码详见class217)
  • 当一个线程执行的的代码出现异常时,其所持有的锁会自动释放。
2.3.8 同步不具有继承性(代码详见class218)
  • 同步不可以继承。从class218示例可以可看出,同步不能继承,所以还得在子类的方法中添加synchronized关键字。

2.4 synchronized同步语句块

  • 用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个长时间的任务,那么B线程则必须等待比较长的时间,这样的情况下可以使用synchronized同步语句块来解决。
2.4.1 synchronized方法的弊端(代码详见class221)
2.4.2 synchronized同步代码块的使用(代码详见class222)
  • 当两个并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
2.4.3 用同步代码块解决同步方法的弊端(代码详见class223)
  • 当一个线程访问object的一个synchronized同步代码块时,另一个线程仍然可以访问该object对象中的非synchronized(this)同步代码块。
2.4.4 一半异步,一半同步(代码详见class224)
  • 不在synchronized块中就是异步执行,在synchronized块中就是同步执行。
2.4.5 synchronized代码块间的同步性(代码详见class225)
  • 在使用同步synchronized(this)代码块时需要注意的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个object中所有其他synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是一个。
2.4.6 验证同步synchronized(this)代码块是锁定当前对象的(代码详见class226)
  • 和synchronized方法一样,synchronized(this)代码块也是锁定当前对象的。
2.4.7 将任意对象作为对象监视器(代码详见class227)
  • 多个线程调用一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的。这说明synchronized同步方法或synchronized(this)同步代码块分别有两种作用:

    • synchronized同步方法
      • 对其他synchronized同步方法或synchronized(this)同步代码块调用呈阻塞状态。
      • 同一时间只有一个线程可以执行synchronized同步方法中的代码
    • synchronized(this)同步代码块
      • 对其他synchronized同步方法或synchronized(this)同步代码块调用呈阻塞状态。
      • 同一时间只有一个线程可以执行synchronized(this)同步代码块中的代码。
  • 使用synchronized(this)格式来同步代码块,其实Java还支持对“任意对象”作为“对象监视器”来实现同步的功能。这个“任意对象”大多数是实例变量及方法的参数,使用格式为synchronized(非this对象)。

  • 根据前面对synchronized(this)同步代码块的作用总结可知,synchronized(非this对象)格式的作用只有1种:synchronized(非this对象 X)同步代码块。

    • 在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象 X)同步代码块中的代码。

    • 当持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this 对象 X)同步代码块中的代码。

  • 锁非this对象具有一定的优点:如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但会受到阻塞,所以影响运行效率,但如果使用同步代码块锁非this对象,则synchronized(非 this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,则可大大提高运行效率。

  • 使用“synchronized(非this对象X)同步代码块”格式时,持有不同的对象监视器是异步的效果。同步代码块放在非同步synchronized方法中进行声明,并不能保证调用方法的线程的执行同步/顺序性,也就是线程调用方法的顺序是无序的,虽然在同步块中执行的顺序是同步的,这样极易出现“脏读”问题。(代码详见class227_2)

  • 多个线程调用同一个方法是随机的。(代码详见class227_3)

2.4.8 细化验证3个结论(代码详见class228)
  • “synchronized(非 this 对象 X)”格式的写法是将X对象本身作为“对象监视器”,这样就可以得出3个结论:
    • 当多个线程同时执行synchronized(X){}同步代码块时呈同步效果。
    • 当其他线程执行X对象中synchronized同步方法时呈同步效果。
    • 当其他线程执行X对象方法里面的synchronized(this)代码块时呈同步效果。
2.4.9 静态同步synchronized方法与synchronized(class)代码块
  • synchronized加到static静态方法上是给Class类上锁,而它加到非static静态方法上是给对象上锁。示例运行结果异步的原因是持有不同的锁,一个是对象锁,另外一个是Class锁,而Class锁可以对类的所有对象实例起作用。
  • 同步synchronized(class)代码块的作用和synchronized static 方法的作用一样。
2.4.10 数据类型String的常量池特性
  • 在JVM中具有String常量池缓存的功能,同步synchronized代码块都不使用String作为锁对象,而改用其他,比如new Object()实例化一个Object对象,但它并不放入缓存中。
2.4.11 同步synchronized方法无限等待与解决

同步方法容易造成死循环,这个问题可以使用同步代码块来解决。

2.4.12 多线程的死锁
  • Java多线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成,在多线程技术中,“死锁”是必须避免的,因为这会造成线程的“假死”。
  • 死锁是程序设计的Bug,在设计程序时要避免双方互相持有对方的锁的情况,死锁与synchronized嵌套的代码结构没有关系,只要互相等待对方释放锁就有可能出现死锁。
2.4.13 内置类与静态内置类
2.4.14 内置类与同步:实验1
  • 在内置类中有两个同步方法,但使用的却是不同的锁,打印的结果是异步的。
2.4.15 内置类与同步:实验2
  • 同步代码块synchronized(class2)对class2上锁后,其他线程只能以同步的方式调用class2中的静态同步方法。
2.4.16 锁对象的改变
  • 在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程同时持有锁对象,如果同时持有相同的锁对象,则这些线程之间就是同步的,如果分别获得锁对象,这些线程之间就是异步的。只要对象不变,即使对象的属性被改变,运行结果还是同步的。

三、volatile 关键字

  • 关键字 volatile的主要作用是使变量在多个线程间可见。

3.1 volatile 关键字与死循环

  • 如果不是在多继承的情况下,使用继承Thread类和实现Runnable接口在取得程序运行的结果上并没有太大的区别,如果一旦出现“多继承”的情况,则实现Runnable接口的方式来处理多线程的问题上就很有必要。

3.2 解决同步死循环

  • 关键字Volatile的作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。

3.3 解决异步死循环

  • 这个问题其实就是私有堆栈中的值和公共堆栈中的值不同步造成的,解决这样的问题就要使用volatile关键字,它主要的作用就是当线程访问xxxx这个变量时,强制性从公共堆栈中进行取值。
  • volatile关键字最致命的缺点是不支持原子性。
  • synchronized 和 volatile 的比较:
    • volatitle是线程同步的轻量级实现,所有volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法以及代码块,随着JDK新版本的发布,synchronized在执行效率上得到很大的提升,在开发中使用synchronized的比率还是比较大的。
    • 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
    • volatile能保证数据的可见性,但不能保证原子性,而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
    • volatile解决的是变量在多个线程之间的可见性,而synchronized解决的是多个线程之间访问资源的同步性。
  • 线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这个两个方面来确保线程安全的。

3.4 volatile 非原子的特性

  • volatile虽然增加了实例变量在多个线程之间的可见性,但它却不具备同步性,那么也就不具备原子性。

  • volatile主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值的使用,也就是多线程读取共享变量时可以获得最新值的使用。

  • volatile提示线程每次从共享内存中读取变量,而不是从私有内存中读取,这样就保证了同步数据的可见性,但是如果修改实例变量中的数据,比如i++,也就是i=i+1,则这样的操作其实并不是一个原子操作,也就是非线程安全。表达式i++的操作步骤分解如下:

    1. 从内存中取出i的值
    2. 计算i的值
    3. 将i的值写到内存中

    假如在第2步计算值的时候,另外一个线程也修改i的值,那么这个时候就会出现“脏读”,解决的办法其实就是使用synchronized,所以说volatile本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存。

  • 用图来演示一下使用volatile时出现非线程安全的原因。变量在内存中工作的过程如下图所示:

    • read阶段和load阶段:从主存复制变量到当前线程工作内存。
    • use和assign阶段:执行代码,改变共享变量值。
    • store和write阶段:用工作内存数据刷新主存对应变量的值。
    • 在多线程环境中,use和assign是多次出现的,但这一操作并不是原子性,也就是在read和load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,所有计算出来的结果会和预期不一样,也就是出现了非线程安全问题。
    • 对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的,例如线程1和线程2在进行read和load的操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。也就是说,volatile关键字解决的是变量读时的可见性问题,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步的。

3.5 使用原子类进行i++操作

  • 除了在i++操作时使用synchronized关键字实现同步外,还可以使用AtomicInteger原子类进行实现。
  • 原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全(thread-safe)。

3.6 原子类也并不完全安全

  • 原子类在具有有逻辑性的情况下输出结果也具有随机性。

3.7 synchronized 代码块有volatile同步的功能

  • synchronized 可以使多个线程访问同一个资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。
  • synchronized 可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥性和可见性。synchronized不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程都看到由同一个锁保护之前所有的修改效果。

四、作业

第一周:理解synchronized的含义、明确synchronized关键字修饰普通方法、静态方法和代码块时锁对象的差异。

问题一:有如下一个类A

class A {
public synchronized void a() {
}
public synchronized void b() {
}
}
复制代码

然后创建两个对象

A a1 = new A();
A a2 = new A();
复制代码

然后在两个线程中并发访问如下代码

Thread1 Thread2
a1.a(); a2.a();
请问二者能否构成线程同步?

问题二:如果A的定义是下面这种呢?

class A {
public static synchronized void a() {
}
public static synchronized void b() {
}
}
复制代码

解答一:两个线程thread1、thread2访问同一个对象A的两个同步方法,thread1线程持有了A对象的Lock锁,thread2线程如果在这时调用A对象中的synchronized类型的方法则需要等待,也就是同步。synchronized取得的锁是对象锁,而不是把一段代码或方法当作锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象,如果多个线程访问多个对象,则JVM会创建多个锁,上面的示例虽然是两条线程并发访问了A对象里面的同步方法a,但是因为创建了2个new A()对象,那么就会产生2个锁,所以thread1和thread2这个两条线程是异步的方式运行的。值得一提的是,和synchronized方法一样,synchronized(this)代码块也是锁定当前对象的。
解答二:synchronized加到static静态方法上是给Class类上锁的,而它加到非static静态方法上是给对象上锁的。Class锁可以对类的所有对象实例起作用,所以虽然创建了两个不同的A对象a1和a2,但是两个线程thread1和thread2是同步的方式运行的。值得一提的是,同步synchronized(Class)代码块的作用和synchronized static 方法的作用是一样。

你可能感兴趣的:(Java学习之synchronized关键字)