Java并发17:synchronized关键字的两种用法-同步代码块(4)和同步方法(2)

版权声明:本文为博主hanchao5272原创文章,转载请注明来源,并留下原文链接地址,谢谢! https://blog.csdn.net/hanchao5272/article/details/79606329

[超级链接:Java并发学习系列-绪论]

volatile关键字在之前的章节中多次提及:
- Java并发02:Java并发Concurrent技术发展简史(各版本JDK中的并发技术)
- Java并发12:并发三特性-原子性、可见性和有序性概述及问题示例
- Java并发13:并发三特性-原子性定义、原子性问题与原子性保证技术
- Java并发14:并发三特性-可见性定义、可见性问题与可见性保证技术
- Java并发15:并发三特性-有序性定义、有序性问题与有序性保证技术

本章主要对synchronized关键字的两种用法进行学习。

1.synchronized简述

引用百度百科的一段解释:

synchronized 关键字,代表这个方法(或代码块)加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法synchronized 代码块

简单来说,synchronized关键字以同步方法和同步代码块的方式,方法和代码块上的对象加锁使得同一时刻,在这个对象上的多个线程,只能由持有这个对象锁的单个线程进行代码的调用执行。

synchronized 关键字能够保证代码的原子性、可见性和有序性

2.synchronized关键字的用法

2.1.用法概述

synchronized关键字主要有两大类6小类用法:

  • 同步代码块
    • 加锁对象是本地变量的同步代码块
    • 加锁对象是类静态变量的同步代码块
    • 加锁对象是共享变量的同步代码块
    • 加锁对象是类对象的同步代码块
  • 同步方法
    • 修饰方法是普通方法的同步方法
    • 修饰方法是类静态方法的同步方法

下面会依次进行实例学习。

2.2.同步代码块和同步方法

相同点:

  • 同步代码块同步方法都通过synchronized关键字实现
  • 同步代码块同步方法都能够保证代码的同步性,即:原子性、有序性和可见性。

不同的:

  • 同步代码块同步方法的锁住的范围更小,所以性能更好。
  • 同步方法同步代码块的编写更简单,只需要在方法定义是加上synchronized关键字即可,而后者还需要确定加锁对象。

2.3.示例场景

  • 定义一个自增器Increment,此自增器执行一次自增方法autoIncrement()会循环进行5次number++操作。
  • 在多线程的环境中,对自增器进行测试。

2.4.原始示例-不采取任何同步措施

从前面的很多章节中可知,自增操作不是原子性操作。如果不采取任何同步手段,多线程下的自增会存在线程安全隐患。

下面的代码展示的是一个不采取任何同步措施的自增器:

/**
 * <p>自增器-无同步</p>
 *
 * @author hanchao 2018/3/18 11:39
 **/
static class Increment {
    public void autoIncrement() {
        for (int i = 0; i < 5; i++) {
            number++;
            System.out.println("线程[" + Thread.currentThread().getName() + "],number:" + number);
        }
    }
}

下面的代码展示的是如何多线程环境下测试这个自增器:

//多线程数量
int num = 100000;
//休眠等待时间
int sleep = 5000;

System.out.println("无同步措施的自增器");
Increment increment0 = new Increment();
for (int i = 0; i < num; i++) {
    new Thread(() -> {
        increment0.autoIncrement();
    }).start();
}
Thread.sleep(sleep);
System.out.println("无同步措施的自增器,最终number=" + number);

运行结果:

...省略
线程[Thread-99722],number:499800
线程[Thread-99722],number:499939
无同步措施的自增器,最终number=499939

显而易见,在多线程环境中,这种做法是存在线程安全隐患的。

后面的章节中,会通过上面提到的两大类6小类同步方式依次对这个问题进行解决,已验证各类同步方式的使用注意事项。

3.同步代码块

同步代码块加锁的范围是代码块{}内的代码,同步对象根据加锁对象的不同而不同。

3.1.加锁对象是本地变量的同步代码块

关于本地变量的概念可以参考:
Java并发11:Java内存模型、指令重排、内存屏障、happens-before原则


自增器代码:

/**
 * <p>自增器-同步代码块-锁对象是类的本地变量</p>
 *
 * @author hanchao 2018/3/18 11:26
 **/
static class SyncCodeBlockIncrement11 {
    // 同步代码10/11-加锁对象是类的本地变量
    private byte[] ordinaryObj = new byte[0];

    public void autoIncrement() {
        synchronized (ordinaryObj) {
            for (int i = 0; i < 5; i++) {
                number++;
                System.out.println("线程[" + Thread.currentThread().getName() + "]获取锁,number:" + number);
            }
        }
    }
}

测试多个对象:

System.out.println("通过synchronized定义同步代码块,作用范围:代码块,同步对象:加锁对象为类本地变量->类的多个对象no");
SyncCodeBlockIncrement11 increment10 = new SyncCodeBlockIncrement11();
for (int i = 0; i < num; i++) {
    //如果调用其他方法的同步方法,则计算结果错误
    if (i % 2 == 0) {//模拟多个对象
        new Thread(() -> {
            new SyncCodeBlockIncrement11().autoIncrement();
        }).start();
    } else {//模拟同一个对象的多次调用
        new Thread(() -> {
            increment10.autoIncrement();
        }).start();
    }
}
Thread.sleep(sleep);
System.out.println("同步代码块:加锁对象为类本地变量,通过类的多个对象调用同步代码块,最终number=" + number);

测试结果:

...省略
线程[Thread-96703]获取锁,number:499987
线程[Thread-96703]获取锁,number:499988
同步代码块:加锁对象为类本地变量,通过类的多个对象调用同步代码块,最终number=499988

测试结果说明:

同步代码块:加锁对象为类本地变量,通过类的多个对象调用同步代码块,并不能保证同步性。


测试单个对象:

System.out.println("通过synchronized定义同步代码块,作用范围:代码块,同步对象:加锁对象为类本地变量->类的单个对象ok");
SyncCodeBlockIncrement11 increment11 = new SyncCodeBlockIncrement11();
for (int i = 0; i < num; i++) {
    new Thread(() -> {
        increment11.autoIncrement();
    }).start();
}
Thread.sleep(sleep);
System.out.println("同步代码块:加锁对象为类本地变量,通过类的单个对象调用同步代码块,最终number=" + number);

测试结果:

...省略
线程[Thread-95431]获取锁,number:499999
线程[Thread-95431]获取锁,number:500000
同步代码块:加锁对象为类本地变量,通过类的单个对象调用同步代码块,最终number=500000

测试结果说明:

同步代码块:加锁对象为类本地变量,通过类的单个对象调用同步代码块,能够保证同步性。

3.2.加锁对象是类静态变量的同步代码块

自增器代码:

/**
 * <p>自增器-同步代码块-锁对象是类的静态变量</p>
 *
 * @author hanchao 2018/3/18 11:26
 **/
static class SyncCodeBlockIncrement12 {
    //同步代码12-加锁对象是类的静态变量
    private static byte[] staticObj = new byte[0];

    public void autoIncrement() {
        synchronized (staticObj) {
            for (int i = 0; i < 5; i++) {
                number++;
                System.out.println("线程[" + Thread.currentThread().getName() + "]获取锁,number:" + number);
            }
        }
    }
}

多线程测试自增器代码:

System.out.println("通过synchronized定义同步代码块,作用范围:代码块,同步对象:加锁对象为类静态变量->类的多个对象ok");
SyncCodeBlockIncrement12 increment12 = new SyncCodeBlockIncrement12();
for (int i = 0; i < num; i++) {
    if (i % 2 == 0) {//模拟多个对象
        new Thread(() -> {
            new SyncCodeBlockIncrement12().autoIncrement();
        }).start();
    } else {//模拟同一个对象的多次调用
        new Thread(() -> {
            increment12.autoIncrement();
        }).start();
    }
}
Thread.sleep(sleep);
System.out.println("同步代码块:加锁对象为类静态变量,通过类的多个对象调用同步代码块,最终number=" + number);

运行结果:

...省略
线程[Thread-99609]获取锁,number:499999
线程[Thread-99609]获取锁,number:500000
同步代码块:加锁对象为类静态变量,通过类的多个对象调用同步代码块,最终number=500000

测试结果说明:

同步代码块:加锁对象为类静态变量,通过类的多个对象调用同步代码块,能够保证同步性。

3.3.加锁对象是共享变量的同步代码块

关于共享变量的概念可以参考:
Java并发11:Java内存模型、指令重排、内存屏障、happens-before原则


共享变量:

//同步代码13-加锁对象是共享变量
private static byte[] heapObj = new byte[0];

自增器代码:

//同步代码13-加锁对象是共享变量
private static byte[] heapObj = new byte[0];
/**
 * <p>自增器-同步代码块-锁对象是共享变量</p>
 *
 * @author hanchao 2018/3/18 11:26
 **/
static class SyncCodeBlockIncrement13 {
    public void autoIncrement() {
        synchronized (heapObj) {
            for (int i = 0; i < 5; i++) {
                number++;
                System.out.println("线程[" + Thread.currentThread().getName() + "]获取锁,number:" + number);
            }
        }
    }
}

多线程测试自增器代码:

System.out.println("通过synchronized定义同步代码块,作用范围:代码块,同步对象:加锁对象为共享变量->类的多个对象ok");
SyncCodeBlockIncrement13 increment13 = new SyncCodeBlockIncrement13();
for (int i = 0; i < num; i++) {
    if (i % 2 == 0) {//模拟多个对象
        new Thread(() -> {
            new SyncCodeBlockIncrement13().autoIncrement();
        }).start();
    } else {//模拟同一个对象的多次调用
        new Thread(() -> {
            increment13.autoIncrement();
        }).start();
    }
}
Thread.sleep(sleep);
System.out.println("同步代码块:加锁对象为共享变量,通过类的多个对象调用同步代码块,最终number=" + number);

运行结果:

...省略
线程[Thread-99178]获取锁,number:499999
线程[Thread-99178]获取锁,number:500000
同步代码块:加锁对象为共享变量,通过类的多个对象调用同步代码块,最终number=500000

测试结果说明:

同步代码块:加锁对象为共享变量,通过类的多个对象调用同步代码块,能够保证同步性。

3.4.加锁对象是类对象的同步代码块

关于类对象指的是Class的对象,即类似于:SyncCodeBlockIncrement14.class


自增器代码:

/**
 * <p>自增器-同步代码块-锁对象是类对象</p>
 *
 * @author hanchao 2018/3/18 13:12
 **/
static class SyncCodeBlockIncrement14 {
    public void autoIncrement() {
        //同步代码14-加锁对象是类对象
        synchronized (SyncCodeBlockIncrement14.class) {
            for (int i = 0; i < 5; i++) {
                number++;
                System.out.println("线程[" + Thread.currentThread().getName() + "]获取锁,number:" + number);
            }
        }
    }
}

多线程测试自增器代码:

System.out.println("通过synchronized定义同步代码块,作用范围:代码块,同步对象:加锁对象为类对象->类的多个对象ok");
SyncCodeBlockIncrement14 increment14 = new SyncCodeBlockIncrement14();
for (int i = 0; i < num; i++) {
    if (i % 2 == 0) {//模拟多个对象
        new Thread(() -> {
            new SyncCodeBlockIncrement14().autoIncrement();
        }).start();
    } else {//模拟同一个对象的多次调用
        new Thread(() -> {
            increment14.autoIncrement();
        }).start();
    }
}
Thread.sleep(sleep);
System.out.println("同步代码块:加锁对象为类对象,通过类的多个对象调用同步代码块,最终number=" + number);

运行结果:

...省略
线程[Thread-97132]获取锁,number:499999
线程[Thread-97132]获取锁,number:500000
同步代码块:加锁对象为类对象,通过类的多个对象调用同步代码块,最终number=500000

测试结果说明:

同步代码块:加锁对象为类对象,通过类的多个对象调用同步代码块,能够保证同步性。

4.同步方法

同步方法加锁的范围是整个方法的代码,同步对象根据加锁对象的不同而不同。

4.1.修饰方法是普通方法的同步方法

自增器代码:

/**
 * <p>自增器-普通同步方法-调用这个方法的对象</p>
 *
 * @author hanchao 2018/3/18 11:36
 **/
static class SyncMethodIncrement {
    public synchronized void autoIncrement() {
        for (int i = 0; i < 5; i++) {
            number++;
            System.out.println("线程[" + Thread.currentThread().getName() + "]获取锁,number:" + number);
        }
    }
}

多线程测试自增器代码(调用多个对象):

System.out.println("通过synchronized定义普通同步方法,作用范围:整个方法,同步对象:类的多个对象no。");
SyncMethodIncrement increment20 = new SyncMethodIncrement();
for (int i = 0; i < num; i++) {
    if (i % 2 == 0) {//模拟多个对象
        new Thread(() -> {
            new SyncMethodIncrement().autoIncrement();
        }).start();
    } else {//模拟同一个对象的多次调用
        new Thread(() -> {
            increment20.autoIncrement();
        }).start();
    }
}
Thread.sleep(sleep);
System.out.println("同步方法:修饰方法为普通方法,通过类的多个对象调用同步方法,最终number=" + number);

运行结果:

...省略
线程[Thread-86059]获取锁,number:499973
线程[Thread-86059]获取锁,number:499974
同步方法:修饰方法为普通方法,通过类的多个对象调用同步方法,最终number=499974

测试结果说明:

同步方法:修饰方法为普通方法,通过类的多个对象调用同步方法,不能够保证同步性。


多线程测试自增器代码(调用单个对象):

System.out.println("通过synchronized定义普通同步方法,作用范围:整个方法,同步对象:类的单个对象ok。");
SyncMethodIncrement increment21 = new SyncMethodIncrement();
for (int i = 0; i < num; i++) {
    new Thread(() -> {
        increment21.autoIncrement();
    }).start();
}
Thread.sleep(sleep);
System.out.println("同步方法:修饰方法为普通方法,通过类的单个对象调用同步方法,最终number=" + number);

运行结果:

...省略
线程[Thread-95746]获取锁,number:499999
线程[Thread-95746]获取锁,number:500000
同步方法:修饰方法为普通方法,通过类的单个对象调用同步方法,最终number=500000

测试结果说明:

同步方法:修饰方法为普通方法,通过类的单个对象调用同步方法,能够保证同步性。

4.2.修饰方法是类静态方法的同步方法

自增器代码:

/**
 * <p>自增器-静态同步方法-此类的所有对象</p>
 *
 * @author hanchao 2018/3/18 11:42
 **/
static class SyncStaticMethodIncrement {
    public static synchronized void autoIncrement() {
        for (int i = 0; i < 5; i++) {
            number++;
            System.out.println("线程[" + Thread.currentThread().getName() + "]获取锁,number:" + number);
        }
    }
}

多线程测试自增器代码:

System.out.println("通过synchronized定义静态同步方法,作用范围:整个方法,同步对象:类的多个对象ok。");
SyncStaticMethodIncrement increment22 = new SyncStaticMethodIncrement();
for (int i = 0; i < num; i++) {
    if (i % 2 == 0) {//模拟多个对象
        new Thread(() -> {
            new SyncStaticMethodIncrement().autoIncrement();
        }).start();
    } else {//模拟同一个对象的多次调用
        new Thread(() -> {
            increment22.autoIncrement();
        }).start();
    }
}
Thread.sleep(sleep);
System.out.println("同步方法:修饰方法为静态方法,通过类的多个对象调用同步方法,最终number=" + number);

运行结果:

...省略
线程[Thread-95871]获取锁,number:499999
线程[Thread-95871]获取锁,number:500000
同步方法:修饰方法为静态方法,通过类的多个对象调用同步方法,最终number=500000

测试结果说明:

同步方法:修饰方法为静态方法,通过类的多个对象调用同步方法,能够保证同步性。

5.synchronized关键字的用法总结

通过前面的实际学习和测试,现在将synchronized关键字的用法总结如下:

序号 大类 小类(锁定对象) 锁定范围 可同步对象 不同步对象
1 同步代码块 加锁对象是本地变量 方法块{}内的代码 单个对象 多个对象
2 同步代码块 加锁对象是类静态变量 方法块{}内的代码 多个对象 -
3 同步代码块 加锁对象是共享变量 方法块{}内的代码 多个对象 -
4 同步代码块 加锁对象是类对象 方法块{}内的代码 多个对象 -
5 同步方法 修饰的是普通方法 整个方法 单个对象 多个对象
6 同步方法 修饰的是静态方法 整个方法 多个对象 -
展开阅读全文

没有更多推荐了,返回首页