JAVA基础-synchronized关键字

synchronized关键字也叫作互斥锁或者同步。
 
这个关键字的存在是为了解决编程中的线程安全问题的,而线程安全问题出现的主要原因一般为:多个线程操作同一个对象的数据,也就是同时操作共享变量的值。
 
synchronized的出现解决了这个问题,互斥锁的含义为,当一个线程操作一个对象的时候,对该对象增加一个锁,任何其他线程都处在等待状态,不可以对该对象进行操作。当持有锁的线程执行完毕后,会释放持有锁,其他等待线程共同竞争锁资源。
同时,synchronized关键字还可以保证线程的变化对其他线程可见,保证共享变量的可见性,也就是volatile功能。
 
synchronized关键字的应用
 
1、修饰实例方法:对当前实例对象加锁,执行同步代码前获取当前实例对象的控制权。
2、修饰静态方法:对当前类对象加锁,执行同步代码前获取当前类对象的控制权。
3、修饰代码块:对任意指定对象加锁,执行同步代码前获取指定对象的控制权。
 
 
修饰实例方法:
 
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
    static int i = 0;
    public synchronized void add(){
        i++;
    }
    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    synchronizedTest.add();
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    synchronizedTest.add();
                }
            }
        });
        thread.start();
        thread1.start();
        // 确保线程执行完毕
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i);
    }
}
 
 
上述代码可以看到,新建了两个线程分别调用同步方法add() 由于两个线程锁的都是synchronizedTest对象,所以可以保证线程运行完毕,i的值为200000。得出结论修饰实例方法是对实例对象进行加锁。
 
接下来看一段代码:
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
    static int i = 0;
    public synchronized void add(){
        i++;
    }
    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        SynchronizedTest synchronizedTest1 = new SynchronizedTest();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    synchronizedTest.add();
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    synchronizedTest1.add();
                }
            }
        });
        thread.start();
        thread1.start();
        // 确保线程执行完毕
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i);
    }
}
 
对之前的代码做了轻微的改动,两个线程分别对两个实例对象加锁,这时当线程运行结束,结果不一定会是200000,得到了198478结果,说明两个线程用的是不同的锁,无法保证线程安全。
 
修饰静态方法:
 
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
    static int i = 0;
    public static synchronized void add(){
        i++;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    add();
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    add();
                }
            }
        });
        thread.start();
        thread1.start();
        // 确保线程执行完毕
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i);
    }
}
 
 
同步方法添加了static关键字,说明对当前类对象加锁。静态方法不属于任何一个实例。
但是如果一个线程A调用静态同步方法,另一个线程B调用实例同步方法,并且对同一个变量进行操作的时候,会发生线程安全问题,因为:静态方法锁的是类对象,实例方法锁的是实例对象,是两个不同的锁。
代码举例:
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
    static int i = 0;
    public static synchronized void add(){
        i++;
    }
    public synchronized void add1(){
        i++;
    }
    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest test = new SynchronizedTest();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    test.add1();
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    add();
                }
            }
        });
        thread.start();
        thread1.start();
        // 确保线程执行完毕
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i);
    }
}
 
 
修饰代码块:
 
    public void add(){
        synchronized (SynchronizedTest.class){
            i++;
        }
    }
    public void add1(){
        synchronized (this){
            i++;
        }
    }
 
当只需要对一部分代码进行同步操作时,可以用synchronized修饰代码片段,锁定的对象可以是实例镀锡或者this当前实例对象,也可以是XX.class类对象。
 
synchronized重入性
 
synchronized是给对象添加一个互斥锁,当一个线程持有对象锁时,其他操作该对象的线程将处于阻塞状态。但是当一个线程持有对象锁,然后再次请求自己持有对象锁的临界资源时,就是重入锁,可以请求成功。也就是说,在一个synchronized方法执行时,可以调用该对象的另一个synchronized方法。
 
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
    static int i = 0;
    public void add(){
        synchronized (SynchronizedTest.class){
            i++;
        }
    }
    public void add1(){
        synchronized (this){
            add();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest test = new SynchronizedTest();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    test.add();
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++ ){
                    test.add();
                }
            }
        });
        thread.start();
        thread1.start();
        // 确保线程执行完毕
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i);
    }
}
 
 
synchronized底层原理
 
JVM中的同步是通过持有和退出Monitor(监视器/管程)对象来实现的。
无论是显式同步(同步代码块)或者是隐式同步(同步方法)都是这样。
显式同步与隐式同步的区别:
显式同步:通过明确的代码指令monitorenter(同步开始)和monitorexit(同步结束)来实现。
隐式同步:通过读取常量池中方法的ACC_SYNCHRONIZED标识来实现。
 
synchronized同步代码块原理:
 
    public void synchronizedAdd(){
        synchronized (this){
            i++;
        }
    }
 
首先,对上面这个同步块代码进行反编译,javap -c SynchronizedTest.class
可以得到这个方法的指令代码。
其中第3行和第13行可以看到是通过monitorenter和monitorexit指令来实现同步。
当执行monitorenter指令时,当前线程获取锁对象的monitor持有权,当monitor持有计数器为0时,线程获得锁成功,计数器+1。如果在运行时,调用该对象其他锁方法,此时为重入状态,计数器再次+1。当同步代码执行完毕,计数器归0。其他线程将会试图获取monitor。
我们可以看到19行多了一个monitorexit,是为了当程序发生异常时,来释放monitor的。否则其他线程将无法获得monitor。
 
public void synchronizedAdd();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: getstatic     #3                  // Field i:I
       7: iconst_1
       8: iadd
       9: putstatic     #3                  // Field i:I
      12: aload_1
      13: monitorexit
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit
      20: aload_2
      21: athrow
      22: return
    Exception table:
       from    to  target type
           4    14    17   any
          17    20    17   any
 
 
synchronized同步方法原理:
 
同步方法,使用的是隐式同步方法,不通过指令来开始或结束同步(持有或释放monitor)。
同步方法通过方法常量池方法表结构中的ACC_SYNCHRONIZED标识区分是否为同步方法,如果在方法调用时发现ACC_SYNCHRONIZED被设置了,那么线程首先会获得monitor,其他线程无法获得这个monitor并处于阻塞状态。当方法执行完成时,释放monitor。如果在同步方法期间抛出异常,并且没有捕捉处理,那么该线程所持有的monitor在抛到同步方法之外时释放。
同步方法指令:
 
  public synchronized void synchronizedAdd();
    Code:
       0: getstatic     #2                  // Field i:I
       3: iconst_1
       4: iadd
       5: putstatic     #2                  // Field i:I
       8: return
 
 
现在在实际的编程中已经几乎很少使用synchronized关键字了,可以用更多的方式或封装类来替代。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章