目录
4.2 同步非静态实例方法(synchronized method)
4.3同步代码块(synchronized (类.class)),锁是小括号 () 中的类对象(Class 对象)
4.4对同步代码块优化的思考之同步代码块(synchronized (类实例对象)),锁是小括号 () 中的实例对象。
前言:
在Java多线程编程中
,我们长需要考虑线程安全问题,其中关键字Synchronized
,在线程同步中扮演着重要的作用,我将讲解 Java
关键字Synchronized
的知识
1.定义
Synchronized 是Java中的一个关键字,被Synchronized
修饰的方法 / 代码,保证同一时刻最多只有1个线程执行 。
2.Synchronized 原理
实现原理: JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。
具体实现是在编译之后在同步方法调用前加入一个monitor.enter
指令,在退出方法和异常处插入monitor.exit
的指令。
对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit
之后才能尝试继续获取锁。
注意:synchronized
方法或声明执行期间,如程序遇到任何异常或return,线程都会释放锁
流程图如下:
3.synchronized 锁的是什么
首先我们明确一点,synchronized
锁的不是代码,锁的都是对象。
锁的对象有以下几种:
-
同步非静态方法(
synchronized method
),锁是当前对象的实例对象。 -
同步静态方法(
synchronized static method
),锁是当前对象的类对象(Class 对象)。 -
同步代码块(
synchronized (this)
,synchronized (类实例对象)
),锁是小括号()
中的实例对象。 -
同步代码块(
synchronized (类.class)
),锁是小括号()
中的类对象(Class 对象)。
3.1 实例对象锁与类对象锁
①实例对象锁,不同的实例拥有不同的实例对象锁,所以对于同一个实例对象,在同一时刻只有一个线程可以访问这个实例对象的同步方法;不同的实例对象,不能保证多线程的同步操作。
②类对象锁(全局锁),在 JVM 中一个类只有一个与之对应的类对象,所以在同一时刻只有一个线程可以访问这个类的同步方法。
4.示例分析
我们用最经典的存钱实例讲解这个问题;
4.1 不引入锁synchronized我们存钱会发生什么
1.新建一个银行类Bank
public class Bank {
private int BankMoney =1000;
/***
* 存钱方法
* @param money
* @param names
*/
public void dopost(int money,String names){
String name = Thread.currentThread().getName();
Log.e("BankT",name+"-----当前余额:"+BankMoney);
BankMoney+=money;
Log.e("BankT",name+"-------"+names+"-----存入银行后余额:"+BankMoney);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.建立顾客完成存钱的操作
//2.runnableT执行体
private class MyRunnableT implements Runnable{
Bank mBk;
int money;
String name;
public MyRunnableT(Bank mk,int money,String name){
this.mBk=mk;
this.money=money;
this.name=name;
}
@Override
public void run() {
mBk.dopost(money,name);
}
}
//3.开启三个线程执行存钱操作
Bank bank = new Bank();
//创建三个不同的对象
Thread thread_1 = new Thread(new MyRunnableT(bank , 200, "小明"));
Thread thread_2 = new Thread(new MyRunnableT(bank , 200, "小张"));
Thread thread_3 = new Thread(new MyRunnableT(bank , 200, "小王"));
thread_1.start();
thread_2.start();
thread_3.start();
运行结果:
看到打印结果我们发现他并没有按照我们期望的结果顺序去显示结果,如:我们期望小明先执行,然后小张执行,最后小王在执行,为什么出现这样的结果呢。这个就是因为在多线程执行的过程中cpu 调度线程执行任务时是随机的选取一个任务去执行。因此出现了乱序执行的现象,且没有保证多线程同时操作同一个资源的同步性。
4.2 同步非静态实例方法(synchronized method
)
①给dopost 方法添加关键子synchronized
public class Bank {
private int BankMoney =1000;
/***
* 存钱方法
* @param money
* @param names
*/
public synchronized void dopost(int money,String names){
//设置当前对象的类对象 实现全局锁,
String name = Thread.currentThread().getName();
Log.e("BankT",name+"-----当前余额:"+BankMoney);
BankMoney+=money;
Log.e("BankT",name+"-------"+names+"-----存入银行后余额:"+BankMoney);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
② 仅仅创建一个bank 实例对象 ,开启线程;
private class MyRunnableT implements Runnable{
Bank mBk;
int money;
String name;
public MyRunnableT(Bank mk,int money,String name){
this.mBk=mk;
this.money=money;
this.name=name;
}
@Override
public void run() {
mBk.dopost(money,name);
}
}
//开启三个线程;
private void sync() {
// 创建当前对象的实例对象//仅仅创建一个bank 实例对象
Bank bank = new Bank();
Thread thread_1 = new Thread(new MyRunnableT(bank, 200, "小明"),"thread_1");
Thread thread_2 = new Thread(new MyRunnableT(bank, 200, "小张"),"thread_2");
Thread thread_3 = new Thread(new MyRunnableT(bank, 200, "小王"),"thread_3");
thread_1.start();
thread_2.start();
thread_3.start();
}
运行结果如下:
可以看到,结果正确执行。因为对于同一个实例对象(仅仅创建一个bank 实例对象),各线程之间访问其中的同步方法是互斥的。
如果我们分别创建实例传入线程中是什么效果呢
private class MyRunnableT implements Runnable{
Bank mBk;
int money;
String name;
public MyRunnableT(Bank mk,int money,String name){
this.mBk=mk;
this.money=money;
this.name=name;
}
@Override
public void run() {
mBk.dopost(money,name);
}
}
private void sync() {
// 创建当前对象的实例对象
Bank bank_1 = new Bank();
Bank bank_2 = new Bank();
Bank bank_3 = new Bank();
Thread thread_1 = new Thread(new MyRunnableT(bank_1, 200, "小明"),"thread_1");
Thread thread_2 = new Thread(new MyRunnableT(bank_2, 200, "小张"),"thread_2");
Thread thread_3 = new Thread(new MyRunnableT(bank_3, 200, "小王"),"thread_3");
thread_1.start();
thread_2.start();
thread_3.start();
}
执行结果如下:
从上面的运行结果,我们发现对 Bank
中 money
的操作并没有同步,synchronized
失效了?这是因为实例对象锁,只对同一个实例生效,对同一个对象的不同实例不保证同步(示例代码中每个线程传入的bank都创建了一个实例,不是同一个实例)。
要解决这个问题有两种方式:①可以改变成4.2同步非静态实例方法(synchronized method
)的实现形式,只创建一个实例对象。② 同步代码块(synchronized (类.class)
),锁是小括号 ()
中的类对象(Class 对象)的实现形式。
4.3同步代码块(synchronized (类.class)
),锁是小括号 ()
中的类对象(Class 对象)
修改dopost 方法为 类对象锁其他部分代码不变,在执行程序;
/***
* 存钱方法
* @param money
* @param names
*/
public void dopost(int money,String names){
//设置当前对象的类对象 实现全局锁,
synchronized (Bank.class){
String name = Thread.currentThread().getName();
Log.e("BankT",name+"-----当前余额:"+BankMoney);
BankMoney+=money;
Log.e("BankT",name+"-------"+names+"-----存入银行后余额:"+BankMoney);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果如下:
从结果中我们发现,线程是同步操作了,但为什么在我们的 BankMoney怎么才 1200 啊?
要回答上面问题也很简单,首先线程是同步操作了,这个没有疑问,说明我们的全局锁生效了,那为什么钱少了,因为我们这里 mew
了三个对象,三个对象都有各自的BankMoney,他们并不共享,所以最后都是 1200,那有没有办法,让这些线程间共享 BankMoney呢?方法很简单,只要设置 BankMoney为 static
即可
4.4对同步代码块优化的思考之同步代码块(synchronized (类实例对象)
),锁是小括号 ()
中的实例对象。
我们知道同步的对象不是实例对象就是类对象。现在假设一个类有多个同步方法,那么当某个线程进入其中一个同步方法时,这个类的其它同步方法也会被锁住,造成其它与当前锁定操作的同步方法毫无关系的同步方法也被锁住,最后的结果就是影响了整个多线程执行的性能,使原本不需要互斥的方法也都进行了互斥操作。比如:
public class Bank {
private static int BankMoney =1000;
/***
* 存钱方法
* @param money
* @param names
*/
public void dopost(int money,String names){
//设置当前对象的实例锁
long begin = System.currentTimeMillis();
synchronized (this){
String name = Thread.currentThread().getName();
Log.e("BankT",name+"-----当前余额:"+BankMoney);
BankMoney+=money;
Log.e("BankT",name+"-------"+names+"-----存入银行后余额:"+BankMoney);
try {
Thread.sleep(1000);
long end = System.currentTimeMillis();
Log.e("BankT", "--存入耗时:" + (end - begin));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 一个与资金操作没有任务关系的同步方法
*/
public void showInfo() {
long begin = System.currentTimeMillis();
String threadName = Thread.currentThread().getName();
synchronized (this) {
try {
Log.e("BankT",threadName + "--开始查看信息");
Thread.sleep(5000);
Log.e("BankT",threadName + "--银行详细信息...");
long end = System.currentTimeMillis();
Log.e("BankT",threadName + "--查看耗时:" + (end - begin));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private class MyRunnableT implements Runnable{
Bank mBk;
int money;
String name;
public MyRunnableT(Bank mk,int money,String name){
this.mBk=mk;
this.money=money;
this.name=name;
}
@Override
public void run() {
mBk.dopost(money,name);
}
}
private void sync() {
// 创建当前对象的实例对象
final Bank bank_1 = new Bank();
Thread thread_1 = new Thread(new MyRunnableT(bank_1, 200, "小明"),"thread_1");
Thread thread_2 = new Thread(new Runnable() {
@Override
public void run() {
//调用一个与资金操作没有任务关系的同步方法
bank_1.showInfo();
}
}, "thread_2");
thread_1.start();
thread_2.start();
}
运行结果如下:
从运行结果中,我们看到小刚这个线程平白无故多等了 1 秒钟,严重影响了线程性能。
针对上面的情况,我们可以采用多个实例对象锁的方案解决,比如:
只修改Bank类 创建两个不同的object对象实例锁,
public class Bank {
private static int BankMoney =1000;
private final Object syncDopost = new Object(); // 同步锁
private final Object syncShowInfo = new Object(); // 同步锁
/***
* 存钱方法
* @param money
* @param names
*/
public void dopost(int money,String names){
//设置当前对象的实例对象
long begin = System.currentTimeMillis();
synchronized (this.syncDopost){
String name = Thread.currentThread().getName();
Log.e("BankT",name+"-----当前余额:"+BankMoney);
BankMoney+=money;
Log.e("BankT",name+"-------"+names+"-----存入银行后余额:"+BankMoney);
try {
Thread.sleep(1000);
long end = System.currentTimeMillis();
Log.e("BankT", "--存入耗时:" + (end - begin));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 一个与资金操作没有任务关系的同步方法
*/
public void showInfo() {
long begin = System.currentTimeMillis();
String threadName = Thread.currentThread().getName();
synchronized (this.syncShowInfo) {
try {
Log.e("BankT",threadName + "--开始查看信息");
Thread.sleep(5000);
Log.e("BankT",threadName + "--银行详细信息...");
long end = System.currentTimeMillis();
Log.e("BankT",threadName + "--查看耗时:" + (end - begin));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果如下:
我们发现,两个线程间同步被取消了,性能问题也解决了。
总结:可以创建不同同步方法的不同同步锁(减小锁的范围)来优化同步代码块。
4.5同步静态方法
同步静态方法,锁的是类对象而不是某个实例对象,所以可以理解为对于静态方法的锁是全局的锁,同步也是全局的同步。
示例如下:
public class Bank {
private static int BankMoney =1000;
/***
* 存钱方法
* @param money
* @param names
*/
public static synchronized void dopost(int money,String names){
//设置当前对象的类对象 实现全局锁,
String name = Thread.currentThread().getName();
Log.e("BankT",name+"-----当前余额:"+BankMoney);
BankMoney+=money;
Log.e("BankT",name+"-------"+names+"-----存入银行后余额:"+BankMoney);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void sync() {
// 创建当前对象的实例对象
Bank bank_1 = new Bank();
Bank bank_2 = new Bank();
Bank bank_3 = new Bank();
Thread thread_1 = new Thread(new MyRunnableT(bank_1, 200, "小明"),"thread_1");
Thread thread_2 = new Thread(new MyRunnableT(bank_2, 200, "小张"),"thread_2");
Thread thread_3 = new Thread(new MyRunnableT(bank_3, 200, "小王"),"thread_3");
thread_1.start();
thread_2.start();
thread_3.start();
}
private class MyRunnableT implements Runnable{
Bank mBk;
int money;
String name;
public MyRunnableT(Bank mk,int money,String name){
this.mBk=mk;
this.money=money;
this.name=name;
}
@Override
public void run() {
mBk.dopost(money,name);
}
}
运行结果如下:
五总结
同步锁 synchronized
:
-
synchronized
锁的不是代码,锁的都是对象。 -
实例对象锁:同步非静态方法(
synchronized method
),同步代码块(synchronized (this)
,synchronized (类实例对象)
)。 -
类对象(Class 对象)锁:同步静态方法(
synchronized static method
),同步代码块(synchronized (类.class)
)。 -
类的不同的实例拥有不同的实例对象锁,但类对象锁(全局锁)有仅只有一个。
-
优化同步代码块的方式有,减少同步区域或减小锁的范围。
-
Synchronized
修饰方法时存在缺陷:若修饰一个大的方法,将会大大影响效率,一般使用Synchronized
关键字声明代码块减小开销。 -
从synchronized的特点中可以看到它是一种重量级锁,会涉及到操作系统状态的切换影响效率,所以JDK1.6中对synchronized进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。
- 当代码段执行结束或出现异常后会自动释放对监视器的锁定
- 是非公平锁,在等待获取锁的过程中不可被中断
- synchronized的内存语义(详见面试打怪升升级-被问烂的volatile关键字,这次我要搞懂它(深入到操作系统层面理解,超多图片示意图))
- 互斥性,被synchronized修饰的方法同时只能由一个线程执行