36

【从入门到放弃-Java】并发编程-锁-synchronized

 5 years ago
source link: https://www.tuicool.com/articles/aUjMvam
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

简介

上篇 【从入门到放弃-Java】并发编程-线程安全 中,我们了解到,可以通过加锁机制来保护共享对象,来实现线程安全。

synchronized是java提供的一种内置的锁机制。通过synchronized关键字同步代码块。线程在进入同步代码块之前会自动获得锁,并在退出同步代码块时自动释放锁。内置锁是一种互斥锁。

本文来深入学习下synchronized。

使用

同步方法

同步非静态方法

public class Synchronized {
    private static int count;

    private synchronized void add1() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();

            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

结果符合预期:synchronized作用于非静态方法,锁定的是实例对象,如上所示锁的是sync对象,因此线程能够正确的运行,count的结果总会是20000。

public class Synchronized {
    private static int count;

    private synchronized void add1() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

结果不符合预期:如上所示,作用于非静态方法,锁的是实例化对象,因此当sync和sync1同时运行时,还是会出现线程安全问题,因为锁的是两个不同的实例化对象。

同步静态方法

public class Synchronized {
    private static int count;

    private static synchronized void add1() {
        count++;
        System.out.println(count);
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                Synchronized.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                Synchronized.add11();

            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

结果符合预期:锁静态方法时,锁的是类对象。因此在不同的线程中调用add1和add11依然会得到正确的结果。

同步代码块

锁当前实例对象

public class Synchronized {
    private static int count;

    private void add1() {
        synchronized (this) {
            count++;
            System.out.println(count);
        }
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

结果不符合预期:当synchronized同步方法块时,锁的是实例对象时,如上示例在不同的实例中调用此方法还是会出现线程安全问题。

锁其它实例对象

public class Synchronized {
    private static int count;
    public String lock = new String();

    private void add1() {
        synchronized (lock) {
            count++;
            System.out.println(count);
        }
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);

        System.out.println(sync.lock == sync1.lock);
    }
}

nEbqUjA.jpg!web

结果不符合预期:当synchronized同步方法块时,锁的是其它实例对象时,如上示例在不同的实例中调用此方法还是会出现线程安全问题。

public class Synchronized {
    private static int count;
    public String lock = "";

    private void add1() {
        synchronized (lock) {
            count++;
            System.out.println(count);
        }
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);

        System.out.println(sync.lock == sync1.lock);
    }
}

QZ3Inqz.jpg!web

结果符合预期:当synchronized同步方法块时,锁的虽然是其它实例对象时,但已上实例中,因为String = "" 是存放在常量池中的,实际上锁的还是相同的对象,因此是线程安全的

锁类对象

public class Synchronized {
    private static int count;

    private void add1() {
        synchronized (Synchronized.class) {
            count++;
            System.out.println(count);
        }
    }

    private static synchronized void add11() {
        count++;
        System.out.println(count);
    }

    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        Synchronized sync1 = new Synchronized();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync.add1();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                sync1.add1();
            }
        });

        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }
}

结果符合预期:当synchronized同步方法块时,锁的是类对象时,如上示例在不同的实例中调用此方法是线程安全的。

锁机制

public class Synchronized {
    private static int count;

    public static void main(String[] args) throws InterruptedException {
        synchronized (Synchronized.class) {
            count++;
        }
    }
}

使用javap -v Synchronized.class反编译class文件。

emInYje.jpg!web

可以看到synchronized实际上是通过monitorenter和monitorexit来实现锁机制的。同一时刻,只能有一个线程进入监视区。从而保证线程的同步。

正常情况下在指令4进入监视区,指令14退出监视区然后指令15直接跳到指令23 return

但是在异常情况下异常都会跳转到指令18,依次执行到指令20monitorexit释放锁,防止出现异常时未释放的情况。

这其实也是synchronized的优点:无论代码执行情况如何,都不会忘记主动释放锁。

想了解Monitors更多的原理可以 点击查看

锁升级

因为monitor依赖操作系统的Mutex lock实现,是一个比较重的操作,需要切换系统至内核态,开销非常大。因此在jdk1.6引入了偏向锁和轻量级锁。

synchronized有四种状态:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。

无锁

没有对资源进行锁定,所有线程都能访问和修改。但同时只有一个线程能修改成功

偏向锁

在锁竞争不强烈的情况下,通常一个线程会多次获取同一个锁,为了减少获取锁的代价 引入了偏向锁,会在java对象头中记录获取锁的线程的threadID。

  • 当线程发现对象头的threadID存在时。判断与当前线程是否是同一线程。
  • 如果是则不需要再次加、解锁。
  • 如果不是,则判断threadID是否存活。不存活:设置为无锁状态,其他线程竞争设置偏向锁。存活:查找threadID堆栈信息判断是否需要继续持有锁。需要持有则升级threadID线程的锁为轻量级锁。不需要持有则撤销锁,设置为无锁状态等待其它线程竞争。

因为偏向锁的撤销操作还是比较重的,导致进入安全点,因此在竞争比较激烈时,会影响性能,可以使用-XX:-UseBiasedLocking=false禁用偏向锁。

轻量级锁

当偏向锁升级为轻量级锁时,其它线程尝试通过CAS方式设置对象头来获取锁。

  • 会先在当前线程的栈帧中设置Lock Record,用于存储当前对象头中的mark word的拷贝。
  • 复制mark word的内容到lock record,并尝试使用cas将mark word的指针指向lock record
  • 如果替换成功,则获取偏向锁
  • 替换不成功,则会自旋重试一定次数。
  • 自旋一定次数或有新的线程来竞争锁时,轻量级锁膨胀为重量级锁。

CAS

CAS即compare and swap(比较并替换)。是一种乐观锁机制。通常有三个值

  • V:内存中的实际值
  • A:旧的预期值
  • B:要修改的新值
    即V与A相等时,则替换V为B。即内存中的实际值与我们的预期值相等时,则替换为新值。

CAS可能遇到ABA问题,即内存中的值为A,变为B后,又变为了A,此时A为新值,不应该替换。

可以采取:A-1,B-2,A-3的方式来避免这个问题

重量级锁

自旋是消耗CPU的,因此在自旋一段时间,或者一个线程在自旋时,又有新的线程来竞争锁,则轻量级锁会膨胀为重量级锁。

重量级锁,通过monitor实现,monitor底层实际是依赖操作系统的mutex lock(互斥锁)实现。

需要从用户态,切换为内核态,成本比较高

总结

本文我们一起学习了

  • synchronized的几种用法:同步方法、同步代码块。实际上是同步类或同步实例对象。
  • 锁升级:无锁、偏向锁、轻量级锁、重量级锁以及其膨胀过程。

synchronized作为内置锁,虽然帮我们解决了线程安全问题,但是带来了性能的损失,因此一定不能滥用。使用时请注意同步块的作用范围。通常,作用范围越小,对性能的影响也就越小(注意权衡获取、释放锁的成本,不能为了缩小作用范围,而频繁的获取、释放)。

本文作者:aloof_

阅读原文

本文为云栖社区原创内容,未经允许不得转载。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK