Java并发17:synchronized关键字的两种用法-同步代码块(4)和同步方法(2),synchronized关键字
Java并发17:synchronized关键字的两种用法-同步代码块(4)和同步方法(2),synchronized关键字
[超级链接:Java并发学习系列-绪论]
volatile关键字在之前的章节中多次提及:
- Java并发02:Java并发Concurrent技术发展简史(各版本JDK中的并发技术)
- Java并发12:并发三特性-原子性、可见性和有序性概述及问题示例
- Java并发13:并发三特性-原子性定义、原子性问题与原子性保证技术
- Java并发14:并发三特性-可见性定义、可见性问题与可见性保证技术
- Java并发15:并发三特性-有序性定义、有序性问题与有序性保证技术
本章主要对synchronized关键字的两种用法进行学习。
1.synchronized简述
引用百度百科的一段解释:
synchronized 关键字,代表这个方法(或代码块)加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法 和 synchronized 代码块 。
简单来说,synchronized关键字以同步方法和同步代码块的方式,为方法和代码块上的对象加锁。使得同一时刻,在这个对象上的多个线程,只能由持有这个对象锁的单个线程进行代码的调用执行。
synchronized 关键字能够保证代码的原子性、可见性和有序性。
2.synchronized关键字的用法
2.1.用法概述
synchronized关键字主要有两大类6小类用法:
- 同步代码块
- 加锁对象是本地变量的同步代码块
- 加锁对象是类静态变量的同步代码块
- 加锁对象是共享变量的同步代码块
- 加锁对象是类对象的同步代码块
- 同步方法
- 修饰方法是普通方法的同步方法
- 修饰方法是类静态方法的同步方法
下面会依次进行实例学习。
2.2.同步代码块和同步方法
相同点:
- 同步代码块和同步方法都通过synchronized关键字实现
- 同步代码块和同步方法都能够保证代码的同步性,即:原子性、有序性和可见性。
不同的:
- 同步代码块比同步方法的锁住的范围更小,所以性能更好。
- 同步方法比同步代码块的编写更简单,只需要在方法定义是加上synchronized关键字即可,而后者还需要确定加锁对象。
2.3.示例场景
- 定义一个自增器Increment,此自增器执行一次自增方法autoIncrement()会循环进行5次
number++
操作。 - 在多线程的环境中,对自增器进行测试。
2.4.原始示例-不采取任何同步措施
从前面的很多章节中可知,自增操作不是原子性操作。如果不采取任何同步手段,多线程下的自增会存在线程安全隐患。
下面的代码展示的是一个不采取任何同步措施的自增器:
/**
* <p>自增器-无同步</p>
*
* @author hanchao 2018/3/18 11:39
**/
static class Increment {
public void autoIncrement() {
for (int i = 0; i < 5; i++) {
number++;
System.out.println("线程[" + Thread.currentThread().getName() + "],number:" + number);
}
}
}
下面的代码展示的是如何多线程环境下测试这个自增器:
//多线程数量
int num = 100000;
//休眠等待时间
int sleep = 5000;
System.out.println("无同步措施的自增器");
Increment increment0 = new Increment();
for (int i = 0; i < num; i++) {
new Thread(() -> {
increment0.autoIncrement();
}).start();
}
Thread.sleep(sleep);
System.out.println("无同步措施的自增器,最终number=" + number);
运行结果:
...省略
线程[Thread-99722],number:499800
线程[Thread-99722],number:499939
无同步措施的自增器,最终number=499939
显而易见,在多线程环境中,这种做法是存在线程安全隐患的。
后面的章节中,会通过上面提到的两大类6小类同步方式依次对这个问题进行解决,已验证各类同步方式的使用注意事项。
3.同步代码块
同步代码块加锁的范围是代码块{}内的代码,同步对象根据加锁对象的不同而不同。
3.1.加锁对象是本地变量的同步代码块
关于本地变量的概念可以参考:
Java并发11:Java内存模型、指令重排、内存屏障、happens-before原则
自增器代码:
/**
* <p>自增器-同步代码块-锁对象是类的本地变量</p>
*
* @author hanchao 2018/3/18 11:26
**/
static class SyncCodeBlockIncrement11 {
// 同步代码10/11-加锁对象是类的本地变量
private byte[] ordinaryObj = new byte[0];
public void autoIncrement() {
synchronized (ordinaryObj) {
for (int i = 0; i < 5; i++) {
number++;
System.out.println("线程[" + Thread.currentThread().getName() + "]获取锁,number:" + number);
}
}
}
}
测试多个对象:
System.out.println("通过synchronized定义同步代码块,作用范围:代码块,同步对象:加锁对象为类本地变量->类的多个对象no");
SyncCodeBlockIncrement11 increment10 = new SyncCodeBlockIncrement11();
for (int i = 0; i < num; i++) {
//如果调用其他方法的同步方法,则计算结果错误
if (i % 2 == 0) {//模拟多个对象
new Thread(() -> {
new SyncCodeBlockIncrement11().autoIncrement();
}).start();
} else {//模拟同一个对象的多次调用
new Thread(() -> {
increment10.autoIncrement();
}).start();
}
}
Thread.sleep(sleep);
System.out.println("同步代码块:加锁对象为类本地变量,通过类的多个对象调用同步代码块,最终number=" + number);
测试结果:
...省略
线程[Thread-96703]获取锁,number:499987
线程[Thread-96703]获取锁,number:499988
同步代码块:加锁对象为类本地变量,通过类的多个对象调用同步代码块,最终number=499988
测试结果说明:
同步代码块:加锁对象为类本地变量,通过类的多个对象调用同步代码块,并不能保证同步性。
测试单个对象:
System.out.println("通过synchronized定义同步代码块,作用范围:代码块,同步对象:加锁对象为类本地变量->类的单个对象ok");
SyncCodeBlockIncrement11 increment11 = new SyncCodeBlockIncrement11();
for (int i = 0; i < num; i++) {
new Thread(() -> {
increment11.autoIncrement();
}).start();
}
Thread.sleep(sleep);
System.out.println("同步代码块:加锁对象为类本地变量,通过类的单个对象调用同步代码块,最终number=" + number);
测试结果:
...省略
线程[Thread-95431]获取锁,number:499999
线程[Thread-95431]获取锁,number:500000
同步代码块:加锁对象为类本地变量,通过类的单个对象调用同步代码块,最终number=500000
测试结果说明:
同步代码块:加锁对象为类本地变量,通过类的单个对象调用同步代码块,能够保证同步性。
3.2.加锁对象是类静态变量的同步代码块
自增器代码:
/**
* <p>自增器-同步代码块-锁对象是类的静态变量</p>
*
* @author hanchao 2018/3/18 11:26
**/
static class SyncCodeBlockIncrement12 {
//同步代码12-加锁对象是类的静态变量
private static byte[] staticObj = new byte[0];
public void autoIncrement() {
synchronized (staticObj) {
for (int i = 0; i < 5; i++) {
number++;
System.out.println("线程[" + Thread.currentThread().getName() + "]获取锁,number:" + number);
}
}
}
}
多线程测试自增器代码:
System.out.println("通过synchronized定义同步代码块,作用范围:代码块,同步对象:加锁对象为类静态变量->类的多个对象ok");
SyncCodeBlockIncrement12 increment12 = new SyncCodeBlockIncrement12();
for (int i = 0; i < num; i++) {
if (i % 2 == 0) {//模拟多个对象
new Thread(() -> {
new SyncCodeBlockIncrement12().autoIncrement();
}).start();
} else {//模拟同一个对象的多次调用
new Thread(() -> {
increment12.autoIncrement();
}).start();
}
}
Thread.sleep(sleep);
System.out.println("同步代码块:加锁对象为类静态变量,通过类的多个对象调用同步代码块,最终number=" + number);
运行结果:
...省略
线程[Thread-99609]获取锁,number:499999
线程[Thread-99609]获取锁,number:500000
同步代码块:加锁对象为类静态变量,通过类的多个对象调用同步代码块,最终number=500000
测试结果说明:
同步代码块:加锁对象为类静态变量,通过类的多个对象调用同步代码块,能够保证同步性。
3.3.加锁对象是共享变量的同步代码块
关于共享变量的概念可以参考:
Java并发11:Java内存模型、指令重排、内存屏障、happens-before原则
共享变量:
//同步代码13-加锁对象是共享变量
private static byte[] heapObj = new byte[0];
自增器代码:
//同步代码13-加锁对象是共享变量
private static byte[] heapObj = new byte[0];
/**
* <p>自增器-同步代码块-锁对象是共享变量</p>
*
* @author hanchao 2018/3/18 11:26
**/
static class SyncCodeBlockIncrement13 {
public void autoIncrement() {
synchronized (heapObj) {
for (int i = 0; i < 5; i++) {
number++;
System.out.println("线程[" + Thread.currentThread().getName() + "]获取锁,number:" + number);
}
}
}
}
多线程测试自增器代码:
System.out.println("通过synchronized定义同步代码块,作用范围:代码块,同步对象:加锁对象为共享变量->类的多个对象ok");
SyncCodeBlockIncrement13 increment13 = new SyncCodeBlockIncrement13();
for (int i = 0; i < num; i++) {
if (i % 2 == 0) {//模拟多个对象
new Thread(() -> {
new SyncCodeBlockIncrement13().autoIncrement();
}).start();
} else {//模拟同一个对象的多次调用
new Thread(() -> {
increment13.autoIncrement();
}).start();
}
}
Thread.sleep(sleep);
System.out.println("同步代码块:加锁对象为共享变量,通过类的多个对象调用同步代码块,最终number=" + number);
运行结果:
...省略
线程[Thread-99178]获取锁,number:499999
线程[Thread-99178]获取锁,number:500000
同步代码块:加锁对象为共享变量,通过类的多个对象调用同步代码块,最终number=500000
测试结果说明:
同步代码块:加锁对象为共享变量,通过类的多个对象调用同步代码块,能够保证同步性。
3.4.加锁对象是类对象的同步代码块
关于类对象指的是Class的对象,即类似于:SyncCodeBlockIncrement14.class
自增器代码:
/**
* <p>自增器-同步代码块-锁对象是类对象</p>
*
* @author hanchao 2018/3/18 13:12
**/
static class SyncCodeBlockIncrement14 {
public void autoIncrement() {
//同步代码14-加锁对象是类对象
synchronized (SyncCodeBlockIncrement14.class) {
for (int i = 0; i < 5; i++) {
number++;
System.out.println("线程[" + Thread.currentThread().getName() + "]获取锁,number:" + number);
}
}
}
}
多线程测试自增器代码:
System.out.println("通过synchronized定义同步代码块,作用范围:代码块,同步对象:加锁对象为类对象->类的多个对象ok");
SyncCodeBlockIncrement14 increment14 = new SyncCodeBlockIncrement14();
for (int i = 0; i < num; i++) {
if (i % 2 == 0) {//模拟多个对象
new Thread(() -> {
new SyncCodeBlockIncrement14().autoIncrement();
}).start();
} else {//模拟同一个对象的多次调用
new Thread(() -> {
increment14.autoIncrement();
}).start();
}
}
Thread.sleep(sleep);
System.out.println("同步代码块:加锁对象为类对象,通过类的多个对象调用同步代码块,最终number=" + number);
运行结果:
...省略
线程[Thread-97132]获取锁,number:499999
线程[Thread-97132]获取锁,number:500000
同步代码块:加锁对象为类对象,通过类的多个对象调用同步代码块,最终number=500000
测试结果说明:
同步代码块:加锁对象为类对象,通过类的多个对象调用同步代码块,能够保证同步性。
4.同步方法
同步方法加锁的范围是整个方法的代码,同步对象根据加锁对象的不同而不同。
4.1.修饰方法是普通方法的同步方法
自增器代码:
/**
* <p>自增器-普通同步方法-调用这个方法的对象</p>
*
* @author hanchao 2018/3/18 11:36
**/
static class SyncMethodIncrement {
public synchronized void autoIncrement() {
for (int i = 0; i < 5; i++) {
number++;
System.out.println("线程[" + Thread.currentThread().getName() + "]获取锁,number:" + number);
}
}
}
多线程测试自增器代码(调用多个对象):
System.out.println("通过synchronized定义普通同步方法,作用范围:整个方法,同步对象:类的多个对象no。");
SyncMethodIncrement increment20 = new SyncMethodIncrement();
for (int i = 0; i < num; i++) {
if (i % 2 == 0) {//模拟多个对象
new Thread(() -> {
new SyncMethodIncrement().autoIncrement();
}).start();
} else {//模拟同一个对象的多次调用
new Thread(() -> {
increment20.autoIncrement();
}).start();
}
}
Thread.sleep(sleep);
System.out.println("同步方法:修饰方法为普通方法,通过类的多个对象调用同步方法,最终number=" + number);
运行结果:
...省略
线程[Thread-86059]获取锁,number:499973
线程[Thread-86059]获取锁,number:499974
同步方法:修饰方法为普通方法,通过类的多个对象调用同步方法,最终number=499974
测试结果说明:
同步方法:修饰方法为普通方法,通过类的多个对象调用同步方法,不能够保证同步性。
多线程测试自增器代码(调用单个对象):
System.out.println("通过synchronized定义普通同步方法,作用范围:整个方法,同步对象:类的单个对象ok。");
SyncMethodIncrement increment21 = new SyncMethodIncrement();
for (int i = 0; i < num; i++) {
new Thread(() -> {
increment21.autoIncrement();
}).start();
}
Thread.sleep(sleep);
System.out.println("同步方法:修饰方法为普通方法,通过类的单个对象调用同步方法,最终number=" + number);
运行结果:
...省略
线程[Thread-95746]获取锁,number:499999
线程[Thread-95746]获取锁,number:500000
同步方法:修饰方法为普通方法,通过类的单个对象调用同步方法,最终number=500000
测试结果说明:
同步方法:修饰方法为普通方法,通过类的单个对象调用同步方法,能够保证同步性。
4.2.修饰方法是类静态方法的同步方法
自增器代码:
/**
* <p>自增器-静态同步方法-此类的所有对象</p>
*
* @author hanchao 2018/3/18 11:42
**/
static class SyncStaticMethodIncrement {
public static synchronized void autoIncrement() {
for (int i = 0; i < 5; i++) {
number++;
System.out.println("线程[" + Thread.currentThread().getName() + "]获取锁,number:" + number);
}
}
}
多线程测试自增器代码:
System.out.println("通过synchronized定义静态同步方法,作用范围:整个方法,同步对象:类的多个对象ok。");
SyncStaticMethodIncrement increment22 = new SyncStaticMethodIncrement();
for (int i = 0; i < num; i++) {
if (i % 2 == 0) {//模拟多个对象
new Thread(() -> {
new SyncStaticMethodIncrement().autoIncrement();
}).start();
} else {//模拟同一个对象的多次调用
new Thread(() -> {
increment22.autoIncrement();
}).start();
}
}
Thread.sleep(sleep);
System.out.println("同步方法:修饰方法为静态方法,通过类的多个对象调用同步方法,最终number=" + number);
运行结果:
...省略
线程[Thread-95871]获取锁,number:499999
线程[Thread-95871]获取锁,number:500000
同步方法:修饰方法为静态方法,通过类的多个对象调用同步方法,最终number=500000
测试结果说明:
同步方法:修饰方法为静态方法,通过类的多个对象调用同步方法,能够保证同步性。
5.synchronized关键字的用法总结
通过前面的实际学习和测试,现在将synchronized关键字的用法总结如下:
序号 | 大类 | 小类(锁定对象) | 锁定范围 | 可同步对象 | 不同步对象 |
---|---|---|---|---|---|
1 | 同步代码块 | 加锁对象是本地变量 | 方法块{}内的代码 | 单个对象 | 多个对象 |
2 | 同步代码块 | 加锁对象是类静态变量 | 方法块{}内的代码 | 多个对象 | - |
3 | 同步代码块 | 加锁对象是共享变量 | 方法块{}内的代码 | 多个对象 | - |
4 | 同步代码块 | 加锁对象是类对象 | 方法块{}内的代码 | 多个对象 | - |
5 | 同步方法 | 修饰的是普通方法 | 整个方法 | 单个对象 | 多个对象 |
6 | 同步方法 | 修饰的是静态方法 | 整个方法 | 多个对象 | - |
相关文章
- 暂无相关文章
用户点评