你知道的越多,不知道的就越多,业余的像一棵小草!
你来,咱们一块儿精进!你不来,我和你的竞争对手一块儿精进!
java
编辑:业余草
blog.csdn.net/x541211190/article/details/106272922
推荐:https://www.xttblog.com/?p=5133
简介
本文将介绍8种同步方法的访问场景,咱们来看看这8种状况下,多线程访问同步方法是否仍是线程安全的。这些场景是多线程编程中常常遇到的,并且也是面试时高频被问到的问题,因此不论是理论仍是实践,这些都是多线程场景必需要掌握的场景。web
八种使用场景:
接下来,咱们来经过代码实现,分别判断如下场景是否是线程安全的,以及缘由是什么。面试
-
两个线程同时访问同一个对象的同步方法编程
-
两个线程同时访问两个对象的同步方法安全
-
两个线程同时访问(一个或两个)对象的静态同步方法微信
-
两个线程分别同时访问(一个或两个)对象的同步方法和非同步方法多线程
-
两个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法app
-
两个线程同时访问同一个对象的不一样的同步方法编辑器
-
两个线程分别同时访问静态synchronized和非静态synchronized方法ide
-
同步方法抛出异常后,JVM会自动释放锁的状况
场景一:两个线程同时访问同一个对象的同步方法
分析:这种状况是经典的对象锁中的方法锁,两个线程争夺同一个对象锁,因此会相互等待,是线程安全的。
「两个线程同时访问同一个对象的同步方法,是线程安全的。」
场景二:两个线程同时访问两个对象的同步方法
这种场景就是对象锁失效的场景,缘由出在访问的是两个对象的同步方法,那么这两个线程分别持有的两个线程的锁,因此是互相不会受限的。加锁的目的是为了让多个线程竞争同一把锁,而这种状况多个线程之间再也不竞争同一把锁,而是分别持有一把锁,因此咱们的结论是:
「两个线程同时访问两个对象的同步方法,是线程不安全的。」
代码验证:
public class Condition2 implements Runnable {
// 建立两个不一样的对象
static Condition2 instance1 = new Condition2();
static Condition2 instance2 = new Condition2();
@Override
public void run() {
method();
}
private synchronized void method() {
System.out.println("线程名:" + Thread.currentThread().getName() + ",运行开始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + ",运行结束");
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance1);
Thread thread2 = new Thread(instance2);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("测试结束");
}
}
运行结果:
两个线程是并行执行的,因此线程不安全。
线程名:Thread-0,运行开始
线程名:Thread-1,运行开始
线程:Thread-0,运行结束
线程:Thread-1,运行结束
测试结束
代码分析:
「问题在此:」
两个线程(thread一、thread2),访问两个对象(instance一、instance2)的同步方法(method()),两个线程都有各自的锁,不能造成两个线程竞争一把锁的局势,因此这时,synchronized修饰的方法method()和不用synchronized修饰的效果同样(不信去把synchronized关键字去掉,运行结果同样),因此此时的method()只是个普通方法。
「如何解决这个问题:」
若要使锁生效,只需将method()方法用static修饰,这样就造成了类锁,多个实例(instance一、instance2)共同竞争一把类锁,就可使两个线程串行执行了。这也就是下一个场景要讲的内容。
场景三:两个线程同时访问(一个或两个)对象的静态同步方法
这个场景解决的是场景二中出现的线程不安全问题,即用类锁实现:
「两个线程同时访问(一个或两个)对象的静态同步方法,是线程安全的。」
场景四:两个线程分别同时访问(一个或两个)对象的同步方法和非同步方法
这个场景是两个线程其中一个访问同步方法,另外一个访问非同步方法,此时程序会不会串行执行呢,也就是说是否是线程安全的呢?
咱们能够肯定是线程不安全的,若是方法不加synchronized
都是安全的,那就不须要同步方法了。验证下咱们的结论:
「两个线程分别同时访问(一个或两个)对象的同步方法和非同步方法,是线程不安全的。」
public class Condition4 implements Runnable {
static Condition4 instance = new Condition4();
@Override
public void run() {
//两个线程访问同步方法和非同步方法
if (Thread.currentThread().getName().equals("Thread-0")) {
//线程0,执行同步方法method0()
method0();
}
if (Thread.currentThread().getName().equals("Thread-1")) {
//线程1,执行非同步方法method1() www.xttblog.com
method1();
}
}
// 同步方法
private synchronized void method0() {
System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法,运行开始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + ",同步方法,运行结束");
}
// 普通方法
private void method1() {
System.out.println("线程名:" + Thread.currentThread().getName() + ",普通方法,运行开始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + ",普通方法,运行结束");
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("测试结束");
}
}
运行结果:
两个线程是并行执行的,因此是线程不安全的。
线程名:Thread-0,同步方法,运行开始
线程名:Thread-1,普通方法,运行开始
线程:Thread-0,同步方法,运行结束
线程:Thread-1,普通方法,运行结束
测试结束
结果分析
问题在于此:method1没有被synchronized修饰,因此不会受到锁的影响。即使是在同一个对象中,固然在多个实例中,更不会被锁影响了。结论:
「非同步方法不受其它由synchronized修饰的同步方法影响」
你可能想到一个相似场景:多个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法,这个场景会是线程安全的吗?
场景五:两个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法
咱们来实验下这个场景,用两个线程调用同步方法,在同步方法中调用普通方法;再用一个线程直接调用普通方法,看看是不是线程安全的?
public class Condition8 implements Runnable {
static Condition8 instance = new Condition8();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
//直接调用普通方法
method2();
} else {
// 先调用同步方法,在同步方法内调用普通方法
method1();
}
}
// 同步方法
private static synchronized void method1() {
System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法,运行开始");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + ",同步方法,运行结束,开始调用普通方法");
method2();
}
// 普通方法
private static void method2() {
System.out.println("线程名:" + Thread.currentThread().getName() + ",普通方法,运行开始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + ",普通方法,运行结束");
}
public static void main(String[] args) {
// 此线程直接调用普通方法
Thread thread0 = new Thread(instance);
// 这两个线程直接调用同步方法
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread0.start();
thread1.start();
thread2.start();
while (thread0.isAlive() || thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("测试结束");
}
}
运行结果:
线程名:Thread-0,普通方法,运行开始
线程名:Thread-1,同步方法,运行开始
线程:Thread-1,同步方法,运行结束,开始调用普通方法
线程名:Thread-1,普通方法,运行开始
线程:Thread-0,普通方法,运行结束
线程:Thread-1,普通方法,运行结束
线程名:Thread-2,同步方法,运行开始
线程:Thread-2,同步方法,运行结束,开始调用普通方法
线程名:Thread-2,普通方法,运行开始
线程:Thread-2,普通方法,运行结束
测试结束
结果分析:
咱们能够看出,普通方法被两个线程并行执行,不是线程安全的。这是为何呢?
由于若是非同步方法,有任何其余线程直接调用,而不是仅在调用同步方法时,才调用非同步方法,此时会出现多个线程并行执行非同步方法的状况,线程就不安全了。
对于同步方法中调用非同步方法时,要想保证线程安全,就必须保证非同步方法的入口,仅出如今同步方法中。但这种控制方式不够优雅,若被不明状况的人直接调用非同步方法,就会致使原有的线程同步再也不安全。因此不推荐你们在项目中这样使用,但咱们要理解这种状况,而且咱们要用语义明确的、让人一看就知道这是同步方法的方式,来处理线程安全的问题。
因此,最简单的方式,是在非同步方法上,也加上synchronized
关键字,使其变成一个同步方法,这样就变成了《场景五:两个线程同时访问同一个对象的不一样的同步方法》,这种场景下,你们就很清楚的看到,同一个对象中的两个同步方法,无论哪一个线程调用,都是线程安全的了。
因此结论是:
「两个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法,仅在没有其余线程直接调用非同步方法的状况下,是线程安全的。如有其余线程直接调用非同步方法,则是线程不安全的。」
场景六:两个线程同时访问同一个对象的不一样的同步方法
这个场景也是在探讨对象锁的做用范围,对象锁的做用范围是对象中的全部同步方法。因此,当访问同一个对象中的多个同步方法时,结论是:
「两个线程同时访问同一个对象的不一样的同步方法时,是线程安全的。」
public class Condition5 implements Runnable {
static Condition5 instance = new Condition5();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
//线程0,执行同步方法method0()
method0();
}
if (Thread.currentThread().getName().equals("Thread-1")) {
//线程1,执行同步方法method1() www.xttblog.com
method1();
}
}
private synchronized void method0() {
System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法0,运行开始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + ",同步方法0,运行结束");
}
private synchronized void method1() {
System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法1,运行开始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + ",同步方法1,运行结束");
}
//运行结果:串行
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("测试结束");
}
}
运行结果:
是线程安全的。
线程名:Thread-1,同步方法1,运行开始
线程:Thread-1,同步方法1,运行结束
线程名:Thread-0,同步方法0,运行开始
线程:Thread-0,同步方法0,运行结束
测试结束
结果分析:
两个方法(method0()和method1())的synchronized修饰符,虽没有指定锁对象,但默认锁对象为this对象为锁对象,
因此对于同一个实例(instance),两个线程拿到的锁是同一把锁,此时同步方法会串行执行。这也是synchronized
关键字的可重入性的一种体现。
场景七:两个线程分别同时访问静态synchronized和非静态synchronized方法
这种场景的本质也是在探讨两个线程获取的是否是同一把锁的问题。静态synchronized
方法属于类锁,锁对象是(*.class)
对象,非静态synchronized
方法属于对象锁中的方法锁,锁对象是this
对象。两个线程拿到的是不一样的锁,天然不会相互影响。结论:
「两个线程分别同时访问静态synchronized和非静态synchronized方法,线程不安全。」
代码实现:
public class Condition6 implements Runnable {
static Condition6 instance = new Condition6();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
//线程0,执行静态同步方法method0()
method0();
}
if (Thread.currentThread().getName().equals("Thread-1")) {
//线程1,执行非静态同步方法method1()
method1();
}
}
// 重点:用static synchronized 修饰的方法,属于类锁,锁对象为(*.class)对象。
private static synchronized void method0() {
System.out.println("线程名:" + Thread.currentThread().getName() + ",静态同步方法0,运行开始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + ",静态同步方法0,运行结束");
}
// 重点:synchronized 修饰的方法,属于方法锁,锁对象为(this)对象。
private synchronized void method1() {
System.out.println("线程名:" + Thread.currentThread().getName() + ",非静态同步方法1,运行开始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + ",非静态同步方法1,运行结束");
}
//运行结果:并行
public static void main(String[] args) {
//问题缘由: 线程1的锁是类锁(*.class)对象,线程2的锁是方法锁(this)对象,两个线程的锁不同,天然不会互相影响,因此会并行执行。
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("测试结束");
}
运行结果:
线程名:Thread-0,静态同步方法0,运行开始
线程名:Thread-1,非静态同步方法1,运行开始
线程:Thread-1,非静态同步方法1,运行结束
线程:Thread-0,静态同步方法0,运行结束
测试结束
场景八:同步方法抛出异常后,JVM会自动释放锁的状况
本场景探讨的是synchronized
释放锁的场景:
「只有当同步方法执行完或执行时抛出异常这两种状况,才会释放锁。」
因此,在一个线程的同步方法中出现异常的时候,会释放锁,另外一个线程获得锁,继续执行。而不会出现一个线程抛出异常后,另外一个线程一直等待获取锁的状况。这是由于JVM在同步方法抛出异常的时候,会自动释放锁对象。
代码实现:
public class Condition7 implements Runnable {
private static Condition7 instance = new Condition7();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
//线程0,执行抛异常方法method0()
method0();
}
if (Thread.currentThread().getName().equals("Thread-1")) {
//线程1,执行正常方法method1()
method1();
}
}
private synchronized void method0() {
System.out.println("线程名:" + Thread.currentThread().getName() + ",运行开始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//同步方法中,当抛出异常时,JVM会自动释放锁,不须要手动释放,其余线程便可获取到该锁
System.out.println("线程名:" + Thread.currentThread().getName() + ",抛出异常,释放锁");
throw new RuntimeException();
}
private synchronized void method1() {
System.out.println("线程名:" + Thread.currentThread().getName() + ",运行开始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + ",运行结束");
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("测试结束");
}
}
运行结果:
线程名:Thread-0,运行开始
线程名:Thread-0,抛出异常,释放锁
线程名:Thread-1,运行开始
Exception in thread "Thread-0" java.lang.RuntimeException
at com.study.synchronize.conditions.Condition7.method0(Condition7.java:34)
at com.study.synchronize.conditions.Condition7.run(Condition7.java:17)
at java.lang.Thread.run(Thread.java:748)
线程:Thread-1,运行结束
测试结束
结果分析:
能够看出线程仍是串行执行的,说明是线程安全的。并且出现异常后,不会形成死锁现象,JVM会自动释放出现异常线程的锁对象,其余线程获取锁继续执行。
总结
本文总结了并用代码实现和验证了synchronized
各类使用场景,以及各类场景发生的缘由和结论。咱们分析的理论基础都是synchronized
关键字的锁对象到底是谁?多个线程之间竞争的是不是同一把锁?根据这个条件来判断线程是不是安全的。因此,有了这些场景的分析锻炼后,咱们在之后使用多线程编程时,也能够经过分析锁对象的方式,判断出线程是不是安全的,从而避免此类问题的出现。
本文涵盖了synchronized
关键字的最重要的各类使用场景,也是面试官经常会问到的高频问题,是一篇值得你们仔细阅读和亲自动手实践的文章,喜欢本文请点赞和收藏。
本文分享自微信公众号 - 业余草(yyucao)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。