前言
在上章中,我们介绍了Java Thread API
.本章我们介绍下Java中的Synchronized
与Volatile
关键字.
在本章中,我们将介绍如下的几个模块:
- Synchronized关键字
- 线程安全问题
- synchronized 关键字的使用
- 对象锁与类锁
- Synchronized关键字修饰final类型的变量 & 静态变量
- Volatile关键字
- Volatile关键字的作用
- Volatile关键字与Synchronized关键字的区别
- Volatile关键字的非原子性
正文
本节主要分为synchronized
关键字部分与volatile
关键字部分.
一 Synchronized关键字
- 线程安全问题
线程安全问题经常出现在多个线程共享一个数据的时候.例子如下:
class SafeThread extends Thread{
public void run(){
int i=0;
System.out.println("Thread:"+Thread.currentThread().getName()+" i="+i);
}
}
public class Main{
public static void main(){
Thread thread1 = new SafeThread();
Thread thread2 = new SafeThread();
Thread thread3 = new SafeThread();
thread1.start();
thread2.start();
thread3.start();
}
}
// 结果如下:
//Thread:Thread-1 i=0
//Thread:Thread-2 i=0
//Thread:Thread-0 i=0
//Thread:Thread-2 i=1
//Thread:Thread-1 i=1
//Thread:Thread-2 i=2
//Thread:Thread-0 i=1
//Thread:Thread-0 i=2
//Thread:Thread-1 i=2
可以看到并没有线程安全问题.因为,局部变量的作用体在方法内部,随着方法的销毁而销毁.
但是,当变量作为类变量存在的时候,就会出现所谓的线程安全的问题了.
class UnSafeObj{
private int i=0;
public void addI(int i){
this.i = i;
}
public int getI(){
return i;
}
}
class UnSafeThread extends Thread{
private UnSafeObj obj;
private int number;
public UnSafeThread(UnSafeObj obj,int number){
this.obj = obj;
this.number = number;
}
public void run(){
obj.addI(number);
System.out.println("Thread:"+Thread.currentThread().getName()+" i="+obj.getI());
}
}
public class UnSafe {
public static void main(String[] args) {
UnSafeObj obj = new UnSafeObj();
Thread threadA = new UnSafeThread(obj,10029);
Thread threadB = new UnSafeThread(obj,9090);
Thread threadC = new UnSafeThread(obj,8080);
threadA.start();
threadB.start();
threadC.start();
}
}
//Thread:Thread-0 i=8080
//Thread:Thread-2 i=8080
//Thread:Thread-1 i=8080
由上方的输出可以看出,所有的输出都变成了8080
.这显然是不正确的.因为,线程在进行更新的时候,变量被其他的线程修改了,导致输出错乱了.
对于这种问题,我们可以使用Synchronized
关键字就可以解决.
public void run(){
synchronized(obj){
obj.addI(number);
System.out.println("Thread:"+Thread.currentThread().getName()+" i="+obj.getI());
}
}
// Thread:Thread-0 i=10029
// Thread:Thread-2 i=8080
// Thread:Thread-1 i=9090
当然,有时synchronized
写在方法上,有时使用synchronized(this)
使得对象同步.
- 脏读现象
对于对象来说synchronized
关键字只会将对象的含有Synchronized
关键字的对象进行锁定,非加锁的对象不会进行锁定.例如:去除上述对象的对象锁,转为addI()
的方法锁.
# 去除锁定
public void run(){
// synchronized(obj){
obj.addI(number);
System.out.println("Thread:"+Thread.currentThread().getName()+" i="+obj.getI());
// }
}
# 加上方法锁
synchronized public void addI(int i){
this.i = i;
}
public int getI(){
return i;
}
// Thread:Thread-0 i=10029
// Thread:Thread-1 i=9090
// Thread:Thread-2 i=9090
可以看到上述的线程输出.(输出不一定).因为getI()
方法是有锁的,但是当方法执行结束之后,释放了对象锁.而getI
是不会保持线程的锁的.
遇到这种情况,我们需要保证线程在运行共享变量时候,是一直保持对象的锁的.
我们可以像上方的写法,也可以像下方的写法进行书写.
synchronized public void addI(int i){
this.i = i;
}
synchronized public int getI(){
return i;
}
于此同时,synchronized
关键字还具有如下的几点特性.
- 支持锁重入
synchronized public void addI(int i){
this.i = i;
System.out.println(getI());
}
synchronized public int getI(){
return i;
}
如果不支持锁重入的话,将会很容易的导致死锁的发生.
- 出现异常,锁会自动释放. (如果出现异常,锁不会进行释放的话.将会导致其他线程一直的进行等待.)
- 同步锁不支持继承性.
- synchronized 与 static关键字
synchronized
关键字修饰static
关键字的时候,获取的是类锁.验证这个方法也非常的容易.我们通过创建多个对象进行操作.如果,多个对象的话.按照之前的常理来说,应该创建多个对象锁.并且线程是并行进行的.如果是同步的话,会进行等待.试验的例子如下所示:
class SyncClassObject{
synchronized static public void getLock(){
System.out.println("Thread:"+Thread.currentThread().getName()+"Get Sync Lock. "+System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread:"+Thread.currentThread().getName()+"Get Sync Lock. "+System.currentTimeMillis());
}
}
class SyncClassThread extends Thread{
SyncClassObject object;
public SyncClassThread(SyncClassObject object){
this.object = object;
}
public void run(){
object.getLock();
}
}
public class SyncClassLock {
public static void main(String[] args) {
SyncClassThread thread1 = new SyncClassThread(new SyncClassObject());
SyncClassThread thread2 = new SyncClassThread(new SyncClassObject());
SyncClassThread thread3 = new SyncClassThread(new SyncClassObject());
thread1.start();
thread2.start();
thread3.start();
}
}
//Thread:Thread-0Get Sync Lock. 1552928317396
//Thread:Thread-0Get Sync Lock. 1552928322398
//Thread:Thread-2Get Sync Lock. 1552928322399
//Thread:Thread-2Get Sync Lock. 1552928327411
//Thread:Thread-1Get Sync Lock. 1552928327417
//Thread:Thread-1Get Sync Lock. 1552928332421
由上述的结果可以看出,线程是串行执行的.因为获取的是类锁
,几个线程等待的其实是同一个锁.
- 反编译
javap -v Synchronized.class
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #1 // class com/yanxml/multithreading/tradition/core/chat/Synchronized
2: dup
3: monitorenter // 监控器进入 获取锁
4: monitorexit // 监控器退出 释放锁
5: invokestatic #16 // Method m:()V
8: return
LineNumberTable:
line 7: 0
line 12: 5
line 13: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
public static synchronized void m();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
同步主要是通过ACC_SYNCHRONIZED
关键字进行实现的.
Volatile关键字
volatile
关键字通常用于使线程内的变量更新到主内存.根据JMM
模型可以知道,每个线程通常具有局部变量.当更新主内存的时候,线程空间内的局部变量是不会更新的.因此会导致,访问不通的情况.我们可以看下如下实例:
class NoVolatileThread extends Thread{
public boolean flag = true;
public void run(){
// 阻塞循环
while(flag){
}
}
}
public class NoVolatileFlag {
public static void main(String[] args) {
NoVolatileThread thread = new NoVolatileThread();
thread.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.flag = false;
}
}
// 运行结果: 死循环
我们可以使用volatile
关键字来处理这个尴尬的局面.
public volatile boolean flag = true;
// 程序运行5s后自动停止
- volatile关键字并不能进行原子操作
import java.util.ArrayList;
import java.util.List;
class VolatileCountThread extends Thread{
volatile private static int count=0;
public VolatileCountThread(){
}
public VolatileCountThread(Thread thread){
super(thread);
}
public static void addNumber(){
for(int i=0;i<100;i++){
count++;
}
System.out.println("Thread:"+Thread.currentThread().getName()+"Count End. "+count);
}
public void run(){
addNumber();
}
}
public class VolatileCountAdd {
public static void main(String []args){
// VolatileCountThread thread = new VolatileCountThread();
List<Thread> list = new ArrayList<>();
for(int i=0;i<100;i++){
VolatileCountThread thread = new VolatileCountThread();
list.add(thread);
}
for(Thread thread : list){
thread.start();
}
}
}
Thread:Thread-1Count End. 400
Thread:Thread-0Count End. 400
Thread:Thread-3Count End. 400
Thread:Thread-2Count End. 400
Thread:Thread-5Count End. 600
- synchronized关键字可以替换volatile关键字
class SyncUpdateVolatileThread extends Thread{
public volatile boolean flag = true;
synchronized public void run(){
// 阻塞循环
while(flag){
}
}
}
public class SyncUpdateVolatile {
public static void main(String[] args) {
SyncUpdateVolatileThread thread = new SyncUpdateVolatileThread();
thread.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.flag = false;
}
}
// 程序运行5秒后自动停止了.
Tips
- 死锁的产生与如何解除死锁?
死锁是面试经常会问到的问题.根据之前操作系统的知识可以得知.死锁部分的知识通常分为死锁的产生/死锁的避免/死锁的解除
死锁的产生
: 死锁的产生通常是因为多个线程分别获取多项资源导致,并且这些资源是互斥的资源.(产生环路 / 冲突)
– to be continue
小结
由本章可知.
synchronized关键字
synchronized
通常是用来解除多线程资源冲突的情况.synchronzied
可以修饰方法体和某个对象方法.- 线程调用
synchronized
关键字修饰的方法时是呈同步效果,但是调用非synchronized
关键字修饰的方法时是非同步效果. synchronzied
还具有可以重入、异常释放锁、修饰static
时获取类锁等特性.
Volatile关键字
volatile
可以将线程内的局部变量与总内存变量进行共享;volatie
并不具有原子性;synchronized
关键字可以替换volatile
的功能;但是有时使用volatile
关键字会使程序运行起来更加高效.
Reference
[1]. Java 多线程编程核心技术
[2]. Java并发编程的艺术