JUC高并发编程(二)——Synchronized关键字

前言

    synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。即synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 翻译为中文的意思是同步,也称之为”同步锁“。它包括两种用法:synchronized 方法和 synchronized 块。

为什么要用Synchronized关键字

    在使用多线程进行并发编程的时候,如果有多个线程来操作共享数据,很有可能共享数据的值会出现错乱,我们称之为线程安全问题。导致出现问题的原因有:可见性问题;原子性问题;有序性问题。

并发编程中的三个问题

可见性

  • 可见性(Visibility):是指一个线程对共享变量进行修改,另一个线程立即得到修改后的新值。
  • 出现可见性问题的两个前提:至少有两个线程、有个共享变量

可见性问题演示:

/**
 * 目标:演示可见性问题
 * 1.创建一个共享变量
 * 2.创建一条线程不断读取共享变量
 * 3.创建一条线程修改共享变量
 */
public class Test01Visibility {
 
    // 1. 创建一个共享变量
    private static boolean flag = true;
 
    public static void main(String[] args) throws InterruptedException {
        // 2. 创建一条线程不断读取共享变量
        new Thread(() -> {
            while (flag) {
 
            }
        }).start();
 
        Thread.sleep(2000);
 
        // 3. 创建一条线程修改共享变量
        new Thread(() -> {
            flag = false;
            System.out.println("线程修改了变量的值为false");
        }).start();
    }
 
}

当打印 “另一个线程修改了flag:false”,程序并没有停止,也就是说,线程1获取的flag还是初始获取的true,并没有立即得到修改后的值。
并发编程时,会出现可见性问题,一个线程对共享变量进行修改,另一个线程不能立即得到修改后的值(获取变量的值还是旧的值)

原子性

  • 原子性(Atomicity):在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。
  • 出现原子性问题的两个前提:至少有两个线程、有个共享变量。
    原子性问题演示:
/**
 * 目标:演示原子性问题
 * 1.定义一个共享变量number
 * 2.对number进行1000的++操作
 * 3.使用5个线程来进行
 */
public class Test02Atomictity {
    // 1. 定义一个共享变量number
    private static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        // 2. 对number进行1000的++操作
        Runnable increment = ()  -> {
            for (int i = 0; i < 1000; i++) {
                number++;
            }
        };
        List<Thread> list = new ArrayList<>();
        // 3. 使用5个线程来进行
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            list.add(t);
        }
        for (Thread t : list) {
            // 等到线程结束,再运行其他的,为了让5个线程都运行结束,再让main线程打印结果
            t.join();
        }
        System.out.println("number = " + number);
    }
}

5个线程,每个线程执行1000次number++,最终结果应该是5000,但打印结果是“number:4130”。说明number++操作并不是原子性操作。
使用javap反汇编class文件(命令为:javap -p -v 文件名.class),得到下面的字节码指令:
在这里插入图片描述
其中,对于 number++ 而言(number 为静态变量),实际会产生如下的 JVM 字节码指令:

9: getstatic #12       // 获取静态字段number的值并将其推送到操作数栈中
12: iconst_1           // 将整数常量1推送到操作数栈中
13: iadd                 // 将操作数栈顶的两个整数相加,并将结果推送到操作数栈中
14: putstatic #12   // 将操作数栈顶的值存储到静态字段number中
注释:这段代码的作用是将静态字段number的值增加1。

由此可见number++是由多条语句组成,以上多条指令在一个线程的情况下是不会出问题的,但是在多线程情况下就可能会出现问题。比如一个线程在执行13: iadd时,另一个线程又执行9: getstatic。会导致两次number++,实际上只加了1。
并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。

有序性

  • 有序性:程序执行的顺序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序。
    例如程序中:“a=6;b=7;”编译器优化后可能变成“b=7;a=6;”,在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的Bug。
    有序性问题代码演示:
    jcstress是java并发压测工具:https://wiki.openjdk.java.net/display/CodeTools/jcstress
    修改pom文件,添加依赖:
      <dependency>
            <groupId>org.openjdk.jcstress</groupId>
            <artifactId>jcstress-core</artifactId>
            <version>0.3</version>
        </dependency>
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
class Test03Orderliness {
    int num = 0;
    boolean ready = false;
    // 线程一执行的代码
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    // 线程2执行的代码
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

I_Result 是一个对象,有一个属性r1 用来保存结果,在多线程情况下可能出现几种结果?
情况1:线程1先执行actor1,这时ready = false,所以进入else分支结果为1。
情况2:线程2执行到actor2,执行了num = 2;和ready = true,线程1执行,这回进入 if 分支,结果为4。
情况3:线程2先执行actor2,只执行num = 2;但没来得及执行 ready = true,线程1执行,还是进入else分支,结果为1。
还有一种结果0。
运行测试:

mvn clean install
java -jar target/jcstress.jar

总结:程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序。

Synchronized保证三大特性

使用synchronized保证可见性

package com.itheima.demo02_concurrent_problem;
/**
案例演示:
一个线程根据boolean类型的标记flag, while循环,另一个线程改变这个flag变量的值,
另一个线程并不会停止循环.
*/
public class Test01Visibility {
// 多个线程都会访问的数据,我们称为线程的共享数据
	private static boolean run = true;
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(() -> {
			while (run) {
				// 增加对象共享数据的打印,println是同步方法
				System.out.println("run = " + run);
			}
		});
		t1.start();
		Thread.sleep(1000);
		Thread t2 = new Thread(() -> {
			run = false;
			System.out.println("时间到,线程2设置为false");
		});
		t2.start();
	}
}

在这里插入图片描述
synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值。那加了Synchronized关键字后,就可保证共享资源的可见性。

使用synchronized保证原子性

public class Test02Atomictity {
    // 1. 定义一个共享变量number
    private static int number = 0;
    public static void main(String[] args) throws InterruptedException {
        // 2. 对number进行1000的++操作
        Runnable increment = ()  -> {
            for (int i = 0; i < 1000; i++) {
            	synchronized (Test01Atomicity.class) {
					number++;
				}
            }
        };
        List<Thread> list = new ArrayList<>();
        // 3. 使用5个线程来进行
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            list.add(t);
        }
        for (Thread t : list) {
            // 等到线程结束,再运行其他的,为了让5个线程都运行结束,再让main线程打印结果
            t.join();
        }
        System.out.println("number = " + number);
    }
}

对number++;增加同步代码块后,保证同一时间只有一个线程操作number++;。就不会出现安全问题。
还是javap反汇编class文件(命令为:javap -p -v 文件名.class),得到下面的字节码指令:
在这里插入图片描述
保证一个线程把这几步执行完之后,另一个线程才能执行不会中途被抢占,从而保证原子性。

用synchronized保证有序性

重排序:为了提高程序的执行效率,编译器和CPU会对程序中代码进行重排序。
编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

int a = 1;
int b = 2;
int c = a + b;

上面3个操作的数据依赖关系如图所示:
在这里插入图片描述
如上图所示a和c之间存在数据依赖关系,同时b和c之间也存在数据依赖关系。因此在最终执行的指令序列中,c不能被重排序到a和b的前面。
但a和b之间没有数据依赖关系,编译器和处理器可以重排序a和b之间的执行顺序。
下图是该程序的两种执行顺序。
在这里插入图片描述

可以这样:
int a = 1;
int b = 2;
int c = a + b;
 
也可以重排序这样:
int b = 2;
int a = 1;
int c = a + b;

使用synchronized保证有序性代码示例:

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
class Test03Orderliness {
	private Object obj = new Object();
    int num = 0;
    boolean ready = false;
    // 线程一执行的代码
    @Actor
    public void actor1(I_Result r) {
    	synchronized(obj){
	        if(ready) {
	            r.r1 = num + num;
	        } else {
	            r.r1 = 1;
	        }
	    }
    }
    // 线程2执行的代码
    @Actor
    public void actor2(I_Result r) {
    	synchronized(obj){
        	num = 2;
        	ready = true;
        }
    }
}

synchronized后,虽然进行了重排序,保证只有一个线程会进入同步代码块,也能保证有序性。

Synchronized的特征

可重入特征

可重入:一个线程可以多次执行synchronized,重复获取同一把锁。
可重入特征代码示例:

public class Demo01 {
	public static void main(String[] args) {
		Runnable sellTicket = new Runnable() {
		@Override
		public void run() {
			synchronized (Demo01.class) {
				System.out.println("我是run");
				test01();
			}
		}
		public void test01() {
			synchronized (Demo01.class) {
				System.out.println("我是test01");
			}
		}
		};
		new Thread(sellTicket).start();
		new Thread(sellTicket).start();
	}
}

可重入原理:synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁。

synchronized是可重入锁,内部锁对象中会有一个计数器记录线程获取几次锁啦,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。

不可中断特征

不可中断:一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。
Synchronized不可中断代码演示:

/**
* 目标:演示synchronized不可中断
* 1. 定义一个Runnable
* 2. 在Runnable定义同步代码块
* 3. 先开启一个线程来执行同步代码块,保证不退出同步代码块
* 4. 后开启一个线程来执行同步代码块(阻塞状态)
* 5. 停止第二个线程
*/
public class Demo02_UnInterruptible {

   private static Object obj = new Object();

   public static void main(String[] args) throws InterruptedException {
       // 1. 定义一个Runnable
       Runnable run = () -> {
           // 2. 在Runnable定义同步代码块
           synchronized (obj) {
               String name = Thread.currentThread().getName();
               System.out.println(name + "进入同步代码块");
               // 保证不退出同步代码块
               try {
                   Thread.sleep(888888);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       };

       // 3. 先开启一个线程来执行同步代码块
       Thread t1 = new Thread(run);
       t1.start();
       Thread.sleep(1000);
       // 4. 后开启一个线程来执行同步代码块(阻塞状态)
       Thread t2 = new Thread(run);
       t2.start();

       // 5.停止第二个线程
       System.out.println("停止线程前");
       t2.interrupt();
       System.out.println("停止线程后");

       System.out.println(t1.getState());

       System.out.println(t2.getState());
   }
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨思默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值