JAVA面试基础-synchronized关键词相关

Java为解决并发编程的原子性、可见性和有序性问题,提供了如synchronized、volatile等关键字。本文详细介绍了synchronized的用法、原理,以及其对原子性、可见性和有序性的保障,还对比了volatile和synchronized的区别,包括执行控制、内存可见性等方面。

 

JAVA语言为了解决并发编程中存在的原子性,可见性和有序性等问题,提供了一系列和并发处理的关键字,比如:synvhronized,volatile,final,concurren包等,这里需要好好的学习一下。

在JVM中说,cynchronized关键词在需要原子性,可见性和有序性这三种特性的时候都可以作为其中一种解决方案,看起来是万能的,的确,大部分并发控制操作都能够使用synchronized来完成。

1.首先介绍synchronized关键词的用法,原理,以及如何提供原子性,可见性和有序性保障;

(1)synchronized用法

        synchronized主要有几种方法,一是,同步方法,二是同步代码块。三是修饰静态方法,四是修饰类。

public class SynchronizedDemo {
     //同步方法
    public synchronized void doSth(){
        System.out.println("Hello World");
    }

    //同步代码块
    public void doSth1(){
        synchronized (SynchronizedDemo.class){
            System.out.println("Hello World");
        }
    }
}

synchronized关键字不能继承。 
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。

  1. 在定义接口方法时不能使用synchronized关键字。
  2. 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
  3. 一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
  4. 当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。
  5. 指定要给某个对象加锁。

synchronized修饰一个静态方法,

public synchronized static void method() {
   // todo
}

静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象

/**
 * 同步线程
 */
class SyncThread implements Runnable {
   private static int count;
 
   public SyncThread() {
      count = 0;
   }
 
   public synchronized static void method() {
      for (int i = 0; i < 5; i ++) {
         try {
            System.out.println(Thread.currentThread().getName() + ":" + (count++));
            Thread.sleep(100);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
 
   public synchronized void run() {
      method();
   }
}
 
public class Demo00{
	
	public static void main(String args[]){
		SyncThread syncThread1 = new SyncThread();
		SyncThread syncThread2 = new SyncThread();
		Thread thread1 = new Thread(syncThread1, "SyncThread1");
		Thread thread2 = new Thread(syncThread2, "SyncThread2");
		thread1.start();
		thread2.start();
	}
}

syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。

修饰一个类

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}

本例的的给class加锁和上例的给静态方法加锁是一样的,所有对象公用一把锁

总结

A. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。 
B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。 
C. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

    锁是和对象相关联的,每个对象有一把锁,为了执行synchronized语句,线程必须能够获得synchronized语句中表达式指定的对象的锁,一个对象只有一把锁,被一个线程获得之后它就不再拥有这把锁,线程在执行完synchronized语句后,将获得锁交还给对象。在方法前面加上synchronized修饰符即可以将一个方法声明为同步化方法。同步化方法在执行之前获得一个锁。如果这是一个类方法,那么获得的锁是和声明方法的类相关的Class类对象的锁。如果这是一个实例方法,那么此锁是this对象的锁。

(2)synchronized实现原理

        在synchronized 原理之前,首先要了解多线程。JVM中主要包含以下重要的区域,栈,堆,方法区等;在栈中存放的是基本类型和对象的引用,对象要保存在堆中。此外,数组也要保存在堆中。除了栈和堆,还有一部分数据可能保存在方法区中,比如类的静态变量,方法区和栈区类似,其中只包含基本类型和对象的引用,和栈不同的是,方法区中的静态变量可以被所有的线程访问到。

下面来说说实现原理:通过反编译之后的代码可以看出,JVM采用的是ACC_SYNCHRONIZED的标记符来实现同步。对于同步代码块JVM使用monitorenter和monitoreixt来实现同步。先判段某个方法是否有ACC_SYNCHRONIZED这个标记,如果有设置,则需要先获得监视器锁,再执行该方法,方法执行后再释放监视器锁。这时如果其他的线程来请求执行方法的时候,就会因为无法获得监视器锁而被阻断。值得注意的是,如果在执行方法的过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

同步代码块会使用monitorenter和monitoreixt来实现同步,monitorenter理解为加锁和monitoreixt理解为释放锁。每个对象都维护一个记录着被锁次数的计数器,未被锁定的对象的该计数器为0,当一个线程获得锁,该计数器自增变为1,当同一个线程再次获得对象锁的时候,计数器再次自增,当同一个线程释放锁的时候,计数器减1,当计数器为0时,锁将被释放,其他线程可以获得。

ObjectMonitor类中提供了几个方法,如:enter,exit,wait,notify,notifyAll,synchronized加锁的时候,会调用objectmonitor的enter方法,解锁的时候会调用objectmonitor的exit方法。

(3)synchronized和原子性

       原子性是指一个操作是不可中断的,要全部执行完成,要不就不执行。

      示例: 线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解锁。即使在执行过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是,他并没有进行解锁。而由于synchronized的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完。这就保证了原子性。

(4)synchronized和可见性

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

原因是在java内存模型中,所有的变量都是存储在主内存中,每个线程都有自己的工作内存,线程先是从主内存中拷贝读取变量到自己的工作内存中操作,然后再将变量存放回主内存中,为了保证可见性,有这样一条规则,对一个变量解锁之前,必须先把此变量同步到主内存中,这样解锁后,后续线程就可以访问到被修改后的值。

(5)synchronized和有序性

      有序性是指程序执行的顺序按照代码的先后顺序执行。

除了引入时间片以外,由于处理器优化和指令重排等,CPU还可能对输入的代码进行乱序执行,而synchronized无法禁止指令重排和处理器优化的,也就是说,synchronized无法避免提到上述问题。

由于synchronized修饰的代码,同一时间只能被同一线程访问,那么也就是单线程执行的,所以可以保证其有序性。

(6)synchronized和volatile的区别主要这两点

执行控制的目的是控制代码执行(顺序)及是否可以并发执行。

内存可见控制的是线程执行结果在内存中对其它线程的可见性。

synchronized关键字解决的是执行控制的问题,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作。

volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

对于volatile关键字,当且仅当满足以下所有条件时可使用:

1.对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。

2.该变量没有包含在具有其他变量的不变式中。

volatile和synchronized的区别

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值