synchronized关键字也叫作互斥锁或者同步。
这个关键字的存在是为了解决编程中的线程安全问题的,而线程安全问题出现的主要原因一般为:多个线程操作同一个对象的数据,也就是同时操作共享变量的值。
synchronized的出现解决了这个问题,互斥锁的含义为,当一个线程操作一个对象的时候,对该对象增加一个锁,任何其他线程都处在等待状态,不可以对该对象进行操作。当持有锁的线程执行完毕后,会释放持有锁,其他等待线程共同竞争锁资源。
同时,synchronized关键字还可以保证线程的变化对其他线程可见,保证共享变量的可见性,也就是volatile功能。
synchronized关键字的应用
1、修饰实例方法:对当前实例对象加锁,执行同步代码前获取当前实例对象的控制权。
2、修饰静态方法:对当前类对象加锁,执行同步代码前获取当前类对象的控制权。
3、修饰代码块:对任意指定对象加锁,执行同步代码前获取指定对象的控制权。
修饰实例方法:
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
static int i = 0;
public synchronized void add(){
i++;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
synchronizedTest.add();
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
synchronizedTest.add();
}
}
});
thread.start();
thread1.start();
// 确保线程执行完毕
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}
上述代码可以看到,新建了两个线程分别调用同步方法add() 由于两个线程锁的都是s
ynchronizedTest对象,所以可以保证线程运行完毕,i的值为200000。得出结论修饰实例方法是对实例对象进行加锁。
接下来看一段代码:
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
static int i = 0;
public synchronized void add(){
i++;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
SynchronizedTest synchronizedTest1 = new SynchronizedTest();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
synchronizedTest.add();
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
synchronizedTest1.add();
}
}
});
thread.start();
thread1.start();
// 确保线程执行完毕
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}
对之前的代码做了轻微的改动,两个线程分别对两个实例对象加锁,这时当线程运行结束,结果不一定会是200000,得到了198478结果,说明两个线程用的是不同的锁,无法保证线程安全。
修饰静态方法:
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
static int i = 0;
public static synchronized void add(){
i++;
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
add();
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
add();
}
}
});
thread.start();
thread1.start();
// 确保线程执行完毕
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}
同步方法添加了static关键字,说明对当前类对象加锁。静态方法不属于任何一个实例。
但是如果一个线程A调用静态同步方法,另一个线程B调用实例同步方法,并且对同一个变量进行操作的时候,会发生线程安全问题,因为:静态方法锁的是类对象,实例方法锁的是实例对象,是两个不同的锁。
代码举例:
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
static int i = 0;
public static synchronized void add(){
i++;
}
public synchronized void add1(){
i++;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest test = new SynchronizedTest();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
test.add1();
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
add();
}
}
});
thread.start();
thread1.start();
// 确保线程执行完毕
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}
修饰代码块:
public void add(){
synchronized (SynchronizedTest.class){
i++;
}
}
public void add1(){
synchronized (this){
i++;
}
}
当只需要对一部分代码进行同步操作时,可以用synchronized修饰代码片段,锁定的对象可以是实例镀锡或者this当前实例对象,也可以是XX.class类对象。
synchronized重入性
synchronized是给对象添加一个互斥锁,当一个线程持有对象锁时,其他操作该对象的线程将处于阻塞状态。但是当一个线程持有对象锁,然后再次请求自己持有对象锁的临界资源时,就是重入锁,可以请求成功。也就是说,在一个synchronized方法执行时,可以调用该对象的另一个synchronized方法。
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
static int i = 0;
public void add(){
synchronized (SynchronizedTest.class){
i++;
}
}
public void add1(){
synchronized (this){
add();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest test = new SynchronizedTest();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
test.add();
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
test.add();
}
}
});
thread.start();
thread1.start();
// 确保线程执行完毕
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}
synchronized底层原理
JVM中的同步是通过持有和退出Monitor(监视器/管程)对象来实现的。
无论是显式同步(同步代码块)或者是隐式同步(同步方法)都是这样。
显式同步与隐式同步的区别:
显式同步:通过明确的代码指令monitorenter(同步开始)和monitorexit(同步结束)来实现。
隐式同步:通过读取常量池中方法的ACC_SYNCHRONIZED标识来实现。
synchronized同步代码块原理:
public void
synchronizedAdd
(){
synchronized (this){
i++;
}
}
首先,对上面这个同步块代码进行反编译,javap -c SynchronizedTest.class
可以得到这个方法的指令代码。
其中第3行和第13行可以看到是通过
monitorenter和
monitorexit指令来实现同步。
当执行monitorenter指令时,当前线程获取锁对象的monitor持有权,当monitor持有计数器为0时,线程获得锁成功,计数器+1。如果在运行时,调用该对象其他锁方法,此时为重入状态,计数器再次+1。当同步代码执行完毕,计数器归0。其他线程将会试图获取monitor。
我们可以看到19行多了一个monitorexit,是为了当程序发生异常时,来释放monitor的。否则其他线程将无法获得monitor。
public void synchronizedAdd();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #3 // Field i:I
7: iconst_1
8: iadd
9: putstatic #3 // Field i:I
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
synchronized同步方法原理:
同步方法,使用的是隐式同步方法,不通过指令来开始或结束同步(持有或释放monitor)。
同步方法通过方法常量池方法表结构中的ACC_SYNCHRONIZED标识区分是否为同步方法,如果在方法调用时发现ACC_SYNCHRONIZED被设置了,那么线程首先会获得monitor,其他线程无法获得这个monitor并处于阻塞状态。当方法执行完成时,释放monitor。如果在同步方法期间抛出异常,并且没有捕捉处理,那么该线程所持有的monitor在抛到同步方法之外时释放。
同步方法指令:
public synchronized void synchronizedAdd();
Code:
0: getstatic #2 // Field i:I
3: iconst_1
4: iadd
5: putstatic #2 // Field i:I
8: return
现在在实际的编程中已经几乎很少使用synchronized关键字了,可以用更多的方式或封装类来替代。