synchronized原理及其相关特性
写在前面:尽量不要使用 synchronized(String a) 因为在JVM中,字符串常量池具有缓冲功能!而这个会导致线程一直循环,因为String的引用只有一个,会导致只有一个线程不断循环执行。
1.synchronized
synchronized:可以在任意对象及方法上加锁,而加锁的这段代码称为互斥区或临界区。
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
synchronized用的锁是存在Java对象头里。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit,如下表所示。
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如表所示。
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据,如表2-4所示。
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如表2-5所示。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
-
普通同步方法,锁是当前实例对象
-
静态同步方法,锁是当前类的class对象
-
同步方法块,锁是括号里面的对象
其实synchronized相当于是一个“对象监控器”锁,这个对象监控器由不同的对象充当。
当一个线程访问同步代码块时,它首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁。
public class MyThread extends Thread {
private int count = 5;
@Override
public synchronized void run() {
count‐‐;
System.out.println(this.currentThread().getName() + " count:" + count);
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread, "thread1");
Thread thread2 = new Thread(myThread, "thread2");
Thread thread3 = new Thread(myThread, "thread3");
Thread thread4 = new Thread(myThread, "thread4");
Thread thread5 = new Thread(myThread, "thread5");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
输出结果:
thread1 count:4
thread2 count:3
thread3 count:2
thread5 count:1
thread4 count:0
这里在run方法上加了synchronized这个修饰。主要作用就是当多个线程访问MyThread的run方法的时候,他们就会以排队的形式(这里排队是按照CPU分配的先后顺序而定的)执行,一个线程如果想执行synchronized修饰的方法里的代码,就要先获得这个对象的锁,如果获取不到,就不能执行,会一直尝试在获得这个锁。
synchronized同步代码块
使用关键字synchronized修饰同步方法是有弊端的,比如当线程A要执行一个比较长时间的同步方法的时候,这个时候线程B就必须要等待比较长的时间。这个时候就可以使用synchronized同步代码块来解决问题。
当两个并发线程访问同一对象object中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另外一个线程必须等待当前线程执行完这个代码块才可以执行改代码块。其实这个有点类似这篇博客中的ReentrantLock分组打印。
所以synchronized同步代码块存在着上面所说的一个问题,就是一次只能执行一个代码块,如果有多个synchronized同步代码块的时候,就会陷入阻塞状态,这样子会影响效率,这个时候我们可以修改synchronized代码块中的监控对象那个,即使用同步代码块锁非this对象的时候,synchronized(非this)代码块中的程序与同步方法是异步的,不与其他this锁this同步方法争抢this锁,可以提高运行效率。
synchronized同步代码块还可以是任意对象,比如:
try{
String x=new String();
synchronized(x){
...
}
}
当调用同步代码块的时候,每次都是一个新的同步代码块对象(new 一个),这样的话就不会出现分组打印,而是变成了交叉打印了。
这里1,3其实是一样的,就是一个分组打印的效果。2 的话就是当程序中即有同步方法又有同步代码块的时候,两者会是同步的,即调用同步方法或者同步代码块都能实现分组效果。
synchronized静态同步方法
锁的是Class对象,跟synchronized同步方法不是同一个锁,所以是异步。但是对于所有的Class实例是同步的。比如在线程中创建一个Class实例,当他调用synchronized静态同步方法的时候,实现了同步。
-
一个对象有一把锁,多个线程多个锁
-
public class MultiThread { private int num = 200; public synchronized void printNum(String threadName, String tag) { if (tag.equals("a")) { num = num ‐ 100; System.out.println(threadName + " tag a,set num over!"); } else { num = num ‐ 200; System.out.println(threadName + " tag b,set num over!"); } System.out.println(threadName + " tag " + tag + ", num = " + num); } public static void main(String[] args) throws InterruptedException { final MultiThread multiThread1 = new MultiThread(); final MultiThread multiThread2 = new MultiThread(); new Thread(new Runnable() { public void run() { multiThread1.printNum("thread1", "a"); } }).start(); new Thread(new Runnable() { public void run() { multiThread2.printNum("thread2", "b"); } }).start(); } }
此外,说明一下这里的new Thread(new Runnable()),其实上是调用了Thread类的带参数的构造法方法。
-
输出结果:
thread1 tag a,set num over! thread1 tag a, num = 100 thread2 tag b,set num over! thread2 tag b, num = 0
这与我们期望的输出结果:thread2 tag b, num = ‐100不大一样,这是因为上面有两个对象:multiThread1 和 multiThread2,他们使用了同一把锁,所以就会出现这种情况。因为这里synchronized锁的是普通方法,所以锁是当前实例对象。可以在变量和方法上加上static关键字,就可以实现我们想要的结果。因为加上了static,实际上锁是变成了class对象了。
-
对象锁的同步和异步
-
同步:synchronized,共享资源,即保证了线程安全中的原子性,此外,线程安全还需要保证可见性,这个就需要volatitle实现
-
异步:asynchronized,多个线程之间不会竞争共享资源。
-
2.synchronized的特性
-
synchronized拥有可重入锁
synchronized拥有锁重入的功能,当一个线程得到一个对象的锁后,在该锁里执行代码的时候可以再次获得该对象的其他锁。当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
如下面这个例子中,同步方法可以调用自己内部的其他同步方法,即使还没有释放自己的同步锁,还是可以获得其他重入锁。
public class SyncDubbo {
public synchronized void method1() {
System.out.println("method1-----");
method2();
}
public synchronized void method2() {
System.out.println("method2-----");
method3();
}
public synchronized void method3() {
System.out.println("method3-----");
}
public static void main(String[] args) {
final SyncDubbo syncDubbo = new SyncDubbo();
new Thread(new Runnable() {
@Override
public void run() {
syncDubbo.method1();
}
}).start();
}
}
//执行结果:
method1-----
method2-----
method3-----
可重入锁就是自己获得自己内部的锁。如果没有重入锁的话,加入有一个线程在获取了对象A的锁之后,再次请求A的锁的时候,由于还没有释放之前获得的锁,所以这个时候就会出现死锁。
假如有一个场景:用户名和密码保存在本地txt文件中,则登录验证方法和更新密码方法都应该被加synchronized,那么当更新密码的时候需要验证密码的合法性,所以需要调用验证方法,此时是可以调用的。
此外,可重入锁的特性还有父子可继承性,如下面的例子:
public class SyncDubbo {
static class Main {
public int i = 5;
public synchronized void operationSup() {
i--;
System.out.println("Main print i =" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Sub extends Main {
public synchronized void operationSub() {
while (i > 0) {
i--;
System.out.println("Sub print i = " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
Sub sub = new Sub();
sub.operationSub();
}
}).start();
}
}
返回结果是:
Sub print i = 4
Sub print i = 3
Sub print i = 2
Sub print i = 1
Sub print i = 0
-
其他特性
-
出现异常时,锁自动释放,即一个线程的代码执行过程中出现异常的话,其所持有的锁会自动释放。
-
将任意对象作为监视器monitor
-
public class StringLock {
private String lock = "lock";
public void method() {
synchronized (lock) {
try {
System.out.println("当前线程: " +
Thread.currentThread().getName() + "开始");
Thread.sleep(1000);
System.out.println("当前线程: " +Thread.currentThread().getName() + "结束");
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) {
final StringLock stringLock = new StringLock();
new Thread(new Runnable() {
public void run() {
stringLock.method();
}
}, "t1").start();
new Thread(new Runnable() {
public void run() {
stringLock.method();
}
}, "t2").start();
}
}
执行结果:
当前线程: t1开始
当前线程: t1结束
当前线程: t2开始
当前线程: t2结束
-
单例模式:双重校验锁
- 普通加锁的单例模式实现:
public class Singleton { private static Singleton instance = null; //懒汉模式 //private static Singleton instance = new Singleton(); //饿汉模式 private Singleton() { } public static synchronized Singleton newInstance() { if (null == instance) { instance = new Singleton(); } return instance; } }
使用上述的方式可以实现多线程的情况下获取到正确的实例对象,但是每次访问new Instance() 方法都会进行加锁和解锁操作,也就是说该锁可能会成为系统的瓶颈。
- .双重校验锁:
public class DubbleSingleton { private static volatile DubbleSingleton instance; public static DubbleSingleton getInstance(){ if(instance == null){ try { //模拟初始化对象的准备时间... Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } //类上加锁,表示当前对象不可以在其他线程的时候创建 synchronized (DubbleSingleton.class) { //如果不加这一层判断的话,这样的话每一个线程会得到一个实例 //而不是所有的线程的到的是一个实例 if(instance == null){ instance = new DubbleSingleton(); } } } return instance; } }
需要注意的是,如果没有加上volatile这个关键字的话是错误的。因为指令重排优化,可能会导致初始化单例对象和将该对象地址赋值给instance字段的顺序与上面Java代码中书写的顺序不同。volatile关键字在这里的含义就是禁止指令的重排序优化(另一个作用是提供内存可见性),从而保证instance字段被初始化时,单例对象已经被完全初始化
- 普通加锁的单例模式实现: