Java 多线程(二) Synchronized与Volatile关键字

版权声明:欢迎转载,转载请说明出处. 大数据Github项目地址https://github.com/SeanYanxml/bigdata。 https://blog.csdn.net/u010416101/article/details/88653637

前言

在上章中,我们介绍了Java Thread API.本章我们介绍下Java中的SynchronizedVolatile关键字.

在本章中,我们将介绍如下的几个模块:

  • 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并发编程的艺术

猜你喜欢

转载自blog.csdn.net/u010416101/article/details/88653637