Java——多线程高并发系列之synchronized关键字
Posted 张起灵-小哥
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java——多线程高并发系列之synchronized关键字相关的知识,希望对你有一定的参考价值。
文章目录:
Demo3(synchronized面对一个 public static final 常量)
Demo4(synchronized同步代码块分别位于实例方法、静态方法中)
Demo5(synchronized同步实例方法体,默认的锁对象是this)
Demo6(synchronized同步静态方法体,默认的锁对象是当前类的运行时对象Test06.class)
Demo9(synchronized同步过程中线程出现异常, 会自动释放锁对象)
写在前面
Java 中的每个对象都有一个与之关联的内部锁(Intrinsic lock)。这种锁也称为监视器(Monitor),这种内部锁是一种排他锁,可以保障原子性,可见性与有序性。内部锁是通过 synchronized 关键字实现的,synchronized 关键字修饰代码块,修饰该方法。
修饰实例方法就称为同步实例方法
修饰静态方法称称为同步静态方法下面,我用10个Demo带大家彻底搞明白synchronized关键字的使用!!!
Demo1(synchronized面对同一个实例对象)
synchronized关键字当前修饰的是一个代码块,其中的this可以这样理解,目前,synchronized这个“东西”目前处在Test01类中,而我们程序的第一句就是 Test01 obj = new Test01(); 那么synchronized中的this代表的就是obj对象,obj对象是属于Test01类的,那这不就对应上this了吗?
package com.szh.synchronizedtest;
/**
* synchronized同步代码块
* this表示的是obj锁对象
*/
public class Test01 {
public static void main(String[] args) {
//先创建Test01对象
Test01 obj=new Test01();
//创建两个线程,分别调用mm方法
new Thread(new Runnable() { //Thread-0
@Override
public void run() {
obj.mm(); //使用的锁对象this就是obj
}
}).start();
new Thread(new Runnable() { //Thread-1
@Override
public void run() {
obj.mm(); //使用的锁对象this也是obj
}
}).start();
}
public void mm() {
synchronized (this) {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ---> " + i);
}
}
}
}
从程序的运行结果可以看出来,第一个线程这里先抢到CPU执行权,然后启动,进入mm方法后,对其上锁,此时第二个线程就进不来了,必须等待第一个线程执行完毕同步代码块之后才可以执行。
也就是Thread-0先打印了10行,之后Thread-1才打印。(我这里只是面对这个执行结果才这样说,线程的执行结果也可能是先Thread-1、再Thread-0)
Demo2(synchronized面对多个实例对象)
这个例子中,synchronized仍然修饰了同步代码块,但是这里Test02类new了两个实例对象,那这就不一样了。
new第一个Thread的时候,我们使用obj1实例对象去调用mm方法,这时候Thread-0线程进入同步代码块,对obj1对象上锁,执行for循环。
new第二个Thread的时候,我们使用obj2实例对象去调用mm方法,这时候Thread-1线程能不能进入同步代码块呢?(不能吧,你Thread-0线程不是已经上锁了吗?)答案当然能!!!Thread-0线程是对obj1对象上的锁,我先Thread-1面对的是obj2对象啊,这个对象还没有上锁呢,我这里为什么不能进入同步代码块呢???所以这里Thread-1完全可以进入同步代码块执行,此时对obj2对象上锁。
也就是说,synchronized关键字要想实现线程同步,这些线程必须面对同一个锁对象!!!
package com.szh.synchronizedtest;
/**
* synchronized同步代码块
* this在这个代码案例中表示了两个不同的锁对象
* 要想实现同步,必须是同一个锁对象
*/
public class Test02 {
public static void main(String[] args) {
//先创建Test01对象
Test02 obj1=new Test02();
Test02 obj2=new Test02();
//创建两个线程,分别调用mm方法
new Thread(new Runnable() { //Thread-0
@Override
public void run() {
obj1.mm(); //使用的锁对象this就是obj1
}
}).start();
new Thread(new Runnable() { //Thread-1
@Override
public void run() {
obj2.mm(); //使用的锁对象this就是obj2
}
}).start();
}
public void mm() {
synchronized (this) {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ---> " + i);
}
}
}
}
执行结果中可以看到,两个线程不同步,一会Thread-0、一会Thread-1。
Demo3(synchronized面对一个 public static final 常量)
这个例子中,synchronized中传入了一个 public static final 修饰的常量,这个自然也是可以实现线程同步的。
因为在Test03这个类中,不管你new多少个对象,在某个线程中,使用哪个实例对象去调同步代码块对应的方法,对于我们 public static final 修饰的常量来说,在这个类中都是可见的!!!
当第一个线程(Thread-0)run启动成功之后,synchronized将这个常量给上了锁,然后线程Thread-0进去执行for循环。
这个时候第二个线程(Thread-1)run也启动成功了,它也想进入同步代码块执行,但是同步代码块面对的是常量,而常量此时已经被Thread-0给锁上了,那么你Thread-1肯定就进不去了呀,它就必须等待Thread-0执行完毕才可以进入同步代码块执行。
(我这里只是面对这个执行结果才这样说,线程的执行结果也可能是先Thread-1、再Thread-0)
package com.szh.synchronizedtest;
/**
* synchronized同步代码块
* 使用一个常量对象作为锁对象
* this表示的是OBJ
*/
public class Test03 {
//定义一个常量
public static final Object OBJ=new Object();
public static void main(String[] args) {
//先创建Test01对象
Test03 obj1=new Test03();
Test03 obj2=new Test03();
//创建两个线程,分别调用mm方法
new Thread(new Runnable() { //Thread-0
@Override
public void run() {
obj1.mm(); //使用的锁对象this就是OBJ常量,这与使用哪个对象调用mm方法无关,这个常量是大家共有的
}
}).start();
new Thread(new Runnable() { //Thread-1
@Override
public void run() {
obj2.mm(); //使用的锁对象this就是OBJ常量,这与使用哪个对象调用mm方法无关
}
}).start();
}
public void mm() {
synchronized (OBJ) {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ---> " + i);
}
}
}
}
Demo4(synchronized同步代码块分别位于实例方法、静态方法中)
这个例子中,我分别将synchronized修饰的同步代码块放在了实例方法和静态方法中,下面,我来说一下我对执行结果的理解:
实例方法中的synchronized理解可以参考Demo3。而这个静态方法中,为什么也实现了线程同步呢?因为我们都知道,static 修饰的方法被称为静态方法,而静态方法一般是由它的所属类直接调用的,而在这个例子中它是属于Test04类的,所以
public static void sm() { synchronized (OBJ) { for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + " ---> " + i); } } }这个代码块就是由Test04直接调用的,而它的同步代码块锁的是OBJ这个常量,这个常量不正好也是Test04类所有的吗?所以说,这三个子线程中的同步代码块面对的都是Test04类所拥有的OBJ常量(就这一把锁)!!!
package com.szh.synchronizedtest;
/**
* synchronized同步代码块
* 这里使用一个常量作为锁对象,这个常量是所有对象都共享的,也就是同一个锁对象
* 不管是实例方法还是静态方法,只要是同一个锁对象,就可以实现同步
*/
public class Test04 {
//定义一个常量
public static final Object OBJ=new Object();
public static void main(String[] args) {
//先创建Test01对象
Test04 obj1=new Test04();
Test04 obj2=new Test04();
//创建两个线程,分别调用mm方法
new Thread(new Runnable() { //Thread-0
@Override
public void run() {
obj1.mm(); //使用的锁对象this就是OBJ常量,这与使用哪个对象调用mm方法无关,这个常量是大家共有的
}
}).start();
new Thread(new Runnable() { //Thread-1
@Override
public void run() {
obj2.mm(); //使用的锁对象this就是OBJ常量,这与使用哪个对象调用mm方法无关
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
sm(); //使用的锁对象this就是OBJ常量
}
}).start();
}
public void mm() {
synchronized (OBJ) {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ---> " + i);
}
}
}
public static void sm() {
synchronized (OBJ) {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ---> " + i);
}
}
}
}
这里可以看到,虽然我们new线程的顺序是Thread-0、Thread-1、Thread-2,但是执行顺序却是0、1、2的线程顺序,所以多线程的执行结果并不是一定按照代码顺序来的。
Demo5(synchronized同步实例方法体,默认的锁对象是this)
这个例子中,synchronized(this)就不再多说了,它锁的就是Test05类的那个实例对象obj。
而 public synchronized void mm2()这个实例方法中,大家不要被迷惑,这里它修饰的是一个实例方法,而实例方法不就是由类的实例对象调用的吗?那说白了,它锁的不还是本类中new的那个obj对象吗?是的吧!!!
package com.szh.synchronizedtest;
/**
* synchronized同步实例方法体,默认的锁对象是this
* this表示的obj锁对象
*/
public class Test05 {
public static void main(String[] args) {
//先创建Test01对象
Test05 obj=new Test05();
//创建两个线程,分别调用mm方法
new Thread(new Runnable() { //Thread-0
@Override
public void run() {
obj.mm(); //使用的锁对象this就是obj
}
}).start();
new Thread(new Runnable() { //Thread-1
@Override
public void run() {
obj.mm2(); //使用的锁对象this也是obj
}
}).start();
}
public void mm() {
synchronized (this) {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ---> " + i);
}
}
}
public synchronized void mm2() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ---> " + i);
}
}
}
Demo6(synchronized同步静态方法体,默认的锁对象是当前类的运行时对象Test06.class)
如果这样写:synchronized(XXX.class)则表示的是当前类的运行时类对象作为锁对象,而在反射中简单的理解,XXX.class不就是这个类的字节码文件吗?
而 public synchronized static void mm2() {} 这样写,则说明synchronized修饰了静态方法体,而静态方法是属于类的,所以它这里锁的也是当前类的运行时对象。也就是说synchronized在这两种方式中面对的都是类锁!!!
package com.szh.synchronizedtest;
/**
* synchronized同步静态方法体,默认的锁对象是当前类的运行时对象Test06.class
* 这个也可以称为类锁
*/
public class Test06 {
public static void main(String[] args) {
//先创建Test01对象
Test06 obj=new Test06();
//创建两个线程,分别调用mm方法
new Thread(new Runnable() { //Thread-0
@Override
public void run() {
obj.mm(); //使用的锁对象是Test06.class
}
}).start();
new Thread(new Runnable() { //Thread-1
@Override
public void run() {
Test06.mm2(); //使用的锁对象是Test06.class
}
}).start();
}
public void mm() {
//使用当前类的运行时类对象作为锁对象,简单的理解为将Test06类的字节码文件作为锁对象
synchronized (Test06.class) {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ---> " + i);
}
}
}
//synchronized修饰静态方法,同步静态方法,默认的锁对象为运行时类对象Test06.class
public synchronized static void mm2() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ---> " + i);
}
}
}
Demo7(同步方法与同步代码块如何选择)
这个就不再给出例子了,就一句话:同步代码块,锁的粒度细,执行效率高。同步方法,锁的粒度粗, 执行效率低。
Demo8(synchronized解决脏读问题)
这个例子中,就是说如果我们在子线程中设置了用户名和密码,我们对写方法的代码块进行了同步,如果不对读方法的代码块进行同步了话,那么这个时候,子线程设置了用户名和密码,而main主线程并不知道你子线程设置的用户名和密码,它此时是可以从getValue中读取到静态内部类PublicValue中提前定义好的 root、12345678(因为读方法并没有同步,子线程对setValue方法上了锁,但是getValue还可以顺利的读)。这显然不对啊,我们希望的是子线程设置完新的用户名密码之后,在main主线程中也可以读取到新的值,而不是读取到的脏数据。(将getValue方法前的synchronized删掉,main主线程输出结果将会是 root、12345678)
这个时候,我们就需要将读方法的代码块也定义为同步的!!!这个时候,子线程一旦对setValue写方法上锁之后,那么main主线程此时再想从getValue方法中读取就不行了,因为getValue方法也上了锁,而这两个方法的synchronized都是面对的实例方法上的锁,而同步实例方法默认的锁对象就是this( PublicValue publicValue=new PublicValue(); )。所以这样等到子线程设置完新值之后,main主线程就可以读取到了。
package com.szh.synchronizedtest;
/**
* 脏读
* 出现读取属性值出现了一些意外, 读取的是中间值,而不是修改之后的值
* 出现脏读的原因是: 对共享数据的修改 与 对共享数据的读取 不同步
* 解决方法:
* 不仅对修改数据的代码块进行同步, 还要对读取数据的代码块同步
*/
public class Test08 {
public static void main(String[] args) throws InterruptedException {
//开启子线程设置用户名和密码
PublicValue publicValue=new PublicValue();
SubThread t1=new SubThread(publicValue);
t1.start();
//为了确定设置成功
Thread.sleep(100);
//在main线程中读取用户名和密码
publicValue.getValue();
}
static class SubThread extends Thread {
private PublicValue publicValue;
public SubThread(PublicValue publicValue) {
this.publicValue=publicValue;
}
@Override
public void run() {
publicValue.setValue("admin","666");
}
}
static class PublicValue {
private String name="root";
private String pwd="12345678";
public synchronized void getValue() {
System.out.println(Thread.currentThread().getName() + ", getter -- name: "
+ name + ", pwd: " + pwd);
}
public synchronized void setValue(String name,String pwd) {
this.name=name;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.pwd=pwd;
System.out.println(Thread.currentThread().getName() + ", setter -- name: "
+ name + ", pwd: " + pwd);
}
}
}
Demo9(synchronized同步过程中线程出现异常, 会自动释放锁对象)
这个例子说的是,同步过程中,如果发生了运行时异常,则占有锁的那个线程会自动释放占有的锁对象。
Thread-0在启动之后,执行同步代码块,它锁的是当前类的运行时对象(Test09.class),也就是当前类的字节码文件(类锁),所以这个时候,Thread-1启动后,它调用mm2()方法时,是无法执行mm2()方法的,因为这个方法是静态方法,默认的锁对象也是当前类的运行时对象(Test09.class),所以这两个子线程面对的是同一个锁!!!
当 i=5 时,Integer.parseInt("abc");这段代码会抛出字符串转换为数字异常,这个时候发生了异常,那么Thread-0子线程就会释放它所占有的Test09类锁,那么此时Thread-1就可以顺序执行了!!!(详情见运行结果图)
package com.szh.synchronizedtest;
/**
* synchronized同步静态方法体,默认的锁对象是当前类的运行时对象Test09.class
* 这个也可以称为类锁
* 同步过程中线程出现异常, 会自动释放锁对象
*/
public class Test09 {
public static void main(String[] args) {
//先创建Test01对象
Test09 obj=new Test09();
//创建两个线程,分别调用mm方法
new Thread(new Runnable() { //Thread-0
@Override
public void run() {
obj.mm(); //使用的锁对象是Test06.class
}
}).start();
new Thread(new Runnable() { //Thread-1
@Override
public void run() {
Test09.mm2(); //使用的锁对象是Test06.class
}
}).start();
}
public void mm() {
//使用当前类的运行时类对象作为锁对象,简单的理解为将Test06类的字节码文件作为锁对象
synchronized (Test09.class) {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ---> " + i);
if (i==5) {
//把字符串转换为 int 类型时,如果字符串不符合 数字格式会产生异常
Integer.parseInt("abc");
}
}
}
}
//synchronized修饰静态方法,同步静态方法,默认的锁对象为运行时类对象Test06.class
public synchronized static void mm2() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " ---> " + i);
}
}
}
Demo10(使用synchronized实现死锁)
这个例子,首先在子线程中有两个常量 lock1、lock2,那么你SubThread无论new多少个子线程对象,它们都是共享这两个常量的。这里对两个线程分别命名为了 a、b,然后启动这两个子线程,它们两个会抢夺CPU资源去执行自己的run方法,假如说 t1 线程先抢到了,那么它首先对 lock1 上了锁,这个时候,t2线程抢到了,那么它首先对 lock2 上了锁,这个时候,假如t1又抢到了,那么它想对 lock2 上锁,此时 lock2 已经被 t2 上过锁了,所以 t1 会在这里等待,一直等到 t2 释放 lock2 的锁;此时 t2 获得了 CPU 的运行权,那么它继续执行run方法,它此时相对 lock1 上锁,而 lock1 已经被 t1 上了锁,所以 t2 会在这里等待,一直等到 t1 释放 lock1 的锁;但是我们前面说了呀,t1在等待 t2 释放 lock2 的锁,t2在等待 t1 释放 lock1 的锁,那我等你,你等我,谁也退出不了,谁也前进不成,那不就死到这里了吗?这就是死锁问题!!!
package com.szh.synchronizedtest;
/**
* 死锁
*/
public class Test10 {
public static void main(String[] args) {
SubThread t1=new SubThread();
SubThread t2=new SubThread();
t1.setName("a");
t2.setName("b");
t1.start();
t2.start();
}
static class SubThread extends Thread {
private static final Object lock1=new Object();
private static final Object lock2=new Object();
@Override
public void run() {
if ("a".equals(Thread.currentThread().getName())) {
synchronized (lock1) {
System.out.println("a线程获得了lock1锁,还需要获得lock2锁");
synchronized (lock2) {
System.out.println("a线程又获得了lock2锁???");
}
}
}
if ("b".equals(Thread.currentThread().getName())) {
synchronized (lock2) {
System.out.println("b线程获得了lock2锁,还需要获得lock1锁");
synchronized (lock1) {
System.out.println("b线程又获得了lock1锁???");
}
}
}
}
}
}
以上是关于Java——多线程高并发系列之synchronized关键字的主要内容,如果未能解决你的问题,请参考以下文章
Java——多线程高并发系列之线程池(Executor)的理解与使用
Java——多线程高并发系列之线程池(Executor)的理解与使用