抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Java多线程的同步

使用线程锁

synchronized内置锁

​ 线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,包括数据之间的共享,协同处理事情。这将会带来巨大的价值。

​ Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。

synchronized 的性质
可重入性
  • 同一线程的外层方法获得锁后,内层方法可以直接再次获取该锁;
  • 避免死锁,提升封装性;
  • 关键字:同一线程,同一把锁
不可中断性
  • 一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁;如果别的线程永远不释放锁,那么我只能永远等待;

  • 相比之下,Lock类提供的锁,拥有中断能力;第一,如果我觉得等待的时间太长了,有权中断现在已经获取到锁的线程的执行;第二,如果我觉得等待的时间太长了不想再等待,也可以退出;

对象锁和类锁

​ 当一个对象中有synchronized method或synchronized block的时候调用此对象的同步方法或进入其同步区域时,就必须先获得对象锁。如果此对象的对象锁已被其他调用者占用,则需要等待此锁被释放,对象锁是用于对象实例方法,或者一个对象实例上的。

同步静态方法/静态变量互斥体
由于一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只由一份。所以,一旦一个静态的方法被申明为synchronized。此类所有的实例化对象在调用此方法,共用同一把锁,我们称之为类锁。一旦一个静态变量被作为synchronized block的mutex。进入此同步区域时,都要先获得此静态变量的对象锁

​ 用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。

​ 但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。

synchronized 的加锁方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* synchronized 锁
*/
public class RunableTest {

private Object lock = new Object();

/**
* 用同步代码块表示lock对象资源互斥
*/
public void test1() {
synchronized (lock) {
System.out.println("test111###" + System.currentTimeMillis());
sleep(2);
}
}

/**
* 用在方法上表示对象锁 表示 synchronized(调用方法的类的对象)
*/
public synchronized void test2() {
System.out.println("test222###"+System.currentTimeMillis());
sleep(2);
}

/**
* 加载静态方法上表示类锁,是synchronized(类.class) {}
*/
public static synchronized void test3() {
System.out.println("test333###"+System.currentTimeMillis());
sleep(2);
}

public static void sleep(int sec) {
try {
Thread.sleep(sec * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized总结
  • synchronized如果加在了非静态方法上,表示的是synchronized(调用方法的类的对象) {}
  • 如果加在了静态方法上,表示的是synchronized(类.class) {}
  • 如果是synchronized代码块表示资源互斥
synchronized的缺陷
  • 效率低
    • 锁的释放情况少,只在程序正常执行完成和抛出异常时释放锁;
    • 试图获得锁是不能设置超时;
    • 不能中断一个正在试图获得锁的线程;
  • 无法知道是否成功获取到锁;

错误的加锁和原因分析

​ 我们常常在程序中使用多线程来处理任务,这个时候是否正确使用加锁就很重要了,有时候看着代码没啥问题,但是执行起来发现结果并不是看到的那样,比如我们看下面的代码

1
2
3
4
5
6
7
8
9
10
private Integer num = 0;

public void test1() {
synchronized (num) {
sleep(1);
num++;
System.out.println("test111"+Thread.currentThread().getId());
}

}

​ 这个时候我们再去看代码,我们对Integer对象num进行加锁,这个时候就有个一个Java基础知识在里面,Integer是不可变对象,是实例对象,对象一但被创建就不能被修改,比如赋值是1,就是1,如果让它变成2,需要重新创建一个Integer对象。

1
2
3
4
5
6
7
8
public void test1() {
synchronized(this.num) {
sleep(1);
Integer var2 = this.num;
Integer var3 = this.num = this.num + 1;
System.out.println("test111" + Thread.currentThread().getId());
}
}

​ 然后下面循环中进行num++,其实这里Java对这个进行了内部转换( Java封箱拆箱) ,其实是执行的这个语句java Integer var3 = this.num = this.num + 1;,我们看反编译后的代码,它会返回一个Integer实例,因此num ++本质是创建一个Integer对象,并将它的引用赋值给num 。

本质上是返回了一个新的Integer对象。也就是每个线程实际加锁的是不同的Integer对象。

volatile关键字

简介

​ volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

特性

可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

volatile 不能解决原子性的问题

​ 原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。

Java中的原子性操作包括:

  • 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作。
  • 所有引用reference的赋值操作
  • java.concurrent.Atomic.* 包中所有类的一切操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package chapter01.volidate;

import util.ThreadUtils;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
* Volatile 不能保证 原子性
*/
public class VolatileTest {

private static ExecutorService executorService = Executors.newFixedThreadPool(10);
//volatile 修饰的变量num
private static volatile int num = 0;

public static void addSum(){
for(int i=0;i<10;i++){
num++;
//休眠10ms
ThreadUtils.sleep(10, TimeUnit.MILLISECONDS);
}
}

public static void main(String[] args) {
for(int i=0;i<10;i++){
executorService.submit(()->{
addSum();
});
}
executorService.shutdown();
ThreadUtils.sleep(2,TimeUnit.SECONDS);
System.out.println(num);
}
}
可见性

指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

​ 在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package chapter01.volidate;

import util.ThreadUtils;
import java.util.concurrent.TimeUnit;

/**
* Volatile 可见性测试
*/
public class VolatileVisibilityTest {
//不加volatile关键字 变量对多线程不可见
private boolean stop = false;
private int num = 0;
//加volatile关键字 变量对多线程可见
//private volatile boolean stop = false;

public static void main(String[] args) throws Exception {
VolatileVisibilityTest volatileVisibilityTest = new VolatileVisibilityTest();
new Thread(() -> {
volatileVisibilityTest.execute();
}).start();

ThreadUtils.sleep(1, TimeUnit.SECONDS);
new Thread(() -> {
volatileVisibilityTest.shutdown();
}).start();
System.out.println(volatileVisibilityTest.num);
}

public void execute() {
//线程无限循环
while (!stop) {
//todo 做一些事情
//注意里面不能有 System.out.println(); 进行打印 因为System.out.println 包含有 synchronized 上下文变量可见,破坏本次测试
// 并且不能有 sleet() 语句 sleep也会进行可见性同步
num++;
}
}

public void shutdown() {
System.out.println("do stop");
stop = true;
}
}
有序性

即程序执行的顺序按照代码的先后顺序执行。

​ java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。
​ 在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

volatile变量的特性

保证可见性,不保证原子性

  • 当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
  • 这个写会操作会导致其他线程中的缓存无效。
禁止指令重排

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:

  • 重排序操作不会对存在数据依赖关系的操作进行重排序。

    比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运 行时这两个操作不会被重排序。

  • 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

    比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

​ 重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,上例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。

单例模式的双重锁为什么要加volatile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package chapter01.volidate;

public class Singleton {
private volatile static Singleton singleton;

private Singleton() {
}

public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第13行会出现问题。

singleton = new Singleton();可以分解为3行伪代码

  1. memory = allocate() //分配内存

  2. ctorInstanc(memory) //初始化对象

  3. singleton= memory //设置instance指向刚分配的地址

​ 上面的代码在编译运行时,可能会出现重排序从1-2-3排序为1-3-2。在多线程的情况下会出现以下问题。线程A在执行第5行代码时,B线程进来,而此时A执行了1和3,没有执行2,此时B线程判断instance不为null,直接返回一个未初始化的对象。

评论